From 1eea4b9eaa5fbe527bd514daf9757159fa8c6984 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 22 Aug 2026 04:45:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=96=B7=E5=B0=84=E6=95=88?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 9 + .github/workflows/build.yml | 30 + .gitignore | 40 + .mcp.json | 11 + DeployPack.ps1 | 46 + LICENSE | 121 + README.md | 9 + build.gradle | 87 + find.py | 163 + gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 248 + gradlew.bat | 82 + settings.gradle | 13 + src/main/java/com/nonid/DebugStaffItem.java | 192 + .../java/com/nonid/DestroyedSkinHolder.java | 11 + src/main/java/com/nonid/EnergyHolder.java | 62 + src/main/java/com/nonid/GenderHolder.java | 16 + .../java/com/nonid/LiquidDonorHistory.java | 110 + src/main/java/com/nonid/LiquidHolder.java | 55 + src/main/java/com/nonid/MessHolder.java | 15 + src/main/java/com/nonid/NeedsOfNature.java | 6388 +++++++++++++++++ .../java/com/nonid/NeedsOfNatureClient.java | 1521 ++++ .../java/com/nonid/NonAccessoryEffects.java | 79 + .../com/nonid/NonAccessoryShedSystem.java | 197 + .../java/com/nonid/NonAdvancementHooks.java | 208 + .../java/com/nonid/NonAnimationMatcher.java | 181 + .../com/nonid/NonAnimationRescueSystem.java | 92 + .../com/nonid/NonAnimationSessionData.java | 111 + src/main/java/com/nonid/NonApiInternals.java | 358 + src/main/java/com/nonid/NonAttackSystem.java | 671 ++ src/main/java/com/nonid/NonAuraSystem.java | 209 + src/main/java/com/nonid/NonCommands.java | 714 ++ src/main/java/com/nonid/NonConfig.java | 2783 +++++++ .../com/nonid/NonDataAccessoryTrinket.java | 73 + .../java/com/nonid/NonDebugChatCategory.java | 9 + src/main/java/com/nonid/NonDebugChatMode.java | 45 + .../nonid/NonDefeatedAnimationSelector.java | 142 + .../com/nonid/NonDestroyedSkinSystem.java | 697 ++ src/main/java/com/nonid/NonDiagnostics.java | 166 + .../java/com/nonid/NonGatherCancelReason.java | 7 + src/main/java/com/nonid/NonGatherSystem.java | 772 ++ src/main/java/com/nonid/NonGenderSystem.java | 408 ++ .../com/nonid/NonInjectorMatchPolicy.java | 47 + src/main/java/com/nonid/NonItemSystem.java | 208 + src/main/java/com/nonid/NonLiquidSystem.java | 301 + .../java/com/nonid/NonLiquidTankType.java | 22 + src/main/java/com/nonid/NonMessSystem.java | 264 + .../java/com/nonid/NonMobEnergySystem.java | 227 + .../com/nonid/NonMobGatherEligibility.java | 36 + .../java/com/nonid/NonPlayerAttackSystem.java | 717 ++ .../com/nonid/NonPlayerAvoidanceState.java | 7 + .../nonid/NonPlayerInteractionProfiles.java | 383 + .../com/nonid/NonPlayerInteractionSystem.java | 1101 +++ .../nonid/NonPregnancyConceptionSystem.java | 226 + .../java/com/nonid/NonSkinRepairBlocks.java | 33 + src/main/java/com/nonid/NonStats.java | 247 + .../java/com/nonid/NonTrinketsCompat.java | 466 ++ .../com/nonid/NonTrinketsIntegration.java | 187 + .../java/com/nonid/PregnancyCycleHolder.java | 15 + .../java/com/nonid/PregnancyCyclePhase.java | 22 + src/main/java/com/nonid/PregnancyHolder.java | 32 + .../api/NonAccessoryEffectsSnapshot.java | 19 + .../java/com/nonid/api/NonChangeSource.java | 10 + .../nonid/api/NonDestroyedSkinSnapshot.java | 4 + .../java/com/nonid/api/NonEnergySnapshot.java | 7 + src/main/java/com/nonid/api/NonEntityApi.java | 45 + src/main/java/com/nonid/api/NonEvents.java | 105 + src/main/java/com/nonid/api/NonGender.java | 27 + .../com/nonid/api/NonLiquidComposition.java | 7 + .../com/nonid/api/NonLiquidTankSnapshot.java | 10 + .../java/com/nonid/api/NonMessSnapshot.java | 7 + src/main/java/com/nonid/api/NonMessType.java | 7 + src/main/java/com/nonid/api/NonPlayerApi.java | 100 + .../nonid/api/NonPlayerInteractionApi.java | 155 + .../com/nonid/api/NonPlayerStateSnapshot.java | 11 + .../com/nonid/api/NonPregnancySnapshot.java | 10 + .../nonid/api/animation/NonActorEvents.java | 55 + .../api/animation/NonAnimationActor.java | 31 + .../nonid/api/animation/NonAnimationApi.java | 358 + .../NonAnimationBlockRequirement.java | 36 + .../api/animation/NonAnimationCandidate.java | 9 + .../animation/NonAnimationDamageBehavior.java | 28 + .../api/animation/NonAnimationDefinition.java | 34 + .../api/animation/NonAnimationEvents.java | 117 + .../nonid/api/animation/NonAnimationPack.java | 33 + .../api/animation/NonAnimationSession.java | 22 + .../api/animation/NonAnimationStage.java | 24 + .../client/NonAnimationParticleEvents.java | 59 + .../api/client/NonAnimationRenderEvents.java | 106 + .../api/client/NonAnimationSoundEvents.java | 67 + .../client/HorseLiquidCollectorGeoModel.java | 33 + .../HorseLiquidCollectorItemGeoModel.java | 27 + .../HorseLiquidCollectorItemRenderer.java | 10 + .../client/HorseLiquidCollectorRenderer.java | 28 + .../client/NonAccessorySkinOverlays.java | 14 + .../nonid/client/NonAccessoryTooltips.java | 270 + .../client/NonAnimationPreviewRenderer.java | 94 + .../com/nonid/client/NonArmorShedClient.java | 307 + .../nonid/client/NonClientDiagnostics.java | 32 + .../nonid/client/NonClientResourceCaches.java | 30 + .../com/nonid/client/NonDebugNameHelper.java | 83 + .../com/nonid/client/NonDebugSpinMode.java | 19 + .../nonid/client/NonDebugSpinMusicClient.java | 171 + .../nonid/client/NonDestroyedSkinClient.java | 2343 ++++++ .../client/NonEnergizedEffectTextures.java | 39 + .../nonid/client/NonExternalSkinCompat.java | 116 + .../nonid/client/NonFilledEffectTextures.java | 39 + .../client/NonGenderSelectionScreen.java | 122 + .../java/com/nonid/client/NonHudOverlay.java | 1051 +++ .../nonid/client/NonIconButtonTextures.java | 14 + .../nonid/client/NonInventoryScreenHooks.java | 270 + .../com/nonid/client/NonLiquidColors.java | 82 + .../nonid/client/NonPackStartupWarnings.java | 229 + .../nonid/client/NonPackWarningScreen.java | 126 + .../client/NonPlayerInteractionClient.java | 848 +++ .../nonid/client/NonPregnancyClientState.java | 50 + .../client/NonPregnantEffectTextures.java | 44 + .../com/nonid/client/NonQuickSkinCompat.java | 117 + .../nonid/client/NonServerSettingsClient.java | 166 + .../com/nonid/client/NonSkinFolderNames.java | 65 + .../nonid/client/NonSkinOverrideCompat.java | 175 + .../java/com/nonid/client/NonSoundCues.java | 151 + .../com/nonid/client/NonSpinModelLock.java | 114 + .../nonid/client/NonStatLabelResolver.java | 105 + .../com/nonid/client/NonTagLocalizer.java | 44 + .../client/NonWildfireGenderScreenHooks.java | 100 + .../nonid/client/NonWildfireGenderSync.java | 630 ++ .../nonid/client/PregnancyEggGeoModel.java | 85 + .../nonid/client/PregnancyEggRenderer.java | 61 + .../client/intiface/NonIntifaceBridge.java | 191 + .../client/intiface/NonIntifaceClient.java | 843 +++ .../NonIntifaceDependencyManager.java | 458 ++ .../particle/LiquidFallingParticle.java | 76 + .../client/particle/LiquidLeakParticle.java | 81 + .../nonid/client/particle/LiquidParticle.java | 57 + .../client/particle/LiquidPuddleParticle.java | 96 + .../client/particle/LiquidWaterParticle.java | 84 + .../client/particle/RippedFabricParticle.java | 92 + .../client/particle/SmallHeartParticle.java | 56 + .../com/nonid/data/NonAccessoryBehavior.java | 76 + .../nonid/data/NonAccessoryDefinitions.java | 316 + .../nonid/data/NonAccessoryItemRegistry.java | 634 ++ .../nonid/data/NonAccessoryLiquidFilling.java | 65 + .../com/nonid/data/NonAccessoryStackData.java | 69 + .../com/nonid/data/NonEntityProfiles.java | 605 ++ .../nonid/data/NonLiquidGainOverrides.java | 218 + .../java/com/nonid/data/NonLiquidRoles.java | 19 + .../nonid/data/NonManualPeakDefinitions.java | 158 + .../java/com/nonid/data/NonPeakStages.java | 64 + .../nonid/effect/EnergizedStatusEffect.java | 10 + .../effect/EnergyReliefStatusEffect.java | 86 + .../com/nonid/effect/EnergyStatusEffect.java | 86 + .../FertilityIncreaserStatusEffect.java | 10 + .../com/nonid/effect/FilledStatusEffect.java | 10 + .../com/nonid/effect/NonStatusEffects.java | 30 + .../nonid/effect/PregnantStatusEffect.java | 10 + .../entity/HorseLiquidCollectorEntity.java | 335 + .../com/nonid/entity/PregnancyEggEntity.java | 539 ++ .../integration/FloatValueSliderWidget.java | 87 + .../integration/NonCreditsButtonHooks.java | 41 + .../com/nonid/integration/NonCreditsData.java | 197 + .../nonid/integration/NonCreditsScreen.java | 402 ++ .../integration/NonModMenuDebugScreens.java | 4450 ++++++++++++ .../NonModMenuExternalScreens.java | 17 + .../NonModMenuGameplayScreens.java | 2383 ++++++ .../NonModMenuHudConfigScreen.java | 653 ++ .../integration/NonModMenuIntegration.java | 10 + .../NonModMenuIntifaceInstallScreen.java | 97 + .../integration/NonModMenuIntifaceScreen.java | 323 + ...odMenuIntifaceStrokerOscillatorScreen.java | 240 + .../NonModMenuIntifaceTuningScreen.java | 111 + .../NonModMenuIntifaceVibratorScreen.java | 290 + .../integration/NonModMenuPlayerScreens.java | 923 +++ .../nonid/integration/NonModMenuScreens.java | 277 + .../NonModMenuSystemSettingsScreens.java | 1394 ++++ .../integration/NonPlayerProfilesScreen.java | 367 + .../NonWildfireGenderConfigCompat.java | 52 + .../integration/PercentSliderWidget.java | 42 + .../com/nonid/integration/SettingsList.java | 202 + .../nonid/integration/jade/NonJadePlugin.java | 58 + .../internal/animation/AnimationEngine.java | 326 + .../animation/AnimationEngineClient.java | 519 ++ .../api/AnimationLifecycleEvents.java | 103 + .../animation/api/AnimationService.java | 508 ++ .../animation/api/NonAnimationApiBridge.java | 74 + .../animation/api/NonAnimationViews.java | 56 + .../camera/AnimatedCameraPoseTracker.java | 76 + .../client/camera/AnimationCameraZoom.java | 64 + .../client/camera/CameraPositionAccess.java | 7 + .../camera/FirstPersonCameraCollision.java | 95 + .../camera/FirstPersonLookConstraint.java | 142 + .../compat/wildfire/WildfireGenderCompat.java | 955 +++ .../compat/wildfire/WildfireGenderState.java | 24 + .../AnimationAssetDiagnostics.java | 391 + .../GeckoModelCopyDiagnostics.java | 277 + .../render/AnimationRenderStateAccess.java | 14 + .../gecko/AnimationActorAnimatable.java | 300 + .../render/gecko/AnimationActorGeoModel.java | 38 + .../render/gecko/BoneTextureOverrides.java | 364 + .../render/gecko/GeckoRenderTickets.java | 41 + .../render/gecko/GeckoReplacedRender.java | 616 ++ .../render/gecko/GeckoResourceResolver.java | 496 ++ .../render/gecko/GenderedModelResolver.java | 102 + .../render/gecko/ModelVariantCandidates.java | 56 + .../render/gecko/ParticleKeyframes.java | 158 + .../render/gecko/ReplacedEntityRenderer.java | 76 + .../render/gecko/VanillaTextureResolver.java | 100 + .../render/gecko/layer/BoneItemGeoLayer.java | 109 + .../gecko/layer/BoneTextureGeoLayer.java | 140 + .../gecko/layer/CameraPoseGeoLayer.java | 208 + .../gecko/layer/EmissiveTextureGeoLayer.java | 190 + .../gecko/layer/LayerTextureGeoLayer.java | 51 + .../gecko/layer/ParticleKeyframeGeoLayer.java | 182 + .../gecko/layer/TranslucentBoneGeoLayer.java | 149 + .../gecko/layer/WildfireGenderGeoLayer.java | 152 + .../runtime/ClientAnimationRuntime.java | 1742 +++++ .../sound/AnimationSoundAnticipation.java | 56 + .../client/sound/AnimationSoundEffects.java | 145 + .../animation/data/AnimationCueEffects.java | 32 + .../animation/data/AnimationCueIndex.java | 434 ++ .../animation/data/AnimationDefinitions.java | 2931 ++++++++ .../data/AnimationGameplayMetadata.java | 484 ++ .../data/AnimationSoundCueParser.java | 191 + .../animation/mixin/EntityPushAwayMixin.java | 25 + .../mixin/LivingEntityDamageMixin.java | 28 + .../mixin/LivingEntityPushableMixin.java | 22 + .../mixin/MobEntityIgnoreAttackersMixin.java | 23 + .../mixin/ServerPlayNetworkHandlerMixin.java | 115 + .../animation/mixin/TargetPredicateMixin.java | 20 + .../mixin/client/CameraAnimationMixin.java | 461 ++ .../client/CameraPositionAccessMixin.java | 20 + .../ClientPlayerInteractionManagerMixin.java | 152 + .../client/ClientPlayerMovementMixin.java | 34 + ...lientWorldAnimatedPlayerParticleMixin.java | 53 + .../client/CuboidGeoBoneHiddenCubeMixin.java | 59 + .../EntityAnimationParticlePositionMixin.java | 49 + .../mixin/client/EntityPushAwayMixin.java | 23 + .../client/EntityRenderManagerAccessor.java | 18 + .../client/EntityRenderManagerMixin.java | 73 + .../mixin/client/EntityRenderStateMixin.java | 36 + .../EntityRendererAnimationNameTagMixin.java | 42 + .../mixin/client/EntityRendererInvoker.java | 15 + .../mixin/client/HeldItemRendererMixin.java | 27 + .../animation/mixin/client/InputAccessor.java | 12 + .../mixin/client/KeyboardInputMixin.java | 31 + .../mixin/client/LivingEntityJumpMixin.java | 20 + .../client/LivingEntityPushableMixin.java | 21 + ...ngEntityRendererAnimationNameTagMixin.java | 29 + .../client/LivingEntityRendererMixin.java | 34 + .../mixin/client/MinecraftClientMixin.java | 25 + .../client/MouseScrollAnimationZoomMixin.java | 25 + .../WorldRendererFirstPersonActorMixin.java | 32 + .../AdjustAnimationSpeedC2SPayload.java | 27 + .../AdvanceAnimationStageS2CPayload.java | 31 + .../AnimationSpeedUpdateS2CPayload.java | 31 + .../animation/network/AnimationStageInfo.java | 318 + .../network/DebugAdvanceStageC2SPayload.java | 22 + .../DebugChatPreferenceC2SPayload.java | 28 + .../network/DebugJoinAnimationC2SPayload.java | 28 + .../DebugStartAnimationC2SPayload.java | 31 + .../DebugStopAllAnimationsC2SPayload.java | 18 + .../network/DebugStopAnimationC2SPayload.java | 22 + .../network/StartAnimationS2CPayload.java | 227 + .../network/StopAllAnimationsS2CPayload.java | 21 + .../network/StopAnimationS2CPayload.java | 23 + .../server/AnimationSessionData.java | 4 + .../server/AnimationStageTiming.java | 76 + .../server/AnimationStartContext.java | 39 + .../server/ServerAnimationController.java | 4466 ++++++++++++ .../internal/animation/util/AnimationIds.java | 74 + .../animation/util/EntityVariants.java | 30 + .../animation/util/StageTimeWarp.java | 78 + .../animation/util/WeightedPicker.java | 53 + .../com/nonid/item/EnergyGainAdjustItem.java | 88 + .../com/nonid/item/EnergyStabilizerItem.java | 128 + .../nonid/item/HorseLiquidCollectorItem.java | 98 + .../com/nonid/item/NonDataAccessoryItem.java | 45 + .../mixin/AnimalEntityBreedingMixin.java | 40 + .../AnimalMateGoalAnimationBreedingMixin.java | 44 + .../mixin/BrewingRecipeRegistryMixin.java | 24 + .../mixin/CreeperEntityNoNAnimationMixin.java | 159 + .../mixin/EntityEnergyPersistenceMixin.java | 52 + .../mixin/EntityGenderPersistenceMixin.java | 51 + .../FileResourcePackProviderAccessor.java | 16 + .../com/nonid/mixin/FleeEntityGoalMixin.java | 45 + .../LivingEntityLiquidBottleConsumeMixin.java | 35 + .../mixin/LivingEntityLiquidJumpMixin.java | 57 + .../com/nonid/mixin/MobEntityEnergyMixin.java | 1003 +++ .../com/nonid/mixin/MobEntityGenderMixin.java | 41 + ...MobEntityPregnancyFriendlyTargetMixin.java | 37 + .../java/com/nonid/mixin/NonMixinPlugin.java | 36 + .../mixin/PlayerEntityDestroyedSkinMixin.java | 102 + .../nonid/mixin/PlayerEntityEnergyMixin.java | 181 + .../nonid/mixin/PlayerEntityGenderMixin.java | 36 + .../com/nonid/mixin/PlayerLiquidMixin.java | 177 + .../mixin/PlayerLiquidMovementMixin.java | 70 + .../com/nonid/mixin/PlayerPregnancyMixin.java | 188 + .../ProjectileEntityAnimationRescueMixin.java | 26 + .../ResourcePackManagerNoNPackMixin.java | 135 + .../mixin/ServerPlayerEntityDamageMixin.java | 19 + .../client/EntityRendererDebugNameMixin.java | 51 + .../GameRendererReactiveImpactFovMixin.java | 20 + .../mixin/client/HandledScreenAccessor.java | 14 + .../HandledScreenTrinketSlotStateMixin.java | 60 + .../nonid/mixin/client/InGameHudMixin.java | 181 + .../LivingEntityRendererDebugLabelMixin.java | 19 + ...ivingEntityRendererPlayerTextureMixin.java | 34 + ...layerEntityRendererDestroyedSkinMixin.java | 51 + ...urcePackOrganizerHideNoNExternalMixin.java | 48 + .../client/StatusEffectsDisplayMixin.java | 45 + .../TranslationStorageStatLabelMixin.java | 25 + .../network/AccessoryShedStateS2CPayload.java | 32 + .../AnimationCatalogChunkS2CPayload.java | 27 + .../nonid/network/AnimationCatalogData.java | 256 + .../nonid/network/AttackStateS2CPayload.java | 84 + .../DestroyedSkinMaskSyncS2CPayload.java | 22 + .../DestroyedSkinMaskUploadC2SPayload.java | 25 + .../DestroyedSkinParticlesS2CPayload.java | 25 + .../network/DestroyedSkinStateS2CPayload.java | 21 + .../network/DestroyedSkinSyncS2CPayload.java | 22 + .../DestroyedSkinUploadC2SPayload.java | 24 + .../GenderSelectionPromptS2CPayload.java | 26 + .../com/nonid/network/HostConfigSyncData.java | 337 + .../network/HostConfigSyncS2CPayload.java | 28 + .../nonid/network/LiquidStateS2CPayload.java | 26 + .../ManualDamageDestroyedSkinC2SPayload.java | 17 + .../nonid/network/MessStateS2CPayload.java | 26 + .../PlayerAnimationCancelC2SPayload.java | 20 + .../PlayerAnimationChoiceC2SPayload.java | 25 + .../PlayerAnimationConsentC2SPayload.java | 25 + .../PlayerAnimationConsentS2CPayload.java | 37 + ...layerAnimationRequestStatusS2CPayload.java | 27 + .../PlayerAnimationSelectionS2CPayload.java | 32 + .../network/PlayerAttackInputC2SPayload.java | 22 + .../PlayerProfileActionC2SPayload.java | 25 + .../PlayerProfileSnapshotS2CPayload.java | 23 + .../PlayerRuntimeSettingsS2CPayload.java | 20 + .../PregnancyCycleStateS2CPayload.java | 35 + .../network/PregnancyStateS2CPayload.java | 20 + .../RepairDestroyedSkinC2SPayload.java | 17 + .../ServerConfigRequestC2SPayload.java | 16 + .../network/ServerConfigResultS2CPayload.java | 25 + .../network/ServerConfigUpdateC2SPayload.java | 23 + .../network/SetPlayerGenderC2SPayload.java | 20 + .../SkinRippedInfoRequestS2CPayload.java | 17 + .../network/StartManualPeakC2SPayload.java | 17 + .../StopAttackAnimationC2SPayload.java | 21 + .../StopVoluntaryAnimationC2SPayload.java | 21 + .../nonid/pack/NonExternalPackCatalog.java | 99 + .../nonid/pack/NonExternalPackProvider.java | 191 + ...onGeneratedAccessoryModelPackProvider.java | 147 + .../NonGeneratedAccessoryTagPackProvider.java | 72 + .../pack/NonGeneratedCemPackProvider.java | 425 ++ .../nonid/pack/NonGeneratedInMemoryPack.java | 138 + .../com/nonid/pack/NonPackRootResolver.java | 237 + .../pack/NonPrefixedZipResourcePack.java | 164 + .../java/com/nonid/particle/NonParticles.java | 81 + .../com/nonid/potion/NonBrewingRecipes.java | 55 + .../java/com/nonid/potion/NonPotions.java | 150 + .../recipe/AbstractEnergyAdjustRecipe.java | 140 + .../nonid/recipe/EnergyAugmenterRecipe.java | 34 + .../nonid/recipe/EnergyDiminisherRecipe.java | 34 + .../nonid/recipe/EnergyStabilizerRecipe.java | 160 + .../com/nonid/recipe/FertileNectarRecipe.java | 114 + .../recipe/FillAdvancedStraponRecipe.java | 130 + .../nonid/recipe/NonRecipeIngredients.java | 84 + .../nonid/recipe/NonRecipeSerializers.java | 38 + .../java/com/nonid/sound/NonSoundEvents.java | 54 + .../models/entity/missingmodel.geo.json | 79 + .../textures/missingmodel.png | Bin 0 -> 101 bytes .../minecraft/items/lingering_potion.json | 90 + .../assets/minecraft/items/potion.json | 97 + .../assets/minecraft/items/splash_potion.json | 90 + .../assets/minecraft/items/tipped_arrow.json | 62 + .../assets/needsofnature/credits.json | 112 + .../afw/debug_spin/debug_spin.animation.json | 20 + .../entity/horse_liquid_collector.geo.json | 126 + .../geckolib/models/fish.geo.json | 92 + .../resources/assets/needsofnature/icon.png | Bin 0 -> 1680 bytes .../needsofnature/items/debug_staff.json | 6 + .../needsofnature/items/energy_augmenter.json | 6 + .../items/energy_diminisher.json | 6 + .../items/energy_stabilizer.json | 6 + .../needsofnature/items/flower_mix.json | 6 + .../items/horse_liquid_collector.json | 10 + .../assets/needsofnature/lang/en_us.json | 895 +++ .../assets/needsofnature/lang/zh_cn.json | 901 +++ .../models/item/energy_augmenter.json | 6 + .../models/item/energy_diminisher.json | 6 + .../models/item/energy_lingering_potion.json | 7 + .../models/item/energy_potion.json | 7 + .../item/energy_relief_lingering_potion.json | 7 + .../models/item/energy_relief_potion.json | 7 + .../item/energy_relief_splash_potion.json | 7 + .../item/energy_relief_tipped_arrow.json | 7 + .../models/item/energy_splash_potion.json | 7 + .../models/item/energy_stabilizer.json | 6 + .../models/item/energy_tipped_arrow.json | 7 + .../item/fertile_nectar_lingering_potion.json | 7 + .../models/item/fertile_nectar_potion.json | 7 + .../item/fertile_nectar_splash_potion.json | 7 + .../item/fertility_lingering_potion.json | 7 + .../models/item/fertility_potion.json | 7 + .../models/item/fertility_splash_potion.json | 7 + .../needsofnature/models/item/flower_mix.json | 6 + .../models/item/horse_liquid_collector.json | 43 + .../particles/liquid_particle.json | 5 + .../particles/liquid_particle_falling.json | 5 + .../particles/liquid_particle_puddle.json | 5 + .../particles/liquid_particle_water.json | 5 + .../particles/ripped_fabric.json | 5 + .../needsofnature/particles/smallheart.json | 5 + .../assets/needsofnature/sounds.json | 208 + .../assets/needsofnature/sounds/a.ogg | Bin 0 -> 14014 bytes .../actionsounds/dryimpacts/impactdry01.ogg | Bin 0 -> 5535 bytes .../actionsounds/dryimpacts/impactdry02.ogg | Bin 0 -> 5973 bytes .../actionsounds/dryimpacts/impactdry03.ogg | Bin 0 -> 5681 bytes .../actionsounds/dryimpacts/impactdry04.ogg | Bin 0 -> 6507 bytes .../actionsounds/dryimpacts/impactdry05.ogg | Bin 0 -> 6281 bytes .../actionsounds/dryimpacts/impactdry06.ogg | Bin 0 -> 6448 bytes .../actionsounds/dryimpacts/impactdry07.ogg | Bin 0 -> 6189 bytes .../actionsounds/dryimpacts/impactdry08.ogg | Bin 0 -> 6189 bytes .../actionsounds/dryimpacts/impactdry09.ogg | Bin 0 -> 6253 bytes .../actionsounds/dryimpacts/impactdry10.ogg | Bin 0 -> 6164 bytes .../actionsounds/dryimpacts/impactdry11.ogg | Bin 0 -> 5979 bytes .../sounds/actionsounds/motions/motion01.ogg | Bin 0 -> 6829 bytes .../sounds/actionsounds/motions/motion02.ogg | Bin 0 -> 6874 bytes .../sounds/actionsounds/motions/motion03.ogg | Bin 0 -> 5735 bytes .../sounds/actionsounds/motions/motion04.ogg | Bin 0 -> 7203 bytes .../sounds/actionsounds/motions/motion05.ogg | Bin 0 -> 7036 bytes .../sounds/actionsounds/motions/motion06.ogg | Bin 0 -> 7406 bytes .../sounds/actionsounds/motions/motion07.ogg | Bin 0 -> 8961 bytes .../sounds/actionsounds/motions/motion08.ogg | Bin 0 -> 7070 bytes .../sounds/actionsounds/motions/motion09.ogg | Bin 0 -> 8840 bytes .../sounds/actionsounds/motions/motion10.ogg | Bin 0 -> 8149 bytes .../sounds/actionsounds/retract/retract01.ogg | Bin 0 -> 7905 bytes .../sounds/actionsounds/retract/retract02.ogg | Bin 0 -> 7182 bytes .../sounds/actionsounds/retract/retract03.ogg | Bin 0 -> 8078 bytes .../sounds/actionsounds/shots/shot_in01.ogg | Bin 0 -> 9726 bytes .../sounds/actionsounds/shots/shot_in02.ogg | Bin 0 -> 11064 bytes .../sounds/actionsounds/shots/shot_in03.ogg | Bin 0 -> 12468 bytes .../sounds/actionsounds/shots/shot_out01.ogg | Bin 0 -> 6022 bytes .../sounds/actionsounds/shots/shot_out02.ogg | Bin 0 -> 8304 bytes .../sounds/actionsounds/shots/shot_out03.ogg | Bin 0 -> 7776 bytes .../sounds/actionsounds/shots/shot_out04.ogg | Bin 0 -> 9637 bytes .../sounds/actionsounds/shots/shot_out05.ogg | Bin 0 -> 14597 bytes .../sounds/actionsounds/shots/shot_out06.ogg | Bin 0 -> 11677 bytes .../sounds/actionsounds/wet/wet01.ogg | Bin 0 -> 7353 bytes .../sounds/actionsounds/wet/wet02.ogg | Bin 0 -> 10419 bytes .../sounds/actionsounds/wet/wet03.ogg | Bin 0 -> 8387 bytes .../sounds/actionsounds/wet/wet04.ogg | Bin 0 -> 9396 bytes .../sounds/actionsounds/wet/wet05.ogg | Bin 0 -> 14835 bytes .../sounds/actionsounds/wet/wet06.ogg | Bin 0 -> 10019 bytes .../sounds/actionsounds/wet/wet07.ogg | Bin 0 -> 8810 bytes .../sounds/actionsounds/wet/wet08.ogg | Bin 0 -> 7790 bytes .../sounds/actionsounds/wet/wet09.ogg | Bin 0 -> 8610 bytes .../sounds/actionsounds/wet/wet10.ogg | Bin 0 -> 8062 bytes .../sounds/actionsounds/wet/wet11.ogg | Bin 0 -> 8967 bytes .../sounds/actionsounds/wet/wet12.ogg | Bin 0 -> 7201 bytes .../actionsounds/wetimpacts/impactwet01.ogg | Bin 0 -> 6042 bytes .../actionsounds/wetimpacts/impactwet02.ogg | Bin 0 -> 6049 bytes .../actionsounds/wetimpacts/impactwet03.ogg | Bin 0 -> 6245 bytes .../actionsounds/wetimpacts/impactwet04.ogg | Bin 0 -> 5858 bytes .../actionsounds/wetimpacts/impactwet05.ogg | Bin 0 -> 5884 bytes .../actionsounds/wetimpacts/impactwet06.ogg | Bin 0 -> 6285 bytes .../actionsounds/wetimpacts/impactwet07.ogg | Bin 0 -> 5985 bytes .../actionsounds/wetimpacts/impactwet08.ogg | Bin 0 -> 6390 bytes .../actionsounds/wetimpacts/impactwet09.ogg | Bin 0 -> 5633 bytes .../actionsounds/wetimpacts/impactwet10.ogg | Bin 0 -> 6201 bytes .../actionsounds/wetimpacts/impactwet11.ogg | Bin 0 -> 6652 bytes .../actionsounds/wetimpacts/impactwet12.ogg | Bin 0 -> 5933 bytes .../actionsounds/wetimpacts/impactwet13.ogg | Bin 0 -> 6565 bytes .../actionsounds/wetimpacts/impactwet14.ogg | Bin 0 -> 6439 bytes .../actionsounds/wetimpacts/impactwet15.ogg | Bin 0 -> 6617 bytes .../actionsounds/wetimpacts/impactwet16.ogg | Bin 0 -> 6747 bytes .../assets/needsofnature/sounds/funkytown.ogg | Bin 0 -> 1847419 bytes .../assets/needsofnature/sounds/pop.ogg | Bin 0 -> 4487 bytes .../assets/needsofnature/sounds/rip01.ogg | Bin 0 -> 9161 bytes .../assets/needsofnature/sounds/rip02.ogg | Bin 0 -> 9367 bytes .../assets/needsofnature/sounds/rip03.ogg | Bin 0 -> 9146 bytes .../horse_liquid_collector_empty.png | Bin 0 -> 2526 bytes .../horse_liquid_collector_full.png | Bin 0 -> 2585 bytes .../textures/gui/overlays/energy_outline.png | Bin 0 -> 8617 bytes .../textures/gui/pregnancy_cycle_normal.png | Bin 0 -> 379 bytes .../gui/pregnancy_cycle_ovulation.png | Bin 0 -> 321 bytes .../textures/gui/pregnancy_cycle_risky.png | Bin 0 -> 5510 bytes .../textures/gui/pregnancy_cycle_safe.png | Bin 0 -> 5510 bytes .../textures/gui/skin_repair.png | Bin 0 -> 212 bytes .../needsofnature/textures/gui/slots/a.png | Bin 0 -> 157 bytes .../needsofnature/textures/gui/slots/d.png | Bin 0 -> 185 bytes .../needsofnature/textures/gui/slots/v.png | Bin 0 -> 195 bytes .../textures/gui/sprites/hud/ad_mash.png | Bin 0 -> 1723 bytes .../gui/sprites/hud/attack_background.png | Bin 0 -> 187 bytes .../textures/gui/sprites/hud/attack_full.png | Bin 0 -> 874 bytes .../gui/sprites/hud/liquid_background.png | Bin 0 -> 263 bytes .../textures/gui/sprites/hud/liquid_full.png | Bin 0 -> 679 bytes .../gui/sprites/hud/progress_background.png | Bin 0 -> 242 bytes .../gui/sprites/hud/progress_full.png | Bin 0 -> 1850 bytes .../gui/sprites/mob_effect/energy.png | Bin 0 -> 457 bytes .../gui/sprites/mob_effect/energy_relief.png | Bin 0 -> 453 bytes .../mob_effect/fertility_increaser.png | Bin 0 -> 637 bytes .../gui/sprites/mob_effect/pregnant.png | Bin 0 -> 544 bytes .../textures/item/energy_augmenter.png | Bin 0 -> 568 bytes .../textures/item/energy_diminisher.png | Bin 0 -> 567 bytes .../textures/item/energy_potion_overlay.png | Bin 0 -> 205 bytes .../item/energy_relief_potion_overlay.png | Bin 0 -> 204 bytes .../item/energy_relief_tipped_arrow_head.png | Bin 0 -> 211 bytes .../textures/item/energy_stabilizer.png | Bin 0 -> 549 bytes .../item/energy_tipped_arrow_head.png | Bin 0 -> 211 bytes .../textures/item/fertile_nectar_overlay.png | Bin 0 -> 210 bytes .../item/fertility_potion_overlay.png | Bin 0 -> 268 bytes .../textures/item/flower_mix.png | Bin 0 -> 413 bytes .../textures/mob_effects/energized1.png | Bin 0 -> 254 bytes .../textures/mob_effects/energized2.png | Bin 0 -> 321 bytes .../textures/mob_effects/energized3.png | Bin 0 -> 380 bytes .../textures/mob_effects/filled1.png | Bin 0 -> 325 bytes .../textures/mob_effects/filled2.png | Bin 0 -> 322 bytes .../textures/mob_effects/filled3.png | Bin 0 -> 384 bytes .../textures/mob_effects/pregnant.png | Bin 0 -> 544 bytes .../textures/particle/liquid_drip_fall.png | Bin 0 -> 115 bytes .../textures/particle/liquid_drip_hang.png | Bin 0 -> 124 bytes .../textures/particle/liquid_drip_land.png | Bin 0 -> 121 bytes .../textures/particle/liquid_drip_water.png | Bin 0 -> 192 bytes .../textures/particle/liquid_puddle.png | Bin 0 -> 147 bytes .../textures/particle/ripped_fabric.png | Bin 0 -> 217 bytes .../particle/ripped_fabric_color_mask.png | Bin 0 -> 304 bytes .../textures/particle/smallheart.png | Bin 0 -> 173 bytes .../tags/block/wall_surfaces.json | 22 + .../advancement/acquire_hardware.json | 23 + .../advancement/bad_idea_beacon.json | 23 + .../advancement/collectors_habit.json | 23 + .../advancement/emergency_snack.json | 23 + .../advancement/favouritism.json | 23 + .../advancement/field_research.json | 23 + .../advancement/filled_to_the_brim.json | 23 + .../advancement/first_pregnancy.json | 23 + .../advancement/group_activity.json | 23 + .../needsofnature/advancement/messed_up.json | 23 + .../advancement/mother_nature.json | 23 + .../advancement/natural_curiosity.json | 23 + .../advancement/productive_pregnancy.json | 23 + .../recipes/misc/energy_augmenter.json | 43 + .../recipes/misc/energy_diminisher.json | 43 + .../recipes/misc/energy_stabilizer.json | 43 + .../recipes/misc/fertile_nectar.json | 43 + .../advancement/recipes/misc/flower_mix.json | 32 + .../recipes/misc/horse_liquid_collector.json | 54 + .../data/needsofnature/advancement/root.json | 23 + .../advancement/stable_diet.json | 23 + .../advancement/tossed_around.json | 23 + .../advancement/udderly_unfortunate.json | 23 + .../recipe/energy_augmenter.json | 4 + .../recipe/energy_diminisher.json | 4 + .../recipe/energy_stabilizer.json | 4 + .../needsofnature/recipe/fertile_nectar.json | 4 + .../data/needsofnature/recipe/flower_mix.json | 13 + .../recipe/horse_liquid_collector.json | 19 + .../data/trinkets/entities/player.json | 10 + .../resources/data/trinkets/slots/legs/a.json | 8 + .../resources/data/trinkets/slots/legs/d.json | 9 + .../resources/data/trinkets/slots/legs/v.json | 9 + src/main/resources/fabric.mod.json | 48 + ...needsofnature.animation.client.mixins.json | 31 + .../needsofnature.animation.mixins.json | 23 + src/main/resources/needsofnature.mixins.json | 49 + 567 files changed, 81290 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .mcp.json create mode 100644 DeployPack.ps1 create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 find.py create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/nonid/DebugStaffItem.java create mode 100644 src/main/java/com/nonid/DestroyedSkinHolder.java create mode 100644 src/main/java/com/nonid/EnergyHolder.java create mode 100644 src/main/java/com/nonid/GenderHolder.java create mode 100644 src/main/java/com/nonid/LiquidDonorHistory.java create mode 100644 src/main/java/com/nonid/LiquidHolder.java create mode 100644 src/main/java/com/nonid/MessHolder.java create mode 100644 src/main/java/com/nonid/NeedsOfNature.java create mode 100644 src/main/java/com/nonid/NeedsOfNatureClient.java create mode 100644 src/main/java/com/nonid/NonAccessoryEffects.java create mode 100644 src/main/java/com/nonid/NonAccessoryShedSystem.java create mode 100644 src/main/java/com/nonid/NonAdvancementHooks.java create mode 100644 src/main/java/com/nonid/NonAnimationMatcher.java create mode 100644 src/main/java/com/nonid/NonAnimationRescueSystem.java create mode 100644 src/main/java/com/nonid/NonAnimationSessionData.java create mode 100644 src/main/java/com/nonid/NonApiInternals.java create mode 100644 src/main/java/com/nonid/NonAttackSystem.java create mode 100644 src/main/java/com/nonid/NonAuraSystem.java create mode 100644 src/main/java/com/nonid/NonCommands.java create mode 100644 src/main/java/com/nonid/NonConfig.java create mode 100644 src/main/java/com/nonid/NonDataAccessoryTrinket.java create mode 100644 src/main/java/com/nonid/NonDebugChatCategory.java create mode 100644 src/main/java/com/nonid/NonDebugChatMode.java create mode 100644 src/main/java/com/nonid/NonDefeatedAnimationSelector.java create mode 100644 src/main/java/com/nonid/NonDestroyedSkinSystem.java create mode 100644 src/main/java/com/nonid/NonDiagnostics.java create mode 100644 src/main/java/com/nonid/NonGatherCancelReason.java create mode 100644 src/main/java/com/nonid/NonGatherSystem.java create mode 100644 src/main/java/com/nonid/NonGenderSystem.java create mode 100644 src/main/java/com/nonid/NonInjectorMatchPolicy.java create mode 100644 src/main/java/com/nonid/NonItemSystem.java create mode 100644 src/main/java/com/nonid/NonLiquidSystem.java create mode 100644 src/main/java/com/nonid/NonLiquidTankType.java create mode 100644 src/main/java/com/nonid/NonMessSystem.java create mode 100644 src/main/java/com/nonid/NonMobEnergySystem.java create mode 100644 src/main/java/com/nonid/NonMobGatherEligibility.java create mode 100644 src/main/java/com/nonid/NonPlayerAttackSystem.java create mode 100644 src/main/java/com/nonid/NonPlayerAvoidanceState.java create mode 100644 src/main/java/com/nonid/NonPlayerInteractionProfiles.java create mode 100644 src/main/java/com/nonid/NonPlayerInteractionSystem.java create mode 100644 src/main/java/com/nonid/NonPregnancyConceptionSystem.java create mode 100644 src/main/java/com/nonid/NonSkinRepairBlocks.java create mode 100644 src/main/java/com/nonid/NonStats.java create mode 100644 src/main/java/com/nonid/NonTrinketsCompat.java create mode 100644 src/main/java/com/nonid/NonTrinketsIntegration.java create mode 100644 src/main/java/com/nonid/PregnancyCycleHolder.java create mode 100644 src/main/java/com/nonid/PregnancyCyclePhase.java create mode 100644 src/main/java/com/nonid/PregnancyHolder.java create mode 100644 src/main/java/com/nonid/api/NonAccessoryEffectsSnapshot.java create mode 100644 src/main/java/com/nonid/api/NonChangeSource.java create mode 100644 src/main/java/com/nonid/api/NonDestroyedSkinSnapshot.java create mode 100644 src/main/java/com/nonid/api/NonEnergySnapshot.java create mode 100644 src/main/java/com/nonid/api/NonEntityApi.java create mode 100644 src/main/java/com/nonid/api/NonEvents.java create mode 100644 src/main/java/com/nonid/api/NonGender.java create mode 100644 src/main/java/com/nonid/api/NonLiquidComposition.java create mode 100644 src/main/java/com/nonid/api/NonLiquidTankSnapshot.java create mode 100644 src/main/java/com/nonid/api/NonMessSnapshot.java create mode 100644 src/main/java/com/nonid/api/NonMessType.java create mode 100644 src/main/java/com/nonid/api/NonPlayerApi.java create mode 100644 src/main/java/com/nonid/api/NonPlayerInteractionApi.java create mode 100644 src/main/java/com/nonid/api/NonPlayerStateSnapshot.java create mode 100644 src/main/java/com/nonid/api/NonPregnancySnapshot.java create mode 100644 src/main/java/com/nonid/api/animation/NonActorEvents.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationActor.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationApi.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationBlockRequirement.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationCandidate.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationDamageBehavior.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationDefinition.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationEvents.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationPack.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationSession.java create mode 100644 src/main/java/com/nonid/api/animation/NonAnimationStage.java create mode 100644 src/main/java/com/nonid/api/client/NonAnimationParticleEvents.java create mode 100644 src/main/java/com/nonid/api/client/NonAnimationRenderEvents.java create mode 100644 src/main/java/com/nonid/api/client/NonAnimationSoundEvents.java create mode 100644 src/main/java/com/nonid/client/HorseLiquidCollectorGeoModel.java create mode 100644 src/main/java/com/nonid/client/HorseLiquidCollectorItemGeoModel.java create mode 100644 src/main/java/com/nonid/client/HorseLiquidCollectorItemRenderer.java create mode 100644 src/main/java/com/nonid/client/HorseLiquidCollectorRenderer.java create mode 100644 src/main/java/com/nonid/client/NonAccessorySkinOverlays.java create mode 100644 src/main/java/com/nonid/client/NonAccessoryTooltips.java create mode 100644 src/main/java/com/nonid/client/NonAnimationPreviewRenderer.java create mode 100644 src/main/java/com/nonid/client/NonArmorShedClient.java create mode 100644 src/main/java/com/nonid/client/NonClientDiagnostics.java create mode 100644 src/main/java/com/nonid/client/NonClientResourceCaches.java create mode 100644 src/main/java/com/nonid/client/NonDebugNameHelper.java create mode 100644 src/main/java/com/nonid/client/NonDebugSpinMode.java create mode 100644 src/main/java/com/nonid/client/NonDebugSpinMusicClient.java create mode 100644 src/main/java/com/nonid/client/NonDestroyedSkinClient.java create mode 100644 src/main/java/com/nonid/client/NonEnergizedEffectTextures.java create mode 100644 src/main/java/com/nonid/client/NonExternalSkinCompat.java create mode 100644 src/main/java/com/nonid/client/NonFilledEffectTextures.java create mode 100644 src/main/java/com/nonid/client/NonGenderSelectionScreen.java create mode 100644 src/main/java/com/nonid/client/NonHudOverlay.java create mode 100644 src/main/java/com/nonid/client/NonIconButtonTextures.java create mode 100644 src/main/java/com/nonid/client/NonInventoryScreenHooks.java create mode 100644 src/main/java/com/nonid/client/NonLiquidColors.java create mode 100644 src/main/java/com/nonid/client/NonPackStartupWarnings.java create mode 100644 src/main/java/com/nonid/client/NonPackWarningScreen.java create mode 100644 src/main/java/com/nonid/client/NonPlayerInteractionClient.java create mode 100644 src/main/java/com/nonid/client/NonPregnancyClientState.java create mode 100644 src/main/java/com/nonid/client/NonPregnantEffectTextures.java create mode 100644 src/main/java/com/nonid/client/NonQuickSkinCompat.java create mode 100644 src/main/java/com/nonid/client/NonServerSettingsClient.java create mode 100644 src/main/java/com/nonid/client/NonSkinFolderNames.java create mode 100644 src/main/java/com/nonid/client/NonSkinOverrideCompat.java create mode 100644 src/main/java/com/nonid/client/NonSoundCues.java create mode 100644 src/main/java/com/nonid/client/NonSpinModelLock.java create mode 100644 src/main/java/com/nonid/client/NonStatLabelResolver.java create mode 100644 src/main/java/com/nonid/client/NonTagLocalizer.java create mode 100644 src/main/java/com/nonid/client/NonWildfireGenderScreenHooks.java create mode 100644 src/main/java/com/nonid/client/NonWildfireGenderSync.java create mode 100644 src/main/java/com/nonid/client/PregnancyEggGeoModel.java create mode 100644 src/main/java/com/nonid/client/PregnancyEggRenderer.java create mode 100644 src/main/java/com/nonid/client/intiface/NonIntifaceBridge.java create mode 100644 src/main/java/com/nonid/client/intiface/NonIntifaceClient.java create mode 100644 src/main/java/com/nonid/client/intiface/NonIntifaceDependencyManager.java create mode 100644 src/main/java/com/nonid/client/particle/LiquidFallingParticle.java create mode 100644 src/main/java/com/nonid/client/particle/LiquidLeakParticle.java create mode 100644 src/main/java/com/nonid/client/particle/LiquidParticle.java create mode 100644 src/main/java/com/nonid/client/particle/LiquidPuddleParticle.java create mode 100644 src/main/java/com/nonid/client/particle/LiquidWaterParticle.java create mode 100644 src/main/java/com/nonid/client/particle/RippedFabricParticle.java create mode 100644 src/main/java/com/nonid/client/particle/SmallHeartParticle.java create mode 100644 src/main/java/com/nonid/data/NonAccessoryBehavior.java create mode 100644 src/main/java/com/nonid/data/NonAccessoryDefinitions.java create mode 100644 src/main/java/com/nonid/data/NonAccessoryItemRegistry.java create mode 100644 src/main/java/com/nonid/data/NonAccessoryLiquidFilling.java create mode 100644 src/main/java/com/nonid/data/NonAccessoryStackData.java create mode 100644 src/main/java/com/nonid/data/NonEntityProfiles.java create mode 100644 src/main/java/com/nonid/data/NonLiquidGainOverrides.java create mode 100644 src/main/java/com/nonid/data/NonLiquidRoles.java create mode 100644 src/main/java/com/nonid/data/NonManualPeakDefinitions.java create mode 100644 src/main/java/com/nonid/data/NonPeakStages.java create mode 100644 src/main/java/com/nonid/effect/EnergizedStatusEffect.java create mode 100644 src/main/java/com/nonid/effect/EnergyReliefStatusEffect.java create mode 100644 src/main/java/com/nonid/effect/EnergyStatusEffect.java create mode 100644 src/main/java/com/nonid/effect/FertilityIncreaserStatusEffect.java create mode 100644 src/main/java/com/nonid/effect/FilledStatusEffect.java create mode 100644 src/main/java/com/nonid/effect/NonStatusEffects.java create mode 100644 src/main/java/com/nonid/effect/PregnantStatusEffect.java create mode 100644 src/main/java/com/nonid/entity/HorseLiquidCollectorEntity.java create mode 100644 src/main/java/com/nonid/entity/PregnancyEggEntity.java create mode 100644 src/main/java/com/nonid/integration/FloatValueSliderWidget.java create mode 100644 src/main/java/com/nonid/integration/NonCreditsButtonHooks.java create mode 100644 src/main/java/com/nonid/integration/NonCreditsData.java create mode 100644 src/main/java/com/nonid/integration/NonCreditsScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuDebugScreens.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuExternalScreens.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuGameplayScreens.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuHudConfigScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuIntegration.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuIntifaceInstallScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuIntifaceScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuIntifaceStrokerOscillatorScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuIntifaceTuningScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuIntifaceVibratorScreen.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuPlayerScreens.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuScreens.java create mode 100644 src/main/java/com/nonid/integration/NonModMenuSystemSettingsScreens.java create mode 100644 src/main/java/com/nonid/integration/NonPlayerProfilesScreen.java create mode 100644 src/main/java/com/nonid/integration/NonWildfireGenderConfigCompat.java create mode 100644 src/main/java/com/nonid/integration/PercentSliderWidget.java create mode 100644 src/main/java/com/nonid/integration/SettingsList.java create mode 100644 src/main/java/com/nonid/integration/jade/NonJadePlugin.java create mode 100644 src/main/java/com/nonid/internal/animation/AnimationEngine.java create mode 100644 src/main/java/com/nonid/internal/animation/AnimationEngineClient.java create mode 100644 src/main/java/com/nonid/internal/animation/api/AnimationLifecycleEvents.java create mode 100644 src/main/java/com/nonid/internal/animation/api/AnimationService.java create mode 100644 src/main/java/com/nonid/internal/animation/api/NonAnimationApiBridge.java create mode 100644 src/main/java/com/nonid/internal/animation/api/NonAnimationViews.java create mode 100644 src/main/java/com/nonid/internal/animation/client/camera/AnimatedCameraPoseTracker.java create mode 100644 src/main/java/com/nonid/internal/animation/client/camera/AnimationCameraZoom.java create mode 100644 src/main/java/com/nonid/internal/animation/client/camera/CameraPositionAccess.java create mode 100644 src/main/java/com/nonid/internal/animation/client/camera/FirstPersonCameraCollision.java create mode 100644 src/main/java/com/nonid/internal/animation/client/camera/FirstPersonLookConstraint.java create mode 100644 src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderCompat.java create mode 100644 src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderState.java create mode 100644 src/main/java/com/nonid/internal/animation/client/diagnostics/AnimationAssetDiagnostics.java create mode 100644 src/main/java/com/nonid/internal/animation/client/diagnostics/GeckoModelCopyDiagnostics.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/AnimationRenderStateAccess.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorAnimatable.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorGeoModel.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/BoneTextureOverrides.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoRenderTickets.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoReplacedRender.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoResourceResolver.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/GenderedModelResolver.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/ModelVariantCandidates.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/ParticleKeyframes.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/ReplacedEntityRenderer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/VanillaTextureResolver.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneItemGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneTextureGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/CameraPoseGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/EmissiveTextureGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/LayerTextureGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/ParticleKeyframeGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/TranslucentBoneGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/render/gecko/layer/WildfireGenderGeoLayer.java create mode 100644 src/main/java/com/nonid/internal/animation/client/runtime/ClientAnimationRuntime.java create mode 100644 src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundAnticipation.java create mode 100644 src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundEffects.java create mode 100644 src/main/java/com/nonid/internal/animation/data/AnimationCueEffects.java create mode 100644 src/main/java/com/nonid/internal/animation/data/AnimationCueIndex.java create mode 100644 src/main/java/com/nonid/internal/animation/data/AnimationDefinitions.java create mode 100644 src/main/java/com/nonid/internal/animation/data/AnimationGameplayMetadata.java create mode 100644 src/main/java/com/nonid/internal/animation/data/AnimationSoundCueParser.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/EntityPushAwayMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/LivingEntityDamageMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/LivingEntityPushableMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/MobEntityIgnoreAttackersMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/ServerPlayNetworkHandlerMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/TargetPredicateMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/CameraAnimationMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/CameraPositionAccessMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerInteractionManagerMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerMovementMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/ClientWorldAnimatedPlayerParticleMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/CuboidGeoBoneHiddenCubeMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityAnimationParticlePositionMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityPushAwayMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerAccessor.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderStateMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererAnimationNameTagMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererInvoker.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/HeldItemRendererMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/InputAccessor.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/KeyboardInputMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityJumpMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityPushableMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererAnimationNameTagMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/MinecraftClientMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/MouseScrollAnimationZoomMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/mixin/client/WorldRendererFirstPersonActorMixin.java create mode 100644 src/main/java/com/nonid/internal/animation/network/AdjustAnimationSpeedC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/AdvanceAnimationStageS2CPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/AnimationSpeedUpdateS2CPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/AnimationStageInfo.java create mode 100644 src/main/java/com/nonid/internal/animation/network/DebugAdvanceStageC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/DebugChatPreferenceC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/DebugJoinAnimationC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/DebugStartAnimationC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/DebugStopAllAnimationsC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/DebugStopAnimationC2SPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/StartAnimationS2CPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/StopAllAnimationsS2CPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/network/StopAnimationS2CPayload.java create mode 100644 src/main/java/com/nonid/internal/animation/server/AnimationSessionData.java create mode 100644 src/main/java/com/nonid/internal/animation/server/AnimationStageTiming.java create mode 100644 src/main/java/com/nonid/internal/animation/server/AnimationStartContext.java create mode 100644 src/main/java/com/nonid/internal/animation/server/ServerAnimationController.java create mode 100644 src/main/java/com/nonid/internal/animation/util/AnimationIds.java create mode 100644 src/main/java/com/nonid/internal/animation/util/EntityVariants.java create mode 100644 src/main/java/com/nonid/internal/animation/util/StageTimeWarp.java create mode 100644 src/main/java/com/nonid/internal/animation/util/WeightedPicker.java create mode 100644 src/main/java/com/nonid/item/EnergyGainAdjustItem.java create mode 100644 src/main/java/com/nonid/item/EnergyStabilizerItem.java create mode 100644 src/main/java/com/nonid/item/HorseLiquidCollectorItem.java create mode 100644 src/main/java/com/nonid/item/NonDataAccessoryItem.java create mode 100644 src/main/java/com/nonid/mixin/AnimalEntityBreedingMixin.java create mode 100644 src/main/java/com/nonid/mixin/AnimalMateGoalAnimationBreedingMixin.java create mode 100644 src/main/java/com/nonid/mixin/BrewingRecipeRegistryMixin.java create mode 100644 src/main/java/com/nonid/mixin/CreeperEntityNoNAnimationMixin.java create mode 100644 src/main/java/com/nonid/mixin/EntityEnergyPersistenceMixin.java create mode 100644 src/main/java/com/nonid/mixin/EntityGenderPersistenceMixin.java create mode 100644 src/main/java/com/nonid/mixin/FileResourcePackProviderAccessor.java create mode 100644 src/main/java/com/nonid/mixin/FleeEntityGoalMixin.java create mode 100644 src/main/java/com/nonid/mixin/LivingEntityLiquidBottleConsumeMixin.java create mode 100644 src/main/java/com/nonid/mixin/LivingEntityLiquidJumpMixin.java create mode 100644 src/main/java/com/nonid/mixin/MobEntityEnergyMixin.java create mode 100644 src/main/java/com/nonid/mixin/MobEntityGenderMixin.java create mode 100644 src/main/java/com/nonid/mixin/MobEntityPregnancyFriendlyTargetMixin.java create mode 100644 src/main/java/com/nonid/mixin/NonMixinPlugin.java create mode 100644 src/main/java/com/nonid/mixin/PlayerEntityDestroyedSkinMixin.java create mode 100644 src/main/java/com/nonid/mixin/PlayerEntityEnergyMixin.java create mode 100644 src/main/java/com/nonid/mixin/PlayerEntityGenderMixin.java create mode 100644 src/main/java/com/nonid/mixin/PlayerLiquidMixin.java create mode 100644 src/main/java/com/nonid/mixin/PlayerLiquidMovementMixin.java create mode 100644 src/main/java/com/nonid/mixin/PlayerPregnancyMixin.java create mode 100644 src/main/java/com/nonid/mixin/ProjectileEntityAnimationRescueMixin.java create mode 100644 src/main/java/com/nonid/mixin/ResourcePackManagerNoNPackMixin.java create mode 100644 src/main/java/com/nonid/mixin/ServerPlayerEntityDamageMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/EntityRendererDebugNameMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/GameRendererReactiveImpactFovMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/HandledScreenAccessor.java create mode 100644 src/main/java/com/nonid/mixin/client/HandledScreenTrinketSlotStateMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/InGameHudMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/LivingEntityRendererDebugLabelMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/LivingEntityRendererPlayerTextureMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/PlayerEntityRendererDestroyedSkinMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/ResourcePackOrganizerHideNoNExternalMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/StatusEffectsDisplayMixin.java create mode 100644 src/main/java/com/nonid/mixin/client/TranslationStorageStatLabelMixin.java create mode 100644 src/main/java/com/nonid/network/AccessoryShedStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/AnimationCatalogChunkS2CPayload.java create mode 100644 src/main/java/com/nonid/network/AnimationCatalogData.java create mode 100644 src/main/java/com/nonid/network/AttackStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/DestroyedSkinMaskSyncS2CPayload.java create mode 100644 src/main/java/com/nonid/network/DestroyedSkinMaskUploadC2SPayload.java create mode 100644 src/main/java/com/nonid/network/DestroyedSkinParticlesS2CPayload.java create mode 100644 src/main/java/com/nonid/network/DestroyedSkinStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/DestroyedSkinSyncS2CPayload.java create mode 100644 src/main/java/com/nonid/network/DestroyedSkinUploadC2SPayload.java create mode 100644 src/main/java/com/nonid/network/GenderSelectionPromptS2CPayload.java create mode 100644 src/main/java/com/nonid/network/HostConfigSyncData.java create mode 100644 src/main/java/com/nonid/network/HostConfigSyncS2CPayload.java create mode 100644 src/main/java/com/nonid/network/LiquidStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/ManualDamageDestroyedSkinC2SPayload.java create mode 100644 src/main/java/com/nonid/network/MessStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAnimationCancelC2SPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAnimationChoiceC2SPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAnimationConsentC2SPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAnimationConsentS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAnimationRequestStatusS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAnimationSelectionS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerAttackInputC2SPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerProfileActionC2SPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerProfileSnapshotS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PlayerRuntimeSettingsS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PregnancyCycleStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/PregnancyStateS2CPayload.java create mode 100644 src/main/java/com/nonid/network/RepairDestroyedSkinC2SPayload.java create mode 100644 src/main/java/com/nonid/network/ServerConfigRequestC2SPayload.java create mode 100644 src/main/java/com/nonid/network/ServerConfigResultS2CPayload.java create mode 100644 src/main/java/com/nonid/network/ServerConfigUpdateC2SPayload.java create mode 100644 src/main/java/com/nonid/network/SetPlayerGenderC2SPayload.java create mode 100644 src/main/java/com/nonid/network/SkinRippedInfoRequestS2CPayload.java create mode 100644 src/main/java/com/nonid/network/StartManualPeakC2SPayload.java create mode 100644 src/main/java/com/nonid/network/StopAttackAnimationC2SPayload.java create mode 100644 src/main/java/com/nonid/network/StopVoluntaryAnimationC2SPayload.java create mode 100644 src/main/java/com/nonid/pack/NonExternalPackCatalog.java create mode 100644 src/main/java/com/nonid/pack/NonExternalPackProvider.java create mode 100644 src/main/java/com/nonid/pack/NonGeneratedAccessoryModelPackProvider.java create mode 100644 src/main/java/com/nonid/pack/NonGeneratedAccessoryTagPackProvider.java create mode 100644 src/main/java/com/nonid/pack/NonGeneratedCemPackProvider.java create mode 100644 src/main/java/com/nonid/pack/NonGeneratedInMemoryPack.java create mode 100644 src/main/java/com/nonid/pack/NonPackRootResolver.java create mode 100644 src/main/java/com/nonid/pack/NonPrefixedZipResourcePack.java create mode 100644 src/main/java/com/nonid/particle/NonParticles.java create mode 100644 src/main/java/com/nonid/potion/NonBrewingRecipes.java create mode 100644 src/main/java/com/nonid/potion/NonPotions.java create mode 100644 src/main/java/com/nonid/recipe/AbstractEnergyAdjustRecipe.java create mode 100644 src/main/java/com/nonid/recipe/EnergyAugmenterRecipe.java create mode 100644 src/main/java/com/nonid/recipe/EnergyDiminisherRecipe.java create mode 100644 src/main/java/com/nonid/recipe/EnergyStabilizerRecipe.java create mode 100644 src/main/java/com/nonid/recipe/FertileNectarRecipe.java create mode 100644 src/main/java/com/nonid/recipe/FillAdvancedStraponRecipe.java create mode 100644 src/main/java/com/nonid/recipe/NonRecipeIngredients.java create mode 100644 src/main/java/com/nonid/recipe/NonRecipeSerializers.java create mode 100644 src/main/java/com/nonid/sound/NonSoundEvents.java create mode 100644 src/main/resources/assets/animationframework/geckolib/models/entity/missingmodel.geo.json create mode 100644 src/main/resources/assets/animationframework/textures/missingmodel.png create mode 100644 src/main/resources/assets/minecraft/items/lingering_potion.json create mode 100644 src/main/resources/assets/minecraft/items/potion.json create mode 100644 src/main/resources/assets/minecraft/items/splash_potion.json create mode 100644 src/main/resources/assets/minecraft/items/tipped_arrow.json create mode 100644 src/main/resources/assets/needsofnature/credits.json create mode 100644 src/main/resources/assets/needsofnature/geckolib/animations/afw/debug_spin/debug_spin.animation.json create mode 100644 src/main/resources/assets/needsofnature/geckolib/models/entity/horse_liquid_collector.geo.json create mode 100644 src/main/resources/assets/needsofnature/geckolib/models/fish.geo.json create mode 100644 src/main/resources/assets/needsofnature/icon.png create mode 100644 src/main/resources/assets/needsofnature/items/debug_staff.json create mode 100644 src/main/resources/assets/needsofnature/items/energy_augmenter.json create mode 100644 src/main/resources/assets/needsofnature/items/energy_diminisher.json create mode 100644 src/main/resources/assets/needsofnature/items/energy_stabilizer.json create mode 100644 src/main/resources/assets/needsofnature/items/flower_mix.json create mode 100644 src/main/resources/assets/needsofnature/items/horse_liquid_collector.json create mode 100644 src/main/resources/assets/needsofnature/lang/en_us.json create mode 100644 src/main/resources/assets/needsofnature/lang/zh_cn.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_augmenter.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_diminisher.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_lingering_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_relief_lingering_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_relief_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_relief_splash_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_relief_tipped_arrow.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_splash_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_stabilizer.json create mode 100644 src/main/resources/assets/needsofnature/models/item/energy_tipped_arrow.json create mode 100644 src/main/resources/assets/needsofnature/models/item/fertile_nectar_lingering_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/fertile_nectar_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/fertile_nectar_splash_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/fertility_lingering_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/fertility_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/fertility_splash_potion.json create mode 100644 src/main/resources/assets/needsofnature/models/item/flower_mix.json create mode 100644 src/main/resources/assets/needsofnature/models/item/horse_liquid_collector.json create mode 100644 src/main/resources/assets/needsofnature/particles/liquid_particle.json create mode 100644 src/main/resources/assets/needsofnature/particles/liquid_particle_falling.json create mode 100644 src/main/resources/assets/needsofnature/particles/liquid_particle_puddle.json create mode 100644 src/main/resources/assets/needsofnature/particles/liquid_particle_water.json create mode 100644 src/main/resources/assets/needsofnature/particles/ripped_fabric.json create mode 100644 src/main/resources/assets/needsofnature/particles/smallheart.json create mode 100644 src/main/resources/assets/needsofnature/sounds.json create mode 100644 src/main/resources/assets/needsofnature/sounds/a.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry04.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry05.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry06.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry07.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry08.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry09.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry10.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry11.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion04.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion05.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion06.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion07.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion08.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion09.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion10.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out04.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out05.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out06.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet04.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet05.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet06.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet07.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet08.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet09.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet10.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet11.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet12.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet03.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet04.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet05.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet06.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet07.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet08.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet09.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet10.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet11.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet12.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet13.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet14.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet15.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet16.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/funkytown.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/pop.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/rip01.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/rip02.ogg create mode 100644 src/main/resources/assets/needsofnature/sounds/rip03.ogg create mode 100644 src/main/resources/assets/needsofnature/textures/entity/horse_liquid_collector/horse_liquid_collector_empty.png create mode 100644 src/main/resources/assets/needsofnature/textures/entity/horse_liquid_collector/horse_liquid_collector_full.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/overlays/energy_outline.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/pregnancy_cycle_normal.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/pregnancy_cycle_ovulation.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/pregnancy_cycle_risky.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/pregnancy_cycle_safe.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/skin_repair.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/slots/a.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/slots/d.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/slots/v.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/ad_mash.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/attack_background.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/attack_full.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/liquid_background.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/liquid_full.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/progress_background.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/hud/progress_full.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/mob_effect/energy.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/mob_effect/energy_relief.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/mob_effect/fertility_increaser.png create mode 100644 src/main/resources/assets/needsofnature/textures/gui/sprites/mob_effect/pregnant.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_augmenter.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_diminisher.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_potion_overlay.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_relief_potion_overlay.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_relief_tipped_arrow_head.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_stabilizer.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/energy_tipped_arrow_head.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/fertile_nectar_overlay.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/fertility_potion_overlay.png create mode 100644 src/main/resources/assets/needsofnature/textures/item/flower_mix.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/energized1.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/energized2.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/energized3.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/filled1.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/filled2.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/filled3.png create mode 100644 src/main/resources/assets/needsofnature/textures/mob_effects/pregnant.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/liquid_drip_fall.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/liquid_drip_hang.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/liquid_drip_land.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/liquid_drip_water.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/liquid_puddle.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/ripped_fabric.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/ripped_fabric_color_mask.png create mode 100644 src/main/resources/assets/needsofnature/textures/particle/smallheart.png create mode 100644 src/main/resources/data/animationframework/tags/block/wall_surfaces.json create mode 100644 src/main/resources/data/needsofnature/advancement/acquire_hardware.json create mode 100644 src/main/resources/data/needsofnature/advancement/bad_idea_beacon.json create mode 100644 src/main/resources/data/needsofnature/advancement/collectors_habit.json create mode 100644 src/main/resources/data/needsofnature/advancement/emergency_snack.json create mode 100644 src/main/resources/data/needsofnature/advancement/favouritism.json create mode 100644 src/main/resources/data/needsofnature/advancement/field_research.json create mode 100644 src/main/resources/data/needsofnature/advancement/filled_to_the_brim.json create mode 100644 src/main/resources/data/needsofnature/advancement/first_pregnancy.json create mode 100644 src/main/resources/data/needsofnature/advancement/group_activity.json create mode 100644 src/main/resources/data/needsofnature/advancement/messed_up.json create mode 100644 src/main/resources/data/needsofnature/advancement/mother_nature.json create mode 100644 src/main/resources/data/needsofnature/advancement/natural_curiosity.json create mode 100644 src/main/resources/data/needsofnature/advancement/productive_pregnancy.json create mode 100644 src/main/resources/data/needsofnature/advancement/recipes/misc/energy_augmenter.json create mode 100644 src/main/resources/data/needsofnature/advancement/recipes/misc/energy_diminisher.json create mode 100644 src/main/resources/data/needsofnature/advancement/recipes/misc/energy_stabilizer.json create mode 100644 src/main/resources/data/needsofnature/advancement/recipes/misc/fertile_nectar.json create mode 100644 src/main/resources/data/needsofnature/advancement/recipes/misc/flower_mix.json create mode 100644 src/main/resources/data/needsofnature/advancement/recipes/misc/horse_liquid_collector.json create mode 100644 src/main/resources/data/needsofnature/advancement/root.json create mode 100644 src/main/resources/data/needsofnature/advancement/stable_diet.json create mode 100644 src/main/resources/data/needsofnature/advancement/tossed_around.json create mode 100644 src/main/resources/data/needsofnature/advancement/udderly_unfortunate.json create mode 100644 src/main/resources/data/needsofnature/recipe/energy_augmenter.json create mode 100644 src/main/resources/data/needsofnature/recipe/energy_diminisher.json create mode 100644 src/main/resources/data/needsofnature/recipe/energy_stabilizer.json create mode 100644 src/main/resources/data/needsofnature/recipe/fertile_nectar.json create mode 100644 src/main/resources/data/needsofnature/recipe/flower_mix.json create mode 100644 src/main/resources/data/needsofnature/recipe/horse_liquid_collector.json create mode 100644 src/main/resources/data/trinkets/entities/player.json create mode 100644 src/main/resources/data/trinkets/slots/legs/a.json create mode 100644 src/main/resources/data/trinkets/slots/legs/d.json create mode 100644 src/main/resources/data/trinkets/slots/legs/v.json create mode 100644 src/main/resources/fabric.mod.json create mode 100644 src/main/resources/needsofnature.animation.client.mixins.json create mode 100644 src/main/resources/needsofnature.animation.mixins.json create mode 100644 src/main/resources/needsofnature.mixins.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..524fb1b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [pull_request, push] + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: checkout repository + uses: actions/checkout@v6 + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: setup jdk + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'microsoft' + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + uses: actions/upload-artifact@v7 + with: + name: Artifacts + path: build/libs/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c476faf --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# macos + +*.DS_Store + +# fabric + +run/ + +# java + +hs_err_*.log +replay_*.log +*.hprof +*.jfr diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..674c7c8 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "blockbench": { + "command": "node", + "args": ["D:\\tools\\blockbench-mcp\\dist\\index.js"], + "env": { + "BLOCKBENCH_MCP_PORT": "8787" + } + } + } +} diff --git a/DeployPack.ps1 b/DeployPack.ps1 new file mode 100644 index 0000000..3a786e9 --- /dev/null +++ b/DeployPack.ps1 @@ -0,0 +1,46 @@ +# DeployPack.ps1 - Deploy built mod jar to client & server mods, plus the v1.4.1 resource pack to server +$ErrorActionPreference = 'Stop' + +$Jar = 'D:\Users\TermiNexus\Desktop\NeedsOfNature-1.20.1-port-main\build\libs\needsofnature-1.4.4.jar' +$ClientMods = 'D:\Users\TermiNexus\Desktop\HMCL\OLD\.minecraft\versions\1.21.11-Fabric-Fuck\mods' +$ServerMods = 'D:\MCServer\MCN\mods' + +# --- Resource pack (original feature) --- +$PackSrc = 'D:\Users\TermiNexus\Desktop\HMCL\OLD\.minecraft\versions\1.21.11-Fabric-Fuck\resourcepacks\needs_of_nature_default_packv1.4.1' +$PackDst = 'D:\MCServer\MCN\world\datapacks\needs_of_nature_default_packv1.4.1' + +if (-not (Test-Path -LiteralPath $Jar)) { throw "Built jar not found: $Jar" } + +# 1. Client mods: remove previous deploy, back up original, copy new jar +$Old = "$ClientMods\needsofnature-1.0.0.jar" +if (Test-Path -LiteralPath $Old) { + Remove-Item -LiteralPath $Old -Force + Write-Output "Removed old client deploy: $Old" +} +$OrigClient = "$ClientMods\needsofnature-1.4.4..jar" +if (Test-Path -LiteralPath $OrigClient) { + Move-Item -LiteralPath $OrigClient -Destination "$OrigClient.bak" -Force + Write-Output "Backed up client mod: $OrigClient -> .bak" +} +Copy-Item -LiteralPath $Jar -Destination "$ClientMods\needsofnature-1.4.4.jar" -Force +Write-Output "Client mod deployed: $ClientMods\needsofnature-1.4.4.jar" + +# 2. Server mods: back up original, copy new jar (server may be running -> file locked) +$OrigServer = "$ServerMods\needsofnature-1.4.4..jar" +try { + if (Test-Path -LiteralPath $OrigServer) { + Move-Item -LiteralPath $OrigServer -Destination "$OrigServer.bak" -Force + Write-Output "Backed up server mod: $OrigServer -> .bak" + } + Copy-Item -LiteralPath $Jar -Destination "$ServerMods\needsofnature-1.4.4.jar" -Force + Write-Output "Server mod deployed: $ServerMods\needsofnature-1.4.4.jar" +} catch { + Write-Output "WARNING: Server mod deploy failed - file is locked. Stop the server first, then re-run this script." + throw $_ +} + +# 3. Resource pack to server datapacks (original feature) +if (-not (Test-Path -LiteralPath $PackSrc)) { throw "Resource pack not found: $PackSrc" } +if (Test-Path -LiteralPath $PackDst) { Remove-Item -LiteralPath $PackDst -Recurse -Force } +Copy-Item -LiteralPath $PackSrc -Destination $PackDst -Recurse -Force +Write-Output "Resource pack deployed: $PackSrc -> $PackDst" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1625c17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..edfaa05 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# NeedsOfNature + +## Setup + +For setup instructions, please see the [Fabric Documentation page](https://docs.fabricmc.net/develop/getting-started/creating-a-project#setting-up) related to the IDE that you are using. + +## License + +This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..35d1cd4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,87 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" + id 'maven-publish' +} + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + // 原版 mod 源码已在本工程内(src),这里只挂外部依赖(本地 mods 文件夹) + def modsDir = "D:/Users/TermiNexus/Desktop/HMCL/OLD/.minecraft/versions/1.21.11-Fabric-Fuck/mods" + modImplementation files("${modsDir}/geckolib-fabric-1.21.11-5.4.5.jar") + modImplementation files("${modsDir}/trinkets-3.11.0-beta.2+1.21.11.jar") + modImplementation files("${modsDir}/cardinal-components-api-7.3.2.jar") + modImplementation files("${modsDir}/cardinal-components-base-7.3.2.jar") + modImplementation files("${modsDir}/placeholder-api-2.8.2+1.21.10.jar") + modImplementation files("${modsDir}/modmenu-17.0.1-beta.1.jar") + modImplementation files("${modsDir}/needsofnature-intiface-libs-generated.jar") + // Jade(snownee.jade.api)—— 编译需要 + def jadeJar = "${modsDir}/Jade-1.21.11.jar" + if (!new File(jadeJar).exists()) { + throw new GradleException("缺少 Jade mod jar,请放到: ${jadeJar}") + } + modImplementation files(jadeJar) +} + +processResources { + def version = project.version + inputs.property "version", version + + filesMatching("fabric.mod.json") { + expand "version": version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + def projectName = project.name + inputs.property "projectName", projectName + + from("LICENSE") { + rename { "${it}_$projectName"} + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/find.py b/find.py new file mode 100644 index 0000000..1af572b --- /dev/null +++ b/find.py @@ -0,0 +1,163 @@ +""" +REPL 模式文件内容搜索工具 +支持正则或纯文本搜索,默认搜索当前目录 +""" +import os +import re +import sys + +# 硬编码排除目录 +EXCLUDE_DIRS = { + '.git', '.vs', 'node_modules', '__pycache__', '.idea', + 'miniprogram_npm', '_backup_pre_redesign', +} + +# 排除的文件扩展名 +EXCLUDE_EXTS = { + '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.bmp', + '.mp3', '.mp4', '.wav', '.avi', + '.zip', '.rar', '.7z', '.tar', '.gz', + '.exe', '.dll', '.so', '.dylib', + '.pyc', '.pyo', '.class', '.o', '.obj', + '.db', '.sqlite', '.vsidx', +} + +# 默认搜索根目录 +DEFAULT_ROOT = os.path.dirname(os.path.abspath(__file__)) + + +def should_skip(dirpath): + """判断目录是否应跳过""" + parts = dirpath.replace('\\', '/').split('/') + return any(p in EXCLUDE_DIRS for p in parts) + + +def search(pattern, root=DEFAULT_ROOT, use_regex=True, max_results=50): + """搜索文件内容""" + if use_regex: + try: + compiled = re.compile(pattern, re.IGNORECASE) + except re.error as e: + print(f' 正则语法错误: {e}') + return + + count = 0 + for dirpath, dirnames, filenames in os.walk(root): + # 就地修改 dirnames 以跳过排除目录 + dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] + + for fname in filenames: + ext = os.path.splitext(fname)[1].lower() + if ext in EXCLUDE_EXTS: + continue + + fpath = os.path.join(dirpath, fname) + relpath = os.path.relpath(fpath, root) + try: + with open(fpath, 'r', encoding='utf-8', errors='ignore') as f: + for lineno, line in enumerate(f, 1): + line_stripped = line.rstrip('\n\r') + if use_regex: + match = compiled.search(line_stripped) + if match: + highlight = line_stripped[:match.start()] + \ + f'>>> {match.group()} <<<' + \ + line_stripped[match.end():] + print(f' {relpath}:{lineno} {highlight}') + count += 1 + else: + if pattern.lower() in line_stripped.lower(): + print(f' {relpath}:{lineno} {line_stripped}') + count += 1 + + if count >= max_results: + print(f'\n ... 已达上限 {max_results} 条,停止') + return + except (PermissionError, OSError): + continue + + if count == 0: + print(' 未找到匹配内容') + else: + print(f'\n 共 {count} 条匹配') + + +def main(): + print('=== 文件内容搜索工具 ===') + print(f'搜索根目录: {DEFAULT_ROOT}') + print(f'排除目录: {EXCLUDE_DIRS}') + print() + print('命令:') + print(' /regex 正则搜索') + print(' /text 纯文本搜索') + print(' /max 设置结果上限(默认50)') + print(' /dir 切换搜索目录') + print(' /help 显示帮助') + print(' /quit 退出') + print() + print('直接输入内容默认使用【纯文本】搜索,需要正则请加 /regex 前缀') + print() + + root = DEFAULT_ROOT + use_regex = False # 修改1:默认纯文本 + max_results = 50 + + while True: + try: + raw = input('find> ').strip() + except (EOFError, KeyboardInterrupt): + print('\n再见') + break + + if not raw: + continue + + if raw == '/quit': + print('再见') + break + + if raw == '/help': + print(' /regex 正则搜索') + print(' /text 纯文本搜索') + print(' /max 设置结果上限') + print(' /dir 切换搜索目录') + print(' /help 显示帮助') + print(' /quit 退出') + print(' 直接输入内容默认纯文本搜索') + continue + + if raw.startswith('/regex '): + pattern = raw[7:].strip() + if pattern: + search(pattern, root, use_regex=True, max_results=max_results) + continue + + if raw.startswith('/text '): + pattern = raw[6:].strip() + if pattern: + search(pattern, root, use_regex=False, max_results=max_results) + continue + + if raw.startswith('/max '): + try: + max_results = int(raw[5:].strip()) + print(f' 结果上限设为 {max_results}') + except ValueError: + print(' 请输入数字') + continue + + if raw.startswith('/dir '): + new_dir = raw[5:].strip() + if os.path.isdir(new_dir): + root = os.path.abspath(new_dir) + print(f' 搜索目录切换为: {root}') + else: + print(f' 目录不存在: {new_dir}') + continue + + # 修改2:直接输入走纯文本搜索 + search(raw, root, use_regex=False, max_results=max_results) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..625eb84 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,20 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 +org.gradle.configuration-cache=false + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=1.21.11 +yarn_mappings=1.21.11+build.6 +loader_version=0.19.3 +loom_version=1.17-SNAPSHOT + +# Mod Properties +version=1.4.4 +group=com.nonid + +# Dependencies +fabric_api_version=0.141.6+1.21.11 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..084d8bb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=file:///D:/%E8%BF%85%E9%9B%B7%E4%B8%8B%E8%BD%BD/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..b9bb139 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..24c62d5 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b749c75 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +// Should match your modid +rootProject.name = 'needsofnature' diff --git a/src/main/java/com/nonid/DebugStaffItem.java b/src/main/java/com/nonid/DebugStaffItem.java new file mode 100644 index 0000000..f83d152 --- /dev/null +++ b/src/main/java/com/nonid/DebugStaffItem.java @@ -0,0 +1,192 @@ +package com.nonid; + +import com.nonid.data.NonEntityProfiles; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Item.Settings; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; + +public class DebugStaffItem extends Item { + public DebugStaffItem(Settings settings) { + super(settings); + } + + public ActionResult useOnEntity(ItemStack stack, PlayerEntity user, LivingEntity entity, Hand hand) { + if (entity instanceof EnergyHolder holder) { + if (user.getEntityWorld().isClient()) { + return ActionResult.SUCCESS; + } else { + ServerPlayerEntity serverPlayer = (ServerPlayerEntity)user; + if (user.isSneaking()) { + int current = holder.getEnergy(); + int max = holder.getMaxEnergy(); + int next = Math.min(max, current + 25); + holder.setEnergy(next); + return ActionResult.SUCCESS; + } else { + this.sendDebugReport(serverPlayer, entity, holder); + return ActionResult.SUCCESS; + } + } + } else { + return ActionResult.PASS; + } + } + + private void sendDebugReport(ServerPlayerEntity player, LivingEntity entity, EnergyHolder holder) { + Identifier entityId = Registries.ENTITY_TYPE.getId(entity.getType()); + player.sendMessage(Text.literal("[NoN 调试] " + entity.getDisplayName().getString() + " (" + entityId + ")").formatted(Formatting.GREEN), false); + player.sendMessage(Text.literal("UUID: " + entity.getUuid()).formatted(Formatting.GRAY), false); + player.sendMessage(Text.literal("性别: " + this.resolveGender(entity)).formatted(Formatting.GRAY), false); + player.sendMessage( + Text.literal("能量: " + holder.getEnergy() + "/" + holder.getMaxEnergy() + " | 已初始化: " + holder.isEnergyInitialized()) + .formatted(Formatting.GRAY), + false + ); + player.sendMessage( + Text.literal( + "能量增益: 倍率 " + + this.format(holder.getEnergyGainMultiplier()) + + " | 漂移 " + + this.format(holder.getEnergyGainDrift()) + + " | 实体 " + + this.format(NonEntityProfiles.resolveEnergyGainMultiplier(entity)) + ) + .formatted(Formatting.GRAY), + false + ); + if (entity.getEntityWorld() instanceof ServerWorld world) { + boolean activeOrPending = NeedsOfNature.isActorPendingOrActive(world, entity.getUuid()); + player.sendMessage(Text.literal("动画: 活跃/待定 " + activeOrPending).formatted(Formatting.GRAY), false); + this.sendAuraReport(player, world, entity, holder); + this.sendCooldownReport(player, world, entity, holder); + } + + if (entity instanceof LiquidHolder liquidHolder) { + Identifier liquidType = liquidHolder.getLiquidEntityTypeId(); + String liquidInfo = liquidHolder.getLiquidStored() + + "/" + + liquidHolder.getLiquidCapacity() + + "ml | composition " + + liquidHolder.getLiquidComposition() + + (liquidType == null ? "" : " | 实体 " + liquidType); + if (entity instanceof ServerPlayerEntity) { + liquidInfo = liquidInfo + " | 槽类型 " + NonGenderSystem.getLiquidTankType(entity); + } + + player.sendMessage(Text.literal("液体: " + liquidInfo).formatted(Formatting.GRAY), false); + } + + if (entity instanceof PregnancyHolder pregnancyHolder) { + Identifier pregnancyType = pregnancyHolder.getPregnantEntityTypeId(); + String pregnancyInfo = pregnancyType == null + ? "none" + : pregnancyType + " | 结束tick " + pregnancyHolder.getPregnancyEndTick() + " | 待孵化 " + pregnancyHolder.isPregnancyPendingHatch(); + player.sendMessage(Text.literal("孕期: " + pregnancyInfo).formatted(Formatting.GRAY), false); + } + } + + private void sendAuraReport(ServerPlayerEntity player, ServerWorld world, LivingEntity entity, EnergyHolder holder) { + if (entity instanceof MobEntity mob) { + NeedsOfNature.PlayerEnergyAuraDebug aura = NeedsOfNature.getPlayerEnergyAuraDebug(world, mob, holder); + if (aura == null) { + player.sendMessage(Text.literal("光环: 无 | 携带 " + this.format(holder.getEnergyAuraCarry())).formatted(Formatting.GRAY), false); + } else { + String sourceName = aura.player().getName().getString(); + player.sendMessage( + Text.literal( + "光环: " + + this.format(aura.totalMultiplier()) + + "x 来自 " + + sourceName + + " | 能量 " + + aura.playerEnergy() + + " | 破坏 " + + aura.destroyedSkinStage() + + " | 污渍 " + + aura.messTotal() + + " | 半径 " + + this.format(aura.radius()) + + " | 携带 " + + this.format(holder.getEnergyAuraCarry()) + ) + .formatted(Formatting.GRAY), + false + ); + } + } else { + player.sendMessage(Text.literal("光环: 无").formatted(Formatting.GRAY), false); + } + } + + private void sendCooldownReport(ServerPlayerEntity player, ServerWorld world, LivingEntity entity, EnergyHolder holder) { + List cooldowns = new ArrayList<>(); + String attackFailsafe = NeedsOfNature.getMobAttackFailsafeDebug(world, entity.getUuid()); + if (attackFailsafe != null && !attackFailsafe.isBlank()) { + cooldowns.add(attackFailsafe); + } + + cooldowns.addAll(holder.getActiveCooldownDebugLines(world)); + if (cooldowns.isEmpty()) { + player.sendMessage(Text.literal("冷却: 无").formatted(Formatting.GRAY), false); + } else { + player.sendMessage(Text.literal("冷却:").formatted(Formatting.GRAY), false); + + for (String line : cooldowns) { + player.sendMessage(Text.literal("- " + line).formatted(Formatting.DARK_GRAY), false); + } + } + } + + private String resolveGender(LivingEntity entity) { + if (entity instanceof GenderHolder genderHolder) { + return this.formatGenderMask(genderHolder.getGenderMask()); + } else { + boolean hasMale = entity.getCommandTags().contains("gender.male"); + boolean hasFemale = entity.getCommandTags().contains("gender.female"); + int mask = 0; + if (hasMale) { + mask |= 1; + } + + if (hasFemale) { + mask |= 2; + } + + return this.formatGenderMask(mask); + } + } + + private String formatGenderMask(int mask) { + boolean male = (mask & 1) != 0; + boolean female = (mask & 2) != 0; + if (male && female) { + return "male & female"; + } else if (male) { + return "male"; + } else { + return female ? "female" : "none"; + } + } + + private String format(float value) { + return String.format(Locale.ROOT, "%.2f", value); + } + + private String format(double value) { + return String.format(Locale.ROOT, "%.2f", value); + } +} diff --git a/src/main/java/com/nonid/DestroyedSkinHolder.java b/src/main/java/com/nonid/DestroyedSkinHolder.java new file mode 100644 index 0000000..bd86440 --- /dev/null +++ b/src/main/java/com/nonid/DestroyedSkinHolder.java @@ -0,0 +1,11 @@ +package com.nonid; + +public interface DestroyedSkinHolder { + int getDestroyedSkinStage(); + + void setDestroyedSkinStage(int var1); + + int getDestroyedSkinDamage(); + + void setDestroyedSkinDamage(int var1); +} diff --git a/src/main/java/com/nonid/EnergyHolder.java b/src/main/java/com/nonid/EnergyHolder.java new file mode 100644 index 0000000..8271d28 --- /dev/null +++ b/src/main/java/com/nonid/EnergyHolder.java @@ -0,0 +1,62 @@ +package com.nonid; + +import java.util.List; +import java.util.UUID; +import net.minecraft.entity.LivingEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; + +public interface EnergyHolder { + int getEnergy(); + + int getMaxEnergy(); + + void setEnergy(int var1); + + default float getEnergyGainMultiplier() { + return 1.0F; + } + + default void setEnergyGainMultiplier(float value) { + } + + default float getEnergyGainDrift() { + return 1.0F; + } + + default void setEnergyGainDrift(float value) { + } + + default boolean isEnergyInitialized() { + return false; + } + + default void markEnergyInitialized() { + } + + default void ensureEnergyInitialized(ServerWorld world) { + } + + default void onNonAnimationStarted() { + } + + default void onNonAttackEscaped(LivingEntity player, int retryDelayTicks) { + } + + default void requestAttackOnPlayer(ServerWorld world, ServerPlayerEntity player) { + } + + default void applyPartnerCooldown(UUID partnerId, long nowTick, int cooldownTicks) { + } + + default List getActiveCooldownDebugLines(ServerWorld world) { + return List.of(); + } + + default double getEnergyAuraCarry() { + return 0.0; + } + + default void setEnergyAuraCarry(double value) { + } +} diff --git a/src/main/java/com/nonid/GenderHolder.java b/src/main/java/com/nonid/GenderHolder.java new file mode 100644 index 0000000..bb392f1 --- /dev/null +++ b/src/main/java/com/nonid/GenderHolder.java @@ -0,0 +1,16 @@ +package com.nonid; + +public interface GenderHolder { + int GENDER_NONE = 0; + int GENDER_MALE = 1; + int GENDER_FEMALE = 2; + int GENDER_BOTH = 3; + + int getGenderMask(); + + void setGenderMask(int var1); + + default boolean hasGender(int mask) { + return (this.getGenderMask() & mask) != 0; + } +} diff --git a/src/main/java/com/nonid/LiquidDonorHistory.java b/src/main/java/com/nonid/LiquidDonorHistory.java new file mode 100644 index 0000000..831e20f --- /dev/null +++ b/src/main/java/com/nonid/LiquidDonorHistory.java @@ -0,0 +1,110 @@ +package com.nonid; + +import com.google.gson.Gson; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class LiquidDonorHistory { + public static final int MAX_ENTRIES = 3; + private static final Gson GSON = new Gson(); + + private LiquidDonorHistory() { + } + + public static List append( + Collection current, Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + ArrayList entries = new ArrayList<>(normalize(current)); + entries.add(new LiquidDonorHistory.Entry(entityTypeId, variantData)); + return keepNewest(entries); + } + + public static List normalize(@Nullable Collection entries) { + if (entries != null && !entries.isEmpty()) { + ArrayList normalized = new ArrayList<>(); + + for (LiquidDonorHistory.Entry entry : entries) { + if (entry != null && entry.entityTypeId() != null) { + normalized.add(new LiquidDonorHistory.Entry(entry.entityTypeId(), entry.variantData())); + } + } + + return keepNewest(normalized); + } else { + return List.of(); + } + } + + @Nullable + public static String encode(Collection entries) { + List normalized = normalize(entries); + if (normalized.isEmpty()) { + return null; + } + + LiquidDonorHistory.StoredEntry[] stored = normalized.stream() + .map(entry -> new LiquidDonorHistory.StoredEntry(entry.entityTypeId().toString(), entry.variantData())) + .toArray(LiquidDonorHistory.StoredEntry[]::new); + return GSON.toJson(stored); + } + + public static List decode(@Nullable String raw) { + if (raw != null && !raw.isBlank()) { + try { + LiquidDonorHistory.StoredEntry[] stored = (LiquidDonorHistory.StoredEntry[])GSON.fromJson(raw, LiquidDonorHistory.StoredEntry[].class); + if (stored != null && stored.length != 0) { + ArrayList decoded = new ArrayList<>(); + + for (LiquidDonorHistory.StoredEntry entry : stored) { + if (entry != null && entry.entityTypeId() != null) { + Identifier entityTypeId = Identifier.tryParse(entry.entityTypeId()); + if (entityTypeId != null) { + decoded.add(new LiquidDonorHistory.Entry(entityTypeId, entry.variantData())); + } + } + } + + return keepNewest(decoded); + } else { + return List.of(); + } + } catch (RuntimeException ignored) { + return List.of(); + } + } else { + return List.of(); + } + } + + private static List keepNewest(List entries) { + if (entries != null && !entries.isEmpty()) { + int fromIndex = Math.max(0, entries.size() - 3); + return List.copyOf(entries.subList(fromIndex, entries.size())); + } else { + return List.of(); + } + } + + @Nullable + private static NeedsOfNature.PregnancyVariantData normalizeVariantData(@Nullable NeedsOfNature.PregnancyVariantData variantData) { + return variantData != null && !variantData.isEmpty() + ? new NeedsOfNature.PregnancyVariantData(variantData.components(), variantData.horseMarkingIndex()) + : null; + } + + public record Entry(Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) { + public Entry { + if (entityTypeId == null) { + throw new IllegalArgumentException("entityTypeId cannot be null"); + } + + variantData = LiquidDonorHistory.normalizeVariantData(variantData); + } + } + + private record StoredEntry(String entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) { + } +} diff --git a/src/main/java/com/nonid/LiquidHolder.java b/src/main/java/com/nonid/LiquidHolder.java new file mode 100644 index 0000000..b0941a5 --- /dev/null +++ b/src/main/java/com/nonid/LiquidHolder.java @@ -0,0 +1,55 @@ +package com.nonid; + +import java.util.List; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public interface LiquidHolder { + int getLiquidStored(); + + int getLiquidCapacity(); + + void setLiquidStored(int var1); + + LiquidHolder.LiquidComposition getLiquidComposition(); + + @Nullable + Identifier getLiquidEntityTypeId(); + + @Nullable + NeedsOfNature.PregnancyVariantData getLiquidDonorVariantData(); + + void setLiquidCompositionMixed(); + + void setLiquidCompositionEntity(Identifier var1); + + void setLiquidDonorVariantData(@Nullable NeedsOfNature.PregnancyVariantData var1); + + List getRecentLiquidDonors(); + + void setRecentLiquidDonors(List var1); + + default void recordRecentLiquidDonor(Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) { + if (entityTypeId != null) { + this.setRecentLiquidDonors(LiquidDonorHistory.append(this.getRecentLiquidDonors(), entityTypeId, variantData)); + } + } + + default void clearRecentLiquidDonors() { + this.setRecentLiquidDonors(List.of()); + } + + void clearLiquidComposition(); + + default void addLiquidStored(int amount) { + if (amount != 0) { + this.setLiquidStored(this.getLiquidStored() + amount); + } + } + + enum LiquidComposition { + EMPTY, + MIXED, + ENTITY; + } +} diff --git a/src/main/java/com/nonid/MessHolder.java b/src/main/java/com/nonid/MessHolder.java new file mode 100644 index 0000000..73db3e3 --- /dev/null +++ b/src/main/java/com/nonid/MessHolder.java @@ -0,0 +1,15 @@ +package com.nonid; + +public interface MessHolder { + int getVMess(); + + void setVMess(int var1); + + int getAMess(); + + void setAMess(int var1); + + int getMMess(); + + void setMMess(int var1); +} diff --git a/src/main/java/com/nonid/NeedsOfNature.java b/src/main/java/com/nonid/NeedsOfNature.java new file mode 100644 index 0000000..2d8eb97 --- /dev/null +++ b/src/main/java/com/nonid/NeedsOfNature.java @@ -0,0 +1,6388 @@ +package com.nonid; + +import com.google.gson.Gson; +import com.nonid.api.NonEvents; +import com.nonid.api.animation.NonActorEvents; +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.client.NonLiquidColors; +import com.nonid.data.NonAccessoryDefinitions; +import com.nonid.data.NonAccessoryItemRegistry; +import com.nonid.data.NonEntityProfiles; +import com.nonid.data.NonLiquidGainOverrides; +import com.nonid.data.NonLiquidRoles; +import com.nonid.data.NonManualPeakDefinitions; +import com.nonid.data.NonPeakStages; +import com.nonid.effect.NonStatusEffects; +import com.nonid.entity.HorseLiquidCollectorEntity; +import com.nonid.entity.PregnancyEggEntity; +import com.nonid.integration.NonWildfireGenderConfigCompat; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.api.AnimationLifecycleEvents; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationCueIndex; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.server.AnimationStartContext; +import com.nonid.internal.animation.server.ServerAnimationController; +import com.nonid.internal.animation.util.AnimationIds; +import com.nonid.internal.animation.util.StageTimeWarp; +import com.nonid.item.EnergyGainAdjustItem; +import com.nonid.item.EnergyStabilizerItem; +import com.nonid.item.HorseLiquidCollectorItem; +import com.nonid.network.AccessoryShedStateS2CPayload; +import com.nonid.network.AnimationCatalogChunkS2CPayload; +import com.nonid.network.AnimationCatalogData; +import com.nonid.network.AttackStateS2CPayload; +import com.nonid.network.DestroyedSkinMaskSyncS2CPayload; +import com.nonid.network.DestroyedSkinMaskUploadC2SPayload; +import com.nonid.network.DestroyedSkinParticlesS2CPayload; +import com.nonid.network.DestroyedSkinStateS2CPayload; +import com.nonid.network.DestroyedSkinSyncS2CPayload; +import com.nonid.network.DestroyedSkinUploadC2SPayload; +import com.nonid.network.GenderSelectionPromptS2CPayload; +import com.nonid.network.HostConfigSyncData; +import com.nonid.network.HostConfigSyncS2CPayload; +import com.nonid.network.LiquidStateS2CPayload; +import com.nonid.network.ManualDamageDestroyedSkinC2SPayload; +import com.nonid.network.MessStateS2CPayload; +import com.nonid.network.PlayerAnimationCancelC2SPayload; +import com.nonid.network.PlayerAnimationChoiceC2SPayload; +import com.nonid.network.PlayerAnimationConsentC2SPayload; +import com.nonid.network.PlayerAnimationConsentS2CPayload; +import com.nonid.network.PlayerAnimationRequestStatusS2CPayload; +import com.nonid.network.PlayerAnimationSelectionS2CPayload; +import com.nonid.network.PlayerAttackInputC2SPayload; +import com.nonid.network.PlayerProfileActionC2SPayload; +import com.nonid.network.PlayerProfileSnapshotS2CPayload; +import com.nonid.network.PlayerRuntimeSettingsS2CPayload; +import com.nonid.network.PregnancyCycleStateS2CPayload; +import com.nonid.network.PregnancyStateS2CPayload; +import com.nonid.network.RepairDestroyedSkinC2SPayload; +import com.nonid.network.ServerConfigRequestC2SPayload; +import com.nonid.network.ServerConfigResultS2CPayload; +import com.nonid.network.ServerConfigUpdateC2SPayload; +import com.nonid.network.SetPlayerGenderC2SPayload; +import com.nonid.network.SkinRippedInfoRequestS2CPayload; +import com.nonid.network.StartManualPeakC2SPayload; +import com.nonid.network.StopAttackAnimationC2SPayload; +import com.nonid.network.StopVoluntaryAnimationC2SPayload; +import com.nonid.particle.NonParticles; +import com.nonid.potion.NonBrewingRecipes; +import com.nonid.potion.NonPotions; +import com.nonid.recipe.NonRecipeSerializers; +import com.nonid.sound.NonSoundEvents; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDamage; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDeath; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AllowDamage; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AllowDeath; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents.AfterRespawn; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents.CopyFrom; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.EndDataPackReload; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStarted; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStopped; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.EndWorldTick; +import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; +import net.fabricmc.fabric.api.event.player.UseItemCallback; +import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; +import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents; +import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents.ModifyEntriesAll; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Join; +import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry; +import net.minecraft.block.Blocks; +import net.minecraft.component.ComponentType; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.ExperienceOrbEntity; +import net.minecraft.entity.LazyEntityReference; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.SpawnGroup; +import net.minecraft.entity.SpawnReason; +import net.minecraft.entity.EntityType.Builder; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.mob.Angerable; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.passive.AbstractHorseEntity; +import net.minecraft.entity.passive.AnimalEntity; +import net.minecraft.entity.passive.HorseColor; +import net.minecraft.entity.passive.HorseEntity; +import net.minecraft.entity.passive.HorseMarking; +import net.minecraft.entity.passive.PassiveEntity; +import net.minecraft.entity.passive.TameableEntity; +import net.minecraft.entity.passive.FoxEntity.Variant; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.Item.Settings; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.LazyRegistryEntryReference; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.tag.FluidTags; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerConfigEntry; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NeedsOfNature implements ModInitializer { + public static final String MOD_ID = "needsofnature"; + public static final int MAX_ENERGY = 200; + private static final int PLAYER_ANIMATION_START_ENERGY_GAIN = 20; + public static final float MIN_ENERGY_GAIN_MULT = 0.4F; + public static final float MAX_ENERGY_GAIN_MULT = 6.0F; + public static final String ENERGY_NBT_KEY = "NeedsOfNatureEnergy"; + public static final String ENERGY_GAIN_MULT_NBT_KEY = "NeedsOfNatureEnergyGainMult"; + public static final String ENERGY_GAIN_DRIFT_NBT_KEY = "NeedsOfNatureEnergyGainDrift"; + public static final String GENDER_NBT_KEY = "NeedsOfNatureGender"; + public static final String LIQUID_NBT_KEY = "NeedsOfNatureLiquid"; + public static final String LIQUID_TYPE_NBT_KEY = "NeedsOfNatureLiquidType"; + public static final String LIQUID_DONOR_VARIANT_NBT_KEY = "NeedsOfNatureLiquidDonorVariantData"; + public static final String LIQUID_DONOR_HISTORY_NBT_KEY = "NeedsOfNatureLiquidDonorHistory"; + public static final String LIQUID_TYPE_MIXED = "mixed"; + public static final String LIQUID_COLOR_MIXED_KEY = "needsofnature:mixed"; + public static final String PREGNANCY_TYPE_NBT_KEY = "NeedsOfNaturePregnancyType"; + public static final String PREGNANCY_END_TICK_NBT_KEY = "NeedsOfNaturePregnancyEndTick"; + public static final String PREGNANCY_PENDING_NBT_KEY = "NeedsOfNaturePregnancyPending"; + public static final String PREGNANCY_VARIANT_NBT_KEY = "NeedsOfNaturePregnancyVariantData"; + public static final String PREGNANCY_OFFSPRING_COUNT_NBT_KEY = "NeedsOfNaturePregnancyOffspringCount"; + public static final String PREGNANCY_CYCLE_PROGRESS_NBT_KEY = "NeedsOfNaturePregnancyCycleProgress"; + public static final String PREGNANCY_CYCLE_LAST_GAME_TICK_NBT_KEY = "NeedsOfNaturePregnancyCycleLastGameTick"; + public static final String PREGNANCY_CYCLE_LAST_TIME_OF_DAY_NBT_KEY = "NeedsOfNaturePregnancyCycleLastTimeOfDay"; + public static final String DESTROYED_SKIN_STAGE_NBT_KEY = "NeedsOfNatureDestroyedSkinStage"; + public static final String DESTROYED_SKIN_DAMAGE_NBT_KEY = "NeedsOfNatureDestroyedSkinDamage"; + public static final String MESS_V_NBT_KEY = "NeedsOfNatureMessV"; + public static final String MESS_A_NBT_KEY = "NeedsOfNatureMessA"; + public static final String MESS_M_NBT_KEY = "NeedsOfNatureMessM"; + public static final float FILLED_STAGE_ONE_THRESHOLD = 0.3F; + public static final float FILLED_STAGE_TWO_THRESHOLD = 0.5F; + public static final float FILLED_STAGE_THREE_THRESHOLD = 0.8F; + public static final float FILLED_STAGE_ONE_SPEED_MULT = 0.8F; + public static final float FILLED_STAGE_TWO_SPEED_MULT = 0.7F; + public static final float FILLED_STAGE_THREE_SPEED_MULT = 0.6F; + public static final float FILLED_STAGE_ONE_JUMP_MULT = 0.9F; + public static final float FILLED_STAGE_TWO_JUMP_MULT = 0.75F; + public static final float FILLED_STAGE_THREE_JUMP_MULT = 0.6F; + public static final float PREGNANCY_MIN_MOVEMENT_MULT = 0.5F; + public static final float PREGNANCY_SLOWDOWN_CURVE_POWER = 5.0F; + public static final Logger LOGGER = LoggerFactory.getLogger("needsofnature"); + private static final Gson HOST_CONFIG_SYNC_GSON = new Gson(); + public static final RegistryKey DEBUG_STAFF_KEY = RegistryKey.of(Registries.ITEM.getKey(), id("debug_staff")); + public static final Item DEBUG_STAFF = registerItem(DEBUG_STAFF_KEY, new DebugStaffItem(new Settings().registryKey(DEBUG_STAFF_KEY).maxCount(1))); + public static final RegistryKey FLOWER_MIX_KEY = RegistryKey.of(Registries.ITEM.getKey(), id("flower_mix")); + public static final Item FLOWER_MIX = registerItem(FLOWER_MIX_KEY, new Item(new Settings().registryKey(FLOWER_MIX_KEY))); + public static final RegistryKey ENERGY_AUGMENTER_KEY = RegistryKey.of(Registries.ITEM.getKey(), id("energy_augmenter")); + public static final Item ENERGY_AUGMENTER = registerItem( + ENERGY_AUGMENTER_KEY, new EnergyGainAdjustItem(new Settings().registryKey(ENERGY_AUGMENTER_KEY), 0.2F) + ); + public static final RegistryKey ENERGY_DIMINISHER_KEY = RegistryKey.of(Registries.ITEM.getKey(), id("energy_diminisher")); + public static final Item ENERGY_DIMINISHER = registerItem( + ENERGY_DIMINISHER_KEY, new EnergyGainAdjustItem(new Settings().registryKey(ENERGY_DIMINISHER_KEY), -0.2F) + ); + public static final RegistryKey ENERGY_STABILIZER_KEY = RegistryKey.of(Registries.ITEM.getKey(), id("energy_stabilizer")); + public static final Item ENERGY_STABILIZER = registerItem(ENERGY_STABILIZER_KEY, new EnergyStabilizerItem(new Settings().registryKey(ENERGY_STABILIZER_KEY))); + public static final List DATA_DRIVEN_ACCESSORIES = registerDataDrivenAccessories(); + public static final RegistryKey> HORSE_LIQUID_COLLECTOR_ENTITY_TYPE_KEY = RegistryKey.of( + Registries.ENTITY_TYPE.getKey(), id("horse_liquid_collector") + ); + public static final EntityType HORSE_LIQUID_COLLECTOR_ENTITY_TYPE = (EntityType)Registry.register( + Registries.ENTITY_TYPE, + id("horse_liquid_collector"), + Builder.create(HorseLiquidCollectorEntity::new, SpawnGroup.MISC).dimensions(0.8F, 1.2F).build(HORSE_LIQUID_COLLECTOR_ENTITY_TYPE_KEY) + ); + public static final RegistryKey> PREGNANCY_EGG_ENTITY_TYPE_KEY = RegistryKey.of(Registries.ENTITY_TYPE.getKey(), id("pregnancy_egg")); + public static final EntityType PREGNANCY_EGG_ENTITY_TYPE = (EntityType)Registry.register( + Registries.ENTITY_TYPE, + id("pregnancy_egg"), + Builder.create(PregnancyEggEntity::new, SpawnGroup.MISC).dimensions(0.3F, 0.3F).build(PREGNANCY_EGG_ENTITY_TYPE_KEY) + ); + public static final RegistryKey HORSE_LIQUID_COLLECTOR_KEY = RegistryKey.of(Registries.ITEM.getKey(), id("horse_liquid_collector")); + public static final Item HORSE_LIQUID_COLLECTOR = registerItem( + HORSE_LIQUID_COLLECTOR_KEY, new HorseLiquidCollectorItem(new Settings().registryKey(HORSE_LIQUID_COLLECTOR_KEY).maxCount(1)) + ); + private static final Map, NeedsOfNature.ScanBudget> SCAN_BUDGETS = new ConcurrentHashMap<>(); + private static final Map PENDING_POST_DEATH_STATE_SYNCS = new ConcurrentHashMap<>(); + private static final ItemGroup ITEM_GROUP = (ItemGroup)Registry.register( + Registries.ITEM_GROUP, + id("main"), + FabricItemGroup.builder() + .displayName(Text.translatable("itemGroup.needsofnature.main")) + .icon(() -> new ItemStack(HORSE_LIQUID_COLLECTOR)) + .entries((context, entries) -> { + entries.add(FLOWER_MIX); + entries.add(ENERGY_AUGMENTER); + entries.add(ENERGY_DIMINISHER); + DATA_DRIVEN_ACCESSORIES.forEach(entries::add); + entries.add(HORSE_LIQUID_COLLECTOR); + NonItemSystem.addEntityLiquidAndStabilizersToItemGroup(entries, getConfig()); + NonItemSystem.addNonPotionVariantsToItemGroup(entries); + }) + .build() + ); + private static NonConfig CONFIG; + private static volatile HostConfigSyncData REMOTE_HOST_RULES; + private static volatile NonConfig REMOTE_EFFECTIVE_CONFIG; + private static final AtomicLong SERVER_CONFIG_REVISION = new AtomicLong(); + private static final AtomicLong ANIMATION_CATALOG_REVISION = new AtomicLong(); + private static final int ANIMATION_CATALOG_CHUNK_CHARS = 24000; + private static final Map PEAK_STATES = new ConcurrentHashMap<>(); + private static final int PEAK_XP_CAP = 20; + private static final Identifier PLAYER_ENTITY_TYPE_ID = Identifier.of("minecraft", "player"); + private static final int PENDING_PREGNANCY_EFFECT_TICKS = 21; + private static final int MESS_AURA_MAX_TOTAL = 15; + private static final double MESS_AURA_MAX_MULTIPLIER = 10.0; + private static final int GATHER_TIMEOUT_COOLDOWN_TICKS = 1200; + private static final int GATHER_PAIR_TRACK_TICKS = 6000; + private static final int BREEDING_PENDING_TIMEOUT_TICKS = 600; + private static final int REGULAR_PRUNE_INTERVAL_TICKS = 20; + private static final int FERTILITY_INCREASER_I_MIN_CHANCE = 80; + private static final int FERTILITY_INCREASER_II_MIN_CHANCE = 95; + private static final double LIQUID_SNEAK_DECAY_MULTIPLIER = 16.0; + private static final double LIQUID_WATER_DECAY_MULTIPLIER = 2.0; + private static final double LIQUID_DECAY_MIN_MULT = 0.1; + private static final double LIQUID_DECAY_MAX_MULT = 1.5; + private static final double NEAR_ANIMATION_RADIUS = 20.0; + private static final Map PENDING_GATHER_PAIRS = new ConcurrentHashMap<>(); + private static final Map NON_PENDING_GATHER_ACTORS = new ConcurrentHashMap<>(); + private static final String ANIMATION_TAG_DEFEAT_PREFIX = "defeat."; + private static final String ANIMATION_TAG_MANUAL_PEAK = "manualPeak"; + private static final String ANIMATION_TAG_FILL_BOTTLE = "fillBottle"; + private static final String ANIMATION_TAG_BIRTH = "birth"; + private static final String ANIMATION_TAG_BIRTH_PREFIX = "birth_"; + private static final int BIRTH_LOOP_STAGE = 2; + private static final int BIRTH_FINAL_STAGE = 3; + private static final double BIRTH_LOOP_MAX_SPEED_MULTIPLIER = 2.0; + private static final double BIRTH_LOOP_SPEED_TAPER_EXPONENT = 2.5; + static final String NON_PREGNANCY_FRIENDLY_TAG = "non.pregnancy_friendly"; + public static final String NON_ENERGY_STABILIZED_TAG = "non.energy_stabilized"; + private static final Set DEFEATED_QUEUED_INSTANCES = ConcurrentHashMap.newKeySet(); + private static final Map LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER = new ConcurrentHashMap<>(); + private static final Map POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER = new ConcurrentHashMap<>(); + private static final int ATTACK_ESCAPE_DAMAGE_DELAY_TICKS = 1; + private static final Map PENDING_ATTACK_ESCAPE_RESOLUTIONS = new ConcurrentHashMap<>(); + private static final List PREGNANCY_VARIANT_COMPONENTS = List.of( + registryVariant("wolf_variant", DataComponentTypes.WOLF_VARIANT, RegistryKeys.WOLF_VARIANT), + registryVariant("wolf_sound_variant", DataComponentTypes.WOLF_SOUND_VARIANT, RegistryKeys.WOLF_SOUND_VARIANT), + registryVariant("pig_variant", DataComponentTypes.PIG_VARIANT, RegistryKeys.PIG_VARIANT), + registryVariant("cow_variant", DataComponentTypes.COW_VARIANT, RegistryKeys.COW_VARIANT), + lazyRegistryVariant("chicken_variant", DataComponentTypes.CHICKEN_VARIANT, RegistryKeys.CHICKEN_VARIANT), + registryVariant("frog_variant", DataComponentTypes.FROG_VARIANT, RegistryKeys.FROG_VARIANT), + registryVariant("cat_variant", DataComponentTypes.CAT_VARIANT, RegistryKeys.CAT_VARIANT), + lazyRegistryVariant("zombie_nautilus_variant", DataComponentTypes.ZOMBIE_NAUTILUS_VARIANT, RegistryKeys.ZOMBIE_NAUTILUS_VARIANT), + enumVariant("fox_variant", DataComponentTypes.FOX_VARIANT, Variant.class), + enumVariant("salmon_size", DataComponentTypes.SALMON_SIZE, net.minecraft.entity.passive.SalmonEntity.Variant.class), + enumVariant("parrot_variant", DataComponentTypes.PARROT_VARIANT, net.minecraft.entity.passive.ParrotEntity.Variant.class), + enumVariant("mooshroom_variant", DataComponentTypes.MOOSHROOM_VARIANT, net.minecraft.entity.passive.MooshroomEntity.Variant.class), + enumVariant("rabbit_variant", DataComponentTypes.RABBIT_VARIANT, net.minecraft.entity.passive.RabbitEntity.Variant.class), + enumVariant("horse_variant", DataComponentTypes.HORSE_VARIANT, HorseColor.class), + enumVariant("llama_variant", DataComponentTypes.LLAMA_VARIANT, net.minecraft.entity.passive.LlamaEntity.Variant.class), + enumVariant("axolotl_variant", DataComponentTypes.AXOLOTL_VARIANT, net.minecraft.entity.passive.AxolotlEntity.Variant.class), + enumVariant("sheep_color", DataComponentTypes.SHEEP_COLOR, DyeColor.class), + enumVariant("tropical_fish_base_color", DataComponentTypes.TROPICAL_FISH_BASE_COLOR, DyeColor.class), + enumVariant("tropical_fish_pattern_color", DataComponentTypes.TROPICAL_FISH_PATTERN_COLOR, DyeColor.class) + ); + private static final int INFINITE_PEAK_LIQUID_PULSE_ML = 5; + private static final Map PEAK_LIQUID_PROGRESS = new ConcurrentHashMap<>(); + private static final Map PEAK_XP_ELIGIBLE_ML = new ConcurrentHashMap<>(); + private static final Map FILL_BOTTLE_STATES = new ConcurrentHashMap<>(); + private static final Map BREEDING_PENDING_BY_PAIR = new ConcurrentHashMap<>(); + private static final Map BREEDING_PENDING_BY_REQUEST = new ConcurrentHashMap<>(); + private static final Map BREEDING_ACTIVE_BY_INSTANCE = new ConcurrentHashMap<>(); + private static final Map BREEDING_ACTIVE_INSTANCE_BY_PAIR = new ConcurrentHashMap<>(); + private static final Map PREGNANCY_HATCH_BY_INSTANCE = new ConcurrentHashMap<>(); + private static final Map PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER = new ConcurrentHashMap<>(); + private static final Set PREGNANCY_QUEUED_PLAYERS = ConcurrentHashMap.newKeySet(); + private static final Map PREGNANCY_START_RETRY_TICKS = new ConcurrentHashMap<>(); + private static final Map PREGNANCY_CUE_RUNTIME_BY_INSTANCE = new ConcurrentHashMap<>(); + private static final Set REFLECTION_FAILURE_LOGS = ConcurrentHashMap.newKeySet(); + private static final Map RECEIVER_DRIP = new ConcurrentHashMap<>(); + private static final Map PENDING_M_INJECTOR_REWARDS = new ConcurrentHashMap<>(); + private static final int RECEIVER_DRIP_INTERVAL_TICKS = 20; + private static final int RECEIVER_DRIP_DURATION_TICKS = 200; + private static final float FILL_BOTTLE_LOOK_DOWN_MIN_PITCH = 80.0F; + private static final Vec3d RECEIVER_DRIP_OFFSET_DEFAULT = new Vec3d(0.0, 0.0, -0.5); + private static final Map RECEIVER_DRIP_OFFSETS = Map.of(Identifier.of("minecraft", "wolf"), new Vec3d(0.0, 0.0, -0.5)); + + public static float getPregnancyMovementMultiplier(int remainingTicks) { + if (remainingTicks == -1) { + return 1.0F; + } + + if (remainingTicks <= 22) { + return 0.5F; + } + + long configuredTotalTicks = Math.max(1L, getConfig().getPregnancyDurationMinutes() * 1200L); + double remaining01 = MathHelper.clamp((double)remainingTicks / configuredTotalTicks, 0.0, 1.0); + double closeness = 1.0 - remaining01; + double curve = Math.pow(closeness, 5.0); + double multiplier = 1.0 - 0.5 * curve; + return (float)MathHelper.clamp(multiplier, 0.5, 1.0); + } + + public static void applyAnimationToggleConfig() { + NonConfig config = CONFIG; + if (config == null) { + AnimationDefinitions.setDisabledAnimationFilters(Set.of(), Set.of()); + } else { + LinkedHashSet disabledAnimations = new LinkedHashSet<>(); + + for (String raw : config.getDisabledAnimations()) { + Identifier id = Identifier.tryParse(raw); + if (id != null) { + disabledAnimations.add(id); + } + } + + AnimationDefinitions.setDisabledAnimationFilters(disabledAnimations, config.getDisabledAnimationPacks()); + } + } + + public void onInitialize() { + NonWildfireGenderConfigCompat.initialize(); + NonDiagnostics.initialize(); + AnimationEngine.initialize(); + CONFIG = NonConfig.load(); + NonActorEvents.PROVIDE + .register((NonActorEvents.Provider)entity -> entity instanceof LivingEntity living ? NonTrinketsIntegration.resolveVirtualActorTags(living) : Set.of()); + applyAnimationToggleConfig(); + NonStats.registerBaseStats(); + NonSoundEvents.registerAll(); + NonParticles.registerAll(); + NonStatusEffects.registerAll(); + ServerLifecycleEvents.SERVER_STARTED.register((ServerStarted)server -> { + NonStats.registerKnownEntityStats(); + SERVER_CONFIG_REVISION.set(0L); + ANIMATION_CATALOG_REVISION.set(0L); + clearServerRuntimeState(); + }); + ServerLifecycleEvents.SERVER_STOPPED.register((ServerStopped)server -> clearServerRuntimeState()); + ServerLifecycleEvents.END_DATA_PACK_RELOAD.register((EndDataPackReload)(server, resourceManager, success) -> { + if (success) { + SERVER_CONFIG_REVISION.incrementAndGet(); + ANIMATION_CATALOG_REVISION.incrementAndGet(); + syncHostConfigStateToAll(server); + syncAnimationCatalogStateToAll(server); + } + }); + NonPotions.registerAll(); + NonBrewingRecipes.registerAll(); + NonRecipeSerializers.registerAll(); + NonTrinketsIntegration.registerPredicates(); + FabricDefaultAttributeRegistry.register(HORSE_LIQUID_COLLECTOR_ENTITY_TYPE, HorseLiquidCollectorEntity.createAttributes()); + FabricDefaultAttributeRegistry.register(PREGNANCY_EGG_ENTITY_TYPE, PregnancyEggEntity.createAttributes()); + PayloadTypeRegistry.playC2S().register(StopVoluntaryAnimationC2SPayload.ID, StopVoluntaryAnimationC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(StopAttackAnimationC2SPayload.ID, StopAttackAnimationC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(PlayerAttackInputC2SPayload.ID, PlayerAttackInputC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(StartManualPeakC2SPayload.ID, StartManualPeakC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(SetPlayerGenderC2SPayload.ID, SetPlayerGenderC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(RepairDestroyedSkinC2SPayload.ID, RepairDestroyedSkinC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(ManualDamageDestroyedSkinC2SPayload.ID, ManualDamageDestroyedSkinC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DestroyedSkinUploadC2SPayload.ID, DestroyedSkinUploadC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DestroyedSkinMaskUploadC2SPayload.ID, DestroyedSkinMaskUploadC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(PlayerProfileActionC2SPayload.ID, PlayerProfileActionC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(PlayerAnimationChoiceC2SPayload.ID, PlayerAnimationChoiceC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(PlayerAnimationConsentC2SPayload.ID, PlayerAnimationConsentC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(PlayerAnimationCancelC2SPayload.ID, PlayerAnimationCancelC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(ServerConfigRequestC2SPayload.ID, ServerConfigRequestC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(ServerConfigUpdateC2SPayload.ID, ServerConfigUpdateC2SPayload.CODEC); + PayloadTypeRegistry.playS2C().register(AttackStateS2CPayload.ID, AttackStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(LiquidStateS2CPayload.ID, LiquidStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PregnancyStateS2CPayload.ID, PregnancyStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PregnancyCycleStateS2CPayload.ID, PregnancyCycleStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PlayerRuntimeSettingsS2CPayload.ID, PlayerRuntimeSettingsS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(GenderSelectionPromptS2CPayload.ID, GenderSelectionPromptS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(HostConfigSyncS2CPayload.ID, HostConfigSyncS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(ServerConfigResultS2CPayload.ID, ServerConfigResultS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(AnimationCatalogChunkS2CPayload.ID, AnimationCatalogChunkS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(DestroyedSkinSyncS2CPayload.ID, DestroyedSkinSyncS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(DestroyedSkinMaskSyncS2CPayload.ID, DestroyedSkinMaskSyncS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(DestroyedSkinStateS2CPayload.ID, DestroyedSkinStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(DestroyedSkinParticlesS2CPayload.ID, DestroyedSkinParticlesS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(MessStateS2CPayload.ID, MessStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(AccessoryShedStateS2CPayload.ID, AccessoryShedStateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(SkinRippedInfoRequestS2CPayload.ID, SkinRippedInfoRequestS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PlayerProfileSnapshotS2CPayload.ID, PlayerProfileSnapshotS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PlayerAnimationSelectionS2CPayload.ID, PlayerAnimationSelectionS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PlayerAnimationConsentS2CPayload.ID, PlayerAnimationConsentS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(PlayerAnimationRequestStatusS2CPayload.ID, PlayerAnimationRequestStatusS2CPayload.CODEC); + NonPlayerInteractionSystem.register(); + NonAnimationRescueSystem.register(); + NonPlayerAttackSystem.register(); + registerServerSettingsNetworking(); + NonGenderSystem.registerMobGenderTags(); + NonEntityProfiles.registerReloadListener(); + NonLiquidGainOverrides.registerReloadListener(); + this.registerDebugStaffInteraction(); + this.registerEnergyControlItemInteraction(); + this.registerPlayerAnimationJoinInteraction(); + NonGenderSystem.registerCowMilkingGenderGate(); + this.registerHorseLiquidCollectorBottleInteraction(); + this.registerHorseLiquidCollectorDrops(); + NonDestroyedSkinSystem.registerDestroyedSkinRepairNetworking(); + this.registerDebugCommands(); + this.registerLiquidPotionGroupCleanup(); + this.registerAnimationListeners(); + this.registerLoopProgression(); + NonDestroyedSkinSystem.registerDestroyedSkinNetworking(); + this.registerVoluntaryStopRequests(); + this.registerAttackStopRequests(); + this.registerManualPeakRequests(); + this.registerNormalDamageDestroyedSkinDamage(); + NonGenderSystem.registerPlayerGenderRequests(); + this.registerFillBottleUseTrigger(); + this.registerLastDefeatedDeathRescue(); + this.registerPregnancyDeathCleanup(); + this.registerDeathPersistenceCopy(); + this.registerPostEscapeDamageInvulnerability(); + this.registerDirectAttackBlockCleanup(); + this.registerLiquidSync(); + } + + private void registerDebugStaffInteraction() { + AttackEntityCallback.EVENT.register((AttackEntityCallback)(player, world, hand, entity, hitResult) -> { + if (hand != Hand.MAIN_HAND || player.getStackInHand(hand).getItem() != DEBUG_STAFF) { + return ActionResult.PASS; + } + + if (world.isClient()) { + return ActionResult.PASS; + } + + if (entity instanceof PlayerEntity) { + return ActionResult.FAIL; + } + + if (!entity.isRemoved()) { + world.playSound(null, entity.getX(), entity.getY(), entity.getZ(), NonSoundEvents.DEBUG_STAFF_DISAPPEAR, SoundCategory.NEUTRAL, 1.0F, 1.0F); + entity.discard(); + } + + return ActionResult.SUCCESS; + }); + UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> { + if (entity instanceof LivingEntity living) { + ItemStack stack = player.getStackInHand(hand); + if (stack.isEmpty() || stack.getItem() != DEBUG_STAFF) { + return ActionResult.PASS; + } + + if (!(living instanceof EnergyHolder)) { + return ActionResult.PASS; + } + + if (world.isClient()) { + return ActionResult.SUCCESS; + } + + ActionResult result = stack.getItem().useOnEntity(stack, player, living, hand); + return (ActionResult)(result == ActionResult.PASS ? ActionResult.SUCCESS : result); + } else { + return ActionResult.PASS; + } + }); + } + + private void registerEnergyControlItemInteraction() { + UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> { + if (entity instanceof LivingEntity living) { + ItemStack stack = player.getStackInHand(hand); + if (stack.isEmpty()) { + return ActionResult.PASS; + } + + if (!(stack.getItem() instanceof EnergyStabilizerItem) && !(stack.getItem() instanceof EnergyGainAdjustItem)) { + return ActionResult.PASS; + } + + if (!(living instanceof EnergyHolder)) { + return ActionResult.PASS; + } + + ActionResult result = stack.getItem().useOnEntity(stack, player, living, hand); + return (ActionResult)(result == ActionResult.PASS ? ActionResult.SUCCESS : result); + } else { + return ActionResult.PASS; + } + }); + } + + private void registerPlayerAnimationJoinInteraction() { + UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> { + if (!(world instanceof ServerWorld serverWorld && player instanceof ServerPlayerEntity joiningPlayer)) { + return ActionResult.PASS; + } else if (hand != Hand.MAIN_HAND) { + return ActionResult.PASS; + } else if (!joiningPlayer.getStackInHand(Hand.MAIN_HAND).isEmpty()) { + return ActionResult.PASS; + } else if (joiningPlayer.isSpectator()) { + return ActionResult.PASS; + } else if (entity != null && !entity.isRemoved() && !entity.getUuid().equals(joiningPlayer.getUuid())) { + return (ActionResult)(NonPlayerInteractionSystem.handleInteraction(serverWorld, joiningPlayer, entity) ? ActionResult.SUCCESS : ActionResult.PASS); + } else { + return ActionResult.PASS; + } + }); + } + + private void registerHorseLiquidCollectorBottleInteraction() { + UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> { + if (entity instanceof HorseLiquidCollectorEntity collector) { + ItemStack stack = player.getStackInHand(hand); + if (stack.isEmpty() || !stack.isOf(Items.GLASS_BOTTLE)) { + return ActionResult.PASS; + } + + if (world.isClient()) { + return ActionResult.SUCCESS; + } + + if (player instanceof ServerPlayerEntity serverPlayer) { + if (!collector.isCollectorFull()) { + return ActionResult.SUCCESS; + } + + if (!NonItemSystem.consumeOneGlassBottle(serverPlayer, hand)) { + return ActionResult.SUCCESS; + } + + ItemStack resultBottle = NonItemSystem.createLiquidBottleStack(collector.getBottleLiquidEntityTypeId()); + collector.clearStoredLiquid(); + NonItemSystem.giveItemOrDrop(serverPlayer, resultBottle); + world.playSound(null, collector.getX(), collector.getY(), collector.getZ(), SoundEvents.ITEM_BOTTLE_FILL, SoundCategory.PLAYERS, 1.0F, 1.0F); + return ActionResult.SUCCESS; + } else { + return ActionResult.SUCCESS; + } + } else { + return ActionResult.PASS; + } + }); + } + + private void registerHorseLiquidCollectorDrops() { + ServerLivingEntityEvents.AFTER_DEATH.register((AfterDeath)(entity, damageSource) -> { + if (entity instanceof HorseLiquidCollectorEntity) { + if (entity.getEntityWorld() instanceof ServerWorld world) { + if (!(damageSource.getAttacker() instanceof ServerPlayerEntity player && player.isCreative())) { + entity.dropStack(world, new ItemStack(HORSE_LIQUID_COLLECTOR)); + } + } + } + }); + } + + private void registerDebugCommands() { + NonCommands.register(); + } + + public static Identifier id(String path) { + return Identifier.of("needsofnature", path); + } + + private static void grantNoNAdvancement(ServerPlayerEntity player, String path) { + NonAdvancementHooks.grant(player, path); + } + + public static void grantEmergencySnackAdvancement(ServerPlayerEntity player) { + NonAdvancementHooks.grantEmergencySnack(player); + } + + public static void grantUdderlyUnfortunateAdvancementIfPregnant(ServerPlayerEntity player) { + if (getPregnantEntityTypeId(player) != null) { + NonAdvancementHooks.grantUdderlyUnfortunate(player); + } + } + + public static void resetStableDietStreak(ServerPlayerEntity player) { + NonAdvancementHooks.resetStableDietStreak(player); + } + + private static void checkLiquidAdvancements(ServerPlayerEntity player, LiquidHolder holder) { + NonAdvancementHooks.checkLiquidAdvancements(player, holder); + } + + private static void trackFieldResearch(ServerWorld world, ServerPlayerEntity player, Map perEntityAmounts) { + NonAdvancementHooks.trackFieldResearch(world, player, perEntityAmounts); + } + + static void checkBadIdeaBeaconAdvancement(ServerPlayerEntity player) { + NonAdvancementHooks.checkBadIdeaBeacon(player); + } + + static boolean isMessSystemEnabled() { + NonConfig config = getConfig(); + return config == null || config.isMessSystemEnabled(); + } + + static boolean isDestroyedSkinSystemEnabled() { + NonConfig config = getConfig(); + return config == null || config.isDestroyedSkinSystemEnabled(); + } + + static int getDestroyedSkinStageForPlayer(ServerPlayerEntity player) { + return NonDestroyedSkinSystem.getDestroyedSkinStageForPlayer(player); + } + + static int getTotalMessForPlayer(ServerPlayerEntity player) { + if (player == null || !isMessSystemEnabled()) { + return 0; + } else { + return player instanceof MessHolder holder + ? NonMessSystem.clampMess(holder.getVMess()) + NonMessSystem.clampMess(holder.getAMess()) + NonMessSystem.clampMess(holder.getMMess()) + : 0; + } + } + + public static UUID getActivePlayerInstance(ServerPlayerEntity player) { + return player != null && player.getEntityWorld() instanceof ServerWorld world + ? ServerAnimationController.findActiveInstanceId(world, player.getUuid()) + : null; + } + + public static boolean hasActivePlayerAnimation(ServerPlayerEntity player) { + if (player == null) { + return false; + } + + UUID active = getActivePlayerInstance(player); + return active != null; + } + + public static boolean instanceContainsActor(ServerWorld world, UUID instanceId, UUID actorUuid) { + if (world != null && instanceId != null && actorUuid != null) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + return snapshot != null && snapshot.actorUuids().contains(actorUuid); + } else { + return false; + } + } + + public static boolean hasNearbyVisibleAnimation(ServerWorld world, MobEntity viewer) { + return getNearbyVisibleAnimationGainMultiplier(world, viewer) > 1.0; + } + + public static double getNearbyVisibleAnimationGainMultiplier(ServerWorld world, MobEntity viewer) { + if (world != null && viewer != null) { + double maxDistSq = 400.0; + UUID viewerUuid = viewer.getUuid(); + double best = 1.0; + + for (ServerAnimationController.ActiveInstanceSnapshot snapshot : ServerAnimationController.getActiveInstanceSnapshots(world)) { + List actorUuids = snapshot.actorUuids(); + if (actorUuids != null && !actorUuids.isEmpty() && !actorUuids.contains(viewerUuid)) { + for (UUID actorUuid : actorUuids) { + if (actorUuid != null + && world.getEntity(actorUuid) instanceof LivingEntity living + && living.isAlive() + && !living.isRemoved() + && !(viewer.squaredDistanceTo(living) > maxDistSq) + && viewer.canSee(living)) { + double multiplier = getNearAnimationGainMult(); + if (living instanceof ServerPlayerEntity player) { + multiplier *= NonTrinketsIntegration.getAccessoryEffects(player).nearAnimationMobEnergyMultiplier(); + } + + best = Math.max(best, multiplier); + } + } + } + } + + return best; + } else { + return 1.0; + } + } + + public static float getNearAnimationGainMult() { + NonConfig config = getConfig(); + return config == null ? 3.0F : (float)config.getNearAnimationEnergyGainMult(); + } + + public static float getNearAnimationGainMult(@Nullable ServerPlayerEntity player) { + float base = getNearAnimationGainMult(); + return player == null ? base : (float)(base * NonTrinketsIntegration.getAccessoryEffects(player).nearAnimationMobEnergyMultiplier()); + } + + public static void tickPlayerEnergyAura(ServerWorld world, ServerPlayerEntity player) { + NonAuraSystem.tickPlayerEnergyAura(world, player); + } + + public static NeedsOfNature.PlayerEnergyAuraDebug getPlayerEnergyAuraDebug(ServerWorld world, MobEntity mob, EnergyHolder holder) { + NonAuraSystem.PlayerEnergyAuraDebug debug = NonAuraSystem.getPlayerEnergyAuraDebug(world, mob, holder); + return debug == null + ? null + : new NeedsOfNature.PlayerEnergyAuraDebug( + debug.player(), debug.totalMultiplier(), debug.playerEnergy(), debug.destroyedSkinStage(), debug.messTotal(), debug.radius() + ); + } + + private static Item registerItem(RegistryKey key, Item item) { + return (Item)Registry.register(Registries.ITEM, key, item); + } + + private static List registerDataDrivenAccessories() { + NonAccessoryItemRegistry.registerStartupItems(); + NonAccessoryDefinitions.loadStartupDefinitions(); + return NonAccessoryItemRegistry.getRegisteredItems(); + } + + private void registerLiquidPotionGroupCleanup() { + ItemGroupEvents.MODIFY_ENTRIES_ALL.register((ModifyEntriesAll)(group, entries) -> { + Identifier groupId = Registries.ITEM_GROUP.getId(group); + if (groupId == null || !groupId.equals(id("main"))) { + entries.getDisplayStacks().removeIf(NonItemSystem::isNonPotionVariantStack); + entries.getSearchTabStacks().removeIf(NonItemSystem::isNonPotionVariantStack); + } + }); + } + + private void registerPregnancyDeathCleanup() { + ServerLivingEntityEvents.AFTER_DEATH.register((AfterDeath)(entity, damageSource) -> { + if (entity instanceof ServerPlayerEntity player) { + NonConfig config = getConfig(); + if (config != null && config.keepPregnancyAfterDeath()) { + return; + } + + clearPregnancyOnDeath(player); + } + }); + } + + private void registerDeathPersistenceCopy() { + ServerPlayerEvents.COPY_FROM.register((CopyFrom)(oldPlayer, newPlayer, alive) -> { + if (!alive) { + copyDeathPersistentPlayerState(oldPlayer, newPlayer); + } + }); + ServerPlayerEvents.AFTER_RESPAWN.register((AfterRespawn)(oldPlayer, newPlayer, alive) -> { + if (!alive) { + NonPlayerAttackSystem.clearForRespawn(newPlayer); + schedulePostDeathStateSync(newPlayer); + } + }); + } + + private static void copyDeathPersistentPlayerState(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) { + if (oldPlayer != null && newPlayer != null) { + NonConfig config = getConfig(); + if (config != null) { + NonPregnancyConceptionSystem.copyState(oldPlayer, newPlayer); + if (config.isMessSystemEnabled()) { + if (config.keepMessAfterDeath()) { + NeedsOfNature.MessSnapshot.copy(oldPlayer, newPlayer); + } else { + NonMessSystem.clearMessStoredState(newPlayer); + } + } + + if (config.isDestroyedSkinSystemEnabled()) { + if (config.keepRippedSkinAfterDeath()) { + NeedsOfNature.RippedSkinSnapshot.copy(oldPlayer, newPlayer); + } else { + NonDestroyedSkinSystem.clearDestroyedSkinStoredState(newPlayer); + } + } + + if (config.isLiquidTankEnabled()) { + if (config.keepLiquidTankAfterDeath()) { + NeedsOfNature.LiquidSnapshot.copy(oldPlayer, newPlayer); + } else { + clearLiquidStoredState(newPlayer); + } + } + + if (config.isPregnancyEnabled() && config.keepPregnancyAfterDeath()) { + NeedsOfNature.PregnancySnapshot.copy(oldPlayer, newPlayer); + } + } + } + } + + private static void schedulePostDeathStateSync(ServerPlayerEntity player) { + if (player != null && player.getEntityWorld() instanceof ServerWorld world) { + PENDING_POST_DEATH_STATE_SYNCS.put(player.getUuid(), new NeedsOfNature.PostDeathStateSync(world.getRegistryKey(), world.getTime() + 2L)); + } + } + + private static void tickPostDeathStateSync(ServerWorld world, long now) { + if (world != null && !PENDING_POST_DEATH_STATE_SYNCS.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + + for (Entry entry : PENDING_POST_DEATH_STATE_SYNCS.entrySet()) { + UUID playerUuid = entry.getKey(); + NeedsOfNature.PostDeathStateSync sync = entry.getValue(); + if (playerUuid != null && sync != null) { + if (worldKey.equals(sync.worldKey()) && now >= sync.dueTick()) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(playerUuid); + if (player != null) { + syncPostDeathPlayerState(player); + } + + PENDING_POST_DEATH_STATE_SYNCS.remove(playerUuid, sync); + } + } else if (playerUuid != null) { + PENDING_POST_DEATH_STATE_SYNCS.remove(playerUuid); + } + } + } + } + + private static void syncPostDeathPlayerState(ServerPlayerEntity player) { + if (player != null) { + NonConfig config = getConfig(); + if (config != null) { + if (!config.keepMessAfterDeath()) { + NonMessSystem.clearMessStoredState(player); + } + + NonMessSystem.broadcastMessState(player); + if (!config.keepRippedSkinAfterDeath()) { + NonDestroyedSkinSystem.clearDestroyedSkinStoredState(player); + } + + NonDestroyedSkinSystem.broadcastDestroyedSkinStage(player); + if (!config.keepLiquidTankAfterDeath()) { + clearLiquidStoredState(player); + } + + syncLiquidState(player); + syncPregnancyState(player); + NonPregnancyConceptionSystem.syncState(player); + } + } + } + + private static void clearLiquidStoredState(ServerPlayerEntity player) { + if (player instanceof LiquidHolder holder) { + holder.setLiquidStored(0); + holder.clearLiquidComposition(); + } + } + + private void registerLastDefeatedDeathRescue() { + ServerLivingEntityEvents.ALLOW_DEATH + .register( + (AllowDeath)(entity, damageSource, damageAmount) -> { + if (entity instanceof ServerPlayerEntity player) { + if (player.getEntityWorld() instanceof ServerWorld world) { + ServerPlayerEntity damagingPlayer = NonPlayerAttackSystem.resolveAttributedPlayer(damageSource); + if (damagingPlayer != null && NonPlayerAttackSystem.isOwnerFinishingDamage(world, player, damageSource)) { + NonPlayerAttackSystem.onOwnerFinishingDeath(world, player); + return true; + } + + NonConfig config = getConfig(); + if (config == null || !config.isLastDefeatedEnabled()) { + return true; + } + + if (hasVanillaTotemReady(player)) { + return true; + } + + if (AnimationService.isActorPendingOrActive(world, player.getUuid())) { + return true; + } + + long nowTick = getGlobalTick(world); + Long cooldownUntil = LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.get(player.getUuid()); + if (cooldownUntil != null && cooldownUntil > nowTick) { + return true; + } + + if (damagingPlayer != null) { + if (damagingPlayer != player + && damagingPlayer.getEntityWorld() == world + && damagingPlayer.isAlive() + && !damagingPlayer.isRemoved() + && !damagingPlayer.isSpectator()) { + if (!(damagingPlayer instanceof EnergyHolder holder && holder.getEnergy() >= config.getLastDefeatedEnergyThreshold())) { + return true; + } else if (NonPlayerInteractionSystem.areAttacksAllowed(player, damagingPlayer) + && !AnimationService.isActorPendingOrActive(world, damagingPlayer.getUuid())) { + AnimationService.MatchedAnimation attackMatch = NonPlayerAttackSystem.findViableAttack(world, damagingPlayer, player); + if (attackMatch == null) { + return true; + } + + AnimationService.MatchedAnimation defeatMatch = NonDefeatedAnimationSelector.find(world, player, attackMatch.animationId()); + if (defeatMatch == null) { + return true; + } + + player.setHealth(1.0F); + UUID instanceId = startAnimationNowWithContext( + world, + defeatMatch.animationId(), + List.of(player), + defeatMatch.actorKeys(), + defeatMatch.stages(), + NonAnimationDamageBehavior.IGNORE_DAMAGE, + true, + player, + NonPlayerAttackSystem.buildPlayerAttackContext(player.getUuid(), damagingPlayer.getUuid(), Set.of(damagingPlayer.getUuid())) + ); + if (instanceId == null) { + player.setHealth(0.0F); + return true; + } + + int cooldownTicks = config.getLastDefeatedCooldownSeconds() * 20; + if (cooldownTicks > 0) { + LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.put(player.getUuid(), nowTick + cooldownTicks); + } + + logRuntime( + "[NoN] Last defeated rescued player={} from lethal player damage. attacker={} sourceAnimation={} defeatedAnimation={} instance={}", + player.getName().getString(), + damagingPlayer.getName().getString(), + attackMatch.animationId(), + defeatMatch.animationId(), + instanceId + ); + return false; + } else { + return true; + } + } else { + return true; + } + } else { + Entity attacker = damageSource == null ? null : damageSource.getAttacker(); + Entity sourceEntity = damageSource == null ? null : damageSource.getSource(); + MobEntity damagingMob = attacker instanceof MobEntity mob ? mob : (sourceEntity instanceof MobEntity mob ? mob : null); + if (damagingMob == null) { + return true; + } + + NeedsOfNature.LastDefeatedCandidate candidate = findLastDefeatedCandidate(world, player, damagingMob); + if (candidate == null) { + return true; + } + + player.setHealth(1.0F); + UUID instanceId = startAnimationNowWithContext( + world, + candidate.defeatMatch().animationId(), + List.of(player), + candidate.defeatMatch().actorKeys(), + candidate.defeatMatch().stages(), + NonAnimationDamageBehavior.BLOCK_DAMAGE, + true, + player, + buildModeContext(true) + ); + if (instanceId == null) { + player.setHealth(0.0F); + return true; + } + + int cooldownTicks = config.getLastDefeatedCooldownSeconds() * 20; + if (cooldownTicks > 0) { + LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.put(player.getUuid(), nowTick + cooldownTicks); + } + + logRuntime( + "[NoN] Last defeated rescued player={} from lethal mob damage. mob={} sourceAnimation={} defeatedAnimation={} instance={}", + player.getName().getString(), + Registries.ENTITY_TYPE.getId(candidate.mob().getType()), + candidate.gatherCandidate().animationId(), + candidate.defeatMatch().animationId(), + instanceId + ); + return false; + } + } else { + return true; + } + } else { + return true; + } + } + ); + } + + private void registerDirectAttackBlockCleanup() { + ServerLivingEntityEvents.ALLOW_DAMAGE.register((AllowDamage)(entity, source, amount) -> { + if (entity instanceof ServerPlayerEntity player) { + if (player.getEntityWorld() instanceof ServerWorld world) { + Entity attacker = source.getAttacker(); + if (attacker instanceof MobEntity mob) { + Entity directSource = source.getSource(); + if (directSource != null && directSource != attacker) { + return true; + } + + NeedsOfNature.AttackBlockReason reason = getMobAttackBlockReason(world, mob, player); + if (reason == null) { + return true; + } + + onMobAttackBlocked(world, mob, player, reason); + return false; + } else { + return true; + } + } else { + return true; + } + } else { + return true; + } + }); + } + + private void registerPostEscapeDamageInvulnerability() { + ServerLivingEntityEvents.ALLOW_DAMAGE.register((AllowDamage)(entity, source, amount) -> { + if (entity instanceof ServerPlayerEntity player) { + return player.getEntityWorld() instanceof ServerWorld world ? !isPostEscapeDamageInvulnerable(world, player) : true; + } else { + return true; + } + }); + } + + private static void setPostEscapeDamageInvulnerable(ServerPlayerEntity player, int ticks) { + if (player != null) { + if (player.getEntityWorld() instanceof ServerWorld world) { + if (ticks <= 0) { + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.remove(player.getUuid()); + } else { + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.put(player.getUuid(), world.getTime() + ticks); + } + } + } + } + + private static boolean isPostEscapeDamageInvulnerable(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + Long untilTick = POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.get(player.getUuid()); + if (untilTick == null) { + return false; + } else if (world.getTime() >= untilTick) { + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.remove(player.getUuid(), untilTick); + return false; + } else { + return true; + } + } else { + return false; + } + } + + private void registerNormalDamageDestroyedSkinDamage() { + ServerLivingEntityEvents.AFTER_DAMAGE.register((AfterDamage)(entity, source, baseDamageTaken, damageTaken, blocked) -> { + if (!blocked && !(damageTaken <= 0.0F)) { + if (entity instanceof ServerPlayerEntity player) { + if (player.getEntityWorld() instanceof ServerWorld world) { + if (isDestroyedSkinSystemEnabled()) { + if (!AnimationService.isActorPendingOrActive(world, player.getUuid())) { + int chance = getConfig().getNormalDamageDestroyedSkinChancePercent(); + if (chance > 0) { + if (chance >= 100 || world.getRandom().nextInt(100) < chance) { + NonDestroyedSkinSystem.applyDestroyedSkinAttackDamage(world, player); + } + } + } + } + } + } + } + }); + } + + private void registerAnimationListeners() { + AnimationLifecycleEvents.START + .register( + (AnimationLifecycleEvents.Start)event -> { + ServerWorld world = event.world(); + ServerAnimationController.ActiveInstanceSnapshot snapshot = event.snapshot(); + UUID instanceId = snapshot.instanceId(); + Identifier animationId = snapshot.animationId(); + List actorUuids = snapshot.actorUuids(); + List actorKeys = snapshot.actorKeys(); + List stages = snapshot.stages(); + long startTick = snapshot.startTick(); + clearPendingGatherPairsForActors(world, actorUuids); + boolean hasPlayer = false; + List playerActors = new ArrayList<>(); + + for (UUID uuid : actorUuids) { + if (world.getEntity(uuid) instanceof ServerPlayerEntity player) { + hasPlayer = true; + playerActors.add(player); + } + } + + AnimationStartContext startContext = snapshot.startContext(); + NonAnimationSessionData sessionData = getNonSessionData(startContext); + adoptPendingBreedingRequest(world, instanceId, sessionData); + adoptPendingPregnancyHatch(world, instanceId, actorUuids, sessionData); + boolean isPregnancy = hasPlayer && sessionData != null && sessionData.isPregnancy(); + boolean isAttack = hasPlayer && sessionData != null && sessionData.isAttack(); + boolean isDefeatAnimation = hasAnimationTagWithPrefix(animationId, "defeat."); + ServerPlayerEntity attackVictim = isAttack ? NonPlayerAttackSystem.resolveAttackVictim(world, instanceId, actorUuids) : null; + applyPlayerAnimationStartEnergy(world, playerActors, animationId, startContext); + + for (UUID uuid : actorUuids) { + if (world.getEntity(uuid) instanceof EnergyHolder holder) { + holder.onNonAnimationStarted(); + } + } + + if (!playerActors.isEmpty()) { + for (ServerPlayerEntity player : playerActors) { + NonStats.incrementAnimationStarted(player); + if (actorUuids.size() > 2) { + NonStats.incrementMultiActorAnimation(player); + } + + if (actorUuids.size() >= 4) { + NonAdvancementHooks.grantGroupActivity(player); + } + + if (isWaterAnimation(animationId)) { + NonStats.incrementWaterAnimation(player); + } + + if (isAttack && !isDefeatAnimation && player == attackVictim) { + NonStats.incrementAttackAnimationStarted(player); + } else if (!isAttack) { + NonAdvancementHooks.resetTossedAround(player); + } + } + } + + if (isFillBottleAnimation(animationId) && !playerActors.isEmpty()) { + FILL_BOTTLE_STATES.putIfAbsent(instanceId, new NeedsOfNature.FillBottleState(playerActors.getFirst().getUuid(), false)); + } + + if (isPregnancy) { + for (ServerPlayerEntity player : playerActors) { + player.removeStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT)); + } + } + + NonAccessoryShedSystem.ShedState animationAccessoryShedState = NonAccessoryShedSystem.resolveShedStateForAnimation(animationId); + if (!playerActors.isEmpty()) { + for (int actorIndex = 0; actorIndex < actorUuids.size(); actorIndex++) { + if (world.getEntity(actorUuids.get(actorIndex)) instanceof ServerPlayerEntity player) { + NonAccessoryShedSystem.ShedState accessoryShedState = animationAccessoryShedState; + String actorKey = actorIndex < actorKeys.size() ? actorKeys.get(actorIndex) : null; + accessoryShedState = resolveRoleAccessoryShedState(animationId, actorKey, player, accessoryShedState); + if (isPregnancy) { + accessoryShedState = accessoryShedState.merge(NonAccessoryShedSystem.forSlot(NonTrinketsIntegration.getActiveTankSlotId(player))); + } + + if (!accessoryShedState.isEmpty()) { + NonAccessoryShedSystem.addServerShedState(world, player, accessoryShedState); + } + } + } + } + + NonPlayerAttackSystem.onAnimationStarted( + world, instanceId, actorUuids, sessionData == null ? null : sessionData.playerAttack(), startTick, isDefeatAnimation + ); + if (shouldApplyDestroyedSkinAttackDamage(isAttack, startContext, animationId) && attackVictim != null) { + NonDestroyedSkinSystem.applyDestroyedSkinAttackDamage(world, attackVictim); + } + + int startStage = resolveStartStage(stages); + AnimationStageInfo startStageInfo = stageInfoForNumber(stages, startStage); + Integer peakStage = NonPeakStages.getPeakStage(animationId); + if (peakStage != null) { + boolean grantAtStart = startStage == peakStage; + int actualAddedMl = 0; + if (grantAtStart) { + actualAddedMl = applyPeakStageEffects(world, instanceId, actorUuids, actorKeys, animationId, startStageInfo, startStage, startTick); + } + + PEAK_STATES.put( + instanceId, new NeedsOfNature.PeakState(animationId, peakStage, grantAtStart, startStage, hasPlayer, grantAtStart, actualAddedMl) + ); + if (grantAtStart && isAttack && !isManualPeakAnimation(animationId)) { + queueDefeatedTailForInstance(world, instanceId, actorUuids); + } + } + + refreshPregnancyCueRuntime(world, instanceId, startTick, animationId); + if (!playerActors.isEmpty()) { + sendEscapeControlState(world, instanceId, isAttack, isDefeatAnimation, startStageInfo, playerActors); + } + } + ); + AnimationLifecycleEvents.STOP.register((AnimationLifecycleEvents.Stop)event -> { + ServerWorld world = event.world(); + ServerAnimationController.ActiveInstanceSnapshot snapshot = event.snapshot(); + UUID instanceId = snapshot.instanceId(); + Identifier animationId = snapshot.animationId(); + List actorUuids = snapshot.actorUuids(); + List actorKeys = snapshot.actorKeys(); + clearPendingGatherPairsForActors(world, actorUuids); + clearEscapeControlState(world, instanceId, actorUuids); + NonPlayerAttackSystem.onAnimationStopped(world, instanceId); + PEAK_LIQUID_PROGRESS.remove(instanceId); + boolean isAttack = isAttackSnapshot(snapshot); + NeedsOfNature.PeakState peakState = PEAK_STATES.remove(instanceId); + if (peakState != null && peakState.peaked) { + if (isAttack) { + recordAttackOutcomeForMobs(world, actorUuids, "peak"); + } + + if (peakState.hasPlayer) { + calmMobsAfterPeakedPlayerAnimation(world, actorUuids); + } + + if (!isManualPeakAnimation(peakState.animationId) && !isFillBottleAnimation(peakState.animationId)) { + if (peakState.hasPlayer) { + int xpEligibleMl = PEAK_XP_ELIGIBLE_ML.getOrDefault(instanceId, 0); + spawnPeakXp(world, actorUuids, xpEligibleMl); + } + + scheduleReceiverDripOnPeak(world, actorUuids, actorKeys, peakState.animationId, world.getTime()); + } + } + + boolean peaked = peakState != null && peakState.peaked; + finalizeAnimationBreeding(world, instanceId, peaked); + NeedsOfNature.FillBottleState fillState = FILL_BOTTLE_STATES.remove(instanceId); + if (fillState != null) { + handleFillBottleStop(world, fillState, peaked); + } + + finalizePregnancyHatch(world, instanceId, false); + DEFEATED_QUEUED_INSTANCES.remove(instanceId); + PEAK_XP_ELIGIBLE_ML.remove(instanceId); + }); + AnimationLifecycleEvents.STAGE_ADVANCE + .register( + (AnimationLifecycleEvents.StageAdvance)event -> { + ServerWorld world = event.world(); + ServerAnimationController.ActiveInstanceSnapshot snapshot = event.snapshot(); + UUID instanceId = snapshot.instanceId(); + Identifier animationId = snapshot.animationId(); + List actorUuids = snapshot.actorUuids(); + List actorKeys = snapshot.actorKeys(); + long advanceTick = event.advanceTick(); + refreshPregnancyCueRuntime(world, instanceId, advanceTick, animationId); + routePregnancyBirthStage(world, instanceId); + AnimationStageInfo currentStage = event.currentStage(); + syncEscapeControlState(world, instanceId, actorUuids, currentStage); + NeedsOfNature.PeakState peakState = PEAK_STATES.get(instanceId); + if (peakState != null) { + int nextStage = peakState.lastStage; + Integer stageNumber = NonPeakStages.stageNumberFromId(currentStage == null ? null : currentStage.animationId()); + if (stageNumber != null) { + nextStage = stageNumber; + } + + boolean liquidGranted = peakState.liquidGranted; + int actualAddedMl = peakState.actualAddedMl; + if (!liquidGranted && stageNumber != null && stageNumber == peakState.peakStage) { + liquidGranted = true; + actualAddedMl += applyPeakStageEffects(world, instanceId, actorUuids, actorKeys, animationId, currentStage, stageNumber, advanceTick); + ServerAnimationController.ActiveInstanceSnapshot currentSnapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (isAttackSnapshot(currentSnapshot) && peakState.hasPlayer && !isManualPeakAnimation(animationId) && !isFillBottleAnimation(animationId) + ) + { + queueDefeatedTailForInstance(world, instanceId, actorUuids); + } + } else if (liquidGranted && stageNumber != null && stageNumber != peakState.peakStage) { + PEAK_LIQUID_PROGRESS.remove(instanceId); + } + + boolean peaked = peakState.peaked; + if (!peaked) { + if (stageNumber != null && stageNumber == peakState.peakStage) { + peaked = true; + } else if (peakState.lastStage == peakState.peakStage) { + peaked = true; + } + } + + PEAK_STATES.put( + instanceId, + new NeedsOfNature.PeakState( + peakState.animationId, peakState.peakStage, peaked, nextStage, peakState.hasPlayer, liquidGranted, actualAddedMl + ) + ); + } + } + ); + } + + private static NonAccessoryShedSystem.ShedState resolveRoleAccessoryShedState( + Identifier animationId, @Nullable String actorKey, ServerPlayerEntity player, NonAccessoryShedSystem.ShedState baseState + ) { + if (player != null && NonTrinketsIntegration.hasMaleRoleAccessory(player)) { + boolean femaleRole = false; + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId); + AnimationDefinitions.ActorConstraint constraint = AnimationDefinitions.resolveActorConstraintForKey(definition, actorKey); + if (constraint != null) { + femaleRole = constraint.requiredTags().contains("gender.female") || constraint.requiredTagsAny().contains("gender.female"); + } + + boolean receivesV = false; + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null && actorKey != null && roles.receiverKeys().contains(actorKey)) { + receivesV = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.V); + } + + return !femaleRole && !receivesV ? baseState : baseState.merge(NonAccessoryShedSystem.forSlot("legs/v")); + } else { + return baseState; + } + } + + private static void syncEscapeControlState(ServerWorld world, UUID instanceId, List actorUuids, @Nullable AnimationStageInfo currentStage) { + List playerActors = collectPlayerActors(world, actorUuids); + if (!playerActors.isEmpty()) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + boolean isAttack = isAttackSnapshot(snapshot); + Identifier animationId = snapshot == null ? null : snapshot.animationId(); + boolean isDefeatAnimation = hasAnimationTagWithPrefix(animationId, "defeat."); + sendEscapeControlState(world, instanceId, isAttack, isDefeatAnimation, currentStage, playerActors); + } + } + + private static void sendEscapeControlState( + ServerWorld world, + UUID instanceId, + boolean isAttack, + boolean isDefeatAnimation, + @Nullable AnimationStageInfo currentStage, + List playerActors + ) { + if (instanceId != null && playerActors != null && !playerActors.isEmpty()) { + boolean escapable = isAttack && isEscapableStage(currentStage); + if (!isAttack || !NonPlayerAttackSystem.syncEscapeControl(world, instanceId, escapable, isDefeatAnimation)) { + AttackStateS2CPayload.EscapeProfile profile = AttackStateS2CPayload.EscapeProfile.NONE; + if (isAttack) { + profile = isDefeatAnimation ? AttackStateS2CPayload.EscapeProfile.DEFEATED : AttackStateS2CPayload.EscapeProfile.NORMAL; + } + + AttackStateS2CPayload payload = new AttackStateS2CPayload(instanceId, isAttack, escapable, profile); + + for (ServerPlayerEntity player : playerActors) { + ServerPlayNetworking.send(player, payload); + } + } + } + } + + private static void clearEscapeControlState(ServerWorld world, UUID instanceId, List actorUuids) { + if (world != null && instanceId != null) { + List playerActors = collectPlayerActors(world, actorUuids); + if (!playerActors.isEmpty()) { + AttackStateS2CPayload payload = new AttackStateS2CPayload(instanceId, false, false, AttackStateS2CPayload.EscapeProfile.NONE); + + for (ServerPlayerEntity player : playerActors) { + ServerPlayNetworking.send(player, payload); + } + } + } + } + + private static boolean isEscapableStage(@Nullable AnimationStageInfo stage) { + return stage != null && stage.animationId() != null ? AnimationGameplayMetadata.escapable(stage.animationId()) : true; + } + + private static void spawnPeakXp(ServerWorld world, List actorUuids, int xpEligibleMl) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (xpEligibleMl > 0) { + double xpPerMl = getConfig().getPeakXpPerMl(); + if (!(xpPerMl <= 0.0)) { + int xpAmount = Math.max(1, (int)Math.round(xpEligibleMl * xpPerMl)); + xpAmount = Math.min(20, xpAmount); + + for (UUID uuid : actorUuids) { + Entity entity = world.getEntity(uuid); + if (entity != null) { + Vec3d pos = new Vec3d(entity.getX(), entity.getBodyY(0.5), entity.getZ()); + ExperienceOrbEntity.spawn(world, pos, xpAmount); + } + } + } + } + } + } + + private static void applyPlayerAnimationStartEnergy( + ServerWorld world, List playerActors, Identifier animationId, @Nullable AnimationStartContext startContext + ) { + if (world != null && playerActors != null && !playerActors.isEmpty()) { + if (shouldApplyPlayerAnimationStartEnergy(animationId, startContext)) { + for (ServerPlayerEntity player : playerActors) { + if (player instanceof EnergyHolder holder) { + int current = Math.max(0, holder.getEnergy()); + holder.setEnergy(Math.min(holder.getMaxEnergy(), current + 20)); + updateEnergizedEffect(player, holder); + } + } + } + } + } + + private static boolean shouldApplyPlayerAnimationStartEnergy(Identifier animationId, @Nullable AnimationStartContext startContext) { + if (animationId == null) { + return false; + } else if (startContext != null && startContext.isJoinReplacement()) { + return false; + } else if (isManualPeakAnimation(animationId)) { + return false; + } else { + return isBirthAnimation(animationId) ? false : !isFillBottleAnimation(animationId); + } + } + + private static void applyEnergyOnPeak(ServerWorld world, List actorUuids, boolean manualPeak) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + for (UUID uuid : actorUuids) { + Entity entity = world.getEntity(uuid); + if (entity instanceof EnergyHolder holder) { + if (entity instanceof ServerPlayerEntity) { + int current = Math.max(0, holder.getEnergy()); + int next = manualPeak ? (int)Math.floor(current * 0.6666666666666666) : current / 2; + holder.setEnergy(next); + } else { + holder.setEnergy(0); + float currentMult = holder.getEnergyGainMultiplier(); + if (Float.isFinite(currentMult) && currentMult > 0.0F) { + holder.setEnergyGainMultiplier(Math.min(6.0F, currentMult + 0.05F)); + } + } + } + } + } + } + + private static int applyPeakStageEffects( + ServerWorld world, + UUID instanceId, + List actorUuids, + List actorKeys, + Identifier animationId, + @Nullable AnimationStageInfo currentStage, + @Nullable Integer stageNumber, + long stageStartTick + ) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + boolean manualPeak = isManualPeakAnimation(animationId); + boolean fillBottle = isFillBottleAnimation(animationId); + int actualAddedMl = 0; + if (!fillBottle) { + applyEnergyOnPeak(world, actorUuids, manualPeak); + } + + NeedsOfNature.PeakLiquidPlan liquidPlan = new NeedsOfNature.PeakLiquidPlan(Map.of(), Map.of(), Set.of()); + if (!manualPeak && !fillBottle) { + liquidPlan = preparePeakLiquidPlan(world, actorUuids, actorKeys, animationId); + actualAddedMl = startPeakLiquidProgression(world, instanceId, animationId, liquidPlan, currentStage, stageNumber, stageStartTick); + fillHorseLiquidCollectorsOnPeak(world, actorUuids, actorKeys, animationId); + } + + trackPeakStats(world, actorUuids, manualPeak); + if (!manualPeak && !fillBottle) { + maybeStartPregnancyOnPeak(world, actorUuids, actorKeys, animationId, liquidPlan.accessoryPregnancyDonors()); + } + + applyInjectionAccessoryDurabilityCosts(world, actorUuids, actorKeys, animationId); + NonMessSystem.incrementMessOnPeak(world, actorUuids, actorKeys, animationId, liquidPlan.suppliedInjectorRoles()); + scheduleMInjectorRewardsOnPeak(world, actorUuids, actorKeys, animationId, liquidPlan.suppliedInjectorRoles()); + return actualAddedMl; + } else { + return 0; + } + } + + private static void applyInjectionAccessoryDurabilityCosts(ServerWorld world, List actorUuids, List actorKeys, Identifier animationId) { + if (world != null && actorUuids != null && !actorUuids.isEmpty() && animationId != null) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null && !roles.injectorRoles().isEmpty()) { + boolean hasV = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.V); + boolean hasA = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.A); + if (hasV || hasA) { + for (ServerPlayerEntity player : collectPlayerReceiverActors(world, actorUuids, actorKeys, animationId)) { + if (player != null && player.isAlive() && !player.isRemoved()) { + if (hasV) { + NonTrinketsIntegration.applyInjectionDurabilityCost(world, player, NonLiquidRoles.InjectorRole.V); + } + + if (hasA) { + NonTrinketsIntegration.applyInjectionDurabilityCost(world, player, NonLiquidRoles.InjectorRole.A); + } + } + } + } + } + } + } + + private static void scheduleMInjectorRewardsOnPeak( + ServerWorld world, List actorUuids, List actorKeys, Identifier animationId, Set suppliedInjectorRoles + ) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (suppliedInjectorRoles != null && suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.M)) { + for (ServerPlayerEntity player : collectPlayerReceiverActors(world, actorUuids, actorKeys, animationId)) { + if (player != null && player.isAlive() && !player.isRemoved()) { + PENDING_M_INJECTOR_REWARDS.put(player.getUuid(), new NeedsOfNature.PendingMInjectorReward(world.getRegistryKey(), -1L)); + NonStats.addMInjectorPeaksMl(player, 8); + } + } + } + } + } + + @Nullable + private static AnimationStageInfo stageInfoForNumber(@Nullable List stages, int stageNumber) { + if (stages == null || stages.isEmpty()) { + return null; + } + + if (stageNumber <= 0) { + return null; + } + + int index = stageNumber - 1; + return index >= 0 && index < stages.size() ? stages.get(index) : null; + } + + private static NeedsOfNature.PeakLiquidPlan preparePeakLiquidPlan(ServerWorld world, List actorUuids, List actorKeys, Identifier animationId) { + if (world != null && actorUuids != null && actorKeys != null && animationId != null && !actorUuids.isEmpty() && actorUuids.size() == actorKeys.size()) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null && !roles.receiverKeys().isEmpty()) { + Set suppliedInjectorRoles = EnumSet.noneOf(NonLiquidRoles.InjectorRole.class); + double liquidGainMultiplier = resolveLiquidGainMultiplier(animationId); + + for (int i = 0; i < actorUuids.size(); i++) { + NonLiquidRoles.InjectorRole role = roles.injectorRoles().get(actorKeys.get(i)); + if (role != null && role != NonLiquidRoles.InjectorRole.NONE) { + Entity entity = world.getEntity(actorUuids.get(i)); + if (entity != null && !(entity instanceof ServerPlayerEntity player && NonTrinketsIntegration.hasMaleRoleAccessory(player))) { + Identifier typeId = Registries.ENTITY_TYPE.getId(entity.getType()); + int suppliedMl = applyLiquidGainMultiplier(resolveEntityLiquidGainMl(typeId), liquidGainMultiplier); + if (suppliedMl > 0) { + suppliedInjectorRoles.add(role); + } + } + } + } + + Map receivers = new LinkedHashMap<>(); + Map gains = new LinkedHashMap<>(); + + for (int i = 0; i < actorUuids.size(); i++) { + if (roles.receiverKeys().contains(actorKeys.get(i)) + && world.getEntity(actorUuids.get(i)) instanceof ServerPlayerEntity player + && player.isAlive() + && !player.isRemoved()) { + receivers.put(player.getUuid(), player); + NeedsOfNature.LiquidGainProfile normalGain = resolveLiquidGainProfile( + world, actorUuids, actorKeys, animationId, NonGenderSystem.getLiquidTankType(player) + ); + normalGain = applyApiLiquidGainModifier(player, applyAccessoryLiquidGainMultiplier(player, normalGain)); + if (normalGain.amount() > 0) { + gains.put(player.getUuid(), normalGain); + } + } + } + + Map> accessoryDonors = new LinkedHashMap<>(); + + for (int i = 0; i < actorUuids.size(); i++) { + NonLiquidRoles.InjectorRole role = roles.injectorRoles().get(actorKeys.get(i)); + if ((role == NonLiquidRoles.InjectorRole.V || role == NonLiquidRoles.InjectorRole.A || role == NonLiquidRoles.InjectorRole.M) + && world.getEntity(actorUuids.get(i)) instanceof ServerPlayerEntity injector) { + NonTrinketsIntegration.StoredInjectorLiquid stored = NonTrinketsIntegration.peekStoredInjectorLiquid(injector); + if (stored != null && stored.entityTypeId() != null && stored.amountMl() > 0) { + List compatibleReceivers = role == NonLiquidRoles.InjectorRole.M + ? List.copyOf(receivers.values()) + : receivers.values() + .stream() + .filter(receiverx -> isLiquidInjectorAccepted(role, NonGenderSystem.getLiquidTankType(receiverx))) + .toList(); + if (!compatibleReceivers.isEmpty()) { + NonTrinketsIntegration.StoredInjectorLiquid consumed = NonTrinketsIntegration.consumeStoredInjectorLiquid(injector); + if (consumed != null && consumed.entityTypeId() != null && consumed.amountMl() > 0) { + suppliedInjectorRoles.add(role); + if (role != NonLiquidRoles.InjectorRole.M) { + int baseShare = consumed.amountMl() / compatibleReceivers.size(); + int remainder = consumed.amountMl() % compatibleReceivers.size(); + NeedsOfNature.PregnancyDonor donor = role == NonLiquidRoles.InjectorRole.V + ? new NeedsOfNature.PregnancyDonor(consumed.entityTypeId(), null) + : null; + + for (int receiverIndex = 0; receiverIndex < compatibleReceivers.size(); receiverIndex++) { + ServerPlayerEntity receiver = compatibleReceivers.get(receiverIndex); + int share = baseShare + (receiverIndex < remainder ? 1 : 0); + if (share > 0) { + NeedsOfNature.LiquidGainProfile fixedGain = new NeedsOfNature.LiquidGainProfile( + share, + 0, + LiquidHolder.LiquidComposition.ENTITY, + consumed.entityTypeId(), + Map.of(consumed.entityTypeId(), share), + null, + donor == null ? List.of() : List.of(donor) + ); + gains.merge(receiver.getUuid(), fixedGain, NeedsOfNature::mergeLiquidGainProfiles); + if (donor != null) { + accessoryDonors.computeIfAbsent(receiver.getUuid(), ignored -> new ArrayList<>()).add(donor); + } + } + } + } + } + } + } + } + } + + return new NeedsOfNature.PeakLiquidPlan(gains, accessoryDonors, suppliedInjectorRoles); + } else { + return new NeedsOfNature.PeakLiquidPlan(Map.of(), Map.of(), Set.of()); + } + } else { + return new NeedsOfNature.PeakLiquidPlan(Map.of(), Map.of(), Set.of()); + } + } + + private static NeedsOfNature.LiquidGainProfile mergeLiquidGainProfiles(NeedsOfNature.LiquidGainProfile first, NeedsOfNature.LiquidGainProfile second) { + if (first == null || first.amount() <= 0) { + return second; + } + + if (second != null && second.amount() > 0) { + Map entityAmounts = new LinkedHashMap<>(); + first.entityAmounts().forEach((id, amount) -> { + if (id != null && amount != null && amount > 0) { + entityAmounts.merge(id, amount, Integer::sum); + } + }); + second.entityAmounts().forEach((id, amount) -> { + if (id != null && amount != null && amount > 0) { + entityAmounts.merge(id, amount, Integer::sum); + } + }); + Identifier entityTypeId = entityAmounts.size() == 1 ? entityAmounts.keySet().iterator().next() : null; + LiquidHolder.LiquidComposition composition = entityTypeId == null ? LiquidHolder.LiquidComposition.MIXED : LiquidHolder.LiquidComposition.ENTITY; + NeedsOfNature.PregnancyVariantData variantData = null; + if (entityTypeId != null) { + if (entityTypeId.equals(second.entityTypeId()) && second.donorVariantData() != null) { + variantData = second.donorVariantData(); + } else if (entityTypeId.equals(first.entityTypeId())) { + variantData = first.donorVariantData(); + } + } + + List recentDonors = new ArrayList<>(first.recentDonors()); + recentDonors.addAll(second.recentDonors()); + return new NeedsOfNature.LiquidGainProfile( + first.amount() + second.amount(), + first.xpEligibleAmount() + second.xpEligibleAmount(), + composition, + entityTypeId, + Map.copyOf(entityAmounts), + variantData, + recentDonors + ); + } else { + return first; + } + } + + private static int startPeakLiquidProgression( + ServerWorld world, + UUID instanceId, + Identifier animationId, + NeedsOfNature.PeakLiquidPlan plan, + @Nullable AnimationStageInfo currentStage, + @Nullable Integer stageNumber, + long stageStartTick + ) { + if (world != null && instanceId != null && animationId != null) { + PEAK_XP_ELIGIBLE_ML.remove(instanceId); + int plannedAmount = plan == null ? 0 : plan.maxAmount(); + if (plannedAmount <= 0) { + PEAK_LIQUID_PROGRESS.remove(instanceId); + return 0; + } + + Identifier stageClipId = resolvePeakStageClipId(animationId, currentStage, stageNumber); + List cueOffsets = resolveReactiveImpactCueOffsets(stageClipId); + double cycleTicks = resolveReactiveImpactCycleTicks(currentStage, stageClipId); + cueOffsets = resolveWarpedReactiveImpactCueOffsets(currentStage, cycleTicks, cueOffsets); + double stageSpeed = resolveStageSpeed(currentStage); + if (!cueOffsets.isEmpty() && !(cycleTicks <= 0.0)) { + long stageDurationTicks = resolveLoopAdvanceIntervalTicks(animationId, stageNumber, currentStage); + if (stageDurationTicks < 0L) { + PEAK_LIQUID_PROGRESS.put( + instanceId, + NeedsOfNature.PeakLiquidProgressState.infinite(world.getRegistryKey(), plan, cycleTicks, List.copyOf(cueOffsets), stageStartTick, stageSpeed) + ); + return 0; + } else { + NeedsOfNature.FinitePulseSchedule schedule = buildFinitePulseSchedule( + stageStartTick, stageDurationTicks, cycleTicks, cueOffsets, plannedAmount, stageSpeed + ); + if (schedule != null && !schedule.ticks().isEmpty()) { + PEAK_LIQUID_PROGRESS.put( + instanceId, NeedsOfNature.PeakLiquidProgressState.finite(world.getRegistryKey(), plan, schedule.ticks(), schedule.amounts()) + ); + return 0; + } else { + PEAK_LIQUID_PROGRESS.remove(instanceId); + NeedsOfNature.LiquidPulseResult result = grantLiquidPulseToReceivers(world, plan, 0, plannedAmount, new HashSet<>()); + recordPeakLiquidPulse(instanceId, result); + return result.addedMl(); + } + } + } else { + PEAK_LIQUID_PROGRESS.remove(instanceId); + NeedsOfNature.LiquidPulseResult result = grantLiquidPulseToReceivers(world, plan, 0, plannedAmount, new HashSet<>()); + recordPeakLiquidPulse(instanceId, result); + return result.addedMl(); + } + } else { + return 0; + } + } + + private static Identifier resolvePeakStageClipId(Identifier animationId, @Nullable AnimationStageInfo currentStage, @Nullable Integer stageNumber) { + if (currentStage != null) { + Identifier playback = currentStage.effectiveAnimationId(); + if (playback != null) { + return playback; + } + + if (currentStage.animationId() != null) { + return currentStage.animationId(); + } + } + + return resolveStageId(animationId, stageNumber); + } + + private static List resolveReactiveImpactCueOffsets(@Nullable Identifier stageClipId) { + AnimationCueIndex.CueProfile profile = AnimationCueIndex.getProfile(stageClipId); + if (profile == null) { + return List.of(); + } + + List cueTicks = profile.cueTicks("reactiveimpact"); + if (cueTicks.isEmpty()) { + return List.of(); + } + + List out = new ArrayList<>(); + + for (Integer tick : cueTicks) { + if (tick != null && tick >= 0) { + out.add(tick); + } + } + + if (out.isEmpty()) { + return List.of(); + } + + out.sort(Integer::compareTo); + return List.copyOf(out); + } + + private static double resolveReactiveImpactCycleTicks(@Nullable AnimationStageInfo currentStage, @Nullable Identifier stageClipId) { + AnimationCueIndex.CueProfile profile = AnimationCueIndex.getProfile(stageClipId); + if (profile != null && profile.cycleTicks() != null) { + long cueCycleTicks = Math.max(0L, profile.cycleTicks().intValue()); + if (cueCycleTicks > 0L) { + return cueCycleTicks; + } + } + + if (currentStage != null) { + double ticks = exactCycleTicks(currentStage.animationId(), currentStage); + if (ticks > 0.0) { + return ticks; + } + } + + return 0.0; + } + + private static List resolveWarpedReactiveImpactCueOffsets(@Nullable AnimationStageInfo currentStage, double cycleTicks, List cueOffsets) { + if (currentStage != null && cueOffsets != null && !cueOffsets.isEmpty()) { + if (currentStage.loop() && !(cycleTicks <= 0.0)) { + double offsetTicks = StageTimeWarp.offsetSecondsToTicks(currentStage.cycleMidpointOffsetSeconds()); + if (!StageTimeWarp.hasWarp(cycleTicks, offsetTicks)) { + return cueOffsets; + } + + Set warped = new TreeSet<>(); + + for (Integer cueOffset : cueOffsets) { + if (cueOffset != null && cueOffset >= 0) { + double shifted = StageTimeWarp.authoredToWarpedCycleTicks(cueOffset.intValue(), cycleTicks, offsetTicks); + warped.add(Math.max(0, (int)Math.round(shifted))); + } + } + + return warped.isEmpty() ? List.of() : List.copyOf(warped); + } else { + return cueOffsets; + } + } else { + return cueOffsets; + } + } + + private static double resolveStageSpeed(@Nullable AnimationStageInfo currentStage) { + if (currentStage == null) { + return 1.0; + } + + double speed = currentStage.speed(); + return Double.isFinite(speed) && !(speed <= 0.0) ? clampAnimationSpeed(speed) : 1.0; + } + + private static double clampAnimationSpeed(double speed) { + return Double.isFinite(speed) && !(speed <= 0.0) ? Math.max(0.1, Math.min(speed, 4.0)) : 1.0; + } + + private static long elapsedWorldTicksFromAnimationTicks(double animationTicks, double speed) { + if (Double.isFinite(animationTicks) && !(animationTicks <= 0.0)) { + double safeSpeed = Double.isFinite(speed) && !(speed <= 0.0) ? speed : 1.0; + double elapsed = animationTicks / safeSpeed; + return Double.isFinite(elapsed) && !(elapsed <= 0.0) ? Math.max(0L, (long)Math.ceil(elapsed - 1.0E-9)) : 0L; + } else { + return 0L; + } + } + + @Nullable + private static NeedsOfNature.FinitePulseSchedule buildFinitePulseSchedule( + long stageStartTick, long stageDurationTicks, double cycleTicks, List cueOffsets, int totalAmountPerReceiver, double stageSpeed + ) { + if (stageDurationTicks > 0L && !(cycleTicks <= 0.0) && totalAmountPerReceiver > 0) { + if (cueOffsets != null && !cueOffsets.isEmpty()) { + long stageEndTick = stageStartTick + stageDurationTicks; + List pulseTicks = new ArrayList<>(); + List sortedOffsets = new ArrayList<>(); + + for (Integer rawOffset : cueOffsets) { + if (rawOffset != null) { + sortedOffsets.add(Math.max(0, rawOffset)); + } + } + + if (sortedOffsets.isEmpty()) { + return null; + } + + sortedOffsets.sort(Integer::compareTo); + int minOffset = sortedOffsets.getFirst(); + + for (long cycleIndex = 0L; cycleIndex < 100000L; cycleIndex++) { + double firstAnimTick = cycleIndex * cycleTicks + minOffset; + long firstPulseTick = stageStartTick + elapsedWorldTicksFromAnimationTicks(firstAnimTick, stageSpeed); + if (firstPulseTick >= stageEndTick) { + break; + } + + for (int offset : sortedOffsets) { + double cueAnimTick = cycleIndex * cycleTicks + offset; + long pulseTick = stageStartTick + elapsedWorldTicksFromAnimationTicks(cueAnimTick, stageSpeed); + if (pulseTick < stageEndTick) { + pulseTicks.add(pulseTick); + } + } + } + + if (pulseTicks.isEmpty()) { + return null; + } + + pulseTicks.sort(Long::compareTo); + int pulseCount = pulseTicks.size(); + int base = totalAmountPerReceiver / pulseCount; + int remainder = totalAmountPerReceiver % pulseCount; + List scheduledTicks = new ArrayList<>(pulseCount); + List scheduledAmounts = new ArrayList<>(pulseCount); + + for (int i = 0; i < pulseCount; i++) { + int amount = base + (i < remainder ? 1 : 0); + if (amount > 0) { + scheduledTicks.add(pulseTicks.get(i)); + scheduledAmounts.add(amount); + } + } + + return scheduledTicks.isEmpty() ? null : new NeedsOfNature.FinitePulseSchedule(List.copyOf(scheduledTicks), List.copyOf(scheduledAmounts)); + } else { + return null; + } + } else { + return null; + } + } + + private static NeedsOfNature.LiquidPulseResult grantLiquidPulseToReceivers( + ServerWorld world, NeedsOfNature.PeakLiquidPlan plan, int previousBaseAmount, int nextBaseAmount, Set donorHistoryRecordedReceivers + ) { + if (world != null && plan != null && !plan.gainsByReceiver().isEmpty()) { + int planAmount = plan.maxAmount(); + int previous = Math.max(0, Math.min(planAmount, previousBaseAmount)); + int next = Math.max(previous, Math.min(planAmount, nextBaseAmount)); + if (planAmount > 0 && next > previous) { + int totalAdded = 0; + int totalXpEligibleAdded = 0; + + for (Entry entry : plan.gainsByReceiver().entrySet()) { + if (world.getEntity(entry.getKey()) instanceof ServerPlayerEntity player + && player.isAlive() + && !player.isRemoved() + && player instanceof LiquidHolder holder) { + NeedsOfNature.LiquidGainProfile receiverGain = entry.getValue(); + if (receiverGain != null && receiverGain.amount() > 0) { + int previousTarget = (int)Math.round((double)receiverGain.amount() * previous / planAmount); + int nextTarget = (int)Math.round((double)receiverGain.amount() * next / planAmount); + int pulseAmount = Math.max(0, nextTarget - previousTarget); + if (pulseAmount > 0) { + NeedsOfNature.LiquidGainProfile pulseGain = scaleLiquidGainProfile(receiverGain, (double)pulseAmount / receiverGain.amount()); + int added = addLiquidWithComposition(holder, pulseGain); + if (added > 0) { + if (!pulseGain.recentDonors().isEmpty() && donorHistoryRecordedReceivers.add(player.getUuid())) { + recordRecentLiquidDonors(holder, pulseGain.recentDonors()); + } + + totalAdded += added; + int previousXpTarget = (int)Math.round((double)receiverGain.xpEligibleAmount() * previous / planAmount); + int nextXpTarget = (int)Math.round((double)receiverGain.xpEligibleAmount() * next / planAmount); + int pulseXpEligible = Math.max(0, nextXpTarget - previousXpTarget); + if (pulseXpEligible > 0) { + totalXpEligibleAdded += Math.min(added, (int)Math.round((double)pulseXpEligible * added / pulseAmount)); + } + + Map scaledAmounts = scaleEntityAmounts(pulseGain.entityAmounts(), added); + NonStats.addLiquidGained(player, added, scaledAmounts); + trackFieldResearch(world, player, scaledAmounts); + syncLiquidState(player); + } + } + } + } + } + + return new NeedsOfNature.LiquidPulseResult(totalAdded, totalXpEligibleAdded); + } else { + return NeedsOfNature.LiquidPulseResult.EMPTY; + } + } else { + return NeedsOfNature.LiquidPulseResult.EMPTY; + } + } + + private static void recordRecentLiquidDonors(LiquidHolder holder, List donors) { + if (holder != null && donors != null && !donors.isEmpty()) { + for (NeedsOfNature.PregnancyDonor donor : donors) { + if (donor != null) { + NonPregnancyConceptionSystem.recordRecentDonor(holder, donor.entityTypeId(), donor.variantData()); + } + } + } + } + + private static void recordPeakLiquidPulse(UUID instanceId, NeedsOfNature.LiquidPulseResult result) { + if (instanceId != null && result != null && result.xpEligibleAddedMl() > 0) { + PEAK_XP_ELIGIBLE_ML.merge(instanceId, result.xpEligibleAddedMl(), Integer::sum); + } + } + + private static void addActualAddedMlToPeakState(UUID instanceId, int addedMl) { + if (instanceId != null && addedMl > 0) { + NeedsOfNature.PeakState state = PEAK_STATES.get(instanceId); + if (state != null) { + PEAK_STATES.put( + instanceId, + new NeedsOfNature.PeakState( + state.animationId(), + state.peakStage(), + state.peaked(), + state.lastStage(), + state.hasPlayer(), + state.liquidGranted(), + state.actualAddedMl() + addedMl + ) + ); + } + } + } + + private static void trackPeakStats(ServerWorld world, List actorUuids, boolean manualPeak) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + List players = collectPlayerActors(world, actorUuids); + if (!players.isEmpty()) { + Set nonPlayerEntityTypes = collectNonPlayerEntityTypes(world, actorUuids); + + for (ServerPlayerEntity player : players) { + NonStats.incrementPeakedAnimations(player, nonPlayerEntityTypes); + NonAdvancementHooks.grantNaturalCuriosity(player); + if (manualPeak) { + NonStats.incrementManualPeak(player); + } + } + } + } + } + + static List collectPlayerActors(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + List players = new ArrayList<>(); + + for (UUID uuid : actorUuids) { + if (world.getEntity(uuid) instanceof ServerPlayerEntity player) { + players.add(player); + } + } + + return players; + } else { + return List.of(); + } + } + + static List collectPlayerReceiverActors(ServerWorld world, List actorUuids, List actorKeys, Identifier animationId) { + if (world != null && actorUuids != null && actorKeys != null && animationId != null) { + if (!actorUuids.isEmpty() && actorUuids.size() == actorKeys.size()) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null && !roles.receiverKeys().isEmpty()) { + List players = new ArrayList<>(); + + for (int i = 0; i < actorUuids.size(); i++) { + if (roles.receiverKeys().contains(actorKeys.get(i)) && world.getEntity(actorUuids.get(i)) instanceof ServerPlayerEntity player) { + players.add(player); + } + } + + return players; + } else { + return List.of(); + } + } else { + return List.of(); + } + } else { + return List.of(); + } + } + + private static List collectPlayerActors(List actors) { + if (actors != null && !actors.isEmpty()) { + List players = new ArrayList<>(); + + for (Entity actor : actors) { + if (actor instanceof ServerPlayerEntity player) { + players.add(player); + } + } + + return players; + } else { + return List.of(); + } + } + + private static Set collectNonPlayerEntityTypes(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + Set out = new LinkedHashSet<>(); + + for (UUID uuid : actorUuids) { + Entity entity = world.getEntity(uuid); + if (entity != null && !(entity instanceof ServerPlayerEntity)) { + Identifier entityId = Registries.ENTITY_TYPE.getId(entity.getType()); + out.add(entityId); + } + } + + return out; + } else { + return Set.of(); + } + } + + private static void handleFillBottleStop(ServerWorld world, NeedsOfNature.FillBottleState state, boolean peaked) { + if (world != null && state != null) { + ServerPlayerEntity player = findPlayerOnWorldServer(world, state.playerUuid()); + if (player != null && player.getEntityWorld() == world) { + if (player instanceof LiquidHolder holder) { + if (!peaked) { + if (state.reservedBottle()) { + NonItemSystem.refundGlassBottle(player); + } + } else { + int stored = holder.getLiquidStored(); + updateFilledEffect(player, holder); + int requiredMl = getFillBottleRequiredMl(holder); + if (stored >= requiredMl && hasFilledStageOneOrHigher(player)) { + ItemStack resultBottle = NonItemSystem.createBottleStackForTank(holder); + holder.setLiquidStored(stored - requiredMl); + syncLiquidState(player); + NonItemSystem.giveItemOrDrop(player, resultBottle); + NonStats.incrementFillBottle(player); + NonAdvancementHooks.checkCollectorsHabit(player); + } else { + if (state.reservedBottle()) { + NonItemSystem.refundGlassBottle(player); + } + } + } + } else { + if (state.reservedBottle()) { + NonItemSystem.refundGlassBottle(player); + } + } + } + } + } + + private static int getFillBottleRequiredMl(LiquidHolder holder) { + return holder == null ? Integer.MAX_VALUE : getFilledStageOneRequiredMl(holder.getLiquidCapacity()); + } + + private static int getFilledStageOneRequiredMl(int capacity) { + return capacity <= 0 ? Integer.MAX_VALUE : Math.max(1, (int)Math.ceil(capacity * 0.3F)); + } + + private static boolean hasFilledStageOneOrHigher(ServerPlayerEntity player) { + if (player == null) { + return false; + } + + RegistryEntry entry = Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FILLED); + StatusEffectInstance effect = player.getStatusEffect(entry); + return effect != null && effect.getAmplifier() >= 0; + } + + private static int addLiquidWithComposition(LiquidHolder holder, NeedsOfNature.LiquidGainProfile gain) { + if (holder != null && gain != null && gain.amount() > 0) { + int stored = holder.getLiquidStored(); + int capacity = holder.getLiquidCapacity(); + int room = Math.max(0, capacity - stored); + int added = Math.min(room, gain.amount()); + if (added == 0) { + return 0; + } + + if (stored <= 0) { + if (gain.composition() == LiquidHolder.LiquidComposition.ENTITY && gain.entityTypeId() != null) { + holder.setLiquidCompositionEntity(gain.entityTypeId()); + holder.setLiquidDonorVariantData(gain.donorVariantData()); + } else { + holder.setLiquidCompositionMixed(); + } + } else { + LiquidHolder.LiquidComposition existing = holder.getLiquidComposition(); + if (existing == LiquidHolder.LiquidComposition.ENTITY) { + Identifier existingType = holder.getLiquidEntityTypeId(); + boolean sameEntity = gain.composition() == LiquidHolder.LiquidComposition.ENTITY + && gain.entityTypeId() != null + && gain.entityTypeId().equals(existingType); + if (!sameEntity) { + holder.setLiquidCompositionMixed(); + } else { + holder.setLiquidDonorVariantData(gain.donorVariantData()); + } + } else if (existing != LiquidHolder.LiquidComposition.MIXED) { + holder.setLiquidCompositionMixed(); + } + } + + holder.setLiquidStored(stored + added); + return added; + } else { + return 0; + } + } + + private static Map scaleEntityAmounts(Map source, int targetTotal) { + if (source != null && !source.isEmpty() && targetTotal > 0) { + int sourceTotal = 0; + + for (Integer value : source.values()) { + if (value != null && value > 0) { + sourceTotal += value; + } + } + + if (sourceTotal <= 0) { + return Map.of(); + } + + if (targetTotal >= sourceTotal) { + return Map.copyOf(source); + } + + Map scaled = new LinkedHashMap<>(); + List> remainders = new ArrayList<>(); + int assigned = 0; + + for (Entry entry : source.entrySet()) { + if (entry.getKey() != null) { + int amount = entry.getValue() == null ? 0 : entry.getValue(); + if (amount > 0) { + double exact = (double)amount * targetTotal / sourceTotal; + int floor = (int)Math.floor(exact); + if (floor > 0) { + scaled.put(entry.getKey(), floor); + assigned += floor; + } + + remainders.add(Map.entry(entry.getKey(), exact - floor)); + } + } + } + + int remaining = targetTotal - assigned; + if (remaining <= 0) { + return Map.copyOf(scaled); + } + + remainders.sort((a, b) -> Double.compare(b.getValue(), a.getValue())); + + for (int i = 0; i < remainders.size() && remaining > 0; i++) { + Identifier entityId = remainders.get(i).getKey(); + scaled.merge(entityId, 1, Integer::sum); + remaining--; + } + + return Map.copyOf(scaled); + } else { + return Map.of(); + } + } + + public static NonConfig getConfig() { + NonConfig remote = REMOTE_EFFECTIVE_CONFIG; + return remote == null ? CONFIG : remote; + } + + public static NonConfig getLocalConfig() { + return CONFIG; + } + + public static synchronized void applyRemoteHostRules(HostConfigSyncData rules) { + REMOTE_HOST_RULES = rules; + rebuildRemoteEffectiveConfig(); + } + + public static synchronized void clearRemoteHostRules() { + REMOTE_HOST_RULES = null; + REMOTE_EFFECTIVE_CONFIG = null; + } + + static synchronized void onConfigSaved(NonConfig config) { + if (config == CONFIG && REMOTE_HOST_RULES != null) { + rebuildRemoteEffectiveConfig(); + } + } + + private static void rebuildRemoteEffectiveConfig() { + if (CONFIG != null && REMOTE_HOST_RULES != null) { + NonConfig effective = CONFIG.copy(); + REMOTE_HOST_RULES.applyTo(effective); + REMOTE_EFFECTIVE_CONFIG = effective; + } else { + REMOTE_EFFECTIVE_CONFIG = null; + } + } + + private static void clearServerRuntimeState() { + SCAN_BUDGETS.clear(); + PENDING_POST_DEATH_STATE_SYNCS.clear(); + PEAK_STATES.clear(); + PENDING_GATHER_PAIRS.clear(); + NON_PENDING_GATHER_ACTORS.clear(); + DEFEATED_QUEUED_INSTANCES.clear(); + LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.clear(); + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.clear(); + PENDING_ATTACK_ESCAPE_RESOLUTIONS.clear(); + PEAK_LIQUID_PROGRESS.clear(); + PEAK_XP_ELIGIBLE_ML.clear(); + FILL_BOTTLE_STATES.clear(); + BREEDING_PENDING_BY_PAIR.clear(); + BREEDING_PENDING_BY_REQUEST.clear(); + BREEDING_ACTIVE_BY_INSTANCE.clear(); + BREEDING_ACTIVE_INSTANCE_BY_PAIR.clear(); + PREGNANCY_HATCH_BY_INSTANCE.clear(); + PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.clear(); + PREGNANCY_QUEUED_PLAYERS.clear(); + PREGNANCY_START_RETRY_TICKS.clear(); + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.clear(); + REFLECTION_FAILURE_LOGS.clear(); + RECEIVER_DRIP.clear(); + PENDING_M_INJECTOR_REWARDS.clear(); + NonGatherSystem.clearRuntimeState(); + NonLiquidSystem.clearRuntimeState(); + NonMessSystem.clearRuntimeState(); + NonAttackSystem.clearRuntimeState(); + NonPlayerAttackSystem.clearRuntimeState(); + NonDestroyedSkinSystem.clearRuntimeState(); + NonAdvancementHooks.clearRuntimeState(); + NonTrinketsIntegration.clearRuntimeState(); + NonAccessoryShedSystem.clearAll(); + NonStatusEffects.clearRuntimeState(); + } + + public static boolean canMobAttackPlayer(ServerPlayerEntity player) { + return NonAttackSystem.canMobAttackPlayer(player); + } + + public static boolean shouldBypassVanillaPvpForDefeatedFinish(ServerPlayerEntity victim, PlayerEntity attacker) { + return NonPlayerAttackSystem.shouldBypassVanillaPvpCheck(victim, attacker); + } + + public static AnimationStartContext buildModeContext(boolean attack) { + return AnimationStartContext.empty().withSessionData(attack ? NonAnimationSessionData.attack() : NonAnimationSessionData.voluntary()); + } + + @Nullable + private static NonAnimationSessionData getNonSessionData(@Nullable AnimationStartContext context) { + return context != null && context.sessionData() instanceof NonAnimationSessionData data ? data : null; + } + + private static boolean isAttackSnapshot(@Nullable ServerAnimationController.ActiveInstanceSnapshot snapshot) { + NonAnimationSessionData data = snapshot == null ? null : getNonSessionData(snapshot.startContext()); + return data != null && data.isAttack(); + } + + private static boolean isPregnancySnapshot(@Nullable ServerAnimationController.ActiveInstanceSnapshot snapshot) { + NonAnimationSessionData data = snapshot == null ? null : getNonSessionData(snapshot.startContext()); + return data != null && data.isPregnancy(); + } + + public static void setAnimationStartProtection(ServerPlayerEntity player, int ticks) { + NonAttackSystem.setAnimationStartProtection(player, ticks); + } + + public static boolean hasAnimationStartProtection(ServerWorld world, ServerPlayerEntity player) { + return NonAttackSystem.hasAnimationStartProtection(world, player); + } + + public static boolean isActorPendingOrActive(ServerWorld world, UUID actorUuid) { + if (world == null || actorUuid == null) { + return false; + } else { + return isNoNGatheringActor(world, actorUuid) ? true : AnimationService.isActorPendingOrActive(world, actorUuid); + } + } + + public static boolean isNoNGatheringActor(ServerWorld world, UUID actorUuid) { + if (world != null && actorUuid != null) { + NeedsOfNature.NoNGatherActorState nonGatherState = NON_PENDING_GATHER_ACTORS.get(actorUuid); + if (nonGatherState != null) { + if (world.getRegistryKey().equals(nonGatherState.worldKey()) + && nonGatherState.expiresTick() > world.getTime() + && NonGatherSystem.isActorGathering(actorUuid)) { + return true; + } + + NON_PENDING_GATHER_ACTORS.remove(actorUuid, nonGatherState); + } + + return false; + } else { + return false; + } + } + + public static boolean isMobAttackFailsafeActive(ServerWorld world, UUID mobUuid) { + return NonAttackSystem.isMobAttackFailsafeActive(world, mobUuid); + } + + public static List getActivePlayerCooldownDebugLines(ServerWorld world, ServerPlayerEntity player) { + List lines = new ArrayList<>(); + if (world != null && player != null) { + long nowTick = getGlobalTick(world); + Long lastDefeatedUntil = LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.get(player.getUuid()); + if (lastDefeatedUntil != null) { + long remaining = lastDefeatedUntil - nowTick; + if (remaining > 0L) { + lines.add("last defeated: " + remaining + "t/" + formatCooldownSeconds(remaining) + "s"); + } else { + LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.remove(player.getUuid(), lastDefeatedUntil); + } + } + + Long postEscapeDamageInvulnerableUntil = POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.get(player.getUuid()); + if (postEscapeDamageInvulnerableUntil != null) { + long remaining = postEscapeDamageInvulnerableUntil - nowTick; + if (remaining > 0L) { + lines.add("post-escape damage invulnerability: " + remaining + "t/" + formatCooldownSeconds(remaining) + "s"); + } else { + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.remove(player.getUuid(), postEscapeDamageInvulnerableUntil); + } + } + + if (AnimationService.isActorPendingOrActive(world, player.getUuid())) { + lines.add("animation active/pending"); + } + + if (PREGNANCY_QUEUED_PLAYERS.contains(player.getUuid())) { + lines.add("pregnancy queued"); + } + + Long pregnancyRetry = PREGNANCY_START_RETRY_TICKS.get(player.getUuid()); + if (pregnancyRetry != null) { + long remaining = pregnancyRetry - nowTick; + if (remaining > 0L) { + lines.add("pregnancy retry: " + remaining + "t/" + formatCooldownSeconds(remaining) + "s"); + } + } + + NeedsOfNature.PendingMInjectorReward pendingReward = PENDING_M_INJECTOR_REWARDS.get(player.getUuid()); + if (pendingReward != null) { + lines.add( + pendingReward.freeSinceTick() < 0L + ? "M reward pending: waiting for animation end" + : "M reward pending: free for " + Math.max(0L, nowTick - pendingReward.freeSinceTick()) + "t" + ); + } + + return lines; + } else { + return lines; + } + } + + private static String formatCooldownSeconds(long ticks) { + return String.format(Locale.ROOT, "%.1f", ticks / 20.0); + } + + static int clearAttackOutcomeFailsafeEntries() { + return NonAttackSystem.clearAttackOutcomeFailsafeEntries(); + } + + public static String getMobAttackFailsafeDebug(ServerWorld world, UUID mobUuid) { + return NonAttackSystem.getMobAttackFailsafeDebug(world, mobUuid); + } + + public static String formatTicksForDebug(long ticks) { + long safeTicks = Math.max(0L, ticks); + double seconds = safeTicks / 20.0; + return safeTicks + "t/" + String.format(Locale.ROOT, "%.1fs", seconds); + } + + public static boolean handleAnimationBreedingAttempt(ServerWorld world, AnimalEntity first, AnimalEntity second) { + if (world != null && first != null && second != null) { + NonConfig config = getConfig(); + if (config == null || !config.useAnimationBreeding()) { + return false; + } + + if (first == second) { + return true; + } + + if (first.isAlive() && second.isAlive() && !first.isRemoved() && !second.isRemoved()) { + NeedsOfNature.BreedingPairKey pairKey = breedingPairKey(first.getUuid(), second.getUuid()); + if (pairKey == null) { + return true; + } else if (BREEDING_PENDING_BY_PAIR.containsKey(pairKey)) { + return true; + } else if (BREEDING_ACTIVE_INSTANCE_BY_PAIR.containsKey(pairKey)) { + return true; + } else if (config.requireMaleFemaleForBreeding() && !isMaleFemalePair(first, second)) { + return true; + } else { + List actors = new ArrayList<>(2); + actors.add(first); + actors.add(second); + actors.sort(Comparator.comparingInt(Entity::getId)); + AnimationService.MatchedAnimation match = NonAnimationMatcher.findAutomatic(world, actors, Set.of(), null, false, null, Set.of()); + if (match == null) { + return false; + } else { + List actorKeys = match.actorKeys(); + if (actorKeys != null && actorKeys.size() == actors.size()) { + List stages = match.stages(); + UUID requestId = UUID.randomUUID(); + long expiresTick = world.getTime() + 600L; + NeedsOfNature.PendingBreedingState pending = new NeedsOfNature.PendingBreedingState( + world.getRegistryKey(), pairKey.firstUuid(), pairKey.secondUuid(), expiresTick + ); + BREEDING_PENDING_BY_PAIR.put(pairKey, pending); + BREEDING_PENDING_BY_REQUEST.put(requestId, pairKey); + AnimationStartContext startContext = AnimationStartContext.empty().withSessionData(NonAnimationSessionData.breeding(requestId)); + UUID started = startAnimationNowWithContext( + world, match.animationId(), actors, actorKeys, stages, NonAnimationDamageBehavior.STOP_ON_DAMAGE, false, null, startContext + ); + return true; + } else { + return false; + } + } + } + } else { + return true; + } + } else { + return false; + } + } + + @Nullable + private static NeedsOfNature.BreedingPairKey breedingPairKey(UUID firstUuid, UUID secondUuid) { + if (firstUuid == null || secondUuid == null) { + return null; + } else if (firstUuid.equals(secondUuid)) { + return null; + } else { + return firstUuid.compareTo(secondUuid) <= 0 + ? new NeedsOfNature.BreedingPairKey(firstUuid, secondUuid) + : new NeedsOfNature.BreedingPairKey(secondUuid, firstUuid); + } + } + + private static boolean isMaleFemalePair(Entity first, Entity second) { + if (first != null && second != null) { + boolean firstCanBeMale = NonGenderSystem.hasMaleTag(first); + boolean firstCanBeFemale = NonGenderSystem.hasFemaleTag(first); + boolean secondCanBeMale = NonGenderSystem.hasMaleTag(second); + boolean secondCanBeFemale = NonGenderSystem.hasFemaleTag(second); + return firstCanBeMale && secondCanBeFemale || firstCanBeFemale && secondCanBeMale; + } else { + return false; + } + } + + private static void adoptPendingBreedingRequest(ServerWorld world, UUID instanceId, @Nullable NonAnimationSessionData sessionData) { + if (world != null && instanceId != null && sessionData != null) { + UUID requestId = sessionData.breedingRequestId(); + if (requestId != null) { + NeedsOfNature.BreedingPairKey pairKey = BREEDING_PENDING_BY_REQUEST.remove(requestId); + if (pairKey != null) { + NeedsOfNature.PendingBreedingState pending = BREEDING_PENDING_BY_PAIR.remove(pairKey); + if (pending != null) { + if (world.getRegistryKey().equals(pending.worldKey())) { + NeedsOfNature.ActiveBreedingState active = new NeedsOfNature.ActiveBreedingState( + pending.worldKey(), pending.firstUuid(), pending.secondUuid() + ); + BREEDING_ACTIVE_BY_INSTANCE.put(instanceId, active); + BREEDING_ACTIVE_INSTANCE_BY_PAIR.put(pairKey, instanceId); + } + } + } + } + } + } + + private static void adoptPendingPregnancyHatch(ServerWorld world, UUID instanceId, List actorUuids, @Nullable NonAnimationSessionData sessionData) { + if (world != null && instanceId != null && actorUuids != null && sessionData != null && sessionData.isPregnancy()) { + NonAnimationSessionData.Pregnancy pregnancy = sessionData.pregnancy(); + if (pregnancy != null) { + ServerPlayerEntity player = null; + + for (UUID actorUuid : actorUuids) { + if (world.getEntity(actorUuid) instanceof ServerPlayerEntity p) { + player = p; + break; + } + } + + if (player != null) { + Identifier entityTypeId = pregnancy.sourceEntityTypeId(); + Identifier birthEntityTypeId = pregnancy.birthEntityTypeId(); + String birthMode = pregnancy.birthMode(); + int spawnCount = Math.max(1, Math.min(16, pregnancy.offspringCount())); + NeedsOfNature.PregnancyVariantData variantData = pregnancy.variantData(); + PREGNANCY_HATCH_BY_INSTANCE.put( + instanceId, + new NeedsOfNature.PregnancyHatchState(player.getUuid(), entityTypeId, birthEntityTypeId, birthMode, spawnCount, spawnCount, 0, variantData) + ); + PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.put(player.getUuid(), instanceId); + PREGNANCY_QUEUED_PLAYERS.remove(player.getUuid()); + PREGNANCY_START_RETRY_TICKS.remove(player.getUuid()); + } + } + } + } + + private static void finalizeAnimationBreeding(ServerWorld world, UUID instanceId, boolean peaked) { + if (world != null && instanceId != null) { + NeedsOfNature.ActiveBreedingState active = BREEDING_ACTIVE_BY_INSTANCE.remove(instanceId); + if (active != null) { + NeedsOfNature.BreedingPairKey pairKey = breedingPairKey(active.firstUuid(), active.secondUuid()); + if (pairKey != null) { + BREEDING_ACTIVE_INSTANCE_BY_PAIR.remove(pairKey, instanceId); + } + + if (peaked) { + if (world.getRegistryKey().equals(active.worldKey())) { + Entity firstEntity = world.getEntity(active.firstUuid()); + Entity secondEntity = world.getEntity(active.secondUuid()); + if (firstEntity instanceof AnimalEntity first && secondEntity instanceof AnimalEntity second) { + if (first.isAlive() && second.isAlive() && !first.isRemoved() && !second.isRemoved()) { + if (first.getEntityWorld() == world && second.getEntityWorld() == world) { + PassiveEntity baby = first.createChild(world, second); + if (baby != null) { + baby.setBaby(true); + baby.refreshPositionAndAngles(first.getX(), first.getY(), first.getZ(), 0.0F, 0.0F); + first.breed(world, second, baby); + world.spawnEntityAndPassengers(baby); + } + } + } + } + } + } + } + } + } + + private static void clearPendingBreedingForActors(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty() && !BREEDING_PENDING_BY_PAIR.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + Set actorSet = new HashSet<>(actorUuids); + Set removedPairs = new HashSet<>(); + BREEDING_PENDING_BY_PAIR.entrySet().removeIf(entry -> { + NeedsOfNature.PendingBreedingState state = entry.getValue(); + NeedsOfNature.BreedingPairKey key = entry.getKey(); + if (state == null || key == null) { + return true; + } + + if (!worldKey.equals(state.worldKey())) { + return false; + } + + boolean matchesActors = actorSet.contains(key.firstUuid()) && actorSet.contains(key.secondUuid()); + if (matchesActors) { + removedPairs.add(key); + } + + return matchesActors; + }); + if (!removedPairs.isEmpty()) { + BREEDING_PENDING_BY_REQUEST.entrySet().removeIf(entry -> removedPairs.contains(entry.getValue())); + } + } + } + + private static void prunePendingAnimationBreeding(ServerWorld world, long nowTick) { + if (world != null && !BREEDING_PENDING_BY_PAIR.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + Set removedPairs = new HashSet<>(); + BREEDING_PENDING_BY_PAIR.entrySet().removeIf(entry -> { + NeedsOfNature.PendingBreedingState state = entry.getValue(); + NeedsOfNature.BreedingPairKey key = entry.getKey(); + if (state == null || key == null) { + return true; + } + + if (!worldKey.equals(state.worldKey())) { + return false; + } + + if (state.expiresTick() > nowTick) { + return false; + } + + removedPairs.add(key); + return true; + }); + if (!removedPairs.isEmpty()) { + BREEDING_PENDING_BY_REQUEST.entrySet().removeIf(entry -> removedPairs.contains(entry.getValue())); + } + } + } + + private static void trackPendingGatherPairs(ServerWorld world, List actors) { + if (world != null && actors != null && !actors.isEmpty()) { + long expiresTick = world.getTime() + 6000L; + RegistryKey worldKey = world.getRegistryKey(); + List playerUuids = new ArrayList<>(); + List mobUuids = new ArrayList<>(); + + for (Entity actor : actors) { + if (actor != null && actor.getEntityWorld() == world) { + if (actor instanceof ServerPlayerEntity) { + playerUuids.add(actor.getUuid()); + } else if (actor instanceof MobEntity) { + mobUuids.add(actor.getUuid()); + } + } + } + + if (!playerUuids.isEmpty() && !mobUuids.isEmpty()) { + for (UUID mobUuid : mobUuids) { + for (UUID playerUuid : playerUuids) { + PENDING_GATHER_PAIRS.put(new NeedsOfNature.GatherPairKey(mobUuid, playerUuid), new NeedsOfNature.GatherPairState(worldKey, expiresTick)); + } + } + } + } + } + + private static void clearPendingGatherPairsForActors(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty() && !PENDING_GATHER_PAIRS.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + Set actorSet = new HashSet<>(actorUuids); + PENDING_GATHER_PAIRS.entrySet().removeIf(entry -> { + NeedsOfNature.GatherPairState state = entry.getValue(); + NeedsOfNature.GatherPairKey key = entry.getKey(); + if (state == null || key == null) { + return true; + } else { + return !worldKey.equals(state.worldKey()) ? false : actorSet.contains(key.mobUuid()) && actorSet.contains(key.playerUuid()); + } + }); + } + } + + private static void prunePendingGatherPairs(ServerWorld world, long nowTick) { + if (world != null && !PENDING_GATHER_PAIRS.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + PENDING_GATHER_PAIRS.entrySet().removeIf(entry -> { + NeedsOfNature.GatherPairState state = entry.getValue(); + NeedsOfNature.GatherPairKey key = entry.getKey(); + if (state == null || key == null) { + return true; + } else if (!worldKey.equals(state.worldKey())) { + return false; + } else if (state.expiresTick() <= nowTick) { + return true; + } else { + return !isActorPendingOrActive(world, key.mobUuid()) ? true : !isActorPendingOrActive(world, key.playerUuid()); + } + }); + NON_PENDING_GATHER_ACTORS.entrySet().removeIf(entry -> { + NeedsOfNature.NoNGatherActorState state = entry.getValue(); + return state == null ? true : worldKey.equals(state.worldKey()) && state.expiresTick() <= nowTick; + }); + } + } + + static boolean isMobGatheringOrActiveWithPlayer(ServerWorld world, MobEntity mob, ServerPlayerEntity player) { + if (world != null && mob != null && player != null) { + UUID playerInstance = getActivePlayerInstance(player); + if (instanceContainsActor(world, playerInstance, mob.getUuid())) { + return true; + } + + NeedsOfNature.GatherPairKey key = new NeedsOfNature.GatherPairKey(mob.getUuid(), player.getUuid()); + NeedsOfNature.GatherPairState state = PENDING_GATHER_PAIRS.get(key); + if (state == null) { + return false; + } + + if (!world.getRegistryKey().equals(state.worldKey()) || state.expiresTick() <= world.getTime()) { + PENDING_GATHER_PAIRS.remove(key); + return false; + } + + if (isActorPendingOrActive(world, mob.getUuid()) && isActorPendingOrActive(world, player.getUuid())) { + return true; + } + + PENDING_GATHER_PAIRS.remove(key); + return false; + } else { + return false; + } + } + + public static NeedsOfNature.AttackBlockReason getMobAttackBlockReason(ServerWorld world, MobEntity mob, ServerPlayerEntity player) { + return NonAttackSystem.getMobAttackBlockReason(world, mob, player); + } + + public static void onMobAttackBlocked(ServerWorld world, MobEntity mob, ServerPlayerEntity player, NeedsOfNature.AttackBlockReason reason) { + NonAttackSystem.onMobAttackBlocked(world, mob, player, reason); + } + + private static boolean hasPlayerAndHostileActor(List actors) { + return NonAttackSystem.hasPlayerAndHostileActor(actors); + } + + private static void recordAttackOutcomeForMobs(ServerWorld world, List actorUuids, String reason) { + NonAttackSystem.recordAttackOutcomeForMobs(world, actorUuids, reason); + } + + public static boolean enqueueAnimation( + ServerWorld world, + Identifier animationId, + List actors, + int insertIndex, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext, + ServerPlayerEntity requester + ) { + if (world != null && animationId != null && actors != null && !actors.isEmpty()) { + boolean effectiveIgnoreAttackers = ignoreAttackers || hasPlayerAndHostileActor(actors); + return ServerAnimationController.enqueueForPlayerWithContext( + world, + requester, + animationId, + actors, + insertIndex, + damageBehavior, + effectiveIgnoreAttackers, + startContext == null ? AnimationStartContext.empty() : startContext, + false + ); + } else { + return false; + } + } + + public static void clearQueuedAnimations(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + AnimationService.clearQueuedAnimations(world, player); + } + } + + public static List getInstanceActors(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + return snapshot == null ? List.of() : snapshot.actorUuids(); + } else { + return List.of(); + } + } + + public static boolean isInstanceAttack(ServerWorld world, UUID instanceId) { + return world != null && instanceId != null ? isAttackSnapshot(ServerAnimationController.getActiveInstanceSnapshot(world, instanceId)) : false; + } + + static boolean isVoluntaryManualPeakInstance(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + NonAnimationSessionData data = snapshot == null ? null : getNonSessionData(snapshot.startContext()); + return data != null && data.isVoluntary() && isManualPeakAnimation(snapshot.animationId()); + } else { + return false; + } + } + + public static void sendDebugChat(ServerPlayerEntity player, String message) { + sendDebugChat(player, NonDebugChatCategory.INFO, message); + } + + public static void sendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, String message) { + sendDebugChat(player, category, Text.literal(message)); + } + + public static void sendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, Text message) { + if (player != null) { + NonDiagnostics.sendDebugChat(player, category, Text.translatable("debug.needsofnature.prefix", new Object[]{message}), false); + } + } + + public static void sendDebugChatToNearby(ServerWorld world, Entity source, double radius, String message) { + sendDebugChatToNearby(world, source, radius, NonDebugChatCategory.INFO, message); + } + + public static void sendDebugChatToNearby(ServerWorld world, Entity source, double radius, NonDebugChatCategory category, String message) { + sendDebugChatToNearby(world, source, radius, category, Text.literal(message)); + } + + public static void sendDebugChatToNearby(ServerWorld world, Entity source, double radius, NonDebugChatCategory category, Text message) { + if (world != null && source != null) { + double maxDistSq = radius * radius; + + for (ServerPlayerEntity p : world.getPlayers()) { + if (p.squaredDistanceTo(source) <= maxDistSq) { + NonDiagnostics.sendDebugChat(p, category, Text.translatable("debug.needsofnature.prefix", new Object[]{message}), false); + } + } + } + } + + public static void logSetupWarning(String template, Object... args) { + NonDiagnostics.logSetupWarning(template, args); + } + + public static void logRuntime(String template, Object... args) { + NonDiagnostics.logRuntime(template, args); + } + + public static void logSetupError(String template, Object... args) { + NonDiagnostics.logSetupError(template, args); + } + + public static void queueSetupChat(String message) { + NonDiagnostics.queueSetupChat(message); + } + + public static String formatLogTemplate(String template, Object... args) { + return NonDiagnostics.formatLogTemplate(template, args); + } + + public static boolean consumeScanBudget(ServerWorld world) { + if (world == null) { + return false; + } + + RegistryKey key = world.getRegistryKey(); + long now = world.getTime(); + int configured = CONFIG.getScanBudgetPerTick(); + NeedsOfNature.ScanBudget budget = SCAN_BUDGETS.get(key); + if (budget == null || budget.tick != now) { + budget = new NeedsOfNature.ScanBudget(now, configured); + } + + if (budget.remaining <= 0) { + SCAN_BUDGETS.put(key, budget); + return false; + } else { + SCAN_BUDGETS.put(key, new NeedsOfNature.ScanBudget(now, budget.remaining - 1)); + return true; + } + } + + private void registerLoopProgression() { + ServerTickEvents.END_WORLD_TICK.register((EndWorldTick)world -> { + long now = world.getTime(); + tickPregnancyCueRuntime(world, now); + tickLiquidDecay(world); + NonPregnancyConceptionSystem.tick(world); + tickPeakLiquidProgression(world, now); + tickReceiverDripParticles(world, now); + tickPregnancy(world); + tickMInjectorRewards(world, now); + tickPendingAttackEscapeResolutions(world, now); + tickPostDeathStateSync(world, now); + NonGatherSystem.tickServerWorld(world); + NonPlayerInteractionSystem.tick(world); + NonPlayerAttackSystem.tick(world); + NonAccessoryShedSystem.tickServerWorld(world); + NonMessSystem.tickMessCleaning(world, now); + if (now % 20L == 0L) { + prunePendingGatherPairs(world, now); + prunePendingAnimationBreeding(world, now); + pruneAttackOutcomeFailsafe(world, now); + pruneAttackBlockDebug(world, now); + } + }); + } + + private static void tickMInjectorRewards(ServerWorld world, long now) { + if (world != null && !PENDING_M_INJECTOR_REWARDS.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + + for (Entry entry : PENDING_M_INJECTOR_REWARDS.entrySet()) { + UUID playerUuid = entry.getKey(); + NeedsOfNature.PendingMInjectorReward reward = entry.getValue(); + if (playerUuid != null && reward != null) { + if (worldKey.equals(reward.worldKey())) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(playerUuid); + if (player == null || player.getEntityWorld() != world || !player.isAlive() || player.isRemoved()) { + PENDING_M_INJECTOR_REWARDS.remove(playerUuid, reward); + } else if (AnimationService.isActorPendingOrActive(world, playerUuid)) { + if (reward.freeSinceTick() >= 0L) { + PENDING_M_INJECTOR_REWARDS.put(playerUuid, new NeedsOfNature.PendingMInjectorReward(reward.worldKey(), -1L)); + } + } else { + long freeSince = reward.freeSinceTick(); + if (freeSince < 0L) { + PENDING_M_INJECTOR_REWARDS.put(playerUuid, new NeedsOfNature.PendingMInjectorReward(reward.worldKey(), now)); + } else if (now - freeSince >= 20L) { + applyMInjectorReward(world, player); + PENDING_M_INJECTOR_REWARDS.remove(playerUuid, reward); + } + } + } + } else if (playerUuid != null) { + PENDING_M_INJECTOR_REWARDS.remove(playerUuid); + } + } + } + } + + private static void applyMInjectorReward(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + int foodBefore = player.getHungerManager().getFoodLevel(); + player.getHungerManager().add(1, 0.0F); + if (foodBefore < 20) { + grantEmergencySnackAdvancement(player); + } + + world.playSound( + null, + player.getX(), + player.getY() + player.getStandingEyeHeight() * 0.5, + player.getZ(), + SoundEvents.ITEM_HONEY_BOTTLE_DRINK, + SoundCategory.PLAYERS, + 1.0F, + 0.95F + world.getRandom().nextFloat() * 0.1F + ); + } + } + + private static void tickPeakLiquidProgression(ServerWorld world, long nowTick) { + if (world != null && !PEAK_LIQUID_PROGRESS.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + + for (Entry entry : PEAK_LIQUID_PROGRESS.entrySet()) { + UUID instanceId = entry.getKey(); + NeedsOfNature.PeakLiquidProgressState state = entry.getValue(); + if (instanceId != null && state != null) { + if (worldKey.equals(state.worldKey)) { + NeedsOfNature.PeakState peakState = PEAK_STATES.get(instanceId); + if (peakState == null || peakState.lastStage() != peakState.peakStage()) { + PEAK_LIQUID_PROGRESS.remove(instanceId); + } else if (!state.infinite) { + while (state.scheduleIndex < state.scheduledTicks.size() && state.scheduledTicks.get(state.scheduleIndex) <= nowTick) { + int amount = state.scheduledAmounts.get(state.scheduleIndex); + state.scheduleIndex++; + if (amount > 0) { + int previous = state.distributedBaseMl; + int next = Math.min(state.plan.maxAmount(), previous + amount); + NeedsOfNature.LiquidPulseResult result = grantLiquidPulseToReceivers( + world, state.plan, previous, next, state.donorHistoryRecordedReceivers + ); + state.distributedBaseMl = next; + recordPeakLiquidPulse(instanceId, result); + if (result.addedMl() > 0) { + addActualAddedMlToPeakState(instanceId, result.addedMl()); + } + } + } + + if (state.scheduleIndex >= state.scheduledTicks.size()) { + PEAK_LIQUID_PROGRESS.remove(instanceId); + } + } else if (state.remainingBaseMl > 0 && !(state.cycleTicks <= 0.0) && !state.cueOffsets.isEmpty()) { + int guard = 0; + + while (true) { + if (state.remainingBaseMl > 0 && guard < 64) { + int cueOffset = Math.max(0, state.cueOffsets.get(state.cueIndex)); + double cueAnimTick = state.cycleIndex * state.cycleTicks + cueOffset; + long dueTick = state.stageStartTick + elapsedWorldTicksFromAnimationTicks(cueAnimTick, state.stageSpeed); + if (nowTick >= dueTick) { + int amount = Math.min(5, state.remainingBaseMl); + state.remainingBaseMl -= amount; + int previous = state.distributedBaseMl; + int next = Math.min(state.plan.maxAmount(), previous + amount); + NeedsOfNature.LiquidPulseResult result = grantLiquidPulseToReceivers( + world, state.plan, previous, next, state.donorHistoryRecordedReceivers + ); + state.distributedBaseMl = next; + recordPeakLiquidPulse(instanceId, result); + if (result.addedMl() > 0) { + addActualAddedMlToPeakState(instanceId, result.addedMl()); + } + + state.cueIndex++; + if (state.cueIndex >= state.cueOffsets.size()) { + state.cueIndex = 0; + state.cycleIndex++; + } + + guard++; + continue; + } + } + + if (state.remainingBaseMl <= 0) { + PEAK_LIQUID_PROGRESS.remove(instanceId); + } + break; + } + } else { + PEAK_LIQUID_PROGRESS.remove(instanceId); + } + } + } else if (instanceId != null) { + PEAK_LIQUID_PROGRESS.remove(instanceId); + } + } + } + } + + private static void calmMobsAfterPeakedPlayerAnimation(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + Set playerUuids = new HashSet<>(); + + for (UUID actorUuid : actorUuids) { + if (world.getEntity(actorUuid) instanceof ServerPlayerEntity player) { + playerUuids.add(player.getUuid()); + } + } + + if (!playerUuids.isEmpty()) { + for (UUID actorUuid : actorUuids) { + if (world.getEntity(actorUuid) instanceof MobEntity mob) { + if (mob.getTarget() instanceof ServerPlayerEntity player && playerUuids.contains(player.getUuid())) { + mob.setTarget(null); + } + + if (mob instanceof Angerable angerable) { + LazyEntityReference angryAt = angerable.getAngryAt(); + UUID angryAtUuid = angryAt == null ? null : angryAt.getUuid(); + if (angryAtUuid == null || playerUuids.contains(angryAtUuid)) { + angerable.stopAnger(); + } + } + } + } + } + } + } + + private static void pruneAttackOutcomeFailsafe(ServerWorld world, long nowTick) { + NonAttackSystem.pruneAttackOutcomeFailsafe(world, nowTick); + } + + private static void pruneAttackBlockDebug(ServerWorld world, long nowTick) { + NonAttackSystem.pruneAttackBlockDebug(world, nowTick); + } + + private void registerLiquidSync() { + ServerPlayConnectionEvents.JOIN.register((Join)(handler, sender, server) -> { + NonGenderSystem.ensurePlayerGenderInitialized(handler.player); + NonDestroyedSkinSystem.cacheDestroyedSkinStage(handler.player); + NonDestroyedSkinSystem.syncDestroyedSkinStageToOtherPlayers(handler.player); + syncLiquidState(handler.player); + syncPregnancyState(handler.player); + NonPregnancyConceptionSystem.syncState(handler.player); + syncPlayerRuntimeSettings(handler.player); + syncHostConfigState(handler.player); + syncAnimationCatalogState(handler.player); + syncDestroyedSkins(handler.player); + NonMessSystem.syncMessStates(handler.player); + NonAccessoryShedSystem.syncAllToViewer(handler.player); + }); + ServerPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, server) -> { + UUID playerUuid = handler.player.getUuid(); + FILL_BOTTLE_STATES.entrySet().removeIf(entry -> playerUuid.equals(entry.getValue().playerUuid())); + PENDING_GATHER_PAIRS.entrySet().removeIf(entry -> playerUuid.equals(entry.getKey().playerUuid())); + LAST_DEFEATED_COOLDOWN_UNTIL_BY_PLAYER.remove(playerUuid); + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.remove(playerUuid); + PENDING_ATTACK_ESCAPE_RESOLUTIONS.remove(playerUuid); + PENDING_POST_DEATH_STATE_SYNCS.remove(playerUuid); + UUID activeHatchInstance = PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.remove(playerUuid); + if (activeHatchInstance != null) { + PREGNANCY_HATCH_BY_INSTANCE.remove(activeHatchInstance); + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(activeHatchInstance); + } + + PREGNANCY_QUEUED_PLAYERS.remove(playerUuid); + PREGNANCY_START_RETRY_TICKS.remove(playerUuid); + NonTrinketsIntegration.clearAccessoryState(playerUuid); + NonAccessoryShedSystem.clearServerShedState(handler.player.getEntityWorld(), playerUuid); + NonAdvancementHooks.clearPlayerState(playerUuid); + NonDestroyedSkinSystem.clearPlayerRuntimeState(server, playerUuid); + }); + } + + public static void syncLiquidState(ServerPlayerEntity player) { + NonLiquidSystem.syncLiquidState(player); + } + + private static void syncPregnancyState(ServerPlayerEntity player) { + if (player != null) { + Identifier entityTypeId = getPregnantEntityTypeId(player); + String raw = entityTypeId == null ? "" : entityTypeId.toString(); + ServerPlayNetworking.send(player, new PregnancyStateS2CPayload(raw)); + } + } + + public static void syncPlayerRuntimeSettings(ServerPlayerEntity player) { + if (player != null) { + NonConfig config = getConfig(); + int attackHits = config.getAttackEscapeHits() + NonTrinketsIntegration.getAccessoryEffects(player).attackEscapeHitsAdd(); + attackHits = Math.max(1, Math.min(50, attackHits)); + ServerPlayNetworking.send(player, new PlayerRuntimeSettingsS2CPayload(attackHits)); + } + } + + public static void syncPlayerRuntimeSettingsToAll(@Nullable MinecraftServer server) { + if (server != null) { + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + syncPlayerRuntimeSettings(player); + } + } + } + + private static void syncHostConfigState(ServerPlayerEntity player) { + if (player != null) { + NonConfig config = getConfig(); + HostConfigSyncData data = HostConfigSyncData.fromConfig(config); + data.genderSpawnByEntity = new LinkedHashMap<>(NonGenderSystem.resolveEffectiveGenderSpawnMap()); + data.entityProfilesByEntity = new LinkedHashMap<>(NonEntityProfiles.resolveEffectiveProfilesMap()); + data.liquidGainByEntity = new LinkedHashMap<>(resolveEffectiveLiquidGainMap()); + data.liquidColorByEntity = new LinkedHashMap<>(resolveEffectiveLiquidColorMap()); + String json = HOST_CONFIG_SYNC_GSON.toJson(data); + String editableJson = HOST_CONFIG_SYNC_GSON.toJson(HostConfigSyncData.fromEditableConfig(CONFIG)); + ServerPlayNetworking.send(player, new HostConfigSyncS2CPayload(json, editableJson, SERVER_CONFIG_REVISION.get(), canEditServerSettings(player))); + } + } + + private static boolean canEditServerSettings(@Nullable ServerPlayerEntity player) { + if (player != null && player.getEntityWorld().getServer() != null) { + MinecraftServer server = player.getEntityWorld().getServer(); + PlayerConfigEntry entry = new PlayerConfigEntry(player.getGameProfile()); + return server.isHost(entry) || server.getPlayerManager().isOperator(entry); + } else { + return false; + } + } + + private static void registerServerSettingsNetworking() { + ServerPlayNetworking.registerGlobalReceiver(ServerConfigRequestC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + syncHostConfigState(context.player()); + syncAnimationCatalogState(context.player()); + })); + ServerPlayNetworking.registerGlobalReceiver( + ServerConfigUpdateC2SPayload.ID, (payload, context) -> context.server().execute(() -> applyServerConfigUpdate(context.player(), payload)) + ); + } + + private static void applyServerConfigUpdate(ServerPlayerEntity player, ServerConfigUpdateC2SPayload payload) { + if (player != null && payload != null) { + if (!canEditServerSettings(player)) { + rejectServerConfigUpdate(player, "config.needsofnature.server_settings.no_permission"); + } else if (payload.revision() != SERVER_CONFIG_REVISION.get()) { + rejectServerConfigUpdate(player, "config.needsofnature.server_settings.stale"); + } else { + String raw = payload.json(); + if (raw != null && !raw.isBlank() && raw.length() <= 262144) { + try { + HostConfigSyncData requested = (HostConfigSyncData)HOST_CONFIG_SYNC_GSON.fromJson(raw, HostConfigSyncData.class); + if (requested == null) { + rejectServerConfigUpdate(player, "config.needsofnature.server_settings.invalid"); + return; + } + + NonConfig validated = CONFIG.copy(); + requested.applyEditableTo(validated); + HostConfigSyncData.fromEditableConfig(validated).applyEditableTo(CONFIG); + CONFIG.save(); + applyAnimationToggleConfig(); + SERVER_CONFIG_REVISION.incrementAndGet(); + ANIMATION_CATALOG_REVISION.incrementAndGet(); + MinecraftServer server = player.getEntityWorld().getServer(); + syncHostConfigStateToAll(server); + syncAnimationCatalogStateToAll(server); + refreshLiquidBottleTintsToAll(server); + ServerPlayNetworking.send(player, new ServerConfigResultS2CPayload(true, SERVER_CONFIG_REVISION.get(), "")); + } catch (RuntimeException e) { + LOGGER.warn("[NoN] Rejected invalid server settings update from {}.", player.getName().getString(), e); + rejectServerConfigUpdate(player, "config.needsofnature.server_settings.invalid"); + } + } else { + rejectServerConfigUpdate(player, "config.needsofnature.server_settings.invalid"); + } + } + } + } + + private static void rejectServerConfigUpdate(ServerPlayerEntity player, String messageKey) { + syncHostConfigState(player); + syncAnimationCatalogState(player); + ServerPlayNetworking.send(player, new ServerConfigResultS2CPayload(false, SERVER_CONFIG_REVISION.get(), messageKey)); + } + + private static void syncAnimationCatalogState(ServerPlayerEntity player) { + if (player != null) { + sendAnimationCatalog(player, AnimationCatalogData.capture(CONFIG), ANIMATION_CATALOG_REVISION.get()); + } + } + + private static void syncAnimationCatalogStateToAll(@Nullable MinecraftServer server) { + if (server != null) { + AnimationCatalogData catalog = AnimationCatalogData.capture(CONFIG); + long revision = ANIMATION_CATALOG_REVISION.get(); + + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + sendAnimationCatalog(player, catalog, revision); + } + } + } + + private static void sendAnimationCatalog(ServerPlayerEntity player, AnimationCatalogData catalog, long revision) { + String json = HOST_CONFIG_SYNC_GSON.toJson(catalog); + List chunks = splitNetworkString(json, 24000); + + for (int i = 0; i < chunks.size(); i++) { + ServerPlayNetworking.send(player, new AnimationCatalogChunkS2CPayload(revision, i, chunks.size(), chunks.get(i))); + } + } + + private static List splitNetworkString(String value, int maxChars) { + if (value != null && !value.isEmpty()) { + int safeMax = Math.max(1024, maxChars); + List chunks = new ArrayList<>((value.length() + safeMax - 1) / safeMax); + int start = 0; + + while (start < value.length()) { + int end = Math.min(value.length(), start + safeMax); + if (end < value.length() && Character.isHighSurrogate(value.charAt(end - 1)) && Character.isLowSurrogate(value.charAt(end))) { + end--; + } + + chunks.add(value.substring(start, end)); + start = end; + } + + return List.copyOf(chunks); + } else { + return List.of(""); + } + } + + private static void syncDestroyedSkins(ServerPlayerEntity player) { + NonDestroyedSkinSystem.syncDestroyedSkins(player); + } + + private static boolean shouldApplyDestroyedSkinAttackDamage(boolean isAttack, @Nullable AnimationStartContext startContext, @Nullable Identifier animationId) { + if (!isAttack) { + return false; + } else { + boolean joinReplace = startContext != null && startContext.isJoinReplacement(); + boolean defeatedAnimation = hasAnimationTagWithPrefix(animationId, "defeat."); + boolean joinedFromDefeatedAnimation = startContext != null && hasAnimationTagWithPrefix(startContext.joinPredecessor(), "defeat."); + if (!joinReplace || !defeatedAnimation && !joinedFromDefeatedAnimation) { + return joinReplace ? false : !defeatedAnimation; + } else { + return true; + } + } + } + + public static void syncHostConfigStateToAll(@Nullable MinecraftServer server) { + if (server != null) { + clearDisabledSystemStoredStates(server); + NonPlayerInteractionSystem.normalizeAndSyncAll(server); + + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + NonDestroyedSkinSystem.cacheDestroyedSkinStage(player); + syncPlayerRuntimeSettings(player); + syncHostConfigState(player); + syncLiquidState(player); + NonPregnancyConceptionSystem.syncState(player); + syncDestroyedSkins(player); + NonMessSystem.syncMessStates(player); + } + } + } + + private static void clearDisabledSystemStoredStates(MinecraftServer server) { + if (server != null) { + boolean messEnabled = isMessSystemEnabled(); + boolean destroyedSkinEnabled = isDestroyedSkinSystemEnabled(); + if (!messEnabled || !destroyedSkinEnabled) { + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + if (!messEnabled) { + NonMessSystem.clearMessStoredState(player); + } + + if (!destroyedSkinEnabled) { + NonDestroyedSkinSystem.clearDestroyedSkinStoredState(player); + } + } + } + } + } + + public static void syncLiquidStateToAll(@Nullable MinecraftServer server) { + NonLiquidSystem.syncLiquidStateToAll(server); + } + + public static void refreshLiquidBottleTintsToAll(@Nullable MinecraftServer server) { + NonLiquidSystem.refreshLiquidBottleTintsToAll(server); + } + + static int resolveLiquidTintRgb(LiquidHolder holder) { + return NonLiquidSystem.resolveLiquidTintRgb(holder); + } + + public static int resolveBottleLiquidTintRgb(@Nullable Identifier entityTypeId) { + return NonLiquidSystem.resolveBottleTintRgb(entityTypeId); + } + + private static int grantLiquidOnPeak(ServerWorld world, List actorUuids, List actorKeys, Identifier animationId) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (actorKeys != null && !actorKeys.isEmpty()) { + if (actorKeys.size() != actorUuids.size()) { + return 0; + } + + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles == null) { + return 0; + } + + Set receiverKeys = roles.receiverKeys(); + if (receiverKeys.isEmpty()) { + return 0; + } + + int count = Math.min(actorUuids.size(), actorKeys.size()); + int totalAdded = 0; + + for (int i = 0; i < count; i++) { + if (receiverKeys.contains(actorKeys.get(i)) + && world.getEntity(actorUuids.get(i)) instanceof ServerPlayerEntity player + && player instanceof LiquidHolder holder) { + NeedsOfNature.LiquidGainProfile gain = resolveLiquidGainProfile( + world, actorUuids, actorKeys, animationId, NonGenderSystem.getLiquidTankType(player) + ); + if (gain.amount() > 0) { + NeedsOfNature.LiquidGainProfile playerGain = applyApiLiquidGainModifier(player, applyAccessoryLiquidGainMultiplier(player, gain)); + int added = addLiquidWithComposition(holder, playerGain); + if (added > 0) { + recordRecentLiquidDonors(holder, playerGain.recentDonors()); + totalAdded += added; + Map scaledAmounts = scaleEntityAmounts(playerGain.entityAmounts(), added); + NonStats.addLiquidGained(player, added, scaledAmounts); + trackFieldResearch(world, player, scaledAmounts); + syncLiquidState(player); + } + } + } + } + + return totalAdded; + } else { + return 0; + } + } else { + return 0; + } + } + + private static void fillHorseLiquidCollectorsOnPeak(ServerWorld world, List actorUuids, List actorKeys, Identifier animationId) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (actorKeys != null && !actorKeys.isEmpty()) { + if (actorKeys.size() == actorUuids.size()) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null) { + Set receiverKeys = roles.receiverKeys(); + Set injectorKeys = roles.injectorKeys(); + if (!receiverKeys.isEmpty() && !injectorKeys.isEmpty()) { + List collectors = new ArrayList<>(); + Set donorTypes = new LinkedHashSet<>(); + int count = Math.min(actorUuids.size(), actorKeys.size()); + + for (int i = 0; i < count; i++) { + String actorKey = actorKeys.get(i); + Entity entity = world.getEntity(actorUuids.get(i)); + if (entity != null) { + if (receiverKeys.contains(actorKey) && entity instanceof HorseLiquidCollectorEntity collector) { + collectors.add(collector); + } + + if (injectorKeys.contains(actorKey)) { + EntityType entityType = entity.getType(); + if (entityType == EntityType.HORSE || entityType == EntityType.DONKEY || entityType == EntityType.MULE) { + Identifier typeId = Registries.ENTITY_TYPE.getId(entityType); + donorTypes.add(typeId); + } + } + } + } + + if (!collectors.isEmpty() && !donorTypes.isEmpty()) { + boolean mixedDonor = donorTypes.size() > 1; + Identifier donorType = mixedDonor ? null : donorTypes.iterator().next(); + + for (HorseLiquidCollectorEntity collector : collectors) { + if (mixedDonor) { + collector.absorbMixedLiquid(); + } else { + collector.absorbEntityLiquid(donorType); + } + } + } + } + } + } + } + } + } + + private static NeedsOfNature.LiquidGainProfile resolveLiquidGainProfile( + ServerWorld world, List actorUuids, List actorKeys, Identifier animationId + ) { + return resolveLiquidGainProfile(world, actorUuids, actorKeys, animationId, null); + } + + private static NeedsOfNature.LiquidGainProfile resolveLiquidGainProfile( + ServerWorld world, List actorUuids, List actorKeys, Identifier animationId, @Nullable NonLiquidTankType tankType + ) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (actorKeys != null && !actorKeys.isEmpty()) { + if (actorKeys.size() != actorUuids.size()) { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles == null) { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + + Map injectorRoles = roles.injectorRoles(); + if (injectorRoles.isEmpty()) { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + + double liquidGainMultiplier = resolveLiquidGainMultiplier(animationId); + int total = 0; + int xpEligible = 0; + Set injectorEntityTypes = new LinkedHashSet<>(); + Map entityAmounts = new LinkedHashMap<>(); + NeedsOfNature.PregnancyVariantData lastDonorVariantData = null; + List recentDonors = new ArrayList<>(); + int count = Math.min(actorUuids.size(), actorKeys.size()); + + for (int i = 0; i < count; i++) { + String actorKey = actorKeys.get(i); + NonLiquidRoles.InjectorRole role = injectorRoles.get(actorKey); + if (isLiquidInjectorAccepted(role, tankType)) { + Entity entity = world.getEntity(actorUuids.get(i)); + if (entity != null && !(entity instanceof ServerPlayerEntity player && NonTrinketsIntegration.hasMaleRoleAccessory(player))) { + Identifier typeId = Registries.ENTITY_TYPE.getId(entity.getType()); + int gain = applyLiquidGainMultiplier(resolveEntityLiquidGainMl(typeId), liquidGainMultiplier); + if (gain > 0) { + total += gain; + if (!PLAYER_ENTITY_TYPE_ID.equals(typeId)) { + xpEligible += gain; + } + + injectorEntityTypes.add(typeId); + entityAmounts.merge(typeId, gain, Integer::sum); + NeedsOfNature.PregnancyVariantData donorVariantData = entity instanceof ServerPlayerEntity + ? null + : capturePregnancyVariantData(world, entity); + lastDonorVariantData = donorVariantData; + if (role == NonLiquidRoles.InjectorRole.V && !(entity instanceof ServerPlayerEntity)) { + recentDonors.add(new NeedsOfNature.PregnancyDonor(typeId, donorVariantData)); + } + } + } + } + } + + if (total <= 0) { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + + Map frozenEntityAmounts = Map.copyOf(entityAmounts); + return injectorEntityTypes.size() == 1 + ? new NeedsOfNature.LiquidGainProfile( + total, + xpEligible, + LiquidHolder.LiquidComposition.ENTITY, + injectorEntityTypes.iterator().next(), + frozenEntityAmounts, + lastDonorVariantData, + recentDonors + ) + : new NeedsOfNature.LiquidGainProfile(total, xpEligible, LiquidHolder.LiquidComposition.MIXED, null, frozenEntityAmounts, null, recentDonors); + } else { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + } else { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + } + + public static boolean isProtectedByAccessory(ServerWorld world, AnimationDefinitions.Definition definition, List actors) { + if (world != null && definition != null && actors != null && !actors.isEmpty()) { + for (ServerPlayerEntity player : collectPlayerActors(actors)) { + for (NonLiquidRoles.InjectorRole role : resolveEffectiveInjectorRolesForPlayer(definition.id(), player)) { + if (NonTrinketsIntegration.hasProtectorFor(player, role)) { + return true; + } + } + } + + return false; + } else { + return false; + } + } + + public static boolean consumeProtectorForAnimation(ServerWorld world, AnimationDefinitions.Definition definition, List actors) { + if (world != null && definition != null && actors != null && !actors.isEmpty()) { + for (ServerPlayerEntity player : collectPlayerActors(actors)) { + for (NonLiquidRoles.InjectorRole role : resolveEffectiveInjectorRolesForPlayer(definition.id(), player)) { + if (NonTrinketsIntegration.consumeProtectorFor(world, player, role)) { + return true; + } + } + } + + return false; + } else { + return false; + } + } + + public static void trackNoNGatherPair(ServerWorld world, List actors) { + trackPendingGatherPairs(world, actors); + trackNoNGatherActors(world, actors); + } + + public static void clearNoNGatherPair(ServerWorld world, List actorUuids) { + clearPendingGatherPairsForActors(world, actorUuids); + clearNoNGatherActors(world, actorUuids); + } + + private static void trackNoNGatherActors(ServerWorld world, List actors) { + if (world != null && actors != null && !actors.isEmpty()) { + long expiresTick = world.getTime() + 6000L; + RegistryKey worldKey = world.getRegistryKey(); + + for (Entity actor : actors) { + if (actor != null && actor.getEntityWorld() == world) { + NON_PENDING_GATHER_ACTORS.put(actor.getUuid(), new NeedsOfNature.NoNGatherActorState(worldKey, expiresTick)); + } + } + } + } + + private static void clearNoNGatherActors(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + + for (UUID actorUuid : actorUuids) { + if (actorUuid != null) { + NeedsOfNature.NoNGatherActorState state = NON_PENDING_GATHER_ACTORS.get(actorUuid); + if (state == null || worldKey.equals(state.worldKey())) { + NON_PENDING_GATHER_ACTORS.remove(actorUuid); + } + } + } + } + } + + public static void applyNoNGatherCancellation(ServerWorld world, Identifier animationId, List actorUuids, NonGatherCancelReason reason) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + clearPendingGatherPairsForActors(world, actorUuids); + if (reason == NonGatherCancelReason.NO_LOS || reason == NonGatherCancelReason.TOO_FAR || reason == NonGatherCancelReason.TIMEOUT) { + long now = world.getTime(); + + for (UUID actorId : actorUuids) { + if (world.getEntity(actorId) instanceof EnergyHolder holder) { + for (UUID otherId : actorUuids) { + if (!otherId.equals(actorId)) { + holder.applyPartnerCooldown(otherId, now, 1200); + } + } + } + } + + Entity chatSource = null; + + for (UUID actorId : actorUuids) { + Entity actor = world.getEntity(actorId); + if (actor instanceof MobEntity) { + chatSource = actor; + break; + } + + if (chatSource == null && actor != null) { + chatSource = actor; + } + } + + if (chatSource != null) { + sendDebugChatToNearby( + world, + chatSource, + 20.0, + NonDebugChatCategory.WARNING, + Text.translatable( + "debug.needsofnature.gather_cancelled", + new Object[]{animationId == null ? "unknown" : animationId.toString(), reason.name().toLowerCase(Locale.ROOT)} + ) + ); + } + + logRuntime("[NoN] Gather cancelled: animation={} reason={} actors={}", animationId == null ? "unknown" : animationId, reason, actorUuids); + } + } + } + + public static UUID startAnimationNowWithContext( + ServerWorld world, + Identifier animationId, + List actors, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Entity positionAnchor, + AnimationStartContext startContext + ) { + return startAnimationNowWithContext( + world, animationId, actors, actorKeys, stages, damageBehavior, ignoreAttackers, positionAnchor, startContext, null, false + ); + } + + public static UUID startAnimationNowWithContext( + ServerWorld world, + Identifier animationId, + List actors, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Entity positionAnchor, + AnimationStartContext startContext, + @Nullable ServerPlayerEntity requester, + boolean forceChat + ) { + return startAnimationNowWithContext( + world, animationId, actors, actorKeys, stages, damageBehavior, ignoreAttackers, positionAnchor, startContext, requester, forceChat, false + ); + } + + public static UUID startAnimationNowWithContext( + ServerWorld world, + Identifier animationId, + List actors, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Entity positionAnchor, + AnimationStartContext startContext, + @Nullable ServerPlayerEntity requester, + boolean forceChat, + boolean allowMultiplePlayers + ) { + if (world != null && animationId != null && actors != null && !actors.isEmpty()) { + boolean effectiveIgnoreAttackers = ignoreAttackers || hasPlayerAndHostileActor(actors); + AnimationStartContext safeContext = startContext == null ? AnimationStartContext.empty() : startContext; + return AnimationService.startAnimationNowWithContext( + world, + animationId, + actors, + actorKeys, + stages, + new AnimationService.StartOptions( + damageBehavior, effectiveIgnoreAttackers, positionAnchor, requester, safeContext.addonMetadata(), forceChat, allowMultiplePlayers + ), + safeContext + ); + } else { + return null; + } + } + + public static boolean joinAnimationNow(ServerWorld world, UUID instanceId, List newActors, @Nullable ServerPlayerEntity requester) { + return world != null && instanceId != null && newActors != null && !newActors.isEmpty() + ? joinAnimationNow(world, instanceId, newActors, requester, null) + : false; + } + + @Nullable + public static UUID findActiveInstanceContainingActor(ServerWorld world, UUID actorUuid) { + return ServerAnimationController.findActiveInstanceId(world, actorUuid); + } + + public static boolean isVoluntaryJoinableInstance(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (snapshot != null && !isAttackSnapshot(snapshot) && !isPregnancySnapshot(snapshot)) { + AnimationStageInfo stage = AnimationService.getCurrentStage(world, instanceId); + return stage != null && stage.allowJoin(); + } else { + return false; + } + } else { + return false; + } + } + + public static boolean joinAnimationNow( + ServerWorld world, UUID instanceId, List newActors, @Nullable ServerPlayerEntity requester, @Nullable Identifier expandedAnimationId + ) { + return joinAnimationNow(world, instanceId, newActors, requester, expandedAnimationId, null); + } + + public static boolean joinAnimationNow( + ServerWorld world, + UUID instanceId, + List newActors, + @Nullable ServerPlayerEntity requester, + @Nullable Identifier expandedAnimationId, + @Nullable Entity positionAnchor + ) { + return joinAnimationNow(world, instanceId, newActors, requester, expandedAnimationId, positionAnchor, null); + } + + public static boolean joinAnimationNow( + ServerWorld world, + UUID instanceId, + List newActors, + @Nullable ServerPlayerEntity requester, + @Nullable Identifier expandedAnimationId, + @Nullable Entity positionAnchor, + @Nullable List exactActorKeys + ) { + if (world != null && instanceId != null && newActors != null && !newActors.isEmpty()) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (snapshot == null) { + return false; + } + + boolean attackContinuation = isAttackSnapshot(snapshot); + AnimationStageInfo currentStage = AnimationService.getCurrentStage(world, instanceId); + if (currentStage != null && currentStage.allowJoin()) { + Identifier predecessorAnimationId = AnimationService.getActiveAnimationId(world, instanceId); + if (predecessorAnimationId == null) { + return false; + } + + List existingActorUuids = snapshot.actorUuids(); + if (existingActorUuids.isEmpty()) { + return false; + } + + List combined = collectEntities(world, existingActorUuids); + if (combined.isEmpty()) { + return false; + } + + Set existing = new HashSet<>(existingActorUuids); + int originalCount = combined.size(); + + for (Entity actor : newActors) { + if (actor != null + && actor.getEntityWorld() == world + && !actor.isRemoved() + && !existing.contains(actor.getUuid()) + && !( + actor instanceof ServerPlayerEntity playerActor + ? playerActor.isSpectator() + : actor instanceof PlayerEntity || !(actor instanceof MobEntity) + )) { + combined.add(actor); + existing.add(actor.getUuid()); + } + } + + if (combined.size() <= originalCount) { + return false; + } + + combined.sort(Comparator.comparingInt(Entity::getId)); + AnimationDefinitions.Definition definition = findJoinDefinition(world, combined, expandedAnimationId, predecessorAnimationId, attackContinuation); + if (definition == null) { + return false; + } + + boolean eligible = exactActorKeys == null + ? AnimationService.isAnimationStartEligible(world, definition, combined, positionAnchor) + : AnimationService.isAnimationStartEligible(world, definition, combined, exactActorKeys, positionAnchor); + if (!eligible) { + return false; + } + + List actorKeys = exactActorKeys == null + ? AnimationDefinitions.resolveActorKeys(definition, combined, world.getRandom()) + : List.copyOf(exactActorKeys); + if (actorKeys != null && actorKeys.size() == combined.size()) { + AnimationStartContext replacementContext = snapshot.startContext(); + if (requester != null && isDefeatedInstance(world, instanceId)) { + replacementContext = NonPlayerAttackSystem.prepareDefeatedJoinContext(world, existingActorUuids, requester, replacementContext); + } + + replacementContext = replacementContext.withJoinPredecessor(predecessorAnimationId); + if (!ServerAnimationController.stopInstance(world, instanceId, false)) { + return false; + } + + UUID replacement = startAnimationNowWithContext( + world, + definition.id(), + combined, + actorKeys, + definition.stages(), + NonPlayerAttackSystem.isPlayerAttackContext(replacementContext) ? NonAnimationDamageBehavior.STOP_ON_DAMAGE : snapshot.damageBehavior(), + snapshot.ignoreAttackers(), + positionAnchor, + replacementContext, + requester, + false, + true + ); + return replacement != null; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + @Nullable + private static AnimationDefinitions.Definition findJoinDefinition( + ServerWorld world, List combined, @Nullable Identifier expandedAnimationId, Identifier predecessorAnimationId, boolean attackContinuation + ) { + if (world == null || combined == null || combined.isEmpty() || predecessorAnimationId == null) { + return null; + } + + if (expandedAnimationId == null) { + AnimationService.MatchedAnimation matched = attackContinuation + ? NonAnimationMatcher.findAutomaticAttack(world, combined, Set.of(), null, false, null, Set.of(), null, predecessorAnimationId) + : NonAnimationMatcher.findAutomatic(world, combined, Set.of(), null, false, null, Set.of(), null, predecessorAnimationId); + return matched == null ? null : AnimationDefinitions.getDefinition(matched.animationId()); + } + + if (!AnimationService.isJoinTransitionAllowed(predecessorAnimationId, expandedAnimationId)) { + return null; + } + + AnimationDefinitions.MatchResult match = AnimationDefinitions.match(combined); + List candidates = match.candidatesSorted(); + if (candidates != null && !candidates.isEmpty()) { + for (AnimationDefinitions.Definition candidate : candidates) { + if (candidate != null && expandedAnimationId.equals(candidate.id())) { + return attackContinuation && !candidate.attackEligible() ? null : candidate; + } + } + + return null; + } else { + return null; + } + } + + private static List collectEntities(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + List actors = new ArrayList<>(); + + for (UUID actorUuid : actorUuids) { + Entity entity = world.getEntity(actorUuid); + if (entity != null && !entity.isRemoved()) { + actors.add(entity); + } + } + + actors.sort(Comparator.comparingInt(Entity::getId)); + return actors; + } else { + return List.of(); + } + } + + private static Set resolveEffectiveInjectorRolesForPlayer(Identifier animationId, ServerPlayerEntity player) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null && !roles.injectorRoles().isEmpty()) { + LinkedHashSet out = new LinkedHashSet<>(); + + for (NonLiquidRoles.InjectorRole role : roles.injectorRoles().values()) { + NonLiquidRoles.InjectorRole effective = role; + if (effective == NonLiquidRoles.InjectorRole.V + && player != null + && NonGenderSystem.isOnlyMale(player) + && getConfig().convertMaleOnlyVInjectionsToA()) { + effective = NonLiquidRoles.InjectorRole.A; + } + + if (effective != null && effective != NonLiquidRoles.InjectorRole.NONE) { + out.add(effective); + } + } + + return Set.copyOf(out); + } else { + return Set.of(); + } + } + + private static boolean isLiquidInjectorAccepted(@Nullable NonLiquidRoles.InjectorRole role, @Nullable NonLiquidTankType tankType) { + if (role != NonLiquidRoles.InjectorRole.V && role != NonLiquidRoles.InjectorRole.A) { + return false; + } + + if (tankType == null) { + return true; + } + + NonLiquidRoles.InjectorRole effective = role; + if (tankType == NonLiquidTankType.A && role == NonLiquidRoles.InjectorRole.V && getConfig().convertMaleOnlyVInjectionsToA()) { + effective = NonLiquidRoles.InjectorRole.A; + } + + return tankType.accepts(effective); + } + + private static NeedsOfNature.LiquidGainProfile scaleLiquidGainProfile(NeedsOfNature.LiquidGainProfile gain, double scale) { + if (gain != null && gain.amount() > 0) { + double safe = Double.isFinite(scale) && !(scale < 0.0) ? scale : 1.0; + int amount = applyLiquidGainMultiplier(gain.amount(), safe); + int xpEligible = Math.min(amount, applyLiquidGainMultiplier(gain.xpEligibleAmount(), safe)); + Map entityAmounts = new LinkedHashMap<>(); + + for (Entry entry : gain.entityAmounts().entrySet()) { + int scaled = applyLiquidGainMultiplier(entry.getValue(), safe); + if (scaled > 0) { + entityAmounts.put(entry.getKey(), scaled); + } + } + + return new NeedsOfNature.LiquidGainProfile( + amount, + xpEligible, + amount > 0 ? gain.composition() : LiquidHolder.LiquidComposition.EMPTY, + amount > 0 ? gain.entityTypeId() : null, + Map.copyOf(entityAmounts), + amount > 0 ? gain.donorVariantData() : null, + amount > 0 ? gain.recentDonors() : List.of() + ); + } else { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } + } + + private static double resolveLiquidGainMultiplier(@Nullable Identifier animationId) { + Double value = AnimationGameplayMetadata.liquidGainMultiplier(animationId); + return value != null && Double.isFinite(value) && !(value < 0.0) ? value : 1.0; + } + + private static int applyLiquidGainMultiplier(int baseGain, double multiplier) { + if (baseGain <= 0) { + return 0; + } else { + double safe = Double.isFinite(multiplier) && !(multiplier < 0.0) ? multiplier : 1.0; + long scaled = Math.round(baseGain * safe); + if (scaled <= 0L) { + return 0; + } else { + return scaled > 2147483647L ? Integer.MAX_VALUE : (int)scaled; + } + } + } + + private static NeedsOfNature.LiquidGainProfile applyAccessoryLiquidGainMultiplier(ServerPlayerEntity player, NeedsOfNature.LiquidGainProfile gain) { + if (player != null && gain != null && gain.amount() > 0) { + double multiplier = NonTrinketsIntegration.getActiveTankAccessoryEffects(player).liquidGainMultiplier(); + if (Double.isFinite(multiplier) && !(Math.abs(multiplier - 1.0) < 1.0E-6)) { + int amount = applyLiquidGainMultiplier(gain.amount(), multiplier); + int xpEligible = Math.min(amount, applyLiquidGainMultiplier(gain.xpEligibleAmount(), multiplier)); + Map entityAmounts = new LinkedHashMap<>(); + + for (Entry entry : gain.entityAmounts().entrySet()) { + int scaled = applyLiquidGainMultiplier(entry.getValue(), multiplier); + if (scaled > 0) { + entityAmounts.put(entry.getKey(), scaled); + } + } + + return new NeedsOfNature.LiquidGainProfile( + amount, + xpEligible, + amount > 0 ? gain.composition() : LiquidHolder.LiquidComposition.EMPTY, + amount > 0 ? gain.entityTypeId() : null, + Map.copyOf(entityAmounts), + amount > 0 ? gain.donorVariantData() : null, + amount > 0 ? gain.recentDonors() : List.of() + ); + } else { + return gain; + } + } else { + return gain; + } + } + + private static NeedsOfNature.LiquidGainProfile applyApiLiquidGainModifier(ServerPlayerEntity player, NeedsOfNature.LiquidGainProfile gain) { + if (player != null && gain != null && gain.amount() > 0) { + int modifiedAmount = ((NonEvents.ModifyLiquidGain)NonEvents.MODIFY_LIQUID_GAIN.invoker()).modifyLiquidGain(player, gain.entityTypeId(), gain.amount()); + if (modifiedAmount <= 0) { + return new NeedsOfNature.LiquidGainProfile(0, 0, LiquidHolder.LiquidComposition.EMPTY, null, Map.of(), null, List.of()); + } else { + return modifiedAmount == gain.amount() ? gain : scaleLiquidGainProfile(gain, (double)modifiedAmount / gain.amount()); + } + } else { + return gain; + } + } + + private static int resolveEntityLiquidGainMl(@Nullable Identifier entityTypeId) { + if (entityTypeId == null) { + return 0; + } else { + String key = entityTypeId.toString(); + Map configMap = getConfig().getLiquidGainByEntity(); + if (configMap.containsKey(key)) { + Integer configured = configMap.get(key); + int value = configured == null ? 0 : configured; + return Math.max(0, Math.min(1000, value)); + } else { + Integer packOverride = NonLiquidGainOverrides.getGain(entityTypeId); + return packOverride != null ? Math.max(0, Math.min(1000, packOverride)) : 0; + } + } + } + + private static int resolveEntityLiquidColorRgb(@Nullable Identifier entityTypeId) { + if (entityTypeId == null) { + return 16776427; + } + + String key = entityTypeId.toString(); + String configuredHex = getConfig().getLiquidColorByEntity().get(key); + Integer configuredRgb = NonLiquidColors.parseHexColor(configuredHex); + if (configuredRgb != null) { + return configuredRgb; + } + + Integer packOverride = NonLiquidGainOverrides.getColorRgb(entityTypeId); + return packOverride != null ? packOverride : 16776427; + } + + private static int resolveMixedLiquidColorRgb() { + String configuredHex = getConfig().getLiquidColorByEntity().get("needsofnature:mixed"); + Integer configuredRgb = NonLiquidColors.parseHexColor(configuredHex); + if (configuredRgb != null) { + return configuredRgb; + } + + Integer packOverride = NonLiquidGainOverrides.getMixedColorRgb(); + return packOverride != null ? packOverride : 15920063; + } + + private static Map resolveEffectiveLiquidGainMap() { + return NonLiquidSystem.resolveEffectiveLiquidGainMap(); + } + + private static Map resolveEffectiveLiquidColorMap() { + return NonLiquidSystem.resolveEffectiveLiquidColorMap(); + } + + private static int resolveStartStage(List stages) { + int startStage = 1; + if (stages != null && !stages.isEmpty()) { + Integer parsed = NonPeakStages.stageNumberFromId(stages.getFirst().animationId()); + if (parsed != null) { + startStage = parsed; + } + } + + return startStage; + } + + private static int resolveLoopSeconds(Identifier animationId, Integer stageNumber) { + Identifier stageId = resolveStageId(animationId, stageNumber); + Integer override = AnimationGameplayMetadata.stageSeconds(stageId); + if (override != null) { + return override; + } + + Integer baseOverride = AnimationGameplayMetadata.stageSeconds(animationId); + if (baseOverride != null) { + return baseOverride; + } + + Integer peakStage = NonPeakStages.getPeakStage(animationId); + return stageNumber != null && Objects.equals(stageNumber, peakStage) ? CONFIG.getPeakLoopProgressSeconds() : CONFIG.getLoopProgressSeconds(); + } + + private static long resolveLoopAdvanceIntervalTicks(Identifier animationId, Integer stageNumber, @Nullable AnimationStageInfo currentStage) { + long targetTicks = resolveLoopDurationTargetTicks(animationId, stageNumber); + if (targetTicks < 0L) { + return -1L; + } + + Identifier stageId = resolveStageId(animationId, stageNumber); + double exactCycleTicks = exactCycleTicks(stageId, currentStage); + if (currentStage != null && currentStage.loop() && !(exactCycleTicks <= 0.0)) { + double speed = resolveStageSpeed(currentStage); + double cycleWorldTicks = exactCycleTicks / speed; + if (Double.isFinite(cycleWorldTicks) && !(cycleWorldTicks <= 0.0)) { + long cycleCount = Math.max(1L, Math.round(targetTicks / cycleWorldTicks)); + return Math.max(1L, Math.round(cycleCount * cycleWorldTicks)); + } else { + return targetTicks; + } + } else { + return targetTicks; + } + } + + private static long resolveLoopDurationTargetTicks(Identifier animationId, Integer stageNumber) { + int loopSeconds = resolveLoopSeconds(animationId, stageNumber); + if (loopSeconds < 0) { + return -1L; + } + + Identifier stageId = resolveStageId(animationId, stageNumber); + double multiplier = AnimationGameplayMetadata.stageDurationMultiplier(stageId); + return Math.max(1L, Math.round(loopSeconds * 20.0 * multiplier)); + } + + private static double exactCycleTicks(@Nullable Identifier stageId, @Nullable AnimationStageInfo stage) { + double exact = AnimationGameplayMetadata.cycleTicks(stageId); + if (exact > 0.0) { + return exact; + } else { + return stage == null ? 0.0 : Math.max(0L, stage.lengthTicks()); + } + } + + private static Identifier resolveStageId(Identifier animationId, Integer stageNumber) { + return animationId != null && stageNumber != null ? AnimationIds.stageId(animationId, stageNumber) : animationId; + } + + private static void queueDefeatedTailForInstance(ServerWorld world, UUID instanceId, List actorUuids) { + if (world != null && instanceId != null && actorUuids != null && !actorUuids.isEmpty()) { + if (DEFEATED_QUEUED_INSTANCES.add(instanceId)) { + ServerPlayerEntity player = NonPlayerAttackSystem.resolveAttackVictim(world, instanceId, actorUuids); + if (player == null) { + DEFEATED_QUEUED_INSTANCES.remove(instanceId); + } else { + AnimationService.MatchedAnimation defeatMatch = NonDefeatedAnimationSelector.find( + world, player, resolveInstanceBaseAnimationId(world, instanceId) + ); + if (defeatMatch == null) { + DEFEATED_QUEUED_INSTANCES.remove(instanceId); + LOGGER.warn( + "[NoN] No defeat-tag animation matched for player actor. world={} instance={} player={}", + new Object[]{world.getRegistryKey().getValue(), instanceId, player.getName().getString()} + ); + sendDebugChat(player, NonDebugChatCategory.SETUP, Text.translatable("debug.needsofnature.no_defeat_tag_animation")); + } else { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (snapshot == null) { + DEFEATED_QUEUED_INSTANCES.remove(instanceId); + } else { + boolean inheritIgnoreAttackers = snapshot.ignoreAttackers(); + AnimationStartContext defeatedContext = NonPlayerAttackSystem.defeatedTailContext(snapshot.startContext(), player.getUuid()); + boolean queued = enqueueAnimation( + world, + defeatMatch.animationId(), + List.of(player), + -1, + NonPlayerAttackSystem.defeatedDamageBehavior(defeatedContext), + inheritIgnoreAttackers, + defeatedContext, + player + ); + if (!queued) { + DEFEATED_QUEUED_INSTANCES.remove(instanceId); + } else { + NonAdvancementHooks.recordTossedAround(player); + } + } + } + } + } + } + } + + @Nullable + private static NeedsOfNature.LastDefeatedCandidate findLastDefeatedCandidate(ServerWorld world, ServerPlayerEntity player, MobEntity damagingMob) { + if (world != null && player != null && damagingMob != null) { + NeedsOfNature.LastDefeatedCandidate direct = resolveLastDefeatedCandidate(world, player, damagingMob); + if (direct != null) { + return direct; + } + + int radius = getConfig().getLastDefeatedSearchRadius(); + Box box = player.getBoundingBox().expand(radius); + List nearby = world.getEntitiesByClass( + MobEntity.class, box, mobx -> mobx != null && mobx != damagingMob && mobx.isAlive() && !mobx.isRemoved() + ); + nearby.sort(Comparator.comparingDouble(player::squaredDistanceTo)); + + for (MobEntity mob : nearby) { + NeedsOfNature.LastDefeatedCandidate candidate = resolveLastDefeatedCandidate(world, player, mob); + if (candidate != null) { + return candidate; + } + } + + return null; + } else { + return null; + } + } + + @Nullable + private static NeedsOfNature.LastDefeatedCandidate resolveLastDefeatedCandidate(ServerWorld world, ServerPlayerEntity player, MobEntity mob) { + if (world != null && player != null && mob != null) { + if (mob.getEntityWorld() != world || player.getEntityWorld() != world) { + return null; + } + + if (mob.isAlive() && !mob.isRemoved()) { + if (!(mob instanceof EnergyHolder holder && holder.getEnergy() >= getConfig().getLastDefeatedEnergyThreshold())) { + return null; + } else { + if (!canMobAttackPlayer(player) || isMobAttackFailsafeActive(world, mob.getUuid())) { + return null; + } + + if (AnimationService.isActorPendingOrActive(world, mob.getUuid())) { + return null; + } + + List actors = new ArrayList<>(2); + actors.add(mob); + actors.add(player); + actors.sort(Comparator.comparingInt(Entity::getId)); + NonGatherSystem.GatherCandidate gatherCandidate = NonGatherSystem.findUnprotectedGatherCandidate(world, actors, true, mob, player); + if (gatherCandidate == null) { + return null; + } + + AnimationService.MatchedAnimation defeatMatch = NonDefeatedAnimationSelector.find(world, player, gatherCandidate.animationId()); + return defeatMatch == null ? null : new NeedsOfNature.LastDefeatedCandidate(mob, gatherCandidate, defeatMatch); + } + } else { + return null; + } + } else { + return null; + } + } + + private static boolean hasVanillaTotemReady(ServerPlayerEntity player) { + return player == null ? false : player.getMainHandStack().isOf(Items.TOTEM_OF_UNDYING) || player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING); + } + + @Nullable + private static Identifier resolveInstanceBaseAnimationId(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + NeedsOfNature.PeakState peakState = PEAK_STATES.get(instanceId); + if (peakState != null && peakState.animationId() != null) { + return NonPeakStages.baseAnimationId(peakState.animationId()); + } + + AnimationStageInfo stage = AnimationService.getCurrentStage(world, instanceId); + return stage != null && stage.animationId() != null ? NonPeakStages.baseAnimationId(stage.animationId()) : null; + } else { + return null; + } + } + + private static void tickLiquidDecay(ServerWorld world) { + NonLiquidSystem.tickLiquidDecay(world); + } + + private static void updateFilledEffect(ServerPlayerEntity player, LiquidHolder holder) { + NonLiquidSystem.updateFilledEffect(player, holder); + } + + public static void updateEnergizedEffect(ServerPlayerEntity player, EnergyHolder holder) { + if (player != null && holder != null) { + int amplifier = getEnergizedAmplifier(holder.getEnergy()); + RegistryEntry entry = Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGIZED); + StatusEffectInstance current = player.getStatusEffect(entry); + if (amplifier < 0) { + if (current != null) { + player.removeStatusEffect(entry); + } + } else { + if (current != null) { + if (current.getAmplifier() == amplifier) { + return; + } + + player.removeStatusEffect(entry); + } + + player.addStatusEffect(new StatusEffectInstance(entry, -1, amplifier, false, false, true)); + } + } + } + + private static int getEnergizedAmplifier(int energy) { + if (energy >= 200) { + return 2; + } else if (energy >= 150) { + return 1; + } else { + return energy >= 100 ? 0 : -1; + } + } + + private static void scheduleReceiverDripOnPeak(ServerWorld world, List actorUuids, List actorKeys, Identifier animationId, long nowTick) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (actorKeys != null && actorKeys.size() == actorUuids.size()) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null) { + Set receiverKeys = roles.receiverKeys(); + if (!receiverKeys.isEmpty()) { + int count = Math.min(actorUuids.size(), actorKeys.size()); + long nextTick = nowTick + 20L; + long endTick = nowTick + 200L; + NeedsOfNature.LiquidGainProfile gain = resolveLiquidGainProfile(world, actorUuids, actorKeys, animationId); + int tintRgb = gain.composition() == LiquidHolder.LiquidComposition.ENTITY + ? resolveEntityLiquidColorRgb(gain.entityTypeId()) + : resolveMixedLiquidColorRgb(); + + for (int i = 0; i < count; i++) { + if (receiverKeys.contains(actorKeys.get(i)) + && world.getEntity(actorUuids.get(i)) instanceof MobEntity mob + && !(mob instanceof HorseLiquidCollectorEntity) + && mob.isAlive() + && !mob.isRemoved()) { + RECEIVER_DRIP.put(mob.getUuid(), new NeedsOfNature.ReceiverDripState(world.getRegistryKey(), nextTick, endTick, tintRgb)); + } + } + } + } + } + } + } + + private static void tickReceiverDripParticles(ServerWorld world, long nowTick) { + if (world != null && !RECEIVER_DRIP.isEmpty()) { + Map updates = new HashMap<>(); + Set removals = new HashSet<>(); + + for (Entry entry : RECEIVER_DRIP.entrySet()) { + UUID uuid = entry.getKey(); + NeedsOfNature.ReceiverDripState state = entry.getValue(); + if (state.worldKey.equals(world.getRegistryKey())) { + if (nowTick > state.endTick) { + removals.add(uuid); + } else if (!(world.getEntity(uuid) instanceof MobEntity mob && mob.isAlive() && !mob.isRemoved())) { + removals.add(uuid); + } else if (nowTick >= state.nextTick) { + Vec3d spawnPos = resolveReceiverDripSpawnPos(mob); + world.spawnParticles(NonParticles.liquidFalling(state.tintRgb), spawnPos.x, spawnPos.y, spawnPos.z, 1, 0.0, 0.0, 0.0, 0.0); + long nextTick = state.nextTick + 20L; + if (nextTick > state.endTick) { + removals.add(uuid); + } else { + updates.put(uuid, new NeedsOfNature.ReceiverDripState(state.worldKey, nextTick, state.endTick, state.tintRgb)); + } + } + } + } + + for (UUID uuid : removals) { + RECEIVER_DRIP.remove(uuid); + } + + for (Entry update : updates.entrySet()) { + if (!removals.contains(update.getKey())) { + RECEIVER_DRIP.put(update.getKey(), update.getValue()); + } + } + } + } + + private static Vec3d resolveReceiverDripSpawnPos(MobEntity mob) { + Vec3d localOffset = resolveReceiverDripOffset(mob); + double yawRad = Math.toRadians(mob.getYaw()); + double sin = Math.sin(yawRad); + double cos = Math.cos(yawRad); + Vec3d forward = new Vec3d(-sin, 0.0, cos); + Vec3d right = new Vec3d(forward.z, 0.0, -forward.x); + return new Vec3d(mob.getX(), mob.getY(), mob.getZ()).add(right.multiply(localOffset.x)).add(0.0, localOffset.y, 0.0).add(forward.multiply(localOffset.z)); + } + + private static Vec3d resolveReceiverDripOffset(MobEntity mob) { + Identifier entityId = Registries.ENTITY_TYPE.getId(mob.getType()); + return RECEIVER_DRIP_OFFSETS.getOrDefault(entityId, RECEIVER_DRIP_OFFSET_DEFAULT); + } + + private void registerVoluntaryStopRequests() { + ServerPlayNetworking.registerGlobalReceiver(StopVoluntaryAnimationC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (player.getEntityWorld() instanceof ServerWorld world) { + UUID var7 = payload.instanceId(); + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, var7); + NonAnimationSessionData data = snapshot == null ? null : getNonSessionData(snapshot.startContext()); + if (data != null && data.isVoluntary() && !data.isPregnancy()) { + if (snapshot.actorUuids().contains(player.getUuid())) { + clearQueuedAnimations(world, player); + AnimationService.stopAnimation(world, var7); + } + } + } + })); + } + + private void registerManualPeakRequests() { + ServerPlayNetworking.registerGlobalReceiver( + StartManualPeakC2SPayload.ID, + (payload, context) -> context.server() + .execute( + () -> { + ServerPlayerEntity player = context.player(); + if (player.getEntityWorld() instanceof ServerWorld world) { + if (!player.isSpectator()) { + if (!hasActivePlayerAnimation(player)) { + if (player.isOnGround()) { + NonManualPeakDefinitions.Selection selection = NonManualPeakDefinitions.select(world, player); + if (selection != null) { + List actors = List.of(player); + AnimationDefinitions.Definition definition = selection.definition(); + UUID instanceId = startAnimationNowWithContext( + world, + definition.id(), + actors, + selection.actorKeys(), + selection.stages(), + NonAnimationDamageBehavior.STOP_ON_DAMAGE, + false, + null, + buildModeContext(false) + ); + if (instanceId != null && selection.matchedHeldItemId() != null) { + NonAdvancementHooks.grantAcquireHardware(player); + } + } + } + } + } + } + } + ) + ); + } + + private void registerFillBottleUseTrigger() { + UseItemCallback.EVENT.register((UseItemCallback)(player, world, hand) -> { + if (world instanceof ServerWorld serverWorld && player instanceof ServerPlayerEntity serverPlayer) { + return (ActionResult)(tryStartFillBottleAnimationOnUse(serverWorld, serverPlayer, hand) ? ActionResult.SUCCESS : ActionResult.PASS); + } else { + return ActionResult.PASS; + } + }); + UseBlockCallback.EVENT.register((UseBlockCallback)(player, world, hand, hitResult) -> { + if (!(world instanceof ServerWorld serverWorld && player instanceof ServerPlayerEntity serverPlayer)) { + return ActionResult.PASS; + } else if (world.getFluidState(hitResult.getBlockPos()).isIn(FluidTags.WATER)) { + return ActionResult.PASS; + } else if (world.getBlockState(hitResult.getBlockPos()).isOf(Blocks.WATER_CAULDRON)) { + return ActionResult.PASS; + } else { + return (ActionResult)(tryStartFillBottleAnimationOnUse(serverWorld, serverPlayer, hand) ? ActionResult.SUCCESS : ActionResult.PASS); + } + }); + UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> { + if (world instanceof ServerWorld serverWorld && player instanceof ServerPlayerEntity serverPlayer) { + return (ActionResult)(tryStartFillBottleAnimationOnUse(serverWorld, serverPlayer, hand) ? ActionResult.SUCCESS : ActionResult.PASS); + } else { + return ActionResult.PASS; + } + }); + } + + private static boolean tryStartFillBottleAnimationOnUse(ServerWorld world, ServerPlayerEntity player, Hand hand) { + if (world == null || player == null || hand == null) { + return false; + } + + if (player.isSpectator()) { + return false; + } + + if (player.isTouchingWater()) { + return false; + } + + if (hasActivePlayerAnimation(player)) { + return false; + } + + if (!player.isOnGround()) { + return false; + } + + if (player.getPitch() < 80.0F) { + return false; + } + + if (!player.getStackInHand(hand).isOf(Items.GLASS_BOTTLE)) { + return false; + } + + if (player instanceof LiquidHolder liquidHolder) { + updateFilledEffect(player, liquidHolder); + int requiredMl = getFillBottleRequiredMl(liquidHolder); + if (liquidHolder.getLiquidStored() < requiredMl) { + return false; + } else if (!hasFilledStageOneOrHigher(player)) { + return false; + } else { + Identifier fillBottleId = findMatchedAnimationIdByTags(List.of(player), Set.of("fillBottle")); + if (fillBottleId == null) { + return false; + } else { + AnimationDefinitions.Definition fillDefinition = AnimationDefinitions.getDefinition(fillBottleId); + if (fillDefinition == null) { + return false; + } else if (!NonItemSystem.consumeOneGlassBottle(player, hand)) { + return false; + } else { + List actors = List.of(player); + List actorKeys = AnimationDefinitions.resolveActorKeys(fillDefinition, actors, world.getRandom()); + UUID instanceId = startAnimationNowWithContext( + world, + fillDefinition.id(), + actors, + actorKeys, + fillDefinition.stages(), + NonAnimationDamageBehavior.STOP_ON_DAMAGE, + false, + null, + buildModeContext(false) + ); + if (instanceId != null) { + FILL_BOTTLE_STATES.put(instanceId, new NeedsOfNature.FillBottleState(player.getUuid(), true)); + return true; + } else { + NonItemSystem.refundGlassBottle(player); + return false; + } + } + } + } + } else { + return false; + } + } + + private static void applyAttackEscapeDamage( + ServerWorld world, ServerPlayerEntity player, @Nullable List actorUuids, @Nullable UUID preferredAttackerUuid + ) { + NonAttackSystem.applyAttackEscapeDamage(world, player, actorUuids, preferredAttackerUuid); + } + + private static void scheduleAttackEscapeResolution( + ServerWorld world, + ServerPlayerEntity player, + @Nullable List actorUuids, + @Nullable UUID preferredAttackerUuid, + boolean allowReattack, + int invulnerabilityTicks + ) { + if (world != null && player != null) { + List actors = actorUuids != null && !actorUuids.isEmpty() ? List.copyOf(actorUuids) : List.of(); + PENDING_ATTACK_ESCAPE_RESOLUTIONS.put( + player.getUuid(), + new NeedsOfNature.PendingAttackEscapeResolution( + world.getRegistryKey(), actors, preferredAttackerUuid, world.getTime() + 1L, allowReattack, invulnerabilityTicks + ) + ); + } + } + + private static void tickPendingAttackEscapeResolutions(ServerWorld world, long nowTick) { + if (world != null && !PENDING_ATTACK_ESCAPE_RESOLUTIONS.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + + for (Entry entry : PENDING_ATTACK_ESCAPE_RESOLUTIONS.entrySet()) { + UUID playerUuid = entry.getKey(); + NeedsOfNature.PendingAttackEscapeResolution resolution = entry.getValue(); + if (playerUuid != null && resolution != null) { + if (worldKey.equals(resolution.worldKey()) + && nowTick >= resolution.dueTick() + && PENDING_ATTACK_ESCAPE_RESOLUTIONS.remove(playerUuid, resolution)) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(playerUuid); + if (player != null && player.getEntityWorld() == world && player.isAlive() && !player.isRemoved()) { + POST_ESCAPE_DAMAGE_INVULNERABLE_UNTIL_BY_PLAYER.remove(playerUuid); + applyAttackEscapeDamage(world, player, resolution.actorUuids(), resolution.preferredAttackerUuid()); + if (player.isAlive() && !player.isRemoved()) { + if (resolution.invulnerabilityTicks() > 0) { + setPostEscapeDamageInvulnerable(player, resolution.invulnerabilityTicks()); + } + + if (resolution.allowReattack()) { + startPostEscapeReattack(world, player, resolution.actorUuids()); + } + } + } + } + } else if (playerUuid != null) { + PENDING_ATTACK_ESCAPE_RESOLUTIONS.remove(playerUuid); + } + } + } + } + + private static void startPostEscapeReattack(ServerWorld world, ServerPlayerEntity player, @Nullable List actorUuids) { + if (world != null && player != null && player.isAlive() && !player.isRemoved()) { + NonAttackSystem.startNearbyPostEscapeGathers(world, player, actorUuids); + requestOriginalAttackers(world, player, actorUuids); + } + } + + private static void requestOriginalAttackers(ServerWorld world, ServerPlayerEntity player, @Nullable List actorUuids) { + if (world != null && player != null && player.isAlive() && !player.isRemoved()) { + if (actorUuids != null && !actorUuids.isEmpty()) { + for (UUID actor : actorUuids) { + if (actor != null + && !actor.equals(player.getUuid()) + && world.getEntity(actor) instanceof MobEntity mob + && mob.isAlive() + && mob instanceof EnergyHolder holder) { + holder.requestAttackOnPlayer(world, player); + } + } + } + } + } + + public static boolean isDefeatedInstance(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + NeedsOfNature.PeakState peakState = PEAK_STATES.get(instanceId); + if (peakState != null && hasAnimationTagWithPrefix(peakState.animationId, "defeat.")) { + return true; + } else { + AnimationStageInfo stage = AnimationService.getCurrentStage(world, instanceId); + if (stage != null && stage.animationId() != null) { + Identifier baseAnimationId = NonPeakStages.baseAnimationId(stage.animationId()); + return hasAnimationTagWithPrefix(baseAnimationId, "defeat."); + } else { + return false; + } + } + } else { + return false; + } + } + + private static boolean isManualPeakAnimation(Identifier animationId) { + return hasAnimationTag(animationId, "manualPeak"); + } + + private static boolean isFillBottleAnimation(Identifier animationId) { + return hasAnimationTag(animationId, "fillBottle"); + } + + private static boolean isBirthAnimation(Identifier animationId) { + return hasAnimationTag(animationId, "birth") || hasAnimationTagWithPrefix(animationId, "birth_"); + } + + private static boolean isWaterAnimation(Identifier animationId) { + if (animationId == null) { + return false; + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(NonPeakStages.baseAnimationId(animationId)); + return definition != null && definition.waterRequirement() != AnimationDefinitions.WaterRequirement.NONE; + } + + private static boolean hasAnimationTag(Identifier animationId, String tag) { + if (animationId != null && tag != null && !tag.isBlank()) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(NonPeakStages.baseAnimationId(animationId)); + if (definition == null) { + return false; + } + + String expected = tag.trim().toLowerCase(Locale.ROOT); + if (expected.isEmpty()) { + return false; + } + + Set tags = definition.animationTags(); + if (tags != null && !tags.isEmpty()) { + for (String value : tags) { + if (value != null && expected.equals(value.trim().toLowerCase(Locale.ROOT))) { + return true; + } + } + + return false; + } else { + return false; + } + } else { + return false; + } + } + + private static boolean hasAnimationTagWithPrefix(Identifier animationId, String prefix) { + if (animationId != null && prefix != null && !prefix.isBlank()) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(NonPeakStages.baseAnimationId(animationId)); + if (definition == null) { + return false; + } + + String expectedPrefix = prefix.trim().toLowerCase(Locale.ROOT); + if (expectedPrefix.isEmpty()) { + return false; + } + + Set tags = definition.animationTags(); + if (tags != null && !tags.isEmpty()) { + for (String value : tags) { + if (value != null && value.trim().toLowerCase(Locale.ROOT).startsWith(expectedPrefix)) { + return true; + } + } + + return false; + } else { + return false; + } + } else { + return false; + } + } + + private static Identifier findMatchedAnimationIdByTags(List actors, Set requiredTags) { + if (actors != null && !actors.isEmpty() && requiredTags != null && !requiredTags.isEmpty()) { + List sorted = new ArrayList<>(actors.size()); + sorted.addAll(actors); + sorted.sort(Comparator.comparingInt(Entity::getId)); + Set safeTags = new LinkedHashSet<>(); + + for (String tag : requiredTags) { + if (tag != null) { + String normalized = tag.trim(); + if (!normalized.isEmpty()) { + safeTags.add(normalized); + } + } + } + + return safeTags.isEmpty() ? null : AnimationService.findMatchedAnimationId(sorted, safeTags); + } else { + return null; + } + } + + private static void maybeStartPregnancyOnPeak( + ServerWorld world, + List actorUuids, + List actorKeys, + Identifier animationId, + Map> accessoryDonorsByReceiver + ) { + if (!getConfig().isLiquidBasedPregnancyEnabled()) { + if (world != null && actorUuids != null && actorKeys != null) { + if (!actorUuids.isEmpty() && !actorKeys.isEmpty()) { + if (actorUuids.size() == actorKeys.size()) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null) { + Map injectorRoles = roles.injectorRoles(); + if (!injectorRoles.isEmpty()) { + List donors = new ArrayList<>(); + + for (int i = 0; i < actorUuids.size(); i++) { + if (isLiquidInjectorAccepted(injectorRoles.get(actorKeys.get(i)), NonLiquidTankType.V)) { + Entity actor = world.getEntity(actorUuids.get(i)); + if (actor != null && !(actor instanceof ServerPlayerEntity)) { + donors.add( + new NeedsOfNature.PregnancyDonor(Registries.ENTITY_TYPE.getId(actor.getType()), capturePregnancyVariantData(world, actor)) + ); + } + } + } + + long nowTick = getGlobalTick(world); + + for (ServerPlayerEntity player : collectPlayerReceiverActors(world, actorUuids, actorKeys, animationId)) { + if (player != null + && player.isAlive() + && !player.isRemoved() + && !NonGenderSystem.isOnlyMale(player) + && getPregnantEntityTypeId(player) == null + && !NonTrinketsIntegration.blocksPregnancy(player)) { + NonAccessoryEffects accessoryEffects = NonTrinketsIntegration.getActiveTankAccessoryEffects(player); + List eligibleDonors = new ArrayList<>(donors); + if (accessoryDonorsByReceiver != null) { + eligibleDonors.addAll(accessoryDonorsByReceiver.getOrDefault(player.getUuid(), List.of())); + } + + if (!eligibleDonors.isEmpty()) { + NeedsOfNature.PregnancyDonor chosen = eligibleDonors.get(world.getRandom().nextInt(eligibleDonors.size())); + Identifier chosenType = chosen.entityTypeId(); + int chancePercent = NonEntityProfiles.resolve(chosenType).pregnancyChancePercent(); + if (chancePercent > 0) { + chancePercent = applyFertilityIncreaserChance(player, chancePercent); + chancePercent = (int)Math.round(chancePercent * accessoryEffects.pregnancyChanceMultiplier()); + chancePercent = Math.max(0, Math.min(100, chancePercent)); + if (chancePercent > 0 && (chancePercent >= 100 || world.getRandom().nextInt(100) < chancePercent)) { + long durationTicks = Math.max( + 1L, Math.round(getConfig().getPregnancyDurationTicks() * accessoryEffects.pregnancyDurationMultiplier()) + ); + beginPregnancy(player, chosenType, nowTick + durationTicks, chosen.variantData()); + NonStats.incrementPregnancyStarted(player); + NonAdvancementHooks.grantFirstPregnancy(player); + } + } + } + } + } + } + } + } + } + } + } + } + + @Nullable + static NeedsOfNature.PregnancyVariantData capturePregnancyVariantData(ServerWorld world, Entity entity) { + if (world != null && entity != null) { + Map components = new LinkedHashMap<>(); + + for (NeedsOfNature.VariantComponentDescriptor descriptor : PREGNANCY_VARIANT_COMPONENTS) { + descriptor.capture(world, entity, components); + } + + Integer horseMarking = null; + if (entity instanceof HorseEntity horse) { + horseMarking = horse.getMarking().getIndex(); + } + + return normalizePregnancyVariantData(new NeedsOfNature.PregnancyVariantData(components, horseMarking)); + } else { + return null; + } + } + + @Nullable + public static NeedsOfNature.PregnancyVariantData normalizePregnancyVariantData(@Nullable NeedsOfNature.PregnancyVariantData data) { + return data != null && !data.isEmpty() ? data : null; + } + + @Nullable + public static String encodePregnancyVariantData(@Nullable NeedsOfNature.PregnancyVariantData data) { + NeedsOfNature.PregnancyVariantData normalized = normalizePregnancyVariantData(data); + return normalized == null ? null : HOST_CONFIG_SYNC_GSON.toJson(normalized); + } + + @Nullable + public static NeedsOfNature.PregnancyVariantData decodePregnancyVariantData(@Nullable String raw) { + if (raw != null && !raw.isBlank()) { + try { + return normalizePregnancyVariantData( + (NeedsOfNature.PregnancyVariantData)HOST_CONFIG_SYNC_GSON.fromJson(raw, NeedsOfNature.PregnancyVariantData.class) + ); + } catch (RuntimeException e) { + LOGGER.warn("[NoN] Ignoring malformed pregnancy variant data.", e); + return null; + } + } else { + return null; + } + } + + private static void applyPregnancyVariantData(ServerWorld world, Entity entity, @Nullable NeedsOfNature.PregnancyVariantData data) { + NeedsOfNature.PregnancyVariantData normalized = normalizePregnancyVariantData(data); + if (world != null && entity != null && normalized != null) { + Map components = normalized.components(); + + for (NeedsOfNature.VariantComponentDescriptor descriptor : PREGNANCY_VARIANT_COMPONENTS) { + descriptor.apply(world, entity, components.get(descriptor.key())); + } + + if (normalized.horseMarkingIndex() != null && entity instanceof HorseEntity horse) { + HorseMarking marking = HorseMarking.byIndex(normalized.horseMarkingIndex()); + HorseColor color = horse.getHorseColor(); + Method method = findMethod(horse.getClass(), "setHorseVariant", HorseColor.class, HorseMarking.class); + if (method != null) { + tryInvokeMethod(horse, method, color, marking); + } + } + } + } + + public static void applyPregnancyVariantDataForEgg(ServerWorld world, Entity entity, @Nullable NeedsOfNature.PregnancyVariantData data) { + applyPregnancyVariantData(world, entity, data); + } + + private static NeedsOfNature.VariantComponentDescriptor registryVariant( + String key, ComponentType> componentType, RegistryKey> registryKey + ) { + return new NeedsOfNature.VariantComponentDescriptor() { + @Override + public String key() { + return key; + } + + @Override + public void capture(ServerWorld world, Entity entity, Map out) { + RegistryEntry entry = (RegistryEntry)entity.get(componentType); + if (entry != null) { + entry.getKey().map(RegistryKey::getValue).ifPresent(id -> out.put(key, id.toString())); + } + } + + @Override + public void apply(ServerWorld world, Entity entity, String value) { + Identifier id = Identifier.tryParse(value == null ? "" : value); + if (id != null) { + world.getRegistryManager() + .getOptional(registryKey) + .flatMap(registry -> registry.getEntry(id)) + .ifPresent(entry -> entity.setComponent(componentType, entry)); + } + } + }; + } + + private static NeedsOfNature.VariantComponentDescriptor lazyRegistryVariant( + String key, ComponentType> componentType, RegistryKey> registryKey + ) { + return new NeedsOfNature.VariantComponentDescriptor() { + @Override + public String key() { + return key; + } + + @Override + public void capture(ServerWorld world, Entity entity, Map out) { + LazyRegistryEntryReference value = (LazyRegistryEntryReference)entity.get(componentType); + if (value != null) { + Optional> keyValue = value.getKey(); + if (keyValue.isEmpty()) { + keyValue = value.resolveEntry(world.getRegistryManager()).flatMap(RegistryEntry::getKey); + } + + keyValue.map(RegistryKey::getValue).ifPresent(id -> out.put(key, id.toString())); + } + } + + @Override + public void apply(ServerWorld world, Entity entity, String value) { + Identifier id = Identifier.tryParse(value == null ? "" : value); + if (id != null) { + RegistryKey valueKey = RegistryKey.of(registryKey, id); + entity.setComponent(componentType, new LazyRegistryEntryReference(valueKey)); + } + } + }; + } + + private static > NeedsOfNature.VariantComponentDescriptor enumVariant(String key, ComponentType componentType, Class enumClass) { + return new NeedsOfNature.VariantComponentDescriptor() { + @Override + public String key() { + return key; + } + + @Override + public void capture(ServerWorld world, Entity entity, Map out) { + E value = (E)entity.get(componentType); + if (value != null) { + out.put(key, value.name()); + } + } + + @Override + public void apply(ServerWorld world, Entity entity, String value) { + if (value != null && !value.isBlank()) { + try { + entity.setComponent(componentType, Enum.valueOf(enumClass, value)); + } catch (IllegalArgumentException ignored) { + NeedsOfNature.LOGGER.warn("[NoN] Ignoring unknown pregnancy variant value {}={}.", key, value); + } + } + } + }; + } + + static int applyFertilityIncreaserChance(ServerPlayerEntity player, int baseChancePercent) { + int base = Math.max(0, Math.min(100, baseChancePercent)); + return Math.max(base, getFertilityIncreaserChanceFloor(player)); + } + + static int getFertilityIncreaserChanceFloor(ServerPlayerEntity player) { + if (player == null) { + return 0; + } + + int boostedFloor = 0; + + for (StatusEffectInstance effect : player.getStatusEffects()) { + if (effect != null && effect.getEffectType().value() == NonStatusEffects.FERTILITY_INCREASER) { + int floor = effect.getAmplifier() >= 1 ? 95 : 80; + boostedFloor = Math.max(boostedFloor, floor); + } + } + + return boostedFloor; + } + + private static void refreshPregnancyCueRuntime(ServerWorld world, UUID instanceId, long stageStartTick, Identifier fallbackAnimationId) { + if (world != null && instanceId != null) { + NeedsOfNature.PregnancyHatchState hatchState = PREGNANCY_HATCH_BY_INSTANCE.get(instanceId); + if (hatchState == null) { + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(instanceId); + } else { + AnimationStageInfo currentStage = AnimationService.getCurrentStage(world, instanceId); + Identifier clipId = currentStage != null && currentStage.animationId() != null ? currentStage.animationId() : fallbackAnimationId; + if (clipId == null) { + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(instanceId); + } else { + Integer cueTick = AnimationCueIndex.getFirstCueTick(clipId, "birth"); + long startTick = Math.max(0L, stageStartTick); + double birthLoopMultiplier = pregnancyBirthLoopSpeedMultiplier(clipId, hatchState); + double runtimeStageSpeed = clampAnimationSpeed(resolveStageSpeed(currentStage) * birthLoopMultiplier); + NeedsOfNature.PregnancyCueRuntime existing = PREGNANCY_CUE_RUNTIME_BY_INSTANCE.get(instanceId); + if (existing == null + || !existing.worldKey().equals(world.getRegistryKey()) + || existing.stageStartTick() != startTick + || !existing.clipId().equals(clipId) + || !Objects.equals(existing.cueTick(), cueTick) + || !(Math.abs(existing.stageSpeed() - runtimeStageSpeed) < 1.0E-4)) { + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.put( + instanceId, new NeedsOfNature.PregnancyCueRuntime(world.getRegistryKey(), startTick, clipId, cueTick, false, runtimeStageSpeed) + ); + applyPregnancyBirthLoopSpeed(world, instanceId, birthLoopMultiplier); + syncPregnancyLoopProgressDuration(world, instanceId, currentStage, runtimeStageSpeed); + Integer stageNumber = NonPeakStages.stageNumberFromId(clipId); + boolean cueExpected = stageNumber != null && stageNumber >= 2; + if (cueTick == null && cueExpected && (existing == null || !clipId.equals(existing.clipId()) || existing.cueTick() != null)) { + LOGGER.warn("[NoN] Missing birth cue in birth clip {}.", clipId); + ServerPlayerEntity player = findPlayerOnWorldServer(world, hatchState.playerUuid()); + if (player != null && player.getEntityWorld() == world) { + sendDebugChat( + player, NonDebugChatCategory.SETUP, Text.translatable("debug.needsofnature.missing_birth_cue", new Object[]{clipId.toString()}) + ); + } + } + } + } + } + } + } + + private static void applyPregnancyBirthLoopSpeed(ServerWorld world, UUID instanceId, double multiplier) { + if (world != null && instanceId != null) { + if (!(Math.abs(multiplier - 1.0) < 1.0E-4)) { + ServerAnimationController.adjustSpeed(world, instanceId, multiplier, null); + } + } + } + + private static void syncPregnancyLoopProgressDuration(ServerWorld world, UUID instanceId, @Nullable AnimationStageInfo stage, double stageSpeed) { + if (world != null && instanceId != null && stage != null) { + Integer stageNumber = NonPeakStages.stageNumberFromId(stage.animationId()); + if (stageNumber != null && stageNumber == 2) { + long cycleWorldTicks = Math.max(1L, elapsedWorldTicksFromAnimationTicks(Math.max(1L, stage.lengthTicks()), stageSpeed)); + ServerAnimationController.setCurrentStageProgressDuration(world, instanceId, cycleWorldTicks); + } + } + } + + private static double pregnancyBirthLoopSpeedMultiplier(Identifier clipId, NeedsOfNature.PregnancyHatchState hatchState) { + Integer stageNumber = NonPeakStages.stageNumberFromId(clipId); + if (stageNumber == null || stageNumber != 2) { + return 1.0; + } + + if (hatchState == null) { + return 1.0; + } + + int pendingRepeatsBeforeFinalLoop = Math.max(0, hatchState.remainingSpawns() - 2); + if (pendingRepeatsBeforeFinalLoop <= 0) { + return 1.0; + } + + int variableRepeatCount = Math.max(1, hatchState.totalSpawnCount() - 2); + double normalizedPending = Math.max(0.0, Math.min(1.0, (double)pendingRepeatsBeforeFinalLoop / variableRepeatCount)); + double eased = 1.0 - Math.pow(1.0 - normalizedPending, 2.5); + double speedRange = 1.0; + return Math.max(1.0, Math.min(2.0, 1.0 + speedRange * eased)); + } + + private static void tickPregnancyCueRuntime(ServerWorld world, long nowTick) { + if (world != null && !PREGNANCY_CUE_RUNTIME_BY_INSTANCE.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + + for (Entry entry : PREGNANCY_CUE_RUNTIME_BY_INSTANCE.entrySet()) { + UUID instanceId = entry.getKey(); + NeedsOfNature.PregnancyCueRuntime runtime = entry.getValue(); + if (runtime.worldKey().equals(worldKey)) { + Integer cueTick = runtime.cueTick(); + long cueDueTick = runtime.stageStartTick() + + elapsedWorldTicksFromAnimationTicks(cueTick == null ? -1.0 : cueTick.intValue(), runtime.stageSpeed()); + if (!runtime.cueTriggered() && cueTick != null && cueTick >= 0 && nowTick >= cueDueTick) { + NeedsOfNature.PregnancyCueRuntime triggered = new NeedsOfNature.PregnancyCueRuntime( + runtime.worldKey(), runtime.stageStartTick(), runtime.clipId(), runtime.cueTick(), true, runtime.stageSpeed() + ); + if (PREGNANCY_CUE_RUNTIME_BY_INSTANCE.replace(instanceId, runtime, triggered)) { + handlePregnancyBirthCue(world, instanceId); + runtime = triggered; + } + } + + if (runtime.cueTriggered()) { + routePregnancyBirthStageAfterCycle(world, instanceId, runtime, nowTick); + } + } + } + } + } + + private static void tickPregnancy(ServerWorld world) { + if (world != null && !world.getPlayers().isEmpty()) { + long globalTick = getGlobalTick(world); + + for (ServerPlayerEntity player : world.getPlayers()) { + tickPregnancyForPlayer(world, player, globalTick); + } + } + } + + private static void tickPregnancyForPlayer(ServerWorld world, ServerPlayerEntity player, long nowTick) { + if (world != null && player != null) { + PregnancyHolder holder = getPregnancyHolder(player); + if (holder != null) { + Identifier entityTypeId = holder.getPregnantEntityTypeId(); + if (entityTypeId == null) { + PREGNANCY_START_RETRY_TICKS.remove(player.getUuid()); + } else if (NonGenderSystem.isOnlyMale(player)) { + clearPregnancyState(player, true); + } else if (player.isAlive() && !player.isRemoved()) { + if (!holder.isPregnancyPendingHatch()) { + long endTick = holder.getPregnancyEndTick(); + if (endTick > nowTick) { + if (!player.hasStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT))) { + clearPregnancyState(player, false); + } + + return; + } + + markPregnancyPending(player, nowTick); + } else { + refreshPendingPregnancyEffect(player); + } + + tryStartPendingPregnancyHatch(world, player, nowTick); + } + } + } + } + + private static void tryStartPendingPregnancyHatch(ServerWorld world, ServerPlayerEntity player, long nowTick) { + if (world != null && player != null) { + PregnancyHolder holder = getPregnancyHolder(player); + if (holder != null && holder.isPregnancyPendingHatch()) { + if (NonGenderSystem.isOnlyMale(player)) { + clearPregnancyState(player, true); + } else if (player.isAlive() && !player.isRemoved() && !player.isSpectator()) { + if (player.getEntityWorld() == world) { + UUID activeInstance = PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.get(player.getUuid()); + if (activeInstance != null) { + if (hasActivePlayerAnimation(player)) { + return; + } + + PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.remove(player.getUuid(), activeInstance); + PREGNANCY_HATCH_BY_INSTANCE.remove(activeInstance); + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(activeInstance); + } + + if (hasActivePlayerAnimation(player)) { + queuePendingPregnancyHatch(world, player, holder, nowTick); + } else { + if (PREGNANCY_QUEUED_PLAYERS.contains(player.getUuid())) { + if (AnimationService.isActorPendingOrActive(world, player.getUuid())) { + return; + } + + PREGNANCY_QUEUED_PLAYERS.remove(player.getUuid()); + } + + if (!player.isTouchingWater() && !player.isSubmergedInWater()) { + if (player.isOnGround()) { + long retryAt = PREGNANCY_START_RETRY_TICKS.getOrDefault(player.getUuid(), 0L); + if (retryAt <= nowTick) { + Identifier entityTypeId = holder.getPregnantEntityTypeId(); + if (entityTypeId != null) { + Identifier animationId = resolveBirthAnimationId(player, entityTypeId); + if (animationId == null) { + LOGGER.warn( + "[NoN] Missing birth-tag animation for player {}; clearing pregnancy for {}.", + player.getName().getString(), + entityTypeId + ); + sendDebugChat( + player, + NonDebugChatCategory.SETUP, + Text.translatable("debug.needsofnature.missing_birth_animation", new Object[]{entityTypeId.toString()}) + ); + clearPregnancyState(player, true); + } else { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId); + if (definition == null) { + LOGGER.warn("[NoN] Missing birth animation definition {}.", animationId); + sendDebugChat( + player, + NonDebugChatCategory.SETUP, + Text.translatable("debug.needsofnature.missing_birth_animation", new Object[]{entityTypeId.toString()}) + ); + clearPregnancyState(player, true); + } else { + List actors = List.of(player); + List actorKeys = AnimationDefinitions.resolveActorKeys(definition, actors, world.getRandom()); + int spawnCount = resolvePregnancyOffspringCount(holder); + NonEntityProfiles.ResolvedProfile profile = NonEntityProfiles.resolve(entityTypeId); + AnimationStartContext startContext = buildPregnancyHatchContext( + entityTypeId, profile, spawnCount, holder.getPregnancyVariantData() + ); + UUID started = startAnimationNowWithContext( + world, + definition.id(), + actors, + actorKeys, + definition.stages(), + NonAnimationDamageBehavior.BLOCK_DAMAGE, + true, + null, + startContext + ); + if (started == null) { + PREGNANCY_START_RETRY_TICKS.put(player.getUuid(), nowTick + 20L); + } else { + PREGNANCY_HATCH_BY_INSTANCE.putIfAbsent( + started, + new NeedsOfNature.PregnancyHatchState( + player.getUuid(), + entityTypeId, + profile.birthEntityTypeId(), + profile.birthMode(), + spawnCount, + spawnCount, + 0, + holder.getPregnancyVariantData() + ) + ); + PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.put(player.getUuid(), started); + PREGNANCY_START_RETRY_TICKS.remove(player.getUuid()); + refreshPregnancyCueRuntime(world, started, nowTick, definition.id()); + } + } + } + } + } + } + } + } + } + } + } + } + } + + private static void queuePendingPregnancyHatch(ServerWorld world, ServerPlayerEntity player, PregnancyHolder holder, long nowTick) { + if (world != null && player != null && holder != null) { + if (NonGenderSystem.isOnlyMale(player)) { + clearPregnancyState(player, true); + } else { + UUID playerUuid = player.getUuid(); + if (!PREGNANCY_QUEUED_PLAYERS.contains(playerUuid)) { + long retryAt = PREGNANCY_START_RETRY_TICKS.getOrDefault(playerUuid, 0L); + if (retryAt <= nowTick) { + Identifier entityTypeId = holder.getPregnantEntityTypeId(); + if (entityTypeId != null) { + Identifier animationId = resolveBirthAnimationId(player, entityTypeId); + if (animationId == null) { + LOGGER.warn("[NoN] Missing birth-tag animation for player {}; clearing pregnancy for {}.", player.getName().getString(), entityTypeId); + sendDebugChat( + player, + NonDebugChatCategory.SETUP, + Text.translatable("debug.needsofnature.missing_birth_animation", new Object[]{entityTypeId.toString()}) + ); + clearPregnancyState(player, true); + } else { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId); + if (definition == null) { + LOGGER.warn("[NoN] Missing birth animation definition {}.", animationId); + sendDebugChat( + player, + NonDebugChatCategory.SETUP, + Text.translatable("debug.needsofnature.missing_birth_animation", new Object[]{entityTypeId.toString()}) + ); + clearPregnancyState(player, true); + } else { + int spawnCount = resolvePregnancyOffspringCount(holder); + NonEntityProfiles.ResolvedProfile profile = NonEntityProfiles.resolve(entityTypeId); + boolean queued = enqueueAnimation( + world, + definition.id(), + List.of(player), + 1, + NonAnimationDamageBehavior.BLOCK_DAMAGE, + true, + buildPregnancyHatchContext(entityTypeId, profile, spawnCount, holder.getPregnancyVariantData()), + player + ); + if (!queued) { + PREGNANCY_START_RETRY_TICKS.put(playerUuid, nowTick + 20L); + } else { + PREGNANCY_QUEUED_PLAYERS.add(playerUuid); + PREGNANCY_START_RETRY_TICKS.remove(playerUuid); + } + } + } + } + } + } + } + } + } + + private static AnimationStartContext buildPregnancyHatchContext( + Identifier entityTypeId, NonEntityProfiles.ResolvedProfile profile, int spawnCount, @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + Identifier birthEntityTypeId = profile == null ? entityTypeId : profile.birthEntityTypeId(); + String birthMode = profile == null ? "direct" : profile.birthMode(); + NonAnimationSessionData.Pregnancy pregnancy = new NonAnimationSessionData.Pregnancy( + entityTypeId, birthEntityTypeId, birthMode, Math.max(1, Math.min(16, spawnCount)), variantData + ); + return AnimationStartContext.empty().withSessionData(NonAnimationSessionData.pregnancy(pregnancy)); + } + + private static int resolvePregnancyOffspringCount(@Nullable PregnancyHolder holder) { + int offspringCount = holder == null ? 0 : holder.getPregnancyOffspringCount(); + return offspringCount <= 0 ? 1 : Math.max(1, Math.min(16, offspringCount)); + } + + private static int rollPregnancyOffspringCount(ServerPlayerEntity player, Identifier entityTypeId, int exactOverride) { + if (exactOverride > 0) { + return Math.max(1, Math.min(16, exactOverride)); + } + + NonConfig.OffspringCountRange range = NonEntityProfiles.resolve(entityTypeId).offspringRange(); + int min = range.min(); + int max = range.max(); + return max <= min ? min : min + player.getRandom().nextInt(max - min + 1); + } + + private static Identifier resolveBirthAnimationId(ServerPlayerEntity player, Identifier entityTypeId) { + if (player != null && entityTypeId != null) { + List actors = List.of(player); + Identifier specific = findMatchedAnimationIdByTags(actors, Set.of("birth_" + sanitizeBirthTagPart(entityTypeId))); + return specific != null ? specific : findMatchedAnimationIdByTags(actors, Set.of("birth")); + } else { + return null; + } + } + + private static String sanitizeBirthTagPart(Identifier entityTypeId) { + String raw = entityTypeId.toString().toLowerCase(Locale.ROOT); + StringBuilder out = new StringBuilder(raw.length()); + + for (int i = 0; i < raw.length(); i++) { + char c = raw.charAt(i); + if ((c < 'a' || c > 'z') && (c < '0' || c > '9')) { + out.append('_'); + } else { + out.append(c); + } + } + + return out.toString(); + } + + private static void routePregnancyBirthStage(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + NeedsOfNature.PregnancyHatchState hatchState = PREGNANCY_HATCH_BY_INSTANCE.get(instanceId); + if (hatchState != null && hatchState.remainingSpawns() <= 1) { + AnimationStageInfo currentStage = AnimationService.getCurrentStage(world, instanceId); + Integer stageNumber = NonPeakStages.stageNumberFromId(currentStage == null ? null : currentStage.animationId()); + if (stageNumber != null && stageNumber == 2) { + AnimationService.advanceToStage(world, instanceId, 3); + } + } + } + } + + private static void routePregnancyBirthStageAfterCycle(ServerWorld world, UUID instanceId, NeedsOfNature.PregnancyCueRuntime runtime, long nowTick) { + if (world != null && instanceId != null && runtime != null) { + NeedsOfNature.PregnancyHatchState hatchState = PREGNANCY_HATCH_BY_INSTANCE.get(instanceId); + if (hatchState != null && hatchState.remainingSpawns() > 0) { + AnimationStageInfo currentStage = AnimationService.getCurrentStage(world, instanceId); + Integer stageNumber = NonPeakStages.stageNumberFromId(currentStage == null ? null : currentStage.animationId()); + if (stageNumber != null && stageNumber == 2) { + long cycleTicks = Math.max(1L, currentStage.lengthTicks()); + long elapsedTicks = elapsedWorldTicksFromAnimationTicks(cycleTicks, runtime.stageSpeed()); + long routeTick = runtime.stageStartTick() + Math.max(1L, elapsedTicks); + if (nowTick >= routeTick) { + int targetStage = hatchState.remainingSpawns() > 1 ? 2 : 3; + AnimationService.advanceToStage(world, instanceId, targetStage); + } + } + } + } + } + + private static void handlePregnancyBirthCue(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + NeedsOfNature.PregnancyHatchState hatchState = PREGNANCY_HATCH_BY_INSTANCE.get(instanceId); + if (hatchState != null && hatchState.remainingSpawns() > 0) { + ServerPlayerEntity player = findPlayerOnWorldServer(world, hatchState.playerUuid()); + if (player != null && player.getEntityWorld() == world) { + int spawnNow = 1; + NeedsOfNature.PregnancyVariantData variantData = hatchState.sourceEntityTypeId().equals(hatchState.birthEntityTypeId()) + ? hatchState.variantData() + : null; + int spawned = spawnPregnancyOutcome( + world, player, hatchState.sourceEntityTypeId(), hatchState.birthEntityTypeId(), hatchState.birthMode(), spawnNow, variantData + ); + if (spawned > 0) { + NonStats.addOffspringSpawned(player, hatchState.birthEntityTypeId(), spawned); + } + + NeedsOfNature.PregnancyHatchState updated = hatchState.withProgress(hatchState.remainingSpawns() - spawnNow, hatchState.spawnedCount() + spawned); + if (updated.remainingSpawns() <= 0) { + PREGNANCY_HATCH_BY_INSTANCE.remove(instanceId); + finishPregnancyHatch(world, instanceId, player, updated); + } else { + PREGNANCY_HATCH_BY_INSTANCE.put(instanceId, updated); + } + } + } + } + } + + private static void finalizePregnancyHatch(ServerWorld world, UUID instanceId, boolean cueTriggered) { + if (world != null && instanceId != null) { + NeedsOfNature.PregnancyHatchState hatchState = PREGNANCY_HATCH_BY_INSTANCE.remove(instanceId); + NeedsOfNature.PregnancyCueRuntime cueRuntime = PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(instanceId); + if (hatchState != null) { + ServerPlayerEntity player = findPlayerOnWorldServer(world, hatchState.playerUuid()); + if (player != null && player.getEntityWorld() == world) { + if (!cueTriggered) { + String clip = cueRuntime != null && cueRuntime.clipId() != null ? cueRuntime.clipId().toString() : "unknown"; + LOGGER.warn( + "[NoN] Birth fallback spawn on STOP for instance {} (clip={}, cue={}).", + new Object[]{instanceId, clip, cueRuntime == null ? "none" : String.valueOf(cueRuntime.cueTick())} + ); + sendDebugChat(player, NonDebugChatCategory.SETUP, Text.translatable("debug.needsofnature.birth_fallback_spawn", new Object[]{clip})); + } + + int remaining = Math.max(0, hatchState.remainingSpawns()); + NeedsOfNature.PregnancyVariantData variantData = hatchState.sourceEntityTypeId().equals(hatchState.birthEntityTypeId()) + ? hatchState.variantData() + : null; + int spawned = remaining > 0 + ? spawnPregnancyOutcome( + world, player, hatchState.sourceEntityTypeId(), hatchState.birthEntityTypeId(), hatchState.birthMode(), remaining, variantData + ) + : 0; + if (spawned > 0) { + NonStats.addOffspringSpawned(player, hatchState.birthEntityTypeId(), spawned); + } + + finishPregnancyHatch(world, instanceId, player, hatchState.withProgress(0, hatchState.spawnedCount() + spawned)); + } + } + } + } + + private static void finishPregnancyHatch(ServerWorld world, UUID instanceId, ServerPlayerEntity player, NeedsOfNature.PregnancyHatchState hatchState) { + if (world != null && instanceId != null && player != null && hatchState != null) { + PREGNANCY_HATCH_BY_INSTANCE.remove(instanceId); + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(instanceId); + PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.remove(hatchState.playerUuid(), instanceId); + PREGNANCY_QUEUED_PLAYERS.remove(hatchState.playerUuid()); + NonStats.incrementPregnancyCompleted(player); + NonAdvancementHooks.checkMotherNature(player); + if (hatchState.spawnedCount() > 1) { + NonAdvancementHooks.grantProductivePregnancy(player); + } + + clearPregnancyState(player, true); + } + } + + private static int spawnPregnancyOutcome( + ServerWorld world, + ServerPlayerEntity player, + Identifier sourceEntityTypeId, + Identifier entityTypeId, + String birthMode, + int spawnCount, + @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + if (world == null || player == null || entityTypeId == null || spawnCount <= 0) { + return 0; + } else { + return "egg".equals(NonConfig.normalizeBirthMode(birthMode)) + ? spawnPregnancyEggs(world, player, sourceEntityTypeId, entityTypeId, spawnCount, variantData) + : spawnPregnancyOffspring(world, player, entityTypeId, spawnCount, variantData); + } + } + + private static int spawnPregnancyEggs( + ServerWorld world, + ServerPlayerEntity player, + Identifier sourceEntityTypeId, + Identifier entityTypeId, + int spawnCount, + @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + if (world != null && player != null && entityTypeId != null && spawnCount > 0) { + Identifier eggSourceEntityId = sourceEntityTypeId == null ? entityTypeId : sourceEntityTypeId; + NonConfig.EggProfile eggProfile = NonEntityProfiles.resolve(eggSourceEntityId).eggProfile(); + int hatchSeconds = getConfig().getPregnancyEggHatchSecondsFor(eggSourceEntityId.toString()); + long hatchAtTick = getGlobalTick(world) + Math.max(1, hatchSeconds) * 20L; + int spawned = 0; + + for (int i = 0; i < spawnCount; i++) { + PregnancyEggEntity egg = (PregnancyEggEntity)PREGNANCY_EGG_ENTITY_TYPE.create(world, SpawnReason.BREEDING); + if (egg != null) { + float yaw = world.getRandom().nextFloat() * 360.0F; + Vec3d spawnOffset = randomPregnancyEggSpawnOffset(world, spawnCount); + egg.refreshPositionAndAngles(player.getX() + spawnOffset.x, player.getY(), player.getZ() + spawnOffset.z, yaw, 0.0F); + egg.configure(player.getUuid(), eggSourceEntityId, entityTypeId, hatchAtTick, variantData, eggProfile); + if (world.spawnEntity(egg)) { + spawned++; + } + } + } + + return spawned; + } else { + return 0; + } + } + + private static Vec3d randomPregnancyEggSpawnOffset(ServerWorld world, int spawnCount) { + double angle = world.getRandom().nextDouble() * Math.PI * 2.0; + double radius = 0.08 + world.getRandom().nextDouble() * 0.1; + return new Vec3d(Math.cos(angle) * radius, 0.0, Math.sin(angle) * radius); + } + + private static int spawnPregnancyOffspring( + ServerWorld world, ServerPlayerEntity player, Identifier entityTypeId, int spawnCount, @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + if (world != null && player != null && entityTypeId != null && spawnCount > 0) { + if (!Registries.ENTITY_TYPE.containsId(entityTypeId)) { + LOGGER.warn("[NoN] Cannot spawn pregnancy offspring: unknown entity type {}.", entityTypeId); + sendDebugChat( + player, + NonDebugChatCategory.SETUP, + Text.translatable("debug.needsofnature.missing_pregnancy_entity_type", new Object[]{entityTypeId.toString()}) + ); + return 0; + } + + EntityType entityType = (EntityType)Registries.ENTITY_TYPE.get(entityTypeId); + int spawned = 0; + + for (int i = 0; i < spawnCount; i++) { + Entity entity = entityType.create(world, SpawnReason.BREEDING); + if (entity != null) { + entity.refreshPositionAndAngles(player.getX(), player.getY(), player.getZ(), player.getYaw(), player.getPitch()); + if (entity instanceof MobEntity mob) { + mob.initialize(world, world.getLocalDifficulty(mob.getBlockPos()), SpawnReason.BREEDING, null); + applyPregnancyVariantData(world, mob, variantData); + applyPregnancyMobFlags(mob, player); + } + + if (world.spawnEntity(entity)) { + spawned++; + } + } + } + + return spawned; + } else { + return 0; + } + } + + private static void applyPregnancyMobFlags(MobEntity mob, ServerPlayerEntity player) { + applyPregnancyMobFlagsForEgg(mob, player); + } + + public static void applyPregnancyMobFlagsForEgg(MobEntity mob, @Nullable ServerPlayerEntity player) { + if (mob != null) { + mob.setPersistent(); + if (mob instanceof PassiveEntity passive) { + passive.setBreedingAge(-24000); + } else { + tryInvokeBooleanSetter(mob, "setBaby"); + } + + if (player != null && getConfig().isPregnancyAutoTameMobs()) { + if (mob instanceof TameableEntity tameable) { + tameable.setTamedBy(player); + tameable.setTarget(null); + } else if (mob instanceof AbstractHorseEntity horse) { + horse.bondWithPlayer(player); + horse.setOwner(player); + } else { + tryInvokeBooleanSetter(mob, "setTamed"); + tryInvokeBooleanSetter(mob, "setTame"); + tryInvokeSingleArg(mob, "setOwner", PlayerEntity.class, player); + tryInvokeSingleArg(mob, "setOwner", ServerPlayerEntity.class, player); + tryInvokeSingleArg(mob, "setOwnerUuid", UUID.class, player.getUuid()); + tryInvokeSingleArg(mob, "bondWithPlayer", PlayerEntity.class, player); + tryInvokeSingleArg(mob, "bondWithPlayer", ServerPlayerEntity.class, player); + } + } + + if (getConfig().pregnancyFriendlyMobsIgnorePlayers()) { + if (mob instanceof Angerable angerable) { + angerable.stopAnger(); + } + + if (mob.getTarget() instanceof ServerPlayerEntity) { + mob.setTarget(null); + } + + mob.addCommandTag("non.pregnancy_friendly"); + } + } + } + + private static void tryInvokeBooleanSetter(Object target, String methodName) { + if (target != null && methodName != null && !methodName.isBlank()) { + Method method = findMethod(target.getClass(), methodName, boolean.class); + if (method != null) { + tryInvokeMethod(target, method, true); + } + } + } + + private static void tryInvokeSingleArg(Object target, String methodName, Class argType, Object argValue) { + if (target != null && methodName != null && !methodName.isBlank() && argType != null) { + Method method = findMethod(target.getClass(), methodName, argType); + if (method != null) { + tryInvokeMethod(target, method, argValue); + } + } + } + + private static Method findMethod(Class type, String methodName, Class... parameterTypes) { + for (Class cursor = type; cursor != null; cursor = cursor.getSuperclass()) { + Method declared = findDeclaredMethod(cursor, methodName, parameterTypes); + if (declared != null) { + return declared; + } + } + + return findPublicMethod(Objects.requireNonNull(type), methodName, parameterTypes); + } + + private static Method findDeclaredMethod(Class owner, String methodName, Class... parameterTypes) { + Method[] methods = owner.getDeclaredMethods(); + + for (Method method : methods) { + if (method.getName().equals(methodName) && parameterTypesEqual(method.getParameterTypes(), parameterTypes)) { + try { + method.setAccessible(true); + return method; + } catch (RuntimeException e) { + logReflectionFailureOnce("findDeclared:" + owner.getName() + "#" + methodName, e); + return null; + } + } + } + + return null; + } + + private static Method findPublicMethod(Class owner, String methodName, Class... parameterTypes) { + Method[] methods = owner.getMethods(); + + for (Method method : methods) { + if (method.getName().equals(methodName) && parameterTypesEqual(method.getParameterTypes(), parameterTypes)) { + try { + method.setAccessible(true); + return method; + } catch (RuntimeException e) { + logReflectionFailureOnce("findPublic:" + owner.getName() + "#" + methodName, e); + return null; + } + } + } + + return null; + } + + private static boolean parameterTypesEqual(Class[] left, Class[] right) { + if (left.length != right.length) { + return false; + } + + for (int i = 0; i < left.length; i++) { + if (!left[i].equals(right[i])) { + return false; + } + } + + return true; + } + + private static void tryInvokeMethod(Object target, Method method, Object... args) { + if (target != null && method != null) { + try { + method.invoke(target, args); + } catch (ReflectiveOperationException | RuntimeException e) { + logReflectionFailureOnce("invoke:" + method.toGenericString(), e); + } + } + } + + private static void logReflectionFailureOnce(String key, Throwable error) { + if (key != null && REFLECTION_FAILURE_LOGS.add(key)) { + LOGGER.debug("[NoN] Reflection helper failure at {}", key, error); + } + } + + public static boolean isPregnancyFriendlyMob(MobEntity mob) { + return mob != null && getConfig().pregnancyFriendlyMobsIgnorePlayers() && mob.getCommandTags().contains("non.pregnancy_friendly"); + } + + static long getGlobalTick(ServerWorld world) { + if (world != null && world.getServer() != null) { + ServerWorld overworld = world.getServer().getOverworld(); + return overworld == null ? world.getTime() : overworld.getTime(); + } else { + return 0L; + } + } + + private static PregnancyHolder getPregnancyHolder(ServerPlayerEntity player) { + return player instanceof PregnancyHolder holder ? holder : null; + } + + static Identifier getPregnantEntityTypeId(ServerPlayerEntity player) { + PregnancyHolder holder = getPregnancyHolder(player); + return holder == null ? null : holder.getPregnantEntityTypeId(); + } + + static void beginPregnancy(ServerPlayerEntity player, Identifier entityTypeId, long endTick) { + beginPregnancy(player, entityTypeId, endTick, null); + } + + static void beginPregnancy(ServerPlayerEntity player, Identifier entityTypeId, long endTick, @Nullable NeedsOfNature.PregnancyVariantData variantData) { + beginPregnancy(player, entityTypeId, endTick, variantData, 0); + } + + static void beginPregnancy( + ServerPlayerEntity player, Identifier entityTypeId, long endTick, @Nullable NeedsOfNature.PregnancyVariantData variantData, int offspringCountOverride + ) { + if (player != null && entityTypeId != null) { + if (NonGenderSystem.isOnlyMale(player)) { + clearPregnancyState(player, true); + } else if (((NonEvents.AllowPregnancy)NonEvents.ALLOW_PREGNANCY.invoker()).allowPregnancy(player, entityTypeId)) { + PregnancyHolder holder = getPregnancyHolder(player); + if (holder != null) { + int offspringCount = rollPregnancyOffspringCount(player, entityTypeId, offspringCountOverride); + holder.setPregnancyState(entityTypeId, Math.max(0L, endTick), false, variantData); + holder.setPregnancyOffspringCount(offspringCount); + syncPregnancyState(player); + NonApiInternals.firePregnancyChanged(player); + PREGNANCY_START_RETRY_TICKS.remove(player.getUuid()); + PREGNANCY_QUEUED_PLAYERS.remove(player.getUuid()); + UUID active = PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.remove(player.getUuid()); + if (active != null) { + PREGNANCY_HATCH_BY_INSTANCE.remove(active); + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(active); + } + + long nowTick = player.getEntityWorld() instanceof ServerWorld world ? getGlobalTick(world) : 0L; + long remaining = Math.max(1L, endTick - nowTick); + int duration = (int)Math.min(2147483647L, remaining); + player.addStatusEffect(new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT), duration, 0, false, false, true)); + } + } + } + } + + static void markPregnancyPending(ServerPlayerEntity player, long nowTick) { + if (player != null) { + if (NonGenderSystem.isOnlyMale(player)) { + clearPregnancyState(player, true); + } else { + PregnancyHolder holder = getPregnancyHolder(player); + if (holder != null) { + Identifier entityTypeId = holder.getPregnantEntityTypeId(); + if (entityTypeId != null) { + holder.setPregnancyState(entityTypeId, Math.max(0L, nowTick), true, holder.getPregnancyVariantData()); + refreshPendingPregnancyEffect(player); + NonApiInternals.firePregnancyChanged(player); + } + } + } + } + } + + private static void refreshPendingPregnancyEffect(ServerPlayerEntity player) { + if (player != null) { + RegistryEntry effect = Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT); + StatusEffectInstance current = player.getStatusEffect(effect); + if (current == null || current.getDuration() < 21) { + player.addStatusEffect(new StatusEffectInstance(effect, 21, 0, false, false, true)); + } + } + } + + @Nullable + private static ServerPlayerEntity findPlayerOnWorldServer(ServerWorld world, UUID playerUuid) { + if (world != null && playerUuid != null) { + MinecraftServer server = world.getServer(); + return server == null ? null : server.getPlayerManager().getPlayer(playerUuid); + } else { + return null; + } + } + + static void clearPregnancyState(ServerPlayerEntity player, boolean removeEffect) { + if (player != null) { + PregnancyHolder holder = getPregnancyHolder(player); + if (holder != null) { + holder.clearPregnancyState(); + } + + clearPregnancyRuntime(player.getUuid()); + if (removeEffect) { + player.removeStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT)); + } + + syncPregnancyState(player); + NonApiInternals.firePregnancyChanged(player); + } + } + + private static void clearPregnancyRuntime(UUID playerUuid) { + if (playerUuid != null) { + PREGNANCY_START_RETRY_TICKS.remove(playerUuid); + PREGNANCY_QUEUED_PLAYERS.remove(playerUuid); + UUID active = PREGNANCY_ACTIVE_INSTANCE_BY_PLAYER.remove(playerUuid); + if (active != null) { + PREGNANCY_HATCH_BY_INSTANCE.remove(active); + PREGNANCY_CUE_RUNTIME_BY_INSTANCE.remove(active); + } + } + } + + private static void refreshPregnancyAfterDeathCopy(ServerPlayerEntity player) { + PregnancyHolder holder = getPregnancyHolder(player); + if (holder != null && holder.getPregnantEntityTypeId() != null) { + long nowTick = player.getEntityWorld() instanceof ServerWorld world ? getGlobalTick(world) : 0L; + if (!holder.isPregnancyPendingHatch() && holder.getPregnancyEndTick() > nowTick) { + long remaining = Math.max(1L, holder.getPregnancyEndTick() - nowTick); + int duration = (int)Math.min(2147483647L, remaining); + player.addStatusEffect(new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT), duration, 0, false, false, true)); + } else { + markPregnancyPending(player, nowTick); + } + } + } + + public static void clearPregnancyOnDeath(ServerPlayerEntity player) { + clearPregnancyState(player, true); + } + + private void registerAttackStopRequests() { + ServerPlayNetworking.registerGlobalReceiver(StopAttackAnimationC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (player.getEntityWorld() instanceof ServerWorld world) { + UUID instanceId = payload.instanceId(); + if (!NonPlayerAttackSystem.isContest(instanceId)) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (isAttackSnapshot(snapshot) && snapshot.actorUuids().contains(player.getUuid())) { + completeAttackEscape(world, player, instanceId, null); + } + } + } + })); + } + + static void completeAttackEscape(ServerWorld world, ServerPlayerEntity player, UUID instanceId, @Nullable UUID preferredAttackerUuid) { + if (world != null && player != null && instanceId != null) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (isAttackSnapshot(snapshot) && snapshot.actorUuids().contains(player.getUuid())) { + List actors = snapshot.actorUuids(); + boolean defeatedInstance = isDefeatedInstance(world, instanceId); + int escapeInvulnerabilityTicks = getConfig().getAttackEscapeInvulnerabilityTicks(); + int escapeAnimationProtectionTicks = getConfig().getAttackEscapeAnimationProtectionTicks(); + if (defeatedInstance && escapeInvulnerabilityTicks > 0) { + setPostEscapeDamageInvulnerable(player, escapeInvulnerabilityTicks); + } + + setAnimationStartProtection(player, escapeAnimationProtectionTicks); + NeedsOfNature.PeakState peakState = PEAK_STATES.get(instanceId); + boolean allowReattack = peakState == null || !peakState.peaked; + clearQueuedAnimations(world, player); + if (!defeatedInstance) { + scheduleAttackEscapeResolution(world, player, actors, preferredAttackerUuid, allowReattack, escapeInvulnerabilityTicks); + NonStats.incrementAttackEscapeSuccess(player); + NonAdvancementHooks.resetTossedAround(player); + } + + AnimationService.stopAnimation(world, instanceId); + if (defeatedInstance && allowReattack) { + requestOriginalAttackers(world, player, actors); + } + } + } + } + + private record ActiveBreedingState(RegistryKey worldKey, UUID firstUuid, UUID secondUuid) { + } + + public enum AttackBlockReason { + PREGNANCY_FRIENDLY, + GATHERING_PAIR, + ENERGY_ABOVE_70_PLAYER_BUSY; + } + + private record BreedingPairKey(UUID firstUuid, UUID secondUuid) { + } + + private record FillBottleState(UUID playerUuid, boolean reservedBottle) { + } + + private record FinitePulseSchedule(List ticks, List amounts) { + } + + private record GatherPairKey(UUID mobUuid, UUID playerUuid) { + } + + private record GatherPairState(RegistryKey worldKey, long expiresTick) { + } + + private record LastDefeatedCandidate(MobEntity mob, NonGatherSystem.GatherCandidate gatherCandidate, AnimationService.MatchedAnimation defeatMatch) { + } + + private record LiquidGainProfile( + int amount, + int xpEligibleAmount, + LiquidHolder.LiquidComposition composition, + Identifier entityTypeId, + Map entityAmounts, + @Nullable NeedsOfNature.PregnancyVariantData donorVariantData, + List recentDonors + ) { + LiquidGainProfile { + entityAmounts = entityAmounts != null && !entityAmounts.isEmpty() ? Map.copyOf(entityAmounts) : Map.of(); + recentDonors = recentDonors != null && !recentDonors.isEmpty() ? List.copyOf(recentDonors) : List.of(); + } + } + + private record LiquidPulseResult(int addedMl, int xpEligibleAddedMl) { + private static final NeedsOfNature.LiquidPulseResult EMPTY = new NeedsOfNature.LiquidPulseResult(0, 0); + } + + private record LiquidSnapshot( + int stored, + LiquidHolder.LiquidComposition composition, + @Nullable Identifier entityTypeId, + @Nullable NeedsOfNature.PregnancyVariantData donorVariantData, + List recentDonors + ) { + static void copy(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) { + if (oldPlayer instanceof LiquidHolder oldHolder && newPlayer instanceof LiquidHolder newHolder) { + NeedsOfNature.LiquidSnapshot snapshot = new NeedsOfNature.LiquidSnapshot( + oldHolder.getLiquidStored(), + oldHolder.getLiquidComposition(), + oldHolder.getLiquidEntityTypeId(), + oldHolder.getLiquidDonorVariantData(), + oldHolder.getRecentLiquidDonors() + ); + newHolder.setLiquidStored(snapshot.stored()); + if (newHolder.getLiquidStored() <= 0) { + newHolder.clearLiquidComposition(); + } else { + if (snapshot.composition() == LiquidHolder.LiquidComposition.ENTITY && snapshot.entityTypeId() != null) { + newHolder.setLiquidCompositionEntity(snapshot.entityTypeId()); + newHolder.setLiquidDonorVariantData(snapshot.donorVariantData()); + } else if (snapshot.composition() == LiquidHolder.LiquidComposition.MIXED) { + newHolder.setLiquidCompositionMixed(); + } else { + newHolder.clearLiquidComposition(); + } + + newHolder.setRecentLiquidDonors(snapshot.recentDonors()); + } + } + } + } + + private record MessSnapshot(int v, int a, int m) { + static void copy(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) { + if (oldPlayer instanceof MessHolder oldHolder && newPlayer instanceof MessHolder newHolder) { + NeedsOfNature.MessSnapshot snapshot = new NeedsOfNature.MessSnapshot(oldHolder.getVMess(), oldHolder.getAMess(), oldHolder.getMMess()); + newHolder.setVMess(snapshot.v()); + newHolder.setAMess(snapshot.a()); + newHolder.setMMess(snapshot.m()); + } + } + } + + private record NoNGatherActorState(RegistryKey worldKey, long expiresTick) { + } + + private record PeakLiquidPlan( + Map gainsByReceiver, + Map> accessoryPregnancyDonors, + Set suppliedInjectorRoles + ) { + PeakLiquidPlan { + gainsByReceiver = gainsByReceiver != null && !gainsByReceiver.isEmpty() ? Map.copyOf(gainsByReceiver) : Map.of(); + if (accessoryPregnancyDonors != null && !accessoryPregnancyDonors.isEmpty()) { + Map> frozen = new LinkedHashMap<>(); + accessoryPregnancyDonors.forEach((uuid, donors) -> { + if (uuid != null && donors != null && !donors.isEmpty()) { + frozen.put(uuid, List.copyOf((Collection)donors)); + } + }); + accessoryPregnancyDonors = Map.copyOf(frozen); + } else { + accessoryPregnancyDonors = Map.of(); + } + + suppliedInjectorRoles = suppliedInjectorRoles != null && !suppliedInjectorRoles.isEmpty() ? Set.copyOf(suppliedInjectorRoles) : Set.of(); + } + + int maxAmount() { + int max = 0; + + for (NeedsOfNature.LiquidGainProfile gain : this.gainsByReceiver.values()) { + if (gain != null) { + max = Math.max(max, gain.amount()); + } + } + + return max; + } + } + + private static final class PeakLiquidProgressState { + final RegistryKey worldKey; + final NeedsOfNature.PeakLiquidPlan plan; + final List scheduledTicks; + final List scheduledAmounts; + int scheduleIndex; + final boolean infinite; + final double cycleTicks; + final List cueOffsets; + final long stageStartTick; + final double stageSpeed; + long cycleIndex; + int cueIndex; + int distributedBaseMl; + int remainingBaseMl; + final Set donorHistoryRecordedReceivers = new HashSet<>(); + + private PeakLiquidProgressState( + RegistryKey worldKey, + NeedsOfNature.PeakLiquidPlan plan, + List scheduledTicks, + List scheduledAmounts, + boolean infinite, + double cycleTicks, + List cueOffsets, + long stageStartTick, + double stageSpeed, + int remainingBaseMl + ) { + this.worldKey = worldKey; + this.plan = plan; + this.scheduledTicks = scheduledTicks; + this.scheduledAmounts = scheduledAmounts; + this.scheduleIndex = 0; + this.infinite = infinite; + this.cycleTicks = cycleTicks; + this.cueOffsets = cueOffsets; + this.stageStartTick = stageStartTick; + this.stageSpeed = stageSpeed; + this.cycleIndex = 0L; + this.cueIndex = 0; + this.distributedBaseMl = 0; + this.remainingBaseMl = remainingBaseMl; + } + + static NeedsOfNature.PeakLiquidProgressState finite( + RegistryKey worldKey, NeedsOfNature.PeakLiquidPlan plan, List scheduledTicks, List scheduledAmounts + ) { + return new NeedsOfNature.PeakLiquidProgressState(worldKey, plan, scheduledTicks, scheduledAmounts, false, 0.0, List.of(), 0L, 1.0, 0); + } + + static NeedsOfNature.PeakLiquidProgressState infinite( + RegistryKey worldKey, NeedsOfNature.PeakLiquidPlan plan, double cycleTicks, List cueOffsets, long stageStartTick, double stageSpeed + ) { + return new NeedsOfNature.PeakLiquidProgressState( + worldKey, plan, List.of(), List.of(), true, cycleTicks, cueOffsets, stageStartTick, stageSpeed, plan.maxAmount() + ); + } + } + + private record PeakState(Identifier animationId, int peakStage, boolean peaked, int lastStage, boolean hasPlayer, boolean liquidGranted, int actualAddedMl) { + } + + private record PendingAttackEscapeResolution( + RegistryKey worldKey, List actorUuids, @Nullable UUID preferredAttackerUuid, long dueTick, boolean allowReattack, int invulnerabilityTicks + ) { + } + + private record PendingBreedingState(RegistryKey worldKey, UUID firstUuid, UUID secondUuid, long expiresTick) { + } + + private record PendingMInjectorReward(RegistryKey worldKey, long freeSinceTick) { + } + + public record PlayerEnergyAuraDebug( + ServerPlayerEntity player, double totalMultiplier, int playerEnergy, int destroyedSkinStage, int messTotal, double radius + ) { + } + + private record PostDeathStateSync(RegistryKey worldKey, long dueTick) { + } + + private record PregnancyCueRuntime( + RegistryKey worldKey, long stageStartTick, Identifier clipId, Integer cueTick, boolean cueTriggered, double stageSpeed + ) { + } + + record PregnancyDonor(Identifier entityTypeId, @Nullable NeedsOfNature.PregnancyVariantData variantData) { + } + + private record PregnancyHatchState( + UUID playerUuid, + Identifier sourceEntityTypeId, + Identifier birthEntityTypeId, + String birthMode, + int totalSpawnCount, + int remainingSpawns, + int spawnedCount, + @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + NeedsOfNature.PregnancyHatchState withProgress(int newRemainingSpawns, int newSpawnedCount) { + return new NeedsOfNature.PregnancyHatchState( + this.playerUuid, + this.sourceEntityTypeId, + this.birthEntityTypeId, + this.birthMode, + this.totalSpawnCount, + Math.max(0, newRemainingSpawns), + Math.max(0, newSpawnedCount), + this.variantData + ); + } + } + + private record PregnancySnapshot( + Identifier entityTypeId, long endTick, boolean pendingHatch, @Nullable NeedsOfNature.PregnancyVariantData variantData, int offspringCount + ) { + static void copy(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) { + PregnancyHolder oldHolder = NeedsOfNature.getPregnancyHolder(oldPlayer); + PregnancyHolder newHolder = NeedsOfNature.getPregnancyHolder(newPlayer); + if (oldHolder != null && newHolder != null && oldHolder.getPregnantEntityTypeId() != null) { + if (NonGenderSystem.isOnlyMale(newPlayer)) { + NeedsOfNature.clearPregnancyState(newPlayer, true); + } else { + NeedsOfNature.PregnancySnapshot snapshot = new NeedsOfNature.PregnancySnapshot( + oldHolder.getPregnantEntityTypeId(), + oldHolder.getPregnancyEndTick(), + oldHolder.isPregnancyPendingHatch(), + oldHolder.getPregnancyVariantData(), + oldHolder.getPregnancyOffspringCount() + ); + NeedsOfNature.clearPregnancyRuntime(newPlayer.getUuid()); + newHolder.setPregnancyState(snapshot.entityTypeId(), snapshot.endTick(), snapshot.pendingHatch(), snapshot.variantData()); + newHolder.setPregnancyOffspringCount(snapshot.offspringCount()); + NeedsOfNature.refreshPregnancyAfterDeathCopy(newPlayer); + NeedsOfNature.syncPregnancyState(newPlayer); + NonApiInternals.firePregnancyChanged(newPlayer); + } + } + } + } + + public record PregnancyVariantData(Map components, @Nullable Integer horseMarkingIndex) { + public PregnancyVariantData { + components = components != null && !components.isEmpty() ? Map.copyOf(components) : Map.of(); + } + + public boolean isEmpty() { + return this.components.isEmpty() && this.horseMarkingIndex == null; + } + } + + private record ReceiverDripState(RegistryKey worldKey, long nextTick, long endTick, int tintRgb) { + } + + private record RippedSkinSnapshot(int stage, int damage) { + static void copy(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) { + if (oldPlayer instanceof DestroyedSkinHolder oldHolder && newPlayer instanceof DestroyedSkinHolder newHolder) { + NeedsOfNature.RippedSkinSnapshot snapshot = new NeedsOfNature.RippedSkinSnapshot( + oldHolder.getDestroyedSkinStage(), oldHolder.getDestroyedSkinDamage() + ); + newHolder.setDestroyedSkinDamage(snapshot.damage()); + NonDestroyedSkinSystem.setDestroyedSkinStage(newPlayer, snapshot.stage()); + } + } + } + + private record ScanBudget(long tick, int remaining) { + } + + private interface VariantComponentDescriptor { + String key(); + + void capture(ServerWorld var1, Entity var2, Map var3); + + void apply(ServerWorld var1, Entity var2, String var3); + } +} diff --git a/src/main/java/com/nonid/NeedsOfNatureClient.java b/src/main/java/com/nonid/NeedsOfNatureClient.java new file mode 100644 index 0000000..6ba0ad6 --- /dev/null +++ b/src/main/java/com/nonid/NeedsOfNatureClient.java @@ -0,0 +1,1521 @@ +package com.nonid; + +import com.google.gson.Gson; +import com.nonid.api.client.NonAnimationParticleEvents; +import com.nonid.api.client.NonAnimationRenderEvents; +import com.nonid.api.client.NonAnimationSoundEvents; +import com.nonid.client.HorseLiquidCollectorGeoModel; +import com.nonid.client.HorseLiquidCollectorRenderer; +import com.nonid.client.NonAccessoryTooltips; +import com.nonid.client.NonArmorShedClient; +import com.nonid.client.NonClientDiagnostics; +import com.nonid.client.NonClientResourceCaches; +import com.nonid.client.NonDebugSpinMode; +import com.nonid.client.NonDebugSpinMusicClient; +import com.nonid.client.NonDestroyedSkinClient; +import com.nonid.client.NonGenderSelectionScreen; +import com.nonid.client.NonHudOverlay; +import com.nonid.client.NonInventoryScreenHooks; +import com.nonid.client.NonPackStartupWarnings; +import com.nonid.client.NonPlayerInteractionClient; +import com.nonid.client.NonPregnancyClientState; +import com.nonid.client.NonServerSettingsClient; +import com.nonid.client.NonSoundCues; +import com.nonid.client.NonSpinModelLock; +import com.nonid.client.NonWildfireGenderScreenHooks; +import com.nonid.client.NonWildfireGenderSync; +import com.nonid.client.PregnancyEggRenderer; +import com.nonid.client.intiface.NonIntifaceBridge; +import com.nonid.client.particle.LiquidFallingParticle; +import com.nonid.client.particle.LiquidParticle; +import com.nonid.client.particle.LiquidPuddleParticle; +import com.nonid.client.particle.LiquidWaterParticle; +import com.nonid.client.particle.RippedFabricParticle; +import com.nonid.client.particle.SmallHeartParticle; +import com.nonid.data.NonAccessoryItemRegistry; +import com.nonid.data.NonPeakStages; +import com.nonid.entity.HorseLiquidCollectorEntity; +import com.nonid.integration.NonCreditsButtonHooks; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.AnimationEngineClient; +import com.nonid.internal.animation.client.render.gecko.GeckoResourceResolver; +import com.nonid.internal.animation.client.render.gecko.GenderedModelResolver; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import com.nonid.internal.animation.client.sound.AnimationSoundAnticipation; +import com.nonid.internal.animation.data.AnimationCueEffects; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.internal.animation.mixin.client.EntityRenderManagerAccessor; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.util.AnimationIds; +import com.nonid.network.AccessoryShedStateS2CPayload; +import com.nonid.network.AnimationCatalogChunkS2CPayload; +import com.nonid.network.AttackStateS2CPayload; +import com.nonid.network.DestroyedSkinMaskSyncS2CPayload; +import com.nonid.network.DestroyedSkinParticlesS2CPayload; +import com.nonid.network.DestroyedSkinStateS2CPayload; +import com.nonid.network.DestroyedSkinSyncS2CPayload; +import com.nonid.network.GenderSelectionPromptS2CPayload; +import com.nonid.network.HostConfigSyncData; +import com.nonid.network.HostConfigSyncS2CPayload; +import com.nonid.network.LiquidStateS2CPayload; +import com.nonid.network.MessStateS2CPayload; +import com.nonid.network.PlayerAttackInputC2SPayload; +import com.nonid.network.PlayerRuntimeSettingsS2CPayload; +import com.nonid.network.PregnancyCycleStateS2CPayload; +import com.nonid.network.PregnancyStateS2CPayload; +import com.nonid.network.ServerConfigResultS2CPayload; +import com.nonid.network.SetPlayerGenderC2SPayload; +import com.nonid.network.SkinRippedInfoRequestS2CPayload; +import com.nonid.network.StartManualPeakC2SPayload; +import com.nonid.network.StopAttackAnimationC2SPayload; +import com.nonid.network.StopVoluntaryAnimationC2SPayload; +import com.nonid.particle.NonParticles; +import com.nonid.potion.NonPotions; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents.ClientStarted; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.EndTick; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.Disconnect; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.Join; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.option.KeyBinding.Category; +import net.minecraft.client.render.entity.EntityRendererFactories; +import net.minecraft.client.render.entity.equipment.EquipmentModel; +import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; +import net.minecraft.client.render.entity.equipment.EquipmentModel.Dyeable; +import net.minecraft.client.render.entity.equipment.EquipmentModel.Layer; +import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.client.util.InputUtil.Type; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.DyedColorComponent; +import net.minecraft.component.type.EquippableComponent; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.passive.AbstractDonkeyEntity; +import net.minecraft.entity.passive.AbstractHorseEntity; +import net.minecraft.entity.passive.HorseEntity; +import net.minecraft.entity.passive.HorseMarking; +import net.minecraft.entity.passive.SheepEntity; +import net.minecraft.entity.passive.WolfEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerModelPart; +import net.minecraft.entity.player.SkinTextures; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.equipment.EquipmentAsset; +import net.minecraft.item.equipment.EquipmentAssetKeys; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.resource.Resource; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +public class NeedsOfNatureClient implements ClientModInitializer { + private static final Gson HOST_CONFIG_SYNC_GSON = new Gson(); + private static final Identifier WOLF_COLLAR_TEXTURE = Identifier.ofVanilla("textures/entity/wolf/wolf_collar.png"); + private static final Identifier SHEEP_WOOL_TEXTURE = Identifier.ofVanilla("textures/entity/sheep/sheep_wool.png"); + private static final Identifier SHEEP_WOOL_UNDERCOAT_TEXTURE = Identifier.ofVanilla("textures/entity/sheep/sheep_wool_undercoat.png"); + private static final Identifier FILL_BOTTLE_ANIMATION_ID = Identifier.of("needsofnature", "player_fill_bottle"); + private static final String FILL_BOTTLE_PROP_CUE_EFFECT = "fillbottle_prop_fill"; + private static final Identifier VANILLA_BOTTLE_FILL_SOUND = Identifier.ofVanilla("item.bottle.fill"); + private static final String RIGHT_PROP_BONE = "propright"; + private static final float REACTIVE_IMPACT_FOV_DELTA = 0.5F; + private static final float REACTIVE_IMPACT_HOLD_TICKS = 3.0F; + private static final float REACTIVE_IMPACT_RECOVER_TICKS = 14.0F; + private static final double NANOS_PER_CLIENT_TICK = 5.0E7; + private static final int INTIFACE_BIRTH_INTRO_STAGE = 1; + private static final int INTIFACE_BIRTH_LOOP_STAGE = 2; + private static final int INTIFACE_BIRTH_FINAL_STAGE = 3; + private static final double INTIFACE_BIRTH_LOOP_START_SCALAR = 0.6; + private static final Set INVALID_PARTICLE_COLOR_SOURCES_WARNED = new HashSet<>(); + private static long CLIENT_FALLBACK_TICKS = 0L; + private static volatile long REACTIVE_IMPACT_PULSE_START_NANOS = Long.MIN_VALUE; + private static int LAST_LOCAL_GENDER_SYNC_MASK = 0; + private static final Map WOLF_COLLAR_TINT_CACHE = new HashMap<>(); + private static final Map SHEEP_WOOL_TINT_CACHE = new HashMap<>(); + private static final Map SHEEP_WOOL_UNDERCOAT_TINT_CACHE = new HashMap<>(); + private static final Map EQUIPMENT_TINT_CACHE = new HashMap<>(); + private static final Set SHEEP_WOOL_BONES = Set.of("bodywool", "headwool", "leg1wool", "leg2wool", "leg3wool", "leg4wool"); + private static final int PLAYER_OUTER_SKIN_CUBE_INDEX = 1; + private static final Category NON_KEY_CATEGORY = Category.create(Identifier.of("needsofnature", "main")); + private static final KeyBinding START_MANUAL_PEAK_KEY = KeyBindingHelper.registerKeyBinding( + new KeyBinding("key.needsofnature.start_manual_peak", Type.KEYSYM, 77, NON_KEY_CATEGORY) + ); + + public static void sendClientSetupWarning(String message) { + NonClientDiagnostics.sendSetupWarning(message, true); + } + + public void onInitializeClient() { + ClientLifecycleEvents.CLIENT_STARTED.register((ClientStarted)client -> NonStats.registerKnownEntityStats()); + NonDestroyedSkinClient.ensureConfigDirectory(); + AnimationEngineClient.initialize(); + EntityRendererFactories.register(NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE, HorseLiquidCollectorRenderer::new); + EntityRendererFactories.register(NeedsOfNature.PREGNANCY_EGG_ENTITY_TYPE, PregnancyEggRenderer::new); + NonHudOverlay.registerReloadListener(); + NonClientResourceCaches.registerReloadListener(); + ClientTickEvents.END_CLIENT_TICK.register(NonHudOverlay::clientTick); + ClientTickEvents.END_CLIENT_TICK.register(NonSpinModelLock::clientTick); + ClientTickEvents.END_CLIENT_TICK.register(NonDebugSpinMusicClient::clientTick); + ClientTickEvents.END_CLIENT_TICK.register(NonPackStartupWarnings::clientTick); + ClientTickEvents.END_CLIENT_TICK.register(NonDestroyedSkinClient::clientTick); + ClientTickEvents.END_CLIENT_TICK.register(NonArmorShedClient::clientTick); + NonAccessoryTooltips.register(); + NonInventoryScreenHooks.register(); + NonCreditsButtonHooks.register(); + NonPlayerInteractionClient.register(); + NonWildfireGenderScreenHooks.register(); + NonArmorShedClient.register(); + ClientTickEvents.END_CLIENT_TICK.register((EndTick)client -> CLIENT_FALLBACK_TICKS++); + ClientTickEvents.END_CLIENT_TICK.register(NeedsOfNatureClient::syncLocalGenderMirror); + ClientTickEvents.END_CLIENT_TICK.register(NonWildfireGenderSync::clientTick); + ClientTickEvents.END_CLIENT_TICK.register(NeedsOfNatureClient::tickIntifaceBaseline); + ClientPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, client) -> { + NonIntifaceBridge.onWorldDisconnect(NeedsOfNature.getConfig()); + NeedsOfNature.clearRemoteHostRules(); + NonHudOverlay.onDisconnect(client); + NonPregnancyClientState.clear(); + NonDestroyedSkinClient.clear(); + NonArmorShedClient.clear(); + NonAccessoryShedSystem.clearAll(); + NonPeakStages.clearSyncedPeakStages(); + NonDebugSpinMusicClient.stopAll(client); + REACTIVE_IMPACT_PULSE_START_NANOS = Long.MIN_VALUE; + LAST_LOCAL_GENDER_SYNC_MASK = 0; + NonWildfireGenderSync.onWorldDisconnect(); + NonPlayerInteractionClient.clear(); + NonServerSettingsClient.clear(); + }); + ClientPlayConnectionEvents.JOIN.register((Join)(handler, sender, client) -> client.execute(() -> { + NonWildfireGenderSync.onWorldJoin(); + pushConfiguredPlayerGenderToServer(); + NonDestroyedSkinClient.scheduleLocalSkinUploadRetry(); + NonIntifaceBridge.onWorldJoin(NeedsOfNature.getConfig()); + })); + ClientTickEvents.END_CLIENT_TICK.register(NeedsOfNatureClient::handleVoluntaryStopInput); + ClientTickEvents.END_CLIENT_TICK.register(NeedsOfNatureClient::handleAttackInput); + ClientTickEvents.END_CLIENT_TICK.register(NeedsOfNatureClient::handleManualPeakInput); + ClientPlayNetworking.registerGlobalReceiver( + AttackStateS2CPayload.ID, + (payload, context) -> context.client() + .execute( + () -> NonHudOverlay.setEscapeControlState( + payload.instanceId(), payload.attack(), payload.escapable(), payload.escapeProfile(), payload.pvpRole(), payload.progress() + ) + ) + ); + ClientPlayNetworking.registerGlobalReceiver(LiquidStateS2CPayload.ID, (payload, context) -> context.client().execute(() -> { + NonHudOverlay.setLiquidState(payload.stored(), payload.capacity(), payload.tintRgb()); + MinecraftClient client = context.client(); + if (client.player instanceof LiquidHolder holder) { + holder.setLiquidStored(payload.stored()); + } + })); + ClientPlayNetworking.registerGlobalReceiver(PregnancyStateS2CPayload.ID, (payload, context) -> context.client().execute(() -> { + String raw = payload.pregnantEntityTypeId(); + Identifier id = raw != null && !raw.isBlank() ? Identifier.tryParse(raw) : null; + NonPregnancyClientState.setPregnantEntityTypeId(id); + })); + ClientPlayNetworking.registerGlobalReceiver( + PregnancyCycleStateS2CPayload.ID, + (payload, context) -> context.client().execute(() -> NonPregnancyClientState.setCycleState(payload.enabled(), payload.day(), payload.phase())) + ); + ClientPlayNetworking.registerGlobalReceiver( + PlayerRuntimeSettingsS2CPayload.ID, + (payload, context) -> context.client().execute(() -> NonHudOverlay.setRuntimeAttackEscapeHits(payload.attackEscapeHits())) + ); + ClientPlayNetworking.registerGlobalReceiver( + GenderSelectionPromptS2CPayload.ID, + (payload, context) -> context.client() + .execute( + () -> MinecraftClient.getInstance().setScreen(new NonGenderSelectionScreen(payload.allowedMask(), payload.currentMask(), payload.permanent())) + ) + ); + ClientPlayNetworking.registerGlobalReceiver(HostConfigSyncS2CPayload.ID, (payload, context) -> context.client().execute(() -> { + String raw = payload.json(); + if (raw != null && !raw.isBlank()) { + try { + HostConfigSyncData data = (HostConfigSyncData)HOST_CONFIG_SYNC_GSON.fromJson(raw, HostConfigSyncData.class); + if (data == null) { + return; + } + + MinecraftClient client = context.client(); + if (client.getServer() == null) { + NeedsOfNature.applyRemoteHostRules(data); + } + + NonServerSettingsClient.applyConfigSnapshot(payload.editableJson(), payload.revision(), payload.canEdit()); + } catch (RuntimeException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to parse host config sync payload.", e); + } + } + })); + ClientPlayNetworking.registerGlobalReceiver( + AnimationCatalogChunkS2CPayload.ID, + (payload, context) -> context.client() + .execute(() -> NonServerSettingsClient.acceptCatalogChunk(payload.revision(), payload.index(), payload.count(), payload.data())) + ); + ClientPlayNetworking.registerGlobalReceiver( + ServerConfigResultS2CPayload.ID, (payload, context) -> context.client().execute(() -> NonServerSettingsClient.handleConfigResult(payload)) + ); + ClientPlayNetworking.registerGlobalReceiver( + DestroyedSkinSyncS2CPayload.ID, + (payload, context) -> context.client().execute(() -> NonDestroyedSkinClient.setSkin(payload.playerUuid(), payload.stage(), payload.pngBytes())) + ); + ClientPlayNetworking.registerGlobalReceiver( + DestroyedSkinMaskSyncS2CPayload.ID, + (payload, context) -> context.client().execute(() -> NonDestroyedSkinClient.setMask(payload.playerUuid(), payload.stage(), payload.pngBytes())) + ); + ClientPlayNetworking.registerGlobalReceiver( + DestroyedSkinStateS2CPayload.ID, + (payload, context) -> context.client().execute(() -> NonDestroyedSkinClient.setStage(payload.playerUuid(), payload.stage())) + ); + ClientPlayNetworking.registerGlobalReceiver( + DestroyedSkinParticlesS2CPayload.ID, + (payload, context) -> context.client() + .execute( + () -> NonDestroyedSkinClient.spawnRippedFabricParticles( + payload.playerUuid(), payload.x(), payload.y(), payload.z(), payload.count(), payload.seed() + ) + ) + ); + ClientPlayNetworking.registerGlobalReceiver( + MessStateS2CPayload.ID, + (payload, context) -> context.client() + .execute( + () -> NonDestroyedSkinClient.setMess( + payload.playerUuid(), payload.vMess(), payload.aMess(), payload.mMess(), payload.tintRgb(), payload.liquidStored(), payload.liquidCapacity() + ) + ) + ); + ClientPlayNetworking.registerGlobalReceiver( + AccessoryShedStateS2CPayload.ID, + (payload, context) -> context.client() + .execute( + () -> NonAccessoryShedSystem.setClientShedState(payload.playerUuid(), payload.shedV(), payload.shedA(), payload.forcedV(), payload.forcedA()) + ) + ); + ClientPlayNetworking.registerGlobalReceiver( + SkinRippedInfoRequestS2CPayload.ID, (payload, context) -> context.client().execute(NonDestroyedSkinClient::sendDetectedRippedSkinInfo) + ); + registerSoundCueResolver(); + registerIntifaceCueAnticipation(); + registerParticleCueResolver(); + ParticleFactoryRegistry.getInstance().register(NonParticles.LIQUID_PARTICLE, LiquidParticle.Factory::new); + ParticleFactoryRegistry.getInstance().register(NonParticles.LIQUID_PARTICLE_FALLING, LiquidFallingParticle.Factory::new); + ParticleFactoryRegistry.getInstance().register(NonParticles.LIQUID_PARTICLE_PUDDLE, LiquidPuddleParticle.Factory::new); + ParticleFactoryRegistry.getInstance().register(NonParticles.LIQUID_PARTICLE_WATER, LiquidWaterParticle.Factory::new); + ParticleFactoryRegistry.getInstance().register(NonParticles.SMALLHEART, SmallHeartParticle.Factory::new); + ParticleFactoryRegistry.getInstance().register(NonParticles.RIPPED_FABRIC, RippedFabricParticle.Factory::new); + NonAnimationRenderEvents.RESOLVE_RENDER + .register( + (NonAnimationRenderEvents.RenderResolver)(entity, entityTypeId, currentModel, currentTexture) -> { + if (NonDebugSpinMode.isEnabled()) { + return new NonAnimationRenderEvents.RenderOverride(NonDebugSpinMode.DEBUG_SPIN_MODEL_RESOURCE, NonDebugSpinMode.DEBUG_SPIN_TEXTURE_RESOURCE); + } + + NonAnimationRenderEvents.RenderOverride collectorOverride = resolveCollectorRenderOverride(entity); + if (collectorOverride != null) { + return collectorOverride; + } + + Identifier destroyedBaseTexture = NonDestroyedSkinClient.resolveBaseTexture(entity, resolveDestroyedSkinBaseTexture(entity, currentTexture)); + Identifier destroyedOverlayTexture = destroyedBaseTexture == null ? NonDestroyedSkinClient.resolveOverlayTexture(entity) : null; + List layers = new ArrayList<>(); + if (destroyedOverlayTexture != null) { + layers.add(destroyedOverlayTexture); + } + + layers.addAll(resolveSheepOverlays(entity)); + layers.addAll(resolveHorseOverlays(entity)); + layers.addAll(resolveWolfOverlays(entity)); + List layerTextures = layers.isEmpty() ? List.of() : List.copyOf(layers); + ItemStack maleRoleAccessory = NonTrinketsIntegration.getMaleRoleAccessoryStack(entity); + boolean forceMaleModel = !maleRoleAccessory.isEmpty(); + Map boneTextures = mergeIdentifierMaps(resolveSheepBoneTextures(entity), resolveAccessoryBoneTextures(maleRoleAccessory)); + Map boneVisibility = mergeBooleanMaps(resolveDonkeyChestBoneVisibility(entity), resolveSheepBoneVisibility(entity)); + Map> hiddenBoneCubeIndices = resolvePlayerSkinPartHiddenCubeIndices(entity); + boolean hasLayerTextures = !layerTextures.isEmpty(); + boolean hasBoneTextures = !boneTextures.isEmpty(); + boolean hasBoneVisibility = !boneVisibility.isEmpty(); + boolean hasHiddenBoneCubeIndices = !hiddenBoneCubeIndices.isEmpty(); + Identifier baseModel = currentModel; + if (baseModel == null && entityTypeId != null) { + baseModel = AnimationEngine.packId("entity/" + entityTypeId.getPath()); + } + + if (entity instanceof GenderHolder holder) { + GenderedModelResolver.ModelGender modelGender = forceMaleModel + ? GenderedModelResolver.ModelGender.MALE + : GenderedModelResolver.fromMask(holder.getGenderMask()); + if (baseModel == null) { + return destroyedBaseTexture == null && !hasLayerTextures && !hasBoneTextures && !hasBoneVisibility && !hasHiddenBoneCubeIndices + ? null + : createAnimationRenderOverride( + null, + destroyedBaseTexture, + hasLayerTextures ? layerTextures : null, + hasBoneTextures ? boneTextures : null, + null, + hasBoneVisibility ? boneVisibility : null, + hasHiddenBoneCubeIndices ? hiddenBoneCubeIndices : null + ); + } + + if (!forceMaleModel && GenderedModelResolver.hasGenderSuffix(baseModel)) { + return destroyedBaseTexture == null && !hasLayerTextures && !hasBoneTextures && !hasBoneVisibility && !hasHiddenBoneCubeIndices + ? null + : createAnimationRenderOverride( + null, + destroyedBaseTexture, + hasLayerTextures ? layerTextures : null, + hasBoneTextures ? boneTextures : null, + null, + hasBoneVisibility ? boneVisibility : null, + hasHiddenBoneCubeIndices ? hiddenBoneCubeIndices : null + ); + } + + Identifier model; + if (forceMaleModel) { + List modelRoots = ClientAnimationRuntime.findLatestModelRootsForActor(entity.getUuid()); + model = GeckoResourceResolver.resolveGenderedModelAcrossRoots(baseModel, modelRoots, GenderedModelResolver.ModelGender.MALE); + if (model == null) { + model = resolveExistingGenderedModel(GenderedModelResolver.stripGenderSuffix(baseModel), GenderedModelResolver.ModelGender.MALE); + } + } else { + model = resolveExistingGenderedModel(baseModel, modelGender); + } + + if (model != null && model.equals(baseModel)) { + model = null; + } + + return createAnimationRenderOverride( + model, + destroyedBaseTexture, + hasLayerTextures ? layerTextures : null, + hasBoneTextures ? boneTextures : null, + null, + hasBoneVisibility ? boneVisibility : null, + hasHiddenBoneCubeIndices ? hiddenBoneCubeIndices : null + ); + } else { + Identifier defaultGenderModel = resolveExistingGenderedModel(baseModel, GenderedModelResolver.ModelGender.MALE); + if (defaultGenderModel != null && defaultGenderModel.equals(baseModel)) { + defaultGenderModel = null; + } + + return defaultGenderModel == null + && destroyedBaseTexture == null + && !hasLayerTextures + && !hasBoneTextures + && !hasBoneVisibility + && !hasHiddenBoneCubeIndices + ? null + : createAnimationRenderOverride( + defaultGenderModel, + destroyedBaseTexture, + hasLayerTextures ? layerTextures : null, + hasBoneTextures ? boneTextures : null, + null, + hasBoneVisibility ? boneVisibility : null, + hasHiddenBoneCubeIndices ? hiddenBoneCubeIndices : null + ); + } + } + ); + } + + @Nullable + private static Identifier resolveExistingGenderedModel(@Nullable Identifier baseModel, GenderedModelResolver.ModelGender gender) { + return GenderedModelResolver.resolveExisting(baseModel, gender, GeckoResourceResolver::hasGeoModel); + } + + @Nullable + private static NonAnimationRenderEvents.RenderOverride resolveCollectorRenderOverride(LivingEntity entity) { + if (entity instanceof HorseLiquidCollectorEntity collector) { + Identifier texture = collector.isCollectorFull() ? HorseLiquidCollectorGeoModel.FULL_TEXTURE_ID : HorseLiquidCollectorGeoModel.EMPTY_TEXTURE_ID; + return new NonAnimationRenderEvents.RenderOverride(HorseLiquidCollectorGeoModel.MODEL_ID, texture); + } else { + return null; + } + } + + private static NonAnimationRenderEvents.RenderOverride createAnimationRenderOverride( + @Nullable Identifier model, + @Nullable Identifier texture, + @Nullable List layerTextures, + @Nullable Map boneTextures, + @Nullable Map boneItems, + @Nullable Map boneVisibility, + @Nullable Map> hiddenBoneCubeIndices + ) { + return new NonAnimationRenderEvents.RenderOverride(model, texture, layerTextures, boneTextures, boneItems, boneVisibility, hiddenBoneCubeIndices); + } + + @Nullable + private static Identifier resolveDestroyedSkinBaseTexture(LivingEntity entity, @Nullable Identifier currentTexture) { + if (entity instanceof AbstractClientPlayerEntity player) { + try { + SkinTextures skin = player.getSkin(); + if (skin != null && skin.body() != null && skin.body().texturePath() != null) { + return skin.body().texturePath(); + } + } catch (RuntimeException var4) { + } + } + + return currentTexture; + } + + private static Map resolveDonkeyChestBoneVisibility(LivingEntity entity) { + if (entity instanceof AbstractDonkeyEntity donkey) { + return donkey.hasChest() ? Map.of() : Map.of("left_chest", false, "right_chest", false); + } else { + return Map.of(); + } + } + + private static Map> resolvePlayerSkinPartHiddenCubeIndices(LivingEntity entity) { + if (entity instanceof AbstractClientPlayerEntity player) { + HashMap hiddenCubeIndices = new HashMap(); + hideOuterCubeIfDisabled(player, hiddenCubeIndices, PlayerModelPart.HAT, "head"); + hideOuterCubeIfDisabled(player, hiddenCubeIndices, PlayerModelPart.JACKET, "body"); + hideOuterCubeIfDisabled(player, hiddenCubeIndices, PlayerModelPart.RIGHT_SLEEVE, "rightarm"); + hideOuterCubeIfDisabled(player, hiddenCubeIndices, PlayerModelPart.LEFT_SLEEVE, "leftarm"); + hideOuterCubeIfDisabled(player, hiddenCubeIndices, PlayerModelPart.RIGHT_PANTS_LEG, "rightleg"); + hideOuterCubeIfDisabled(player, hiddenCubeIndices, PlayerModelPart.LEFT_PANTS_LEG, "leftleg"); + return hiddenCubeIndices.isEmpty() ? Map.of() : Map.copyOf(hiddenCubeIndices); + } else { + return Map.of(); + } + } + + private static void hideOuterCubeIfDisabled( + AbstractClientPlayerEntity player, Map> hiddenCubeIndices, PlayerModelPart part, String boneName + ) { + if (player != null && hiddenCubeIndices != null && part != null && boneName != null && !boneName.isBlank()) { + if (!player.isModelPartVisible(part)) { + hiddenCubeIndices.put(boneName, Set.of(1)); + } + } + } + + private static Map resolveSheepBoneTextures(LivingEntity entity) { + if (!(entity instanceof SheepEntity sheep)) { + return Map.of(); + } else { + if (sheep.isSheared()) { + return Map.of(); + } + + int rgb = getSheepColorRgb(sheep); + Identifier woolTexture = getTintedTexture(SHEEP_WOOL_TEXTURE, rgb, "sheep_wool", SHEEP_WOOL_TINT_CACHE); + Map textures = new HashMap<>(); + + for (String bone : SHEEP_WOOL_BONES) { + textures.put(bone, woolTexture); + } + + return Map.copyOf(textures); + } + } + + private static Map resolveAccessoryBoneTextures(ItemStack stack) { + if (stack != null && !stack.isEmpty()) { + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(Registries.ITEM.getId(stack.getItem())); + if (definition != null && !definition.actorVisualBehavior().animationBoneTextures().isEmpty()) { + Map textures = new HashMap<>(); + + for (Entry entry : definition.actorVisualBehavior().animationBoneTextures().entrySet()) { + NonTrinketsIntegration.ResolvedVisualLayer layer = NonTrinketsIntegration.resolveVisualLayer(stack, entry.getValue()); + if (layer != null) { + Identifier texture = layer.tintRgb() == 16777215 + ? layer.texture() + : getTintedEquipmentTexture(layer.texture(), layer.tintRgb(), "accessory_bone"); + textures.put(entry.getKey(), texture); + } + } + + return textures.isEmpty() ? Map.of() : Map.copyOf(textures); + } else { + return Map.of(); + } + } else { + return Map.of(); + } + } + + @SafeVarargs + private static Map mergeIdentifierMaps(Map... maps) { + if (maps != null && maps.length != 0) { + Map merged = new HashMap<>(); + + for (Map map : maps) { + if (map != null && !map.isEmpty()) { + merged.putAll(map); + } + } + + return merged.isEmpty() ? Map.of() : Map.copyOf(merged); + } else { + return Map.of(); + } + } + + private static List resolveSheepOverlays(LivingEntity entity) { + if (entity instanceof SheepEntity sheep) { + Identifier undercoatTexture = getTintedTexture( + SHEEP_WOOL_UNDERCOAT_TEXTURE, getSheepColorRgb(sheep), "sheep_wool_undercoat", SHEEP_WOOL_UNDERCOAT_TINT_CACHE + ); + return List.of(undercoatTexture); + } else { + return List.of(); + } + } + + private static Map resolveSheepBoneVisibility(LivingEntity entity) { + if (!(entity instanceof SheepEntity sheep)) { + return Map.of(); + } else { + if (!sheep.isSheared()) { + return Map.of(); + } + + Map visibility = new HashMap<>(); + + for (String bone : SHEEP_WOOL_BONES) { + visibility.put(bone, Boolean.FALSE); + } + + return Map.copyOf(visibility); + } + } + + private static int getSheepColorRgb(SheepEntity sheep) { + if (sheep == null) { + return DyeColor.WHITE.getEntityColor(); + } + + DyeColor color = sheep.getColor(); + return color == null ? DyeColor.WHITE.getEntityColor() : color.getEntityColor(); + } + + @SafeVarargs + private static Map mergeBooleanMaps(Map... maps) { + if (maps != null && maps.length != 0) { + Map merged = new HashMap<>(); + + for (Map map : maps) { + if (map != null && !map.isEmpty()) { + merged.putAll(map); + } + } + + return merged.isEmpty() ? Map.of() : Map.copyOf(merged); + } else { + return Map.of(); + } + } + + private static ItemStack createFilledBottlePropStack(LivingEntity entity) { + int tint = NonHudOverlay.getLiquidTintRgb() & 16777215; + if (tint == 0) { + tint = 15920063; + } + + PotionContentsComponent contents = new PotionContentsComponent( + Optional.of(Registries.POTION.getEntry(NonPotions.LIQUID)), Optional.of(tint), List.of(), Optional.empty() + ); + ItemStack stack = new ItemStack(Items.POTION); + stack.set(DataComponentTypes.POTION_CONTENTS, contents); + if (entity instanceof LiquidHolder holder && holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) { + NonPotions.setLiquidBottleEntityTypeId(stack, holder.getLiquidEntityTypeId()); + } else { + NonPotions.setLiquidBottleEntityTypeId(stack, null); + } + + return stack; + } + + private static boolean isFillBottleAnimation(Identifier animationId) { + if (animationId == null) { + return false; + } else if (!FILL_BOTTLE_ANIMATION_ID.getNamespace().equals(animationId.getNamespace())) { + return false; + } else { + String path = animationId.getPath(); + if (path != null && !path.isBlank()) { + String normalized = path.toLowerCase(Locale.ROOT).replaceFirst("\\.p\\d+$", ""); + return FILL_BOTTLE_ANIMATION_ID.getPath().equals(normalized); + } else { + return false; + } + } + } + + private static List resolveHorseOverlays(LivingEntity entity) { + if (entity instanceof AbstractHorseEntity horse) { + if (horse.isBaby()) { + return List.of(); + } + + List overlays = new ArrayList<>(); + if (horse instanceof HorseEntity vanillaHorse) { + HorseMarking marking = vanillaHorse.getMarking(); + Identifier markingTexture = mapHorseMarkingTexture(marking); + if (markingTexture != null) { + overlays.add(markingTexture); + } + + addSaddleOverlay(overlays, vanillaHorse); + ItemStack armor = vanillaHorse.getEquippedStack(EquipmentSlot.BODY); + if (!armor.isEmpty()) { + overlays.addAll(resolveHorseArmorTextures(armor)); + } + + return overlays.isEmpty() ? List.of() : List.copyOf(overlays); + } else { + addSaddleOverlay(overlays, horse); + return overlays.isEmpty() ? List.of() : List.copyOf(overlays); + } + } else { + return List.of(); + } + } + + private static void addSaddleOverlay(List overlays, AbstractHorseEntity horse) { + if (overlays != null && horse != null) { + ItemStack saddle = horse.getEquippedStack(EquipmentSlot.SADDLE); + if (!saddle.isEmpty()) { + overlays.addAll(resolveEquipmentLayerTextures(EquipmentAssetKeys.SADDLE, saddle, resolveHorseSaddleLayerType(horse), "horse_saddle")); + } + } + } + + private static LayerType resolveHorseSaddleLayerType(AbstractHorseEntity horse) { + EntityType type = horse == null ? null : horse.getType(); + if (type == EntityType.DONKEY) { + return LayerType.DONKEY_SADDLE; + } else if (type == EntityType.MULE) { + return LayerType.MULE_SADDLE; + } else if (type == EntityType.SKELETON_HORSE) { + return LayerType.SKELETON_HORSE_SADDLE; + } else { + return type == EntityType.ZOMBIE_HORSE ? LayerType.ZOMBIE_HORSE_SADDLE : LayerType.HORSE_SADDLE; + } + } + + private static Identifier mapHorseMarkingTexture(HorseMarking marking) { + if (marking == null || marking == HorseMarking.NONE) { + return null; + } else if (marking == HorseMarking.WHITE) { + return Identifier.ofVanilla("textures/entity/horse/horse_markings_white.png"); + } else if (marking == HorseMarking.WHITE_FIELD) { + return Identifier.ofVanilla("textures/entity/horse/horse_markings_whitefield.png"); + } else if (marking == HorseMarking.WHITE_DOTS) { + return Identifier.ofVanilla("textures/entity/horse/horse_markings_whitedots.png"); + } else { + return marking == HorseMarking.BLACK_DOTS ? Identifier.ofVanilla("textures/entity/horse/horse_markings_blackdots.png") : null; + } + } + + private static List resolveHorseArmorTextures(ItemStack armor) { + return resolveEquipmentLayerTextures(armor, LayerType.HORSE_BODY, "horse_armor"); + } + + private static List resolveEquipmentLayerTextures(ItemStack stack, LayerType layerType, String tintCachePrefix) { + if (stack != null && !stack.isEmpty() && layerType != null) { + EquippableComponent equippable = (EquippableComponent)stack.get(DataComponentTypes.EQUIPPABLE); + if (equippable == null) { + return List.of(); + } + + Optional> assetId = equippable.assetId(); + return assetId.isEmpty() ? List.of() : resolveEquipmentLayerTextures(assetId.get(), stack, layerType, tintCachePrefix); + } else { + return List.of(); + } + } + + private static List resolveEquipmentLayerTextures( + RegistryKey assetId, ItemStack stack, LayerType layerType, String tintCachePrefix + ) { + if (assetId != null && layerType != null) { + EquipmentModelLoader loader = resolveEquipmentModelLoader(); + if (loader == null) { + return List.of(); + } + + EquipmentModel model = loader.get(assetId); + if (model == null) { + return List.of(); + } + + List layers = model.getLayers(layerType); + if (layers != null && !layers.isEmpty()) { + DyedColorComponent dyed = (DyedColorComponent)stack.get(DataComponentTypes.DYED_COLOR); + List textures = new ArrayList<>(layers.size()); + + for (Layer layer : layers) { + if (layer != null) { + Identifier texture = layer.getFullTextureId(layerType); + if (texture != null) { + if (layer.dyeable().isPresent()) { + Optional fallbackColor = ((Dyeable)layer.dyeable().get()).colorWhenUndyed(); + Integer tint = dyed != null ? dyed.rgb() : fallbackColor.orElse(null); + if (tint != null) { + texture = getTintedEquipmentTexture(texture, tint, tintCachePrefix); + } + } + + textures.add(texture); + } + } + } + + return textures.isEmpty() ? List.of() : List.copyOf(textures); + } else { + return List.of(); + } + } else { + return List.of(); + } + } + + @Nullable + private static EquipmentModelLoader resolveEquipmentModelLoader() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return null; + } else { + return client.getEntityRenderDispatcher() instanceof EntityRenderManagerAccessor accessor ? accessor.afw$getEquipmentModelLoader() : null; + } + } + + private static List resolveWolfOverlays(LivingEntity entity) { + if (entity instanceof WolfEntity wolf) { + ArrayList overlays = new ArrayList(); + ItemStack armor = wolf.getEquippedStack(EquipmentSlot.BODY); + if (!armor.isEmpty()) { + overlays.addAll(resolveEquipmentLayerTextures(armor, LayerType.WOLF_BODY, "wolf_armor")); + } + + if (wolf.isTamed()) { + DyeColor collarColor = wolf.getCollarColor(); + if (collarColor != null) { + overlays.add(getTintedTexture(WOLF_COLLAR_TEXTURE, collarColor.getEntityColor(), "wolf_collar", WOLF_COLLAR_TINT_CACHE)); + } + } + + return overlays.isEmpty() ? List.of() : List.copyOf(overlays); + } else { + return List.of(); + } + } + + private static Identifier getTintedTexture(Identifier baseTexture, int rgb, String cachePrefix, Map cache) { + Identifier cached = cache.get(rgb); + if (cached != null) { + return cached; + } + + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return baseTexture; + } + + Optional resourceOpt = client.getResourceManager().getResource(baseTexture); + if (resourceOpt.isEmpty()) { + return baseTexture; + } + + Resource resource = resourceOpt.get(); + + try (InputStream in = resource.getInputStream()) { + NativeImage image = NativeImage.read(in); + tintImage(image, rgb); + String idPath = "textures/dynamic/" + cachePrefix + "_" + String.format("%06x", rgb) + ".png"; + Identifier id = Identifier.of("needsofnature", idPath); + client.getTextureManager().registerTexture(id, new NativeImageBackedTexture(id::toString, image)); + cache.put(rgb, id); + return id; + } catch (IOException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to tint texture {} (rgb={})", new Object[]{baseTexture, rgb, e}); + return baseTexture; + } + } + + private static Identifier getTintedEquipmentTexture(Identifier baseTexture, int rgb, String cachePrefix) { + if (baseTexture == null) { + return null; + } + + int tint = rgb & 16777215; + String cacheKey = baseTexture + "|" + tint; + Identifier cached = EQUIPMENT_TINT_CACHE.get(cacheKey); + if (cached != null) { + return cached; + } + + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return baseTexture; + } + + Optional resourceOpt = client.getResourceManager().getResource(baseTexture); + if (resourceOpt.isEmpty()) { + return baseTexture; + } + + Resource resource = resourceOpt.get(); + + try (InputStream in = resource.getInputStream()) { + NativeImage image = NativeImage.read(in); + tintImage(image, tint); + String textureKey = (baseTexture.getNamespace() + "_" + baseTexture.getPath()).replace('/', '_').replace('.', '_'); + String idPath = "textures/dynamic/" + cachePrefix + "_" + textureKey + "_" + String.format("%06x", tint) + ".png"; + Identifier id = Identifier.of("needsofnature", idPath); + client.getTextureManager().registerTexture(id, new NativeImageBackedTexture(id::toString, image)); + EQUIPMENT_TINT_CACHE.put(cacheKey, id); + return id; + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to tint equipment texture {} (rgb={})", new Object[]{baseTexture, tint, e}); + return baseTexture; + } + } + + private static void tintImage(NativeImage image, int rgb) { + int tintR = rgb >> 16 & 0xFF; + int tintG = rgb >> 8 & 0xFF; + int tintB = rgb & 0xFF; + int width = image.getWidth(); + int height = image.getHeight(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int argb = image.getColorArgb(x, y); + int alpha = argb >>> 24 & 0xFF; + if (alpha != 0) { + int r = argb >>> 16 & 0xFF; + int g = argb >>> 8 & 0xFF; + int b = argb & 0xFF; + int outR = r * tintR / 255; + int outG = g * tintG / 255; + int outB = b * tintB / 255; + int outArgb = alpha << 24 | outR << 16 | outG << 8 | outB; + image.setColorArgb(x, y, outArgb); + } + } + } + } + + private static void handleVoluntaryStopInput(MinecraftClient client) { + if (client != null) { + ClientPlayerEntity player = client.player; + if (player != null) { + while (client.options.sneakKey.wasPressed()) { + if (ClientAnimationRuntime.isActorPendingOrActive(player.getUuid())) { + UUID instanceId = ClientAnimationRuntime.findLatestActiveInstanceContaining(player.getUuid()); + if (instanceId != null && ClientPlayNetworking.canSend(StopVoluntaryAnimationC2SPayload.ID)) { + ClientPlayNetworking.send(new StopVoluntaryAnimationC2SPayload(instanceId)); + } + } + } + } + } + } + + private static void handleAttackInput(MinecraftClient client) { + if (client != null && client.world != null) { + ClientPlayerEntity player = client.player; + if (player != null) { + UUID instanceId = ClientAnimationRuntime.findLatestActiveInstanceContaining(player.getUuid()); + if (NonHudOverlay.isAttackInstance(instanceId)) { + if (NonHudOverlay.consumeAttackInputClear()) { + drainPendingPresses(client.options.leftKey); + drainPendingPresses(client.options.rightKey); + } else { + int leftPresses = drainPendingPresses(client.options.leftKey); + int rightPresses = drainPendingPresses(client.options.rightKey); + if (leftPresses <= 0 || rightPresses <= 0) { + NonHudOverlay.AttackKey attackKey = NonHudOverlay.AttackKey.NONE; + if (leftPresses > 0) { + attackKey = NonHudOverlay.AttackKey.LEFT; + } else if (rightPresses > 0) { + attackKey = NonHudOverlay.AttackKey.RIGHT; + } + + if (attackKey != NonHudOverlay.AttackKey.NONE) { + NonHudOverlay.recordAttackInput(instanceId, attackKey); + if (NonHudOverlay.isServerControlledAttack() && ClientPlayNetworking.canSend(PlayerAttackInputC2SPayload.ID)) { + ClientPlayNetworking.send(new PlayerAttackInputC2SPayload(instanceId, attackKey.ordinal())); + } + } + + if (NonHudOverlay.isAttackProgressComplete() && ClientPlayNetworking.canSend(StopAttackAnimationC2SPayload.ID)) { + ClientPlayNetworking.send(new StopAttackAnimationC2SPayload(instanceId)); + NonHudOverlay.resetAttackProgress(); + } + } + } + } + } + } + } + + private static void handleManualPeakInput(MinecraftClient client) { + if (client != null && client.world != null) { + ClientPlayerEntity player = client.player; + if (player != null && !player.isSpectator()) { + while (START_MANUAL_PEAK_KEY.wasPressed()) { + UUID activeInstance = ClientAnimationRuntime.findLatestActiveInstanceContaining(player.getUuid()); + if (activeInstance == null && ClientPlayNetworking.canSend(StartManualPeakC2SPayload.ID)) { + ClientPlayNetworking.send(new StartManualPeakC2SPayload()); + } + } + } + } + } + + public static void pushConfiguredPlayerGenderToServer() { + pushConfiguredPlayerGenderToServer(NeedsOfNature.getConfig()); + } + + public static void pushConfiguredPlayerGenderToServer(@Nullable NonConfig config) { + if (config != null) { + int mask = config.getPlayerGenderMask() & 3; + if (mask == 0) { + mask = NonConfig.PlayerGenderSelection.FEMALE.mask(); + } + + if (ClientPlayNetworking.canSend(SetPlayerGenderC2SPayload.ID)) { + ClientPlayNetworking.send(new SetPlayerGenderC2SPayload(mask, false)); + } + + NonWildfireGenderSync.syncFromNonConfig(config); + NonWildfireGenderSync.syncDestroyedSkinOverrides(config); + } + } + + private static void syncLocalGenderMirror(MinecraftClient client) { + if (client != null && client.player != null) { + if (client.player instanceof GenderHolder holder) { + int mask = holder.getGenderMask() & 3; + if (mask != 0 && mask != LAST_LOCAL_GENDER_SYNC_MASK) { + LAST_LOCAL_GENDER_SYNC_MASK = mask; + NonConfig config = NeedsOfNature.getConfig(); + NonWildfireGenderSync.syncFromMask(config, mask); + NonWildfireGenderSync.syncDestroyedSkinOverrides(config); + } + } + } + } + + private static void registerSoundCueResolver() { + NonAnimationSoundEvents.RESOLVE.register((NonAnimationSoundEvents.SoundResolver)context -> { + if (NonDebugSpinMode.isEnabled()) { + return NonAnimationSoundEvents.SoundOverride.skip(); + } + + if (isFillBottlePropCue(context)) { + UUID instanceId = context.instanceId(); + if (instanceId != null && context.anchor() != null) { + trySetAnimationRightHandPropOverride(instanceId, context.anchor().getUuid(), createFilledBottlePropStack(context.anchor())); + } + + return NonAnimationSoundEvents.SoundOverride.play(VANILLA_BOTTLE_FILL_SOUND, context.volume(), context.pitch()); + } else { + if (AnimationCueEffects.isReactiveImpact(context.effect())) { + boolean localPlayerCue = isLocalPlayerActorInInstance(context.instanceId()); + if (localPlayerCue && isPeakReactiveImpactCue(context)) { + triggerReactiveImpactFovPulse(); + } + + if (AnimationCueEffects.isSilentReactiveImpact(context.effect())) { + return NonAnimationSoundEvents.SoundOverride.skip(); + } + } + + Identifier soundId = NonSoundCues.resolveSoundId(context.effect(), context.anchor(), context.animationId(), context.stageIndex()); + if (soundId == null) { + return null; + } + + float volume = context.volume(); + if (NonSoundCues.isActionSoundId(soundId)) { + volume *= NeedsOfNature.getConfig().getActionSoundVolumeMultiplier(); + } + + return NonAnimationSoundEvents.SoundOverride.play(soundId, volume, context.pitch()); + } + }); + } + + private static void registerIntifaceCueAnticipation() { + AnimationSoundAnticipation.setListener(new AnimationSoundAnticipation.Listener() { + @Override + public int lookaheadMillis() { + NonConfig config = NeedsOfNature.getConfig(); + return config != null && config.isIntifaceEnabled() ? config.getIntifaceLatencyCompensationMs() : 0; + } + + @Override + public void anticipate(NonAnimationSoundEvents.SoundContext context) { + if (context != null && AnimationCueEffects.isReactiveImpact(context.effect())) { + if (NeedsOfNatureClient.isLocalPlayerActorInInstance(context.instanceId())) { + if (NeedsOfNatureClient.isPeakReactiveImpactCue(context)) { + NonIntifaceBridge.pulsePeakReactiveImpact(NeedsOfNature.getConfig(), NeedsOfNatureClient.nextSameEffectCueDelayMs(context)); + } else { + NonIntifaceBridge.pulseReactiveImpact(NeedsOfNature.getConfig(), NeedsOfNatureClient.nextSameEffectCueDelayMs(context)); + } + } + } + } + + @Override + public boolean allowNextStageLookahead(Identifier currentStageId, int stageIndex) { + return !NeedsOfNatureClient.isBirthLoopStage(currentStageId, stageIndex); + } + }); + } + + public static void clearGeneratedTintTextures() { + Set textureIds = new HashSet<>(); + textureIds.addAll(WOLF_COLLAR_TINT_CACHE.values()); + textureIds.addAll(SHEEP_WOOL_TINT_CACHE.values()); + textureIds.addAll(SHEEP_WOOL_UNDERCOAT_TINT_CACHE.values()); + textureIds.addAll(EQUIPMENT_TINT_CACHE.values()); + WOLF_COLLAR_TINT_CACHE.clear(); + SHEEP_WOOL_TINT_CACHE.clear(); + SHEEP_WOOL_UNDERCOAT_TINT_CACHE.clear(); + EQUIPMENT_TINT_CACHE.clear(); + INVALID_PARTICLE_COLOR_SOURCES_WARNED.clear(); + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + for (Identifier textureId : textureIds) { + client.getTextureManager().destroyTexture(textureId); + } + } + } + + private static void registerParticleCueResolver() { + NonAnimationParticleEvents.RESOLVE.register((NonAnimationParticleEvents.ParticleResolver)context -> { + if (context != null && NonParticles.isLiquidParticleId(context.particleId())) { + String colorSource = parseParticleColorSource(context.script()); + int tintRgb; + if (colorSource == null || "tank".equals(colorSource)) { + tintRgb = resolveParticleTankTint(context); + } else if ("actor".equals(colorSource)) { + Identifier actorTypeId = context.actor() == null ? null : Registries.ENTITY_TYPE.getId(context.actor().getType()); + tintRgb = NonLiquidSystem.resolveBottleTintRgb(actorTypeId); + } else { + warnInvalidParticleColorSource(context, colorSource); + tintRgb = resolveParticleTankTint(context); + } + + ParticleEffect particle = NonParticles.createLiquidEffect(context.particleId(), tintRgb); + return particle == null ? null : NonAnimationParticleEvents.ParticleOverride.spawn(particle); + } else { + return null; + } + }); + } + + private static int resolveParticleTankTint(NonAnimationParticleEvents.ParticleContext context) { + if (context.actor() instanceof PlayerEntity player) { + Integer syncedTint = NonDestroyedSkinClient.getLiquidTintRgb(player.getUuid()); + if (syncedTint != null) { + return syncedTint; + } + } + + MinecraftClient client = MinecraftClient.getInstance(); + return client == null || client.player == null || context.actor() != null && !context.actor().getUuid().equals(client.player.getUuid()) + ? NonLiquidSystem.resolveBottleTintRgb(null) + : NonHudOverlay.getLiquidTintRgb(); + } + + @Nullable + private static String parseParticleColorSource(@Nullable String script) { + if (script != null && !script.isBlank()) { + for (String entry : script.split("[;,\\r\\n]+")) { + String[] pair = entry.trim().split("=", 2); + if (pair.length == 2 && "color_source".equalsIgnoreCase(pair[0].trim())) { + return pair[1].trim().toLowerCase(Locale.ROOT); + } + } + + return null; + } else { + return null; + } + } + + private static void warnInvalidParticleColorSource(NonAnimationParticleEvents.ParticleContext context, String colorSource) { + String animation = context.animationId() == null ? "unknown" : context.animationId().toString(); + String warningKey = animation + "|" + colorSource; + if (INVALID_PARTICLE_COLOR_SOURCES_WARNED.add(warningKey)) { + NeedsOfNature.logSetupWarning("[NoN] Unknown particle color_source '{}' in animation {}; expected 'tank' or 'actor'.", colorSource, animation); + } + } + + private static boolean isPeakReactiveImpactCue(NonAnimationSoundEvents.SoundContext context) { + if (context == null) { + return false; + } else { + String effect = context.effect(); + if (!AnimationCueEffects.isReactiveImpact(effect)) { + return false; + } else if (NonSoundCues.isPeakReactiveImpactCue(effect, context.animationId(), context.stageIndex())) { + return true; + } else { + UUID instanceId = context.instanceId(); + if (instanceId == null) { + return false; + } else { + AnimationStageInfo stage = ClientAnimationRuntime.findStage(instanceId, context.stageIndex()); + if (stage != null && stage.animationId() != null) { + Identifier logicalStageId = stage.animationId(); + Integer stageNumber = NonPeakStages.stageNumberFromId(logicalStageId); + Integer peakStage = NonPeakStages.getPeakStage(NonPeakStages.baseAnimationId(logicalStageId)); + return stageNumber != null && peakStage != null && stageNumber.equals(peakStage); + } else { + return false; + } + } + } + } + } + + private static double nextSameEffectCueDelayMs(NonAnimationSoundEvents.SoundContext context) { + return context == null ? 0.0 : Math.max(0.0, context.nextSameEffectCueDelayMs()); + } + + private static boolean isLocalPlayerActorInInstance(@Nullable UUID instanceId) { + if (instanceId == null) { + return false; + } else { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + UUID playerInstance = ClientAnimationRuntime.findLatestActiveInstanceContaining(client.player.getUuid()); + return instanceId.equals(playerInstance); + } else { + return false; + } + } + } + + private static void tickIntifaceBaseline(MinecraftClient client) { + boolean active = false; + boolean peak = false; + double specialVibratorScalar = Double.NaN; + NonConfig config = NeedsOfNature.getConfig(); + int latencyCompensationMs = config == null ? 0 : config.getIntifaceLatencyCompensationMs(); + if (client != null && client.player != null) { + NeedsOfNatureClient.IntifaceAnimationState state = findLocalIntifaceAnimationState(client, latencyCompensationMs); + active = state != null; + if (state != null) { + peak = NonSoundCues.isPeakAnimationStage(state.stageAnimationId(), state.stageIndex()); + specialVibratorScalar = resolveSpecialIntifaceVibratorScalar(state); + } + } + + int energizedLevel = client != null && client.player != null ? NonHudOverlay.getEnergizedLevel(client.player) : 0; + double energizedPulse = energizedLevel > 0 ? NonHudOverlay.computeHeartPulse(energizedLevel, latencyCompensationMs) : 0.0; + NonIntifaceBridge.clientTick(config, active, peak, specialVibratorScalar, energizedLevel, energizedPulse); + } + + @Nullable + private static NeedsOfNatureClient.IntifaceAnimationState findLocalIntifaceAnimationState(MinecraftClient client, int latencyCompensationMs) { + if (client != null && client.player != null && client.world != null) { + UUID playerUuid = client.player.getUuid(); + UUID instanceId = ClientAnimationRuntime.findLatestActiveInstanceContaining(playerUuid); + if (instanceId == null) { + return null; + } + + ClientAnimationRuntime.StageTimelineSnapshot timeline = ClientAnimationRuntime.findStageTimeline(instanceId); + if (timeline != null && !timeline.stages().isEmpty()) { + int timelineStageIndex = Math.max(0, Math.min(timeline.stageIndex(), timeline.stages().size() - 1)); + AnimationStageInfo stage = timeline.stages().get(timelineStageIndex); + double projectedWorldTick = client.world.getTime() + Math.max(0, latencyCompensationMs) / 50.0; + double stageStartTick = timeline.stageStartTick(); + double stageEndTick = timeline.stageEndTick(); + double speed = Math.max(1.0E-4, timeline.speed()); + if (stageEndTick > stageStartTick + && projectedWorldTick >= stageEndTick + && !isBirthLoopStage(stage.animationId(), timelineStageIndex) + && timelineStageIndex + 1 < timeline.stages().size()) { + stage = timeline.stages().get(++timelineStageIndex); + stageStartTick = stageEndTick; + stageEndTick = -1.0; + speed = Math.max(1.0E-4, stage.speed()); + } + + Identifier stageAnimationId = stage.effectiveAnimationId(); + Identifier logicalStageAnimationId = stage.animationId(); + Identifier animationId = NonPeakStages.baseAnimationId(logicalStageAnimationId); + Integer stageNumber = NonPeakStages.stageNumberFromId(logicalStageAnimationId); + int logicalStageIndex = stageNumber == null ? timelineStageIndex : Math.max(0, stageNumber - 1); + double elapsedWorldTicks = Math.max(0.0, projectedWorldTick - stageStartTick); + double elapsedTicks = elapsedWorldTicks * speed; + double cycleTicks = AnimationGameplayMetadata.cycleTicks(logicalStageAnimationId); + if (cycleTicks <= 0.0) { + cycleTicks = stage.cycleTicks() > 0.0 ? stage.cycleTicks() : stage.lengthTicks(); + } + + double durationTicks = resolveIntifaceStageDurationTicks(animationId, logicalStageAnimationId, logicalStageIndex, cycleTicks); + double normalizedStageProgress = stageEndTick > stageStartTick + ? elapsedWorldTicks / (stageEndTick - stageStartTick) + : elapsedWorldTicks / Math.max(1.0, estimatedStageWorldDurationTicks(stage, durationTicks, speed)); + return new NeedsOfNatureClient.IntifaceAnimationState( + animationId, stageAnimationId, logicalStageIndex, elapsedTicks, normalizedStageProgress, cycleTicks + ); + } else { + return null; + } + } else { + return null; + } + } + + private static double estimatedStageWorldDurationTicks(AnimationStageInfo stage, double configuredDurationTicks, double speed) { + if (stage != null && !stage.loop()) { + double cycleTicks = stage.cycleTicks() > 0.0 ? stage.cycleTicks() : stage.lengthTicks(); + if (cycleTicks > 0.0) { + return cycleTicks / Math.max(1.0E-4, speed); + } + } + + return Math.max(1.0, configuredDurationTicks); + } + + private static double resolveIntifaceStageDurationTicks(Identifier animationId, Identifier stageAnimationId, int stageIndex, double cycleLengthTicks) { + double multiplier = AnimationGameplayMetadata.stageDurationMultiplier(stageAnimationId); + Integer override = AnimationGameplayMetadata.stageSeconds(stageAnimationId); + if (AnimationGameplayMetadata.isInfiniteStageSeconds(override)) { + override = null; + } + + if (override != null) { + return Math.max(1.0, Math.max(1, override) * 20.0 * multiplier); + } + + Identifier baseId = NonPeakStages.baseAnimationId(stageAnimationId); + Integer baseOverride = AnimationGameplayMetadata.stageSeconds(baseId); + if (AnimationGameplayMetadata.isInfiniteStageSeconds(baseOverride)) { + baseOverride = null; + } + + if (baseOverride != null) { + return Math.max(1.0, Math.max(1, baseOverride) * 20.0 * multiplier); + } + + Integer peakStage = NonPeakStages.getPeakStage(animationId); + int logicalStage = stageIndex + 1; + int seconds = Objects.equals(logicalStage, peakStage) + ? NeedsOfNature.getConfig().getPeakLoopProgressSeconds() + : NeedsOfNature.getConfig().getLoopProgressSeconds(); + return seconds > 0 ? Math.max(1.0, seconds * 20.0 * multiplier) : Math.max(1.0, cycleLengthTicks); + } + + private static double resolveSpecialIntifaceVibratorScalar(NeedsOfNatureClient.IntifaceAnimationState state) { + if (state == null || state.animationId() == null) { + return Double.NaN; + } else if (isManualPeakAnimation(state.animationId())) { + return resolveManualPeakVibratorScalar(state); + } else { + return isBirthAnimation(state.animationId(), state.stageAnimationId()) ? resolveBirthVibratorScalar(state) : Double.NaN; + } + } + + private static double resolveManualPeakVibratorScalar(NeedsOfNatureClient.IntifaceAnimationState state) { + Integer peakStage = NonPeakStages.getPeakStage(state.animationId()); + if (peakStage == null) { + return Double.NaN; + } else { + int logicalStage = state.stageIndex() + 1; + double progress = state.stageProgress(); + if (logicalStage < peakStage) { + return resolveManualPeakBuildupScalar(state, peakStage, logicalStage, progress); + } else { + return logicalStage == peakStage ? Math.max(0.0, Math.min(1.0, 1.0 - progress)) : Double.NaN; + } + } + } + + private static double resolveManualPeakBuildupScalar( + NeedsOfNatureClient.IntifaceAnimationState state, int peakStage, int logicalStage, double currentStageProgress + ) { + double completedDuration = 0.0; + double totalDuration = 0.0; + + for (int stageNumber = 1; stageNumber < peakStage; stageNumber++) { + Identifier stageId = AnimationIds.stageId(state.animationId(), stageNumber); + double cycleTicks = AnimationGameplayMetadata.cycleTicks(stageId); + if (cycleTicks <= 0.0 && stageNumber == logicalStage) { + cycleTicks = state.cycleDurationTicks(); + } + + double duration = resolveIntifaceStageDurationTicks(state.animationId(), stageId, stageNumber - 1, cycleTicks); + totalDuration += duration; + if (stageNumber < logicalStage) { + completedDuration += duration; + } else if (stageNumber == logicalStage) { + completedDuration += duration * currentStageProgress; + } + } + + return totalDuration <= 0.0 ? 0.0 : Math.max(0.0, Math.min(1.0, completedDuration / totalDuration)); + } + + private static double resolveBirthVibratorScalar(NeedsOfNatureClient.IntifaceAnimationState state) { + int logicalStage = state.stageIndex() + 1; + double progress = state.cycleProgress(); + + return switch (logicalStage) { + case 1 -> progress; + case 2 -> 0.6 + 0.4 * progress; + case 3 -> 1.0 - progress; + default -> Double.NaN; + }; + } + + private static boolean isManualPeakAnimation(Identifier animationId) { + return animationId != null && animationId.getPath().contains("manual_peak"); + } + + private static boolean isBirthAnimation(@Nullable Identifier animationId, @Nullable Identifier stageAnimationId) { + return hasBirthAnimationPath(animationId) || hasBirthAnimationPath(stageAnimationId); + } + + private static boolean isBirthLoopStage(@Nullable Identifier stageAnimationId, int stageIndex) { + Integer stageNumber = NonPeakStages.stageNumberFromId(stageAnimationId); + int logicalStage = stageNumber == null ? stageIndex + 1 : stageNumber; + return logicalStage == 2 && hasBirthAnimationPath(stageAnimationId); + } + + private static boolean hasBirthAnimationPath(@Nullable Identifier animationId) { + return animationId == null ? false : animationId.getPath().contains("birth"); + } + + private static boolean isFillBottlePropCue(NonAnimationSoundEvents.SoundContext context) { + if (context == null) { + return false; + } + + if (!isFillBottleAnimation(context.animationId())) { + return false; + } + + String effect = context.effect(); + return "fillbottle_prop_fill".equalsIgnoreCase(effect); + } + + private static boolean trySetAnimationRightHandPropOverride(UUID instanceId, UUID actorUuid, ItemStack stack) { + if (instanceId != null && actorUuid != null && stack != null && !stack.isEmpty()) { + ClientAnimationRuntime.setRightHandPropOverride(instanceId, actorUuid, stack); + return true; + } else { + return false; + } + } + + private static int drainPendingPresses(KeyBinding key) { + int count = 0; + + while (key.wasPressed()) { + count++; + } + + return count; + } + + private static void triggerReactiveImpactFovPulse() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + REACTIVE_IMPACT_PULSE_START_NANOS = System.nanoTime(); + } + } + + public static float getReactiveImpactFovOffset(float tickDelta) { + long pulseStartNanos = REACTIVE_IMPACT_PULSE_START_NANOS; + if (pulseStartNanos == Long.MIN_VALUE) { + return 0.0F; + } else { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return 0.0F; + } else { + long nowNanos = System.nanoTime(); + if (nowNanos < pulseStartNanos) { + REACTIVE_IMPACT_PULSE_START_NANOS = nowNanos; + return -0.5F; + } else { + float elapsed = (float)((nowNanos - pulseStartNanos) / 5.0E7); + if (elapsed <= 3.0F) { + return -0.5F; + } else { + float recovery = elapsed - 3.0F; + if (recovery >= 14.0F) { + REACTIVE_IMPACT_PULSE_START_NANOS = Long.MIN_VALUE; + return 0.0F; + } else { + float t = MathHelper.clamp(recovery / 14.0F, 0.0F, 1.0F); + float remaining = 1.0F - t; + float eased = remaining * remaining; + return -0.5F * eased; + } + } + } + } + } + } + + private static long getCurrentClientTicks(MinecraftClient client) { + return client != null && client.world != null ? client.world.getTime() : CLIENT_FALLBACK_TICKS; + } + + private record IntifaceAnimationState( + Identifier animationId, Identifier stageAnimationId, int stageIndex, double stageElapsedTicks, double normalizedStageProgress, double cycleDurationTicks + ) { + double stageProgress() { + return Math.max(0.0, Math.min(1.0, this.normalizedStageProgress)); + } + + double cycleProgress() { + return this.cycleDurationTicks <= 0.0 ? 0.0 : Math.max(0.0, Math.min(1.0, this.stageElapsedTicks / this.cycleDurationTicks)); + } + } +} diff --git a/src/main/java/com/nonid/NonAccessoryEffects.java b/src/main/java/com/nonid/NonAccessoryEffects.java new file mode 100644 index 0000000..55e7b80 --- /dev/null +++ b/src/main/java/com/nonid/NonAccessoryEffects.java @@ -0,0 +1,79 @@ +package com.nonid; + +public record NonAccessoryEffects( + double liquidDecayMultiplier, + boolean equalizeLiquidDecayContext, + double playerEnergyGainMultiplier, + int liquidCapacityAdd, + double liquidGainMultiplier, + double filledEffectMultiplier, + double pregnancyChanceMultiplier, + double pregnancyDurationMultiplier, + double messGainMultiplier, + double destroyedSkinDamageMultiplier, + int attackEscapeHitsAdd, + double attackEscapeDamageMultiplier, + double playerEnergyAuraMultiplier, + double nearAnimationMobEnergyMultiplier +) { + public static final NonAccessoryEffects NEUTRAL = new NonAccessoryEffects(1.0, false, 1.0, 0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0, 1.0, 1.0, 1.0); + + public NonAccessoryEffects combine(NonAccessoryEffects other) { + return other == null + ? this + : new NonAccessoryEffects( + this.liquidDecayMultiplier * other.liquidDecayMultiplier, + this.equalizeLiquidDecayContext || other.equalizeLiquidDecayContext, + this.playerEnergyGainMultiplier * other.playerEnergyGainMultiplier, + this.liquidCapacityAdd + other.liquidCapacityAdd, + this.liquidGainMultiplier * other.liquidGainMultiplier, + this.filledEffectMultiplier * other.filledEffectMultiplier, + this.pregnancyChanceMultiplier * other.pregnancyChanceMultiplier, + this.pregnancyDurationMultiplier * other.pregnancyDurationMultiplier, + this.messGainMultiplier * other.messGainMultiplier, + this.destroyedSkinDamageMultiplier * other.destroyedSkinDamageMultiplier, + this.attackEscapeHitsAdd + other.attackEscapeHitsAdd, + this.attackEscapeDamageMultiplier * other.attackEscapeDamageMultiplier, + this.playerEnergyAuraMultiplier * other.playerEnergyAuraMultiplier, + this.nearAnimationMobEnergyMultiplier * other.nearAnimationMobEnergyMultiplier + ); + } + + public NonAccessoryEffects globalOnly() { + return new NonAccessoryEffects( + 1.0, + false, + this.playerEnergyGainMultiplier, + 0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + this.destroyedSkinDamageMultiplier, + this.attackEscapeHitsAdd, + this.attackEscapeDamageMultiplier, + this.playerEnergyAuraMultiplier, + this.nearAnimationMobEnergyMultiplier + ); + } + + public NonAccessoryEffects tankScopedOnly() { + return new NonAccessoryEffects( + this.liquidDecayMultiplier, + this.equalizeLiquidDecayContext, + 1.0, + this.liquidCapacityAdd, + this.liquidGainMultiplier, + this.filledEffectMultiplier, + this.pregnancyChanceMultiplier, + this.pregnancyDurationMultiplier, + this.messGainMultiplier, + 1.0, + 0, + 1.0, + 1.0, + 1.0 + ); + } +} diff --git a/src/main/java/com/nonid/NonAccessoryShedSystem.java b/src/main/java/com/nonid/NonAccessoryShedSystem.java new file mode 100644 index 0000000..1b250d6 --- /dev/null +++ b/src/main/java/com/nonid/NonAccessoryShedSystem.java @@ -0,0 +1,197 @@ +package com.nonid; + +import com.nonid.data.NonAccessoryDefinitions; +import com.nonid.data.NonLiquidRoles; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.network.AccessoryShedStateS2CPayload; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; + +public final class NonAccessoryShedSystem { + private static final String V_SLOT = "legs/v"; + private static final String A_SLOT = "legs/a"; + private static final Map SHED_STATES = new ConcurrentHashMap<>(); + + private NonAccessoryShedSystem() { + } + + public static NonAccessoryShedSystem.ShedState resolveShedStateForAnimation(Identifier animationId) { + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(animationId); + if (roles != null && !roles.injectorRoles().isEmpty()) { + boolean shedV = false; + boolean shedA = false; + + for (NonLiquidRoles.InjectorRole role : roles.injectorRoles().values()) { + if (role == NonLiquidRoles.InjectorRole.V) { + shedV = true; + } else if (role == NonLiquidRoles.InjectorRole.A) { + shedA = true; + } + } + + return new NonAccessoryShedSystem.ShedState(shedV, shedA, false, false); + } else { + return NonAccessoryShedSystem.ShedState.NONE; + } + } + + public static NonAccessoryShedSystem.ShedState forSlot(String slotId) { + if (slotId == null) { + return NonAccessoryShedSystem.ShedState.NONE; + } else { + String slot = slotId.toLowerCase(Locale.ROOT); + if (slot.equals("legs/v") || slot.startsWith("legs/v/")) { + return new NonAccessoryShedSystem.ShedState(true, false, true, false); + } else { + return !slot.equals("legs/a") && !slot.startsWith("legs/a/") + ? NonAccessoryShedSystem.ShedState.NONE + : new NonAccessoryShedSystem.ShedState(false, true, false, true); + } + } + } + + public static void addServerShedState(ServerWorld world, ServerPlayerEntity player, NonAccessoryShedSystem.ShedState state) { + if (world != null && player != null && state != null && !state.isEmpty()) { + UUID uuid = player.getUuid(); + NonAccessoryShedSystem.ShedState merged = SHED_STATES.getOrDefault(uuid, NonAccessoryShedSystem.ShedState.NONE).merge(state); + SHED_STATES.put(uuid, merged); + syncToWorld(world, uuid, merged); + NonTrinketsIntegration.forceAccessorySync(player); + } + } + + public static void clearServerShedState(ServerWorld world, UUID playerUuid) { + if (world != null && playerUuid != null) { + NonAccessoryShedSystem.ShedState removed = SHED_STATES.remove(playerUuid); + if (removed != null && !removed.isEmpty()) { + syncToWorld(world, playerUuid, NonAccessoryShedSystem.ShedState.NONE); + if (world.getEntity(playerUuid) instanceof ServerPlayerEntity player) { + NonTrinketsIntegration.forceAccessorySync(player); + } + } + } + } + + public static void syncAllToViewer(ServerPlayerEntity viewer) { + if (viewer != null && !SHED_STATES.isEmpty()) { + for (Entry entry : SHED_STATES.entrySet()) { + NonAccessoryShedSystem.ShedState state = entry.getValue(); + if (state != null && !state.isEmpty()) { + ServerPlayNetworking.send( + viewer, new AccessoryShedStateS2CPayload(entry.getKey(), state.shedV(), state.shedA(), state.forcedV(), state.forcedA()) + ); + } + } + } + } + + public static void setClientShedState(UUID playerUuid, boolean shedV, boolean shedA, boolean forcedV, boolean forcedA) { + if (playerUuid != null) { + NonAccessoryShedSystem.ShedState state = new NonAccessoryShedSystem.ShedState(shedV, shedA, forcedV, forcedA); + if (state.isEmpty()) { + SHED_STATES.remove(playerUuid); + } else { + SHED_STATES.put(playerUuid, state); + } + } + } + + public static void clearAll() { + SHED_STATES.clear(); + } + + public static void tickServerWorld(ServerWorld world) { + if (world != null && !SHED_STATES.isEmpty()) { + for (UUID playerUuid : List.copyOf(SHED_STATES.keySet())) { + if (world.getEntity(playerUuid) instanceof ServerPlayerEntity player && !AnimationService.isActorPendingOrActive(world, playerUuid)) { + clearServerShedState(world, player.getUuid()); + } + } + } + } + + public static NonAccessoryShedSystem.ShedState getShedState(UUID playerUuid) { + return playerUuid == null ? NonAccessoryShedSystem.ShedState.NONE : SHED_STATES.getOrDefault(playerUuid, NonAccessoryShedSystem.ShedState.NONE); + } + + public static boolean isSlotShed(UUID playerUuid, String slotId) { + return getShedState(playerUuid).isSlotShed(slotId); + } + + public static boolean isSlotVisuallyShed(UUID playerUuid, String slotId, Identifier itemId) { + NonAccessoryShedSystem.ShedState state = getShedState(playerUuid); + if (!state.isSlotShed(slotId)) { + return false; + } else { + return state.isSlotForcedShed(slotId) ? true : !NonAccessoryDefinitions.getBehavior(itemId).ignoreInjectorSlotVisualShedding(); + } + } + + public static boolean isSlotEffectSuppressed(UUID playerUuid, String slotId, Identifier itemId) { + NonAccessoryShedSystem.ShedState state = getShedState(playerUuid); + if (!state.isSlotShed(slotId)) { + return false; + } else { + return state.isSlotForcedShed(slotId) ? true : !NonAccessoryDefinitions.getBehavior(itemId).ignoreInjectorSlotEffectSuppression(); + } + } + + private static void syncToWorld(ServerWorld world, UUID playerUuid, NonAccessoryShedSystem.ShedState state) { + AccessoryShedStateS2CPayload payload = new AccessoryShedStateS2CPayload(playerUuid, state.shedV(), state.shedA(), state.forcedV(), state.forcedA()); + + for (ServerPlayerEntity viewer : world.getPlayers()) { + ServerPlayNetworking.send(viewer, payload); + } + } + + public record ShedState(boolean shedV, boolean shedA, boolean forcedV, boolean forcedA) { + public static final NonAccessoryShedSystem.ShedState NONE = new NonAccessoryShedSystem.ShedState(false, false, false, false); + + public boolean isEmpty() { + return !this.shedV && !this.shedA; + } + + public NonAccessoryShedSystem.ShedState merge(NonAccessoryShedSystem.ShedState other) { + return other != null && !other.isEmpty() + ? new NonAccessoryShedSystem.ShedState( + this.shedV || other.shedV, this.shedA || other.shedA, this.forcedV || other.forcedV, this.forcedA || other.forcedA + ) + : this; + } + + public boolean isSlotShed(String slotId) { + if (slotId == null) { + return false; + } else { + String slot = slotId.toLowerCase(Locale.ROOT); + if (slot.equals("legs/v") || slot.startsWith("legs/v/")) { + return this.shedV; + } else { + return !slot.equals("legs/a") && !slot.startsWith("legs/a/") ? false : this.shedA; + } + } + } + + public boolean isSlotForcedShed(String slotId) { + if (slotId == null) { + return false; + } else { + String slot = slotId.toLowerCase(Locale.ROOT); + if (slot.equals("legs/v") || slot.startsWith("legs/v/")) { + return this.forcedV; + } else { + return !slot.equals("legs/a") && !slot.startsWith("legs/a/") ? false : this.forcedA; + } + } + } + } +} diff --git a/src/main/java/com/nonid/NonAdvancementHooks.java b/src/main/java/com/nonid/NonAdvancementHooks.java new file mode 100644 index 0000000..5d74987 --- /dev/null +++ b/src/main/java/com/nonid/NonAdvancementHooks.java @@ -0,0 +1,208 @@ +package com.nonid; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.advancement.AdvancementEntry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; + +final class NonAdvancementHooks { + private static final int MOTHER_NATURE_PREGNANCIES = 25; + private static final int COLLECTORS_HABIT_BOTTLES = 25; + private static final int FIELD_RESEARCH_TYPES = 5; + private static final int TOSSED_AROUND_CHAIN = 5; + private static final int STABLE_DIET_CHAIN = 3; + private static final String ROOT = "root"; + private static final String NATURAL_CURIOSITY = "natural_curiosity"; + private static final String FILLED_TO_THE_BRIM = "filled_to_the_brim"; + private static final String FAVOURITISM = "favouritism"; + private static final String FIRST_PREGNANCY = "first_pregnancy"; + private static final String MOTHER_NATURE = "mother_nature"; + private static final String MESSED_UP = "messed_up"; + private static final String GROUP_ACTIVITY = "group_activity"; + private static final String TOSSED_AROUND = "tossed_around"; + private static final String COLLECTORS_HABIT = "collectors_habit"; + private static final String FIELD_RESEARCH = "field_research"; + private static final String BAD_IDEA_BEACON = "bad_idea_beacon"; + private static final String PRODUCTIVE_PREGNANCY = "productive_pregnancy"; + private static final String EMERGENCY_SNACK = "emergency_snack"; + private static final String STABLE_DIET = "stable_diet"; + private static final String UDDERLY_UNFORTUNATE = "udderly_unfortunate"; + private static final String ACQUIRE_HARDWARE = "acquire_hardware"; + private static final Map FIELD_RESEARCH_STATES = new ConcurrentHashMap<>(); + private static final Map TOSSED_AROUND_STREAKS = new ConcurrentHashMap<>(); + private static final Map STABLE_DIET_STREAKS = new ConcurrentHashMap<>(); + + private NonAdvancementHooks() { + } + + static void clearRuntimeState() { + FIELD_RESEARCH_STATES.clear(); + TOSSED_AROUND_STREAKS.clear(); + STABLE_DIET_STREAKS.clear(); + } + + static void clearPlayerState(UUID playerUuid) { + if (playerUuid != null) { + FIELD_RESEARCH_STATES.remove(playerUuid); + TOSSED_AROUND_STREAKS.remove(playerUuid); + STABLE_DIET_STREAKS.remove(playerUuid); + } + } + + static void grant(ServerPlayerEntity player, String path) { + if (player != null && path != null && !path.isBlank()) { + if (player.getEntityWorld() instanceof ServerWorld world) { + MinecraftServer server = world.getServer(); + if (server != null) { + if (!"root".equals(path)) { + grant(player, "root"); + } + + AdvancementEntry advancement = server.getAdvancementLoader().get(NeedsOfNature.id(path)); + if (advancement != null) { + player.getAdvancementTracker().grantCriterion(advancement, "done"); + } + } + } + } + } + + static void grantEmergencySnack(ServerPlayerEntity player) { + grant(player, "emergency_snack"); + recordStableDietSnack(player); + } + + static void resetStableDietStreak(ServerPlayerEntity player) { + if (player != null) { + STABLE_DIET_STREAKS.remove(player.getUuid()); + } + } + + private static void recordStableDietSnack(ServerPlayerEntity player) { + if (player != null) { + int streak = STABLE_DIET_STREAKS.merge(player.getUuid(), 1, Integer::sum); + if (streak >= 3) { + grant(player, "stable_diet"); + } + } + } + + static void checkLiquidAdvancements(ServerPlayerEntity player, LiquidHolder holder) { + if (player != null && holder != null) { + int capacity = holder.getLiquidCapacity(); + if (capacity > 0 && holder.getLiquidStored() >= capacity) { + grant(player, "filled_to_the_brim"); + if (holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY && holder.getLiquidEntityTypeId() != null) { + grant(player, "favouritism"); + } + } + } + } + + static void trackFieldResearch(ServerWorld world, ServerPlayerEntity player, Map perEntityAmounts) { + if (world != null && player != null && perEntityAmounts != null && !perEntityAmounts.isEmpty()) { + Set gainedTypes = new LinkedHashSet<>(); + + for (Entry entry : perEntityAmounts.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null && entry.getValue() > 0) { + gainedTypes.add(entry.getKey()); + } + } + + if (!gainedTypes.isEmpty()) { + long day = world.getTimeOfDay() / 24000L; + UUID playerUuid = player.getUuid(); + NonAdvancementHooks.FieldResearchState existing = FIELD_RESEARCH_STATES.get(playerUuid); + Set entityTypes = new LinkedHashSet<>(); + if (existing != null && existing.day() == day) { + entityTypes.addAll(existing.entityTypes()); + } + + entityTypes.addAll(gainedTypes); + FIELD_RESEARCH_STATES.put(playerUuid, new NonAdvancementHooks.FieldResearchState(day, Set.copyOf(entityTypes))); + if (entityTypes.size() >= 5) { + grant(player, "field_research"); + } + } + } + } + + static void checkBadIdeaBeacon(ServerPlayerEntity player) { + if (player != null && player instanceof EnergyHolder energyHolder) { + if (energyHolder.getEnergy() >= 200) { + if (NeedsOfNature.getDestroyedSkinStageForPlayer(player) >= 4) { + grant(player, "bad_idea_beacon"); + } + } + } + } + + static void grantNaturalCuriosity(ServerPlayerEntity player) { + grant(player, "natural_curiosity"); + } + + static void grantGroupActivity(ServerPlayerEntity player) { + grant(player, "group_activity"); + } + + static void grantMessedUp(ServerPlayerEntity player) { + grant(player, "messed_up"); + } + + static void checkCollectorsHabit(ServerPlayerEntity player) { + if (player != null) { + if (NonStats.getBottlesFilled(player) >= 25) { + grant(player, "collectors_habit"); + } + } + } + + static void resetTossedAround(ServerPlayerEntity player) { + if (player != null) { + TOSSED_AROUND_STREAKS.remove(player.getUuid()); + } + } + + static void recordTossedAround(ServerPlayerEntity player) { + if (player != null) { + int streak = TOSSED_AROUND_STREAKS.merge(player.getUuid(), 1, Integer::sum); + if (streak >= 5) { + grant(player, "tossed_around"); + } + } + } + + static void grantFirstPregnancy(ServerPlayerEntity player) { + grant(player, "first_pregnancy"); + } + + static void checkMotherNature(ServerPlayerEntity player) { + if (player != null) { + if (NonStats.getPregnanciesCompleted(player) >= 25) { + grant(player, "mother_nature"); + } + } + } + + static void grantProductivePregnancy(ServerPlayerEntity player) { + grant(player, "productive_pregnancy"); + } + + static void grantUdderlyUnfortunate(ServerPlayerEntity player) { + grant(player, "udderly_unfortunate"); + } + + static void grantAcquireHardware(ServerPlayerEntity player) { + grant(player, "acquire_hardware"); + } + + private record FieldResearchState(long day, Set entityTypes) { + } +} diff --git a/src/main/java/com/nonid/NonAnimationMatcher.java b/src/main/java/com/nonid/NonAnimationMatcher.java new file mode 100644 index 0000000..8a9d76a --- /dev/null +++ b/src/main/java/com/nonid/NonAnimationMatcher.java @@ -0,0 +1,181 @@ +package com.nonid; + +import com.nonid.internal.animation.api.AnimationService; +import java.util.List; +import java.util.Set; +import net.minecraft.entity.Entity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationMatcher { + private NonAnimationMatcher() { + } + + @Nullable + public static AnimationService.MatchedAnimation findAutomatic( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds + ) { + return findAutomatic(world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, null, null, false); + } + + @Nullable + public static AnimationService.MatchedAnimation findAutomatic( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable AnimationService.AnimationCandidateFilter additionalFilter, + @Nullable Identifier joinPredecessorAnimationId + ) { + return findAutomatic( + world, + actors, + requiredTags, + positionAnchor, + requireStartEligibility, + requiredActiveActor, + excludedAnimationIds, + additionalFilter, + joinPredecessorAnimationId, + false + ); + } + + @Nullable + public static AnimationService.MatchedAnimation findAutomaticAttack( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds + ) { + return findAutomatic(world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, null, null, true); + } + + @Nullable + public static AnimationService.MatchedAnimation findAutomaticAttack( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable AnimationService.AnimationCandidateFilter additionalFilter, + @Nullable Identifier joinPredecessorAnimationId + ) { + return findAutomatic( + world, + actors, + requiredTags, + positionAnchor, + requireStartEligibility, + requiredActiveActor, + excludedAnimationIds, + additionalFilter, + joinPredecessorAnimationId, + true + ); + } + + @Nullable + private static AnimationService.MatchedAnimation findAutomatic( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable AnimationService.AnimationCandidateFilter additionalFilter, + @Nullable Identifier joinPredecessorAnimationId, + boolean attackSelection + ) { + AnimationService.AnimationCandidateFilter filter = (candidateWorld, definition, candidateActors) -> (!attackSelection || definition.attackEligible()) + && NonInjectorMatchPolicy.allowsAutomaticNonMatch(candidateWorld, definition, candidateActors) + && (additionalFilter == null || additionalFilter.test(candidateWorld, definition, candidateActors)); + return AnimationService.findAnimationForActors( + world, + actors, + new AnimationService.MatchOptions( + requiredTags == null ? Set.of() : requiredTags, + positionAnchor, + requireStartEligibility, + requiredActiveActor, + excludedAnimationIds == null ? Set.of() : excludedAnimationIds, + filter, + joinPredecessorAnimationId + ) + ); + } + + public static List findEligibleAutomatic( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable Identifier joinPredecessorAnimationId + ) { + return findEligibleAutomatic( + world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, joinPredecessorAnimationId, false + ); + } + + public static List findEligibleAutomaticAttack( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable Identifier joinPredecessorAnimationId + ) { + return findEligibleAutomatic( + world, actors, requiredTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, joinPredecessorAnimationId, true + ); + } + + private static List findEligibleAutomatic( + ServerWorld world, + List actors, + @Nullable Set requiredTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable Identifier joinPredecessorAnimationId, + boolean attackSelection + ) { + AnimationService.AnimationCandidateFilter filter = (candidateWorld, definition, candidateActors) -> (!attackSelection || definition.attackEligible()) + && NonInjectorMatchPolicy.allowsAutomaticNonMatch(candidateWorld, definition, candidateActors); + return AnimationService.findEligibleAnimationsForActors( + world, + actors, + new AnimationService.MatchOptions( + requiredTags == null ? Set.of() : requiredTags, + positionAnchor, + requireStartEligibility, + requiredActiveActor, + excludedAnimationIds == null ? Set.of() : excludedAnimationIds, + filter, + joinPredecessorAnimationId + ) + ); + } +} diff --git a/src/main/java/com/nonid/NonAnimationRescueSystem.java b/src/main/java/com/nonid/NonAnimationRescueSystem.java new file mode 100644 index 0000000..2ae8022 --- /dev/null +++ b/src/main/java/com/nonid/NonAnimationRescueSystem.java @@ -0,0 +1,92 @@ +package com.nonid; + +import com.nonid.internal.animation.server.ServerAnimationController; +import java.util.Comparator; +import java.util.UUID; +import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationRescueSystem { + private NonAnimationRescueSystem() { + } + + static void register() { + AttackEntityCallback.EVENT + .register( + (AttackEntityCallback)(player, world, hand, entity, hitResult) -> { + if (hand == Hand.MAIN_HAND + && world instanceof ServerWorld serverWorld + && player instanceof ServerPlayerEntity attacker + && entity instanceof ServerPlayerEntity animatedPlayer) { + MobEntity mob = findRescueTarget(serverWorld, attacker, animatedPlayer); + if (mob == null) { + return ActionResult.PASS; + } + + attacker.attack(mob); + return ActionResult.SUCCESS; + } else { + return ActionResult.PASS; + } + } + ); + } + + public static Entity redirectProjectileTarget(ServerWorld world, ProjectileEntity projectile, Entity originalTarget) { + if (projectile.getOwner() instanceof ServerPlayerEntity attacker && originalTarget instanceof ServerPlayerEntity animatedPlayer) { + MobEntity mob = findRescueTarget(world, attacker, animatedPlayer); + return (Entity)(mob == null ? originalTarget : mob); + } else { + return originalTarget; + } + } + + @Nullable + public static MobEntity findDirectDamageRedirectTarget(ServerWorld world, LivingEntity originalTarget, DamageSource source) { + if (originalTarget instanceof ServerPlayerEntity animatedPlayer && source != null && source.isDirect()) { + ServerPlayerEntity attacker; + if (source.getAttacker() instanceof ServerPlayerEntity attributedAttacker) { + attacker = attributedAttacker; + } else { + if (!(source.getSource() instanceof ServerPlayerEntity directAttacker)) { + return null; + } + + attacker = directAttacker; + } + + return findRescueTarget(world, attacker, animatedPlayer); + } else { + return null; + } + } + + @Nullable + private static MobEntity findRescueTarget(ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity animatedPlayer) { + UUID instanceId = ServerAnimationController.findActiveInstanceId(world, animatedPlayer.getUuid()); + if (instanceId == null) { + return null; + } + + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + return snapshot != null && !snapshot.actorUuids().contains(attacker.getUuid()) + ? snapshot.actorUuids() + .stream() + .map(world::getEntity) + .filter(MobEntity.class::isInstance) + .map(MobEntity.class::cast) + .filter(Entity::isAlive) + .min(Comparator.comparingDouble(mob -> attacker.squaredDistanceTo(mob)).thenComparingInt(Entity::getId)) + .orElse(null) + : null; + } +} diff --git a/src/main/java/com/nonid/NonAnimationSessionData.java b/src/main/java/com/nonid/NonAnimationSessionData.java new file mode 100644 index 0000000..d08e97c --- /dev/null +++ b/src/main/java/com/nonid/NonAnimationSessionData.java @@ -0,0 +1,111 @@ +package com.nonid; + +import com.nonid.internal.animation.server.AnimationSessionData; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationSessionData( + NonAnimationSessionData.Mode mode, + @Nullable UUID breedingRequestId, + @Nullable NonAnimationSessionData.Pregnancy pregnancy, + @Nullable NonAnimationSessionData.PlayerAttack playerAttack +) implements AnimationSessionData { + public NonAnimationSessionData { + mode = mode == null ? NonAnimationSessionData.Mode.UNSPECIFIED : mode; + } + + public static NonAnimationSessionData voluntary() { + return new NonAnimationSessionData(NonAnimationSessionData.Mode.VOLUNTARY, null, null, null); + } + + public static NonAnimationSessionData attack() { + return new NonAnimationSessionData(NonAnimationSessionData.Mode.ATTACK, null, null, null); + } + + public static NonAnimationSessionData breeding(UUID requestId) { + return new NonAnimationSessionData(NonAnimationSessionData.Mode.UNSPECIFIED, requestId, null, null); + } + + public static NonAnimationSessionData pregnancy(NonAnimationSessionData.Pregnancy pregnancy) { + return new NonAnimationSessionData(NonAnimationSessionData.Mode.PREGNANCY, null, pregnancy, null); + } + + public static NonAnimationSessionData playerAttack(NonAnimationSessionData.PlayerAttack attack) { + return new NonAnimationSessionData(NonAnimationSessionData.Mode.ATTACK, null, null, attack); + } + + public NonAnimationSessionData withPlayerAttack(@Nullable NonAnimationSessionData.PlayerAttack attack) { + return new NonAnimationSessionData(NonAnimationSessionData.Mode.ATTACK, this.breedingRequestId, this.pregnancy, attack); + } + + public boolean isAttack() { + return this.mode == NonAnimationSessionData.Mode.ATTACK; + } + + public boolean isVoluntary() { + return this.mode == NonAnimationSessionData.Mode.VOLUNTARY; + } + + public boolean isPregnancy() { + return this.mode == NonAnimationSessionData.Mode.PREGNANCY; + } + + public enum Mode { + UNSPECIFIED, + VOLUNTARY, + ATTACK, + PREGNANCY; + } + + public record PlayerAttack(UUID victimUuid, UUID finishOwnerUuid, UUID initiatorUuid, Set attackerUuids) { + public PlayerAttack { + if (victimUuid != null && finishOwnerUuid != null && initiatorUuid != null) { + LinkedHashSet attackers = new LinkedHashSet<>(); + if (attackerUuids != null) { + attackers.addAll(attackerUuids); + } + + attackers.remove(victimUuid); + attackers.add(initiatorUuid); + attackerUuids = Set.copyOf(attackers); + } else { + throw new IllegalArgumentException("Player attack participants must not be null"); + } + } + + public NonAnimationSessionData.PlayerAttack withAdditionalAttacker(UUID attackerUuid) { + LinkedHashSet attackers = new LinkedHashSet<>(this.attackerUuids); + if (attackerUuid != null && !attackerUuid.equals(this.victimUuid)) { + attackers.add(attackerUuid); + } + + return new NonAnimationSessionData.PlayerAttack(this.victimUuid, this.finishOwnerUuid, attackerUuid, attackers); + } + + public NonAnimationSessionData.PlayerAttack forDefeatedTail(UUID victim) { + return new NonAnimationSessionData.PlayerAttack(victim, this.finishOwnerUuid, this.initiatorUuid, this.attackerUuids); + } + } + + public record Pregnancy( + Identifier sourceEntityTypeId, + Identifier birthEntityTypeId, + String birthMode, + int offspringCount, + @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + public Pregnancy { + if (sourceEntityTypeId == null) { + throw new IllegalArgumentException("sourceEntityTypeId must not be null"); + } + + birthEntityTypeId = birthEntityTypeId == null ? sourceEntityTypeId : birthEntityTypeId; + birthMode = birthMode == null ? "direct" : birthMode; + offspringCount = Math.max(1, offspringCount); + variantData = NeedsOfNature.normalizePregnancyVariantData(variantData); + } + } +} diff --git a/src/main/java/com/nonid/NonApiInternals.java b/src/main/java/com/nonid/NonApiInternals.java new file mode 100644 index 0000000..2538743 --- /dev/null +++ b/src/main/java/com/nonid/NonApiInternals.java @@ -0,0 +1,358 @@ +package com.nonid; + +import com.nonid.api.NonAccessoryEffectsSnapshot; +import com.nonid.api.NonChangeSource; +import com.nonid.api.NonDestroyedSkinSnapshot; +import com.nonid.api.NonEvents; +import com.nonid.api.NonGender; +import com.nonid.api.NonLiquidComposition; +import com.nonid.api.NonLiquidTankSnapshot; +import com.nonid.api.NonMessSnapshot; +import com.nonid.api.NonMessType; +import com.nonid.api.NonPlayerStateSnapshot; +import com.nonid.api.NonPregnancySnapshot; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonApiInternals { + private static final int DESTROYED_SKIN_MAX_DAMAGE = 10; + + private NonApiInternals() { + } + + public static NonPlayerStateSnapshot snapshotPlayerState(PlayerEntity player) { + return new NonPlayerStateSnapshot( + getGender(player), + snapshotLiquid(player), + snapshotMess(player), + snapshotDestroyedSkin(player), + snapshotPregnancy(player), + snapshotAccessoryEffects(player) + ); + } + + public static NonGender getGender(PlayerEntity player) { + return player instanceof GenderHolder holder ? NonGender.fromMask(holder.getGenderMask()) : NonGender.NONE; + } + + public static boolean setGender(ServerPlayerEntity player, NonGender gender, NonChangeSource source) { + if (player == null || gender == null || gender == NonGender.NONE) { + return false; + } + + if (!canChangeGender(player, source)) { + return false; + } + + int before = player instanceof GenderHolder holder ? holder.getGenderMask() & 3 : 0; + int applied = NonGenderSystem.setPlayerGenderFromCommand(player, gender.mask(), source); + return (before & 3) != (applied & 3); + } + + public static boolean canChangeGender(ServerPlayerEntity player, NonChangeSource source) { + if (player != null && source != null) { + return switch (source) { + case API, COMMAND, ITEM, JOIN_SELECTION, SYSTEM -> true; + case CLIENT_PREFERENCE -> NonGenderSystem.canClientChangeGenderFreely(); + }; + } else { + return false; + } + } + + public static NonLiquidTankSnapshot snapshotLiquid(PlayerEntity player) { + return player instanceof LiquidHolder holder + ? new NonLiquidTankSnapshot( + Math.max(0, holder.getLiquidStored()), + Math.max(0, holder.getLiquidCapacity()), + toApiLiquidComposition(holder.getLiquidComposition()), + holder.getLiquidEntityTypeId(), + NonLiquidSystem.resolveLiquidTintRgb(holder) + ) + : new NonLiquidTankSnapshot(0, 0, NonLiquidComposition.EMPTY, null, 16777215); + } + + public static int addLiquid(ServerPlayerEntity player, @Nullable Identifier sourceEntityType, int amountMl) { + if (!NeedsOfNature.getConfig().isLiquidTankEnabled()) { + return 0; + } + + if (player != null && player instanceof LiquidHolder holder && amountMl > 0) { + int modifiedAmount = ((NonEvents.ModifyLiquidGain)NonEvents.MODIFY_LIQUID_GAIN.invoker()).modifyLiquidGain(player, sourceEntityType, amountMl); + if (modifiedAmount <= 0) { + return 0; + } + + int added = addLiquidWithComposition(holder, sourceEntityType, modifiedAmount); + if (added > 0) { + NonPregnancyConceptionSystem.recordRecentDonor(holder, sourceEntityType, null); + NeedsOfNature.syncLiquidState(player); + } + + return added; + } else { + return 0; + } + } + + public static int drainLiquid(ServerPlayerEntity player, int amountMl) { + if (player != null && player instanceof LiquidHolder holder && amountMl > 0) { + int before = Math.max(0, holder.getLiquidStored()); + int removed = Math.min(before, amountMl); + if (removed <= 0) { + return 0; + } + + int next = before - removed; + holder.setLiquidStored(next); + if (next <= 0) { + holder.clearLiquidComposition(); + } + + NeedsOfNature.syncLiquidState(player); + return removed; + } else { + return 0; + } + } + + public static NonMessSnapshot snapshotMess(PlayerEntity player) { + return player instanceof MessHolder holder + ? new NonMessSnapshot( + NonMessSystem.clampMess(holder.getVMess()), NonMessSystem.clampMess(holder.getAMess()), NonMessSystem.clampMess(holder.getMMess()) + ) + : new NonMessSnapshot(0, 0, 0); + } + + public static boolean addMess(ServerPlayerEntity player, NonMessType type, int amount) { + return changeMess(player, type, Math.max(0, amount)); + } + + public static boolean cleanMess(ServerPlayerEntity player, NonMessType type, int amount) { + return changeMess(player, type, -Math.max(0, amount)); + } + + public static boolean clearMess(ServerPlayerEntity player) { + if (player != null && player instanceof MessHolder holder) { + boolean changed = holder.getVMess() != 0 || holder.getAMess() != 0 || holder.getMMess() != 0; + NonMessSystem.clearMess(player); + return changed; + } else { + return false; + } + } + + public static NonDestroyedSkinSnapshot snapshotDestroyedSkin(PlayerEntity player) { + return player instanceof DestroyedSkinHolder holder + ? new NonDestroyedSkinSnapshot( + Math.max(0, Math.min(4, holder.getDestroyedSkinStage())), Math.max(0, Math.min(10, holder.getDestroyedSkinDamage())), 10 + ) + : new NonDestroyedSkinSnapshot(0, 0, 10); + } + + public static boolean damageDestroyedSkin(ServerPlayerEntity player, int amount) { + return changeDestroyedSkinDamage(player, Math.max(0, amount)); + } + + public static boolean repairDestroyedSkin(ServerPlayerEntity player, int amount) { + return changeDestroyedSkinDamage(player, -Math.max(0, amount)); + } + + public static NonPregnancySnapshot snapshotPregnancy(PlayerEntity player) { + return player instanceof PregnancyHolder holder + ? new NonPregnancySnapshot(holder.getPregnantEntityTypeId(), holder.getPregnancyEndTick(), holder.isPregnancyPendingHatch()) + : new NonPregnancySnapshot(null, 0L, false); + } + + public static boolean beginPregnancy(ServerPlayerEntity player, Identifier entityTypeId, long endTick) { + if (!NeedsOfNature.getConfig().isPregnancyEnabled()) { + return false; + } else if (player != null && entityTypeId != null) { + NeedsOfNature.beginPregnancy(player, entityTypeId, Math.max(1L, endTick)); + return true; + } else { + return false; + } + } + + public static boolean clearPregnancy(ServerPlayerEntity player) { + if (player == null) { + return false; + } + + boolean hadPregnancy = player instanceof PregnancyHolder holder && holder.getPregnantEntityTypeId() != null; + NeedsOfNature.clearPregnancyState(player, true); + return hadPregnancy; + } + + public static NonAccessoryEffectsSnapshot snapshotAccessoryEffects(PlayerEntity player) { + NonAccessoryEffects effects = player instanceof LivingEntity living ? NonTrinketsIntegration.getAccessoryEffects(living) : NonAccessoryEffects.NEUTRAL; + return new NonAccessoryEffectsSnapshot( + effects.liquidDecayMultiplier(), + effects.equalizeLiquidDecayContext(), + effects.playerEnergyGainMultiplier(), + effects.liquidCapacityAdd(), + effects.liquidGainMultiplier(), + effects.filledEffectMultiplier(), + effects.pregnancyChanceMultiplier(), + effects.pregnancyDurationMultiplier(), + effects.messGainMultiplier(), + effects.destroyedSkinDamageMultiplier(), + effects.attackEscapeHitsAdd(), + effects.attackEscapeDamageMultiplier(), + effects.playerEnergyAuraMultiplier(), + effects.nearAnimationMobEnergyMultiplier() + ); + } + + public static boolean isMessSystemEnabled() { + return NeedsOfNature.getConfig().isMessSystemEnabled(); + } + + public static boolean isDestroyedSkinSystemEnabled() { + return NeedsOfNature.getConfig().isDestroyedSkinSystemEnabled(); + } + + public static boolean isLiquidTankEnabled() { + return NeedsOfNature.getConfig().isLiquidTankEnabled(); + } + + public static boolean isPregnancyEnabled() { + return NeedsOfNature.getConfig().isPregnancyEnabled(); + } + + public static void fireLiquidChanged(ServerPlayerEntity player) { + if (player != null) { + ((NonEvents.LiquidTankChanged)NonEvents.LIQUID_TANK_CHANGED.invoker()).onLiquidTankChanged(player, snapshotLiquid(player)); + } + } + + public static void fireMessChanged(ServerPlayerEntity player) { + if (player != null) { + ((NonEvents.MessChanged)NonEvents.MESS_CHANGED.invoker()).onMessChanged(player, snapshotMess(player)); + } + } + + public static void fireDestroyedSkinChanged(ServerPlayerEntity player) { + if (player != null) { + ((NonEvents.DestroyedSkinChanged)NonEvents.DESTROYED_SKIN_CHANGED.invoker()).onDestroyedSkinChanged(player, snapshotDestroyedSkin(player)); + } + } + + public static void firePregnancyChanged(ServerPlayerEntity player) { + if (player != null) { + ((NonEvents.PregnancyChanged)NonEvents.PREGNANCY_CHANGED.invoker()).onPregnancyChanged(player, snapshotPregnancy(player)); + } + } + + public static void fireGenderChanged(ServerPlayerEntity player, int mask, NonChangeSource source) { + if (player != null) { + ((NonEvents.PlayerGenderChanged)NonEvents.PLAYER_GENDER_CHANGED.invoker()).onPlayerGenderChanged(player, NonGender.fromMask(mask), source); + } + } + + private static int addLiquidWithComposition(LiquidHolder holder, @Nullable Identifier sourceEntityType, int amount) { + int stored = Math.max(0, holder.getLiquidStored()); + int capacity = Math.max(0, holder.getLiquidCapacity()); + int room = Math.max(0, capacity - stored); + int added = Math.min(room, amount); + if (added <= 0) { + return 0; + } + + if (stored <= 0) { + if (sourceEntityType != null) { + holder.setLiquidCompositionEntity(sourceEntityType); + } else { + holder.setLiquidCompositionMixed(); + } + } else if (holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) { + Identifier existingType = holder.getLiquidEntityTypeId(); + if (sourceEntityType == null || !sourceEntityType.equals(existingType)) { + holder.setLiquidCompositionMixed(); + } + } else if (holder.getLiquidComposition() != LiquidHolder.LiquidComposition.MIXED) { + holder.setLiquidCompositionMixed(); + } + + holder.setLiquidStored(stored + added); + return added; + } + + private static boolean changeMess(ServerPlayerEntity player, NonMessType type, int delta) { + if (!NeedsOfNature.getConfig().isMessSystemEnabled()) { + return false; + } + + if (player != null && type != null && delta != 0 && player instanceof MessHolder holder) { + int before = switch (type) { + case V -> holder.getVMess(); + case A -> holder.getAMess(); + case M -> holder.getMMess(); + }; + int after = NonMessSystem.clampMess(before + delta); + if (after == before) { + return false; + } + + switch (type) { + case V: + holder.setVMess(after); + break; + case A: + holder.setAMess(after); + break; + case M: + holder.setMMess(after); + } + + NonMessSystem.broadcastMessState(player); + return true; + } else { + return false; + } + } + + private static boolean changeDestroyedSkinDamage(ServerPlayerEntity player, int delta) { + if (!NeedsOfNature.getConfig().isDestroyedSkinSystemEnabled()) { + return false; + } + + if (player != null && delta != 0 && player instanceof DestroyedSkinHolder holder) { + int before = Math.max(0, Math.min(10, holder.getDestroyedSkinDamage())); + int after = Math.max(0, Math.min(10, before + delta)); + if (after == before) { + return false; + } + + holder.setDestroyedSkinDamage(after); + NonDestroyedSkinSystem.setDestroyedSkinStage(player, destroyedSkinStageForDamage(after)); + return true; + } else { + return false; + } + } + + private static int destroyedSkinStageForDamage(int damage) { + if (damage >= 10) { + return 4; + } else if (damage >= 7) { + return 3; + } else if (damage >= 4) { + return 2; + } else { + return damage >= 1 ? 1 : 0; + } + } + + private static NonLiquidComposition toApiLiquidComposition(LiquidHolder.LiquidComposition composition) { + return switch (composition) { + case ENTITY -> NonLiquidComposition.ENTITY; + case MIXED -> NonLiquidComposition.MIXED; + case EMPTY -> NonLiquidComposition.EMPTY; + }; + } +} diff --git a/src/main/java/com/nonid/NonAttackSystem.java b/src/main/java/com/nonid/NonAttackSystem.java new file mode 100644 index 0000000..d8e1876 --- /dev/null +++ b/src/main/java/com/nonid/NonAttackSystem.java @@ -0,0 +1,671 @@ +package com.nonid; + +import com.nonid.internal.animation.api.AnimationService; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LazyEntityReference; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.attribute.EntityAttributeInstance; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.mob.Angerable; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.mob.Monster; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +final class NonAttackSystem { + private static final float ATTACK_ESCAPE_DAMAGE = 4.0F; + private static final float ATTACK_ESCAPE_MIN_DAMAGE = 1.0F; + private static final float ATTACK_ESCAPE_MIN_HEALTH = 1.0F; + private static final int ATTACK_OUTCOME_FAILSAFE_WINDOW_TICKS = 12000; + private static final int ATTACK_OUTCOME_FAILSAFE_COOLDOWN_TICKS = 12000; + private static final int ATTACK_BLOCK_DEBUG_COOLDOWN_TICKS = 400; + private static final double ATTACK_BLOCK_DEBUG_RADIUS = 24.0; + private static final double POST_ESCAPE_GATHER_RADIUS = 10.0; + private static final float ATTACK_ESCAPE_MIN_HEALTH_FACTOR = 0.3F; + private static final long DEFEATED_EASE_DURATION_TICKS = 300L; + private static final float DEFEATED_MAX_STEP_MULTIPLIER = 4.0F; + private static final float PVP_CRITICAL_HEALTH_THRESHOLD = 0.25F; + private static final float PVP_HEALTHY_DEFENDER_THRESHOLD = 0.75F; + private static final float PVP_DEFENDER_HEAT_NEUTRAL_THRESHOLD = 0.6F; + private static final int PVP_MASH_BASE_HITS = 12; + private static final Map ATTACK_OUTCOME_FAILSAFE = new ConcurrentHashMap<>(); + private static final Map ATTACK_BLOCK_DEBUG = new ConcurrentHashMap<>(); + + private NonAttackSystem() { + } + + static void clearRuntimeState() { + ATTACK_OUTCOME_FAILSAFE.clear(); + ATTACK_BLOCK_DEBUG.clear(); + } + + static boolean canMobAttackPlayer(ServerPlayerEntity player) { + if (player == null) { + return false; + } + + if (player.isSpectator()) { + return false; + } + + if (!player.isCreative()) { + return true; + } + + NonConfig config = NeedsOfNature.getConfig(); + return config != null && config.canAttackCreativePlayers(); + } + + static void setAnimationStartProtection(ServerPlayerEntity player, int ticks) { + if (player != null) { + AnimationService.setAnimationStartProtection(player, ticks); + } + } + + static boolean hasAnimationStartProtection(ServerWorld world, ServerPlayerEntity player) { + return world != null && player != null ? AnimationService.hasAnimationStartProtection(world, player) : false; + } + + static boolean isMobAttackFailsafeActive(ServerWorld world, UUID mobUuid) { + if (world != null && mobUuid != null) { + NonAttackSystem.AttackOutcomeFailsafeState state = ATTACK_OUTCOME_FAILSAFE.get(mobUuid); + if (state == null) { + return false; + } + + if (!state.worldKey.equals(world.getRegistryKey())) { + return false; + } + + long nowTick = world.getTime(); + pruneAttackOutcomeWindow(state, nowTick); + if (state.cooldownUntilTick <= nowTick) { + if (state.recentAttackOutcomeTicks.isEmpty()) { + ATTACK_OUTCOME_FAILSAFE.remove(mobUuid, state); + } + + return false; + } else { + return true; + } + } else { + return false; + } + } + + static int clearAttackOutcomeFailsafeEntries() { + int count = ATTACK_OUTCOME_FAILSAFE.size(); + ATTACK_OUTCOME_FAILSAFE.clear(); + return count; + } + + static String getMobAttackFailsafeDebug(ServerWorld world, UUID mobUuid) { + if (world != null && mobUuid != null) { + NonAttackSystem.AttackOutcomeFailsafeState state = ATTACK_OUTCOME_FAILSAFE.get(mobUuid); + if (state != null && state.worldKey.equals(world.getRegistryKey())) { + long nowTick = world.getTime(); + pruneAttackOutcomeWindow(state, nowTick); + long remaining = Math.max(0L, state.cooldownUntilTick - nowTick); + int recent = state.recentAttackOutcomeTicks.size(); + if (remaining <= 0L && recent <= 0) { + ATTACK_OUTCOME_FAILSAFE.remove(mobUuid, state); + return null; + } else { + return remaining > 0L + ? "attack failsafe: " + NeedsOfNature.formatTicksForDebug(remaining) + " remaining; peaks " + recent + "/" + attackOutcomeFailsafeThreshold() + : "attack failsafe: peaks " + recent + "/" + attackOutcomeFailsafeThreshold() + " in 10m window"; + } + } else { + return null; + } + } else { + return null; + } + } + + static NeedsOfNature.AttackBlockReason getMobAttackBlockReason(ServerWorld world, MobEntity mob, ServerPlayerEntity player) { + if (world != null && mob != null && player != null) { + if (mob.getEntityWorld() != world || player.getEntityWorld() != world) { + return null; + } else if (NeedsOfNature.isPregnancyFriendlyMob(mob)) { + return NeedsOfNature.AttackBlockReason.PREGNANCY_FRIENDLY; + } else if (!isAggressiveThreat(mob, player)) { + return null; + } else { + boolean mobBusy = NeedsOfNature.isMobGatheringOrActiveWithPlayer(world, mob, player); + boolean playerBusy = NeedsOfNature.hasActivePlayerAnimation(player); + if (mobBusy) { + return NeedsOfNature.AttackBlockReason.GATHERING_PAIR; + } else { + return playerBusy && mob instanceof EnergyHolder holder && holder.getEnergy() > 70 + ? NeedsOfNature.AttackBlockReason.ENERGY_ABOVE_70_PLAYER_BUSY + : null; + } + } + } else { + return null; + } + } + + static void onMobAttackBlocked(ServerWorld world, MobEntity mob, ServerPlayerEntity player, NeedsOfNature.AttackBlockReason reason) { + if (world != null && mob != null && player != null && reason != null) { + if (reason == NeedsOfNature.AttackBlockReason.PREGNANCY_FRIENDLY) { + if (mob.getTarget() == player) { + mob.setTarget(null); + } + + mob.getNavigation().stop(); + if (mob instanceof Angerable angerable) { + LazyEntityReference angryAt = angerable.getAngryAt(); + UUID angryAtUuid = angryAt == null ? null : angryAt.getUuid(); + if (angryAtUuid == null || angryAtUuid.equals(player.getUuid())) { + angerable.stopAnger(); + } + } + } + + long now = world.getTime(); + RegistryKey worldKey = world.getRegistryKey(); + UUID mobUuid = mob.getUuid(); + NonAttackSystem.AttackBlockDebugState current = ATTACK_BLOCK_DEBUG.get(mobUuid); + if (current == null || !current.worldKey.equals(worldKey) || now >= current.nextTick) { + ATTACK_BLOCK_DEBUG.put(mobUuid, new NonAttackSystem.AttackBlockDebugState(worldKey, now + 400L)); + Text mobName = Text.translatable(mob.getType().getTranslationKey()); + Text playerName = player.getName(); + + Text message = switch (reason) { + case PREGNANCY_FRIENDLY -> Text.translatable("debug.needsofnature.attack_blocked_pregnancy_friendly", new Object[]{mobName, playerName}); + case GATHERING_PAIR -> Text.translatable("debug.needsofnature.attack_blocked_gathering_pair", new Object[]{mobName, playerName}); + case ENERGY_ABOVE_70_PLAYER_BUSY -> Text.translatable("debug.needsofnature.attack_blocked_energy_busy", new Object[]{mobName, playerName}); + }; + NeedsOfNature.sendDebugChatToNearby(world, mob, 24.0, NonDebugChatCategory.WARNING, message); + } + } + } + + private static boolean isAggressiveThreat(MobEntity mob, ServerPlayerEntity player) { + if (mob == null || player == null) { + return false; + } + + if (mob instanceof Monster) { + return true; + } + + if (mob instanceof Angerable angerable) { + LazyEntityReference angryAt = angerable.getAngryAt(); + UUID angryAtUuid = angryAt == null ? null : angryAt.getUuid(); + if (angryAtUuid != null && angryAtUuid.equals(player.getUuid())) { + return true; + } + } + + LivingEntity target = mob.getTarget(); + return target != null && target.getUuid().equals(player.getUuid()); + } + + static boolean hasPlayerAndHostileActor(List actors) { + if (actors != null && !actors.isEmpty()) { + boolean hasPlayer = false; + boolean hasHostile = false; + + for (Entity actor : actors) { + if (actor instanceof ServerPlayerEntity) { + hasPlayer = true; + } else if (isHostileOrAngryNeutral(actor)) { + hasHostile = true; + } + + if (hasPlayer && hasHostile) { + return true; + } + } + + return false; + } else { + return false; + } + } + + private static boolean isHostileOrAngryNeutral(Entity entity) { + if (!(entity instanceof LivingEntity)) { + return false; + } else if (entity instanceof Monster) { + return true; + } else { + return entity instanceof Angerable angerable && entity instanceof MobEntity mob + ? angerable.getAngryAt() != null || mob.getTarget() instanceof ServerPlayerEntity + : false; + } + } + + static void recordAttackOutcomeForMobs(ServerWorld world, List actorUuids, String reason) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + long nowTick = world.getTime(); + + for (UUID actorUuid : actorUuids) { + if (world.getEntity(actorUuid) instanceof MobEntity mob && mob.isAlive() && !mob.isRemoved()) { + NonAttackSystem.AttackOutcomeFailsafeState state = ATTACK_OUTCOME_FAILSAFE.computeIfAbsent( + actorUuid, unused -> new NonAttackSystem.AttackOutcomeFailsafeState(worldKey) + ); + if (!state.worldKey.equals(worldKey)) { + state.worldKey = worldKey; + state.recentAttackOutcomeTicks.clear(); + state.cooldownUntilTick = 0L; + } + + if (state.cooldownUntilTick <= nowTick) { + pruneAttackOutcomeWindow(state, nowTick); + state.recentAttackOutcomeTicks.addLast(nowTick); + if (state.recentAttackOutcomeTicks.size() >= attackOutcomeFailsafeThreshold()) { + state.recentAttackOutcomeTicks.clear(); + state.cooldownUntilTick = nowTick + 12000L; + NeedsOfNature.logRuntime( + "[NoN] Attack peak failsafe armed for mob={} in world={} reason={} untilTick={}", + mob.getUuid(), + worldKey.getValue(), + reason, + state.cooldownUntilTick + ); + } + } + } + } + } + } + + static void startNearbyPostEscapeGathers(ServerWorld world, ServerPlayerEntity player, @Nullable List escapedActorUuids) { + if (world != null && player != null && player.getEntityWorld() == world) { + if (canMobAttackPlayer(player)) { + if (player.isAlive() && !player.isRemoved()) { + if (!NonGatherSystem.isAutomaticMobPlayerStartBlockedByMount(player)) { + int maxMobs = NeedsOfNature.getConfig().getPostEscapeGatherMaxMobs(); + if (maxMobs > 0) { + Box searchBox = player.getBoundingBox().expand(10.0); + List candidates = world.getEntitiesByClass( + MobEntity.class, searchBox, mobx -> isPostEscapeGatherCandidate(world, player, mobx, escapedActorUuids) + ); + if (!candidates.isEmpty()) { + candidates.sort(Comparator.comparingDouble(player::squaredDistanceTo)); + int started = 0; + + for (MobEntity mob : candidates) { + if (started >= maxMobs) { + break; + } + + List actors = new ArrayList<>(2); + actors.add(mob); + actors.add(player); + actors.sort(Comparator.comparingInt(Entity::getId)); + NonGatherSystem.GatherCandidate gatherCandidate = NonGatherSystem.findGatherCandidate(world, actors, true, mob, player); + if (gatherCandidate != null) { + boolean gatherStarted = NonGatherSystem.startMobPlayerGather( + world, mob, player, gatherCandidate.animationId(), gatherCandidate.actorKeys(), gatherCandidate.stages() + ); + if (gatherStarted) { + started++; + NeedsOfNature.logRuntime( + "Post-escape gather started: mobType={} player={} anim={} energy={}", + Registries.ENTITY_TYPE.getId(mob.getType()), + player.getName().getString(), + gatherCandidate.animationId(), + ((EnergyHolder)mob).getEnergy() + ); + } + } + } + + if (started > 0) { + NeedsOfNature.sendDebugChat( + player, NonDebugChatCategory.INFO, Text.translatable("debug.needsofnature.post_escape_gather_started", new Object[]{started}) + ); + } + } + } + } + } + } + } + } + + private static boolean isPostEscapeGatherCandidate(ServerWorld world, ServerPlayerEntity player, MobEntity mob, @Nullable List escapedActorUuids) { + if (mob == null || mob.getEntityWorld() != world) { + return false; + } + + if (!mob.isAlive() || mob.isRemoved()) { + return false; + } + + if (!NonMobGatherEligibility.canInitiateAutoGather(mob)) { + return false; + } + + if (mob instanceof EnergyHolder holder) { + if (holder.getEnergy() < 70) { + return false; + } else if (NonMobEnergySystem.isEnergyAttackStabilized(mob) && holder.getEnergy() >= 200) { + return false; + } else if (!NonMobEnergySystem.hasGenericAnimationSupport(mob.getType(), mob.isBaby())) { + return false; + } else if (NeedsOfNature.isActorPendingOrActive(world, mob.getUuid())) { + return false; + } else if (NeedsOfNature.isMobAttackFailsafeActive(world, mob.getUuid())) { + return false; + } else if (!mob.canSee(player)) { + return false; + } else { + return escapedActorUuids != null && escapedActorUuids.contains(mob.getUuid()) ? true : mob.squaredDistanceTo(player) <= 100.0; + } + } else { + return false; + } + } + + private static int attackOutcomeFailsafeThreshold() { + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 3 : config.getAttackOutcomeFailsafeThreshold(); + } + + private static void pruneAttackOutcomeWindow(NonAttackSystem.AttackOutcomeFailsafeState state, long nowTick) { + if (state != null) { + long oldestAllowedTick = nowTick - 12000L; + + while (!state.recentAttackOutcomeTicks.isEmpty() && state.recentAttackOutcomeTicks.peekFirst() <= oldestAllowedTick) { + state.recentAttackOutcomeTicks.removeFirst(); + } + } + } + + static void pruneAttackOutcomeFailsafe(ServerWorld world, long nowTick) { + if (world != null && !ATTACK_OUTCOME_FAILSAFE.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + ATTACK_OUTCOME_FAILSAFE.entrySet().removeIf(entry -> { + NonAttackSystem.AttackOutcomeFailsafeState state = entry.getValue(); + if (!state.worldKey.equals(worldKey)) { + return false; + } + + pruneAttackOutcomeWindow(state, nowTick); + return state.cooldownUntilTick <= nowTick && state.recentAttackOutcomeTicks.isEmpty(); + }); + } + } + + static void pruneAttackBlockDebug(ServerWorld world, long nowTick) { + if (world != null && !ATTACK_BLOCK_DEBUG.isEmpty()) { + RegistryKey worldKey = world.getRegistryKey(); + ATTACK_BLOCK_DEBUG.entrySet().removeIf(entry -> { + NonAttackSystem.AttackBlockDebugState state = entry.getValue(); + return !state.worldKey.equals(worldKey) ? false : state.nextTick <= nowTick; + }); + } + } + + static void applyAttackEscapeDamage(ServerWorld world, ServerPlayerEntity player, @Nullable List actorUuids) { + applyAttackEscapeDamage(world, player, actorUuids, null); + } + + static void applyAttackEscapeDamage(ServerWorld world, ServerPlayerEntity player, @Nullable List actorUuids, @Nullable UUID preferredAttackerUuid) { + if (world != null && player != null) { + if (player.isAlive()) { + float health = player.getHealth(); + if (!(health <= 1.0F)) { + float maxRaw = health - 1.0F; + if (!(maxRaw <= 0.0F)) { + LivingEntity attacker = resolveAttackEscapeAttacker(world, player, actorUuids, preferredAttackerUuid); + float rawDamage = resolveAttackEscapeRawDamage(attacker); + rawDamage *= (float)NonTrinketsIntegration.getAccessoryEffects(player).attackEscapeDamageMultiplier(); + rawDamage = Math.min(rawDamage, maxRaw); + if (!(rawDamage <= 0.0F)) { + if (attacker instanceof ServerPlayerEntity attackingPlayer) { + NonPlayerAttackSystem.applyPlayerAttributedEscapeDamage(world, player, attackingPlayer, rawDamage); + } else { + DamageSource source = resolveAttackEscapeDamageSource(player, attacker); + player.damage(world, source, rawDamage); + } + + float after = player.getHealth(); + float dealt = health - after; + if (dealt < 1.0F && after > 1.0F) { + float extra = Math.min(1.0F - dealt, after - 1.0F); + if (extra > 0.0F) { + player.setHealth(after - extra); + } + } + } + } + } + } + } + } + + @Nullable + private static LivingEntity resolveAttackEscapeAttacker( + ServerWorld world, ServerPlayerEntity player, @Nullable List actorUuids, @Nullable UUID preferredAttackerUuid + ) { + if (world != null && player != null) { + if (preferredAttackerUuid != null && !preferredAttackerUuid.equals(player.getUuid())) { + Entity preferred = world.getEntity(preferredAttackerUuid); + if (preferred == null) { + preferred = world.getServer().getPlayerManager().getPlayer(preferredAttackerUuid); + } + + if (preferred instanceof LivingEntity living && living.isAlive() && !living.isRemoved()) { + return living; + } + } + + if (actorUuids != null && !actorUuids.isEmpty()) { + for (UUID actorUuid : actorUuids) { + if (actorUuid != null + && !actorUuid.equals(player.getUuid()) + && world.getEntity(actorUuid) instanceof LivingEntity living + && living.isAlive() + && !living.isRemoved()) { + return living; + } + } + + return null; + } else { + return null; + } + } else { + return null; + } + } + + private static float resolveAttackEscapeRawDamage(@Nullable LivingEntity attacker) { + float baseDamage = 4.0F; + if (attacker != null) { + EntityAttributeInstance attackDamage = attacker.getAttributeInstance(EntityAttributes.ATTACK_DAMAGE); + if (attackDamage != null) { + double attributeDamage = attackDamage.getValue(); + if (Double.isFinite(attributeDamage) && attributeDamage > 0.0) { + baseDamage = (float)attributeDamage; + } + } + } + + if (!Float.isFinite(baseDamage)) { + baseDamage = 4.0F; + } + + return Math.max(1.0F, baseDamage); + } + + private static DamageSource resolveAttackEscapeDamageSource(ServerPlayerEntity player, @Nullable LivingEntity attacker) { + if (player == null) { + throw new IllegalArgumentException("player"); + } else { + return attacker instanceof MobEntity mob ? player.getDamageSources().mobAttack(mob) : player.getDamageSources().generic(); + } + } + + static float resolveServerAttackEscapeStep(ServerWorld world, ServerPlayerEntity victim, boolean defeated, long contestStartTick) { + if (world != null && victim != null) { + float base = 1.0F / resolveAttackEscapeHits(victim); + float healthFactor = resolveAttackHealthFactor(victim); + float energyFactor = resolveAttackEnergyFactor(victim); + float randomScale = healthFactor >= 0.999F ? 1.0F : 0.5F + world.getRandom().nextFloat(); + if (!defeated) { + return Math.max(0.0F, base * healthFactor * energyFactor * randomScale); + } + + float ease = MathHelper.clamp((float)Math.max(0L, world.getTime() - contestStartTick) / 300.0F, 0.0F, 1.0F); + float adjustedHealth = MathHelper.lerp(ease, healthFactor, 1.0F); + float adjustedEnergy = MathHelper.lerp(ease, energyFactor, 1.0F); + float adjustedRandom = MathHelper.lerp(ease, randomScale, 1.0F); + float multiplier = MathHelper.lerp(ease, 1.0F, 4.0F); + return Math.max(0.0F, base * adjustedHealth * adjustedEnergy * adjustedRandom * multiplier); + } else { + return 0.0F; + } + } + + static NonAttackSystem.PvpMashFactors resolveServerPlayerContestStep( + ServerWorld world, ServerPlayerEntity participant, boolean defender, boolean defeated, long contestStartTick + ) { + if (world != null && participant != null) { + float base = 0.083333336F; + float healthMultiplier = resolvePvpCriticalHealthMultiplier(participant); + float heatMultiplier = resolvePvpHeatMultiplier(participant, defender); + float healthyDefenderMultiplier = defender ? resolvePvpHealthyDefenderMultiplier(participant) : 1.0F; + float defeatedEaseMultiplier = 1.0F; + if (defender && defeated) { + float ease = MathHelper.clamp((float)Math.max(0L, world.getTime() - contestStartTick) / 300.0F, 0.0F, 1.0F); + healthMultiplier = MathHelper.lerp(ease, healthMultiplier, 1.0F); + heatMultiplier = MathHelper.lerp(ease, heatMultiplier, 1.0F); + healthyDefenderMultiplier = MathHelper.lerp(ease, healthyDefenderMultiplier, 1.0F); + defeatedEaseMultiplier = MathHelper.lerp(ease, 1.0F, 4.0F); + } + + float step = base * healthMultiplier * heatMultiplier * healthyDefenderMultiplier * defeatedEaseMultiplier; + return new NonAttackSystem.PvpMashFactors(Math.max(0.0F, step), healthMultiplier, heatMultiplier, healthyDefenderMultiplier, defeatedEaseMultiplier); + } else { + return NonAttackSystem.PvpMashFactors.none(); + } + } + + private static int resolveAttackEscapeHits(ServerPlayerEntity victim) { + int hits = NeedsOfNature.getConfig().getAttackEscapeHits() + NonTrinketsIntegration.getAccessoryEffects(victim).attackEscapeHitsAdd(); + return Math.max(1, Math.min(50, hits)); + } + + private static float resolvePvpCriticalHealthMultiplier(ServerPlayerEntity player) { + float healthRatio = resolveHealthRatio(player); + if (healthRatio >= 0.25F) { + return 1.0F; + } + + float criticalStrength = NeedsOfNature.getConfig().getPvpCriticalHealthMashStrengthPercent() / 100.0F; + float progress = MathHelper.clamp(healthRatio / 0.25F, 0.0F, 1.0F); + return MathHelper.lerp(progress, criticalStrength, 1.0F); + } + + private static float resolvePvpHealthyDefenderMultiplier(ServerPlayerEntity player) { + float healthRatio = resolveHealthRatio(player); + if (healthRatio <= 0.75F) { + return 1.0F; + } + + float progress = MathHelper.clamp((healthRatio - 0.75F) / 0.25F, 0.0F, 1.0F); + float fullHealthMultiplier = 1.0F + NeedsOfNature.getConfig().getPvpFullHealthEscapeBonusPercent() / 100.0F; + return MathHelper.lerp(progress, 1.0F, fullHealthMultiplier); + } + + private static float resolvePvpHeatMultiplier(ServerPlayerEntity player, boolean defender) { + float energyRatio = resolveEnergyRatio(player); + NonConfig config = NeedsOfNature.getConfig(); + if (!defender) { + float zeroHeatStrength = config.getPvpZeroHeatAttackStrengthPercent() / 100.0F; + return MathHelper.lerp(energyRatio, zeroHeatStrength, 1.0F); + } + + if (energyRatio <= 0.6F) { + return 1.0F; + } + + float progress = MathHelper.clamp((energyRatio - 0.6F) / 0.39999998F, 0.0F, 1.0F); + float maxHeatStrength = config.getPvpMaxHeatEscapeStrengthPercent() / 100.0F; + return MathHelper.lerp(progress, 1.0F, maxHeatStrength); + } + + private static float resolveHealthRatio(ServerPlayerEntity player) { + float maxHealth = player.getMaxHealth(); + return Float.isFinite(maxHealth) && !(maxHealth <= 0.0F) ? MathHelper.clamp(player.getHealth() / maxHealth, 0.0F, 1.0F) : 0.0F; + } + + private static float resolveEnergyRatio(ServerPlayerEntity player) { + if (player instanceof EnergyHolder holder) { + int maxEnergy = Math.max(1, holder.getMaxEnergy()); + return MathHelper.clamp((float)holder.getEnergy() / maxEnergy, 0.0F, 1.0F); + } else { + return 1.0F; + } + } + + private static float resolveAttackHealthFactor(ServerPlayerEntity victim) { + float maxHealth = victim.getMaxHealth(); + float minHealth = 0.5F; + if (maxHealth <= minHealth) { + return 0.3F; + } + + float raw = MathHelper.clamp((victim.getHealth() - minHealth) / (maxHealth - minHealth), 0.0F, 1.0F); + float impact = NeedsOfNature.getConfig().getAttackEscapeDamageDifficultyPercent() / 100.0F; + return MathHelper.clamp(1.0F - (1.0F - raw) * impact, 0.3F, 1.0F); + } + + private static float resolveAttackEnergyFactor(ServerPlayerEntity victim) { + if (victim instanceof EnergyHolder holder) { + int max = Math.max(1, holder.getMaxEnergy()); + float energy = Math.max(0.0F, Math.min(max, holder.getEnergy())); + if (energy <= 100.0F) { + return 1.0F - 0.1F * energy / 100.0F; + } + + float t = Math.min(1.0F, (energy - 100.0F) / 100.0F); + return Math.max(0.08F, 0.9F - 0.82F * t * t); + } else { + return 1.0F; + } + } + + private record AttackBlockDebugState(RegistryKey worldKey, long nextTick) { + } + + private static final class AttackOutcomeFailsafeState { + RegistryKey worldKey; + final ArrayDeque recentAttackOutcomeTicks = new ArrayDeque<>(); + long cooldownUntilTick = 0L; + + AttackOutcomeFailsafeState(RegistryKey worldKey) { + this.worldKey = worldKey; + } + } + + record PvpMashFactors(float step, float healthMultiplier, float heatMultiplier, float healthyDefenderMultiplier, float defeatedEaseMultiplier) { + private static NonAttackSystem.PvpMashFactors none() { + return new NonAttackSystem.PvpMashFactors(0.0F, 0.0F, 0.0F, 0.0F, 0.0F); + } + } +} diff --git a/src/main/java/com/nonid/NonAuraSystem.java b/src/main/java/com/nonid/NonAuraSystem.java new file mode 100644 index 0000000..2f77395 --- /dev/null +++ b/src/main/java/com/nonid/NonAuraSystem.java @@ -0,0 +1,209 @@ +package com.nonid; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; + +final class NonAuraSystem { + private static final int MESS_AURA_MAX_TOTAL = 15; + private static final double MESS_AURA_MAX_MULTIPLIER = 10.0; + + private NonAuraSystem() { + } + + static void tickPlayerEnergyAura(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null && player.isAlive() && !player.isRemoved()) { + if (player instanceof EnergyHolder playerEnergyHolder) { + NonConfig config = NeedsOfNature.getConfig(); + if (config != null) { + int pulseTicks = config.getPlayerEnergyAuraPulseTicks(); + if (pulseTicks > 0 && world.getTime() % pulseTicks == 0L) { + int playerEnergy = playerEnergyHolder.getEnergy(); + int destroyedStage = config.isDestroyedSkinSystemEnabled() ? NeedsOfNature.getDestroyedSkinStageForPlayer(player) : 0; + int messTotal = config.isMessSystemEnabled() ? NeedsOfNature.getTotalMessForPlayer(player) : 0; + if (config.isMessSystemEnabled()) { + NeedsOfNature.checkBadIdeaBeaconAdvancement(player); + } + + double auraMultiplier = getCombinedPlayerAuraMultiplier(config, playerEnergy, destroyedStage, messTotal) + * NonTrinketsIntegration.getAccessoryEffects(player).playerEnergyAuraMultiplier(); + if (Double.isFinite(auraMultiplier) && !(auraMultiplier <= 1.0)) { + double radius = getCombinedPlayerAuraRadius(config, playerEnergy, destroyedStage, messTotal); + if (Double.isFinite(radius) && !(radius <= 0.0)) { + double radiusSq = radius * radius; + Box searchBox = player.getBoundingBox().expand(radius); + List mobs = world.getEntitiesByClass( + MobEntity.class, + searchBox, + mobx -> mobx.isAlive() && !mobx.isRemoved() && mobx.getEntityWorld() == world && mobx.squaredDistanceTo(player) <= radiusSq + ); + if (!mobs.isEmpty()) { + for (MobEntity mob : mobs) { + if (mob instanceof EnergyHolder holder) { + holder.ensureEnergyInitialized(world); + int currentEnergy = holder.getEnergy(); + int maxEnergy = holder.getMaxEnergy(); + if (currentEnergy >= maxEnergy) { + holder.setEnergyAuraCarry(0.0); + } else if (hasPairAnimationWithPlayer(mob, player)) { + double baseGain = getExpectedMobEnergyGainPerPulse(world, mob, holder, pulseTicks); + if (Double.isFinite(baseGain) && !(baseGain <= 0.0)) { + double bonusGain = baseGain * (auraMultiplier - 1.0); + double totalGain = holder.getEnergyAuraCarry() + bonusGain; + int rawApplied = MathHelper.floor(totalGain); + if (rawApplied <= 0) { + holder.setEnergyAuraCarry(totalGain); + } else { + int appliedGain = Math.min(rawApplied, maxEnergy - currentEnergy); + if (appliedGain <= 0) { + holder.setEnergyAuraCarry(0.0); + } else { + holder.setEnergy(currentEnergy + appliedGain); + holder.setEnergyAuraCarry(appliedGain < rawApplied ? 0.0 : totalGain - appliedGain); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + static NonAuraSystem.PlayerEnergyAuraDebug getPlayerEnergyAuraDebug(ServerWorld world, MobEntity mob, EnergyHolder holder) { + if (world != null && mob != null && holder != null) { + NonConfig config = NeedsOfNature.getConfig(); + if (config == null) { + return null; + } + + NonAuraSystem.PlayerEnergyAuraDebug best = null; + + for (ServerPlayerEntity player : world.getPlayers()) { + if (player != null && player.isAlive() && !player.isRemoved() && player instanceof EnergyHolder playerEnergyHolder) { + int playerEnergy = playerEnergyHolder.getEnergy(); + int destroyedStage = config.isDestroyedSkinSystemEnabled() ? NeedsOfNature.getDestroyedSkinStageForPlayer(player) : 0; + int messTotal = config.isMessSystemEnabled() ? NeedsOfNature.getTotalMessForPlayer(player) : 0; + double radius = getCombinedPlayerAuraRadius(config, playerEnergy, destroyedStage, messTotal); + if (Double.isFinite(radius) && !(radius <= 0.0) && !(mob.squaredDistanceTo(player) > radius * radius) && hasPairAnimationWithPlayer(mob, player) + ) + { + double totalMultiplier = getCombinedPlayerAuraMultiplier(config, playerEnergy, destroyedStage, messTotal) + * NonTrinketsIntegration.getAccessoryEffects(player).playerEnergyAuraMultiplier(); + if (Double.isFinite(totalMultiplier) && !(totalMultiplier <= 1.0) && (best == null || totalMultiplier > best.totalMultiplier())) { + best = new NonAuraSystem.PlayerEnergyAuraDebug(player, totalMultiplier, playerEnergy, destroyedStage, messTotal, radius); + } + } + } + } + + return best; + } else { + return null; + } + } + + private static double getCombinedPlayerAuraMultiplier(NonConfig config, int playerEnergy, int destroyedStage, int messTotal) { + if (config == null) { + return 1.0; + } + + double energyBonus = Math.max(0.0, getPlayerEnergyAuraMultiplier(config, playerEnergy) - 1.0); + double destroyedBonus = Math.max(0.0, getDestroyedSkinAuraMultiplier(config, destroyedStage) - 1.0); + double messBonus = Math.max(0.0, getMessAuraMultiplier(messTotal) - 1.0); + return 1.0 + energyBonus + destroyedBonus + messBonus; + } + + private static double getPlayerEnergyAuraMultiplier(NonConfig config, int playerEnergy) { + if (config == null || playerEnergy < 100) { + return 1.0; + } else if (playerEnergy >= 200) { + return config.getPlayerEnergyAuraMultHigh(); + } else { + return playerEnergy >= 150 ? config.getPlayerEnergyAuraMultMid() : config.getPlayerEnergyAuraMultLow(); + } + } + + private static double getDestroyedSkinAuraMultiplier(NonConfig config, int stage) { + if (config == null) { + return 1.0; + } + + return switch (stage) { + case 1 -> config.getDestroyedSkinAuraMultStage1(); + case 2 -> config.getDestroyedSkinAuraMultStage2(); + case 3 -> config.getDestroyedSkinAuraMultStage3(); + case 4 -> config.getDestroyedSkinAuraMultStage4(); + default -> 1.0; + }; + } + + private static double getMessAuraMultiplier(int messTotal) { + if (messTotal <= 0) { + return 1.0; + } + + double progress = MathHelper.clamp(messTotal / 15.0, 0.0, 1.0); + return MathHelper.lerp(progress, 1.0, 10.0); + } + + private static double getPlayerEnergyAuraRadius(NonConfig config, int playerEnergy) { + if (config != null && playerEnergy >= 100) { + double minRadius = config.getPlayerEnergyAuraRadiusMin(); + double maxRadius = config.getPlayerEnergyAuraRadiusMax(); + double progress = MathHelper.clamp((playerEnergy - 100) / 100.0, 0.0, 1.0); + return MathHelper.lerp(progress, minRadius, maxRadius); + } else { + return 0.0; + } + } + + private static double getCombinedPlayerAuraRadius(NonConfig config, int playerEnergy, int destroyedStage, int messTotal) { + if (config == null) { + return 0.0; + } else if (playerEnergy >= 100) { + return getPlayerEnergyAuraRadius(config, playerEnergy); + } else { + return destroyedStage <= 0 && messTotal <= 0 ? 0.0 : config.getPlayerEnergyAuraRadiusMin(); + } + } + + private static boolean hasPairAnimationWithPlayer(MobEntity mob, ServerPlayerEntity player) { + if (mob != null && player != null) { + List pair = new ArrayList<>(2); + pair.add(mob); + pair.add(player); + pair.sort(Comparator.comparingInt(Entity::getId)); + return NonAnimationMatcher.findAutomatic(player.getEntityWorld(), pair, Set.of(), null, false, null, Set.of()) != null; + } else { + return false; + } + } + + private static double getExpectedMobEnergyGainPerPulse(ServerWorld world, MobEntity mob, EnergyHolder holder, int pulseTicks) { + if (world != null && mob != null && holder != null && pulseTicks > 0) { + double nearAnimationMultiplier = NeedsOfNature.getNearbyVisibleAnimationGainMultiplier(world, mob); + double scale = NonMobEnergySystem.passiveGainScale( + mob, holder.getEnergyGainMultiplier(), holder.getEnergyGainDrift(), nearAnimationMultiplier > 1.0, nearAnimationMultiplier + ); + return scale <= 0.0 ? 0.0 : pulseTicks * scale / 120.0; + } else { + return 0.0; + } + } + + record PlayerEnergyAuraDebug(ServerPlayerEntity player, double totalMultiplier, int playerEnergy, int destroyedSkinStage, int messTotal, double radius) { + } +} diff --git a/src/main/java/com/nonid/NonCommands.java b/src/main/java/com/nonid/NonCommands.java new file mode 100644 index 0000000..cb7cf11 --- /dev/null +++ b/src/main/java/com/nonid/NonCommands.java @@ -0,0 +1,714 @@ +package com.nonid; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.nonid.network.SkinRippedInfoRequestS2CPayload; +import java.util.List; +import java.util.Locale; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.registry.Registries; +import net.minecraft.server.PlayerConfigEntry; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +final class NonCommands { + private NonCommands() { + } + + static void register() { + CommandRegistrationCallback.EVENT + .register( + (CommandRegistrationCallback)(dispatcher, registryAccess, environment) -> dispatcher.register( + (LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal( + "needsofnature" + ) + .requires(NonCommands::isOpCommandSource)) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("getplayerinfo").executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature getplayerinfo ")); + return 0; + } else { + return sendPlayerInfo((ServerCommandSource)ctx.getSource(), self); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes(ctx -> sendPlayerInfo((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))) + ) + )) + .then(CommandManager.literal("resetmobcooldowns").executes(ctx -> resetMobCooldowns((ServerCommandSource)ctx.getSource())))) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("skin").then(messCommand())) + .then( + ((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("ripped") + .then(CommandManager.literal("info").executes(ctx -> requestRippedSkinInfo((ServerCommandSource)ctx.getSource())))) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("reset").executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature skin ripped reset ")); + return 0; + } else { + return resetDestroyedSkinCommand((ServerCommandSource)ctx.getSource(), self); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> resetDestroyedSkinCommand( + (ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player") + ) + ) + ) + )) + .then( + CommandManager.literal("set") + .then( + ((RequiredArgumentBuilder)CommandManager.argument("stage", IntegerArgumentType.integer(0, 4)).executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature skin ripped set ")); + return 0; + } else { + return setDestroyedSkinStageCommand( + (ServerCommandSource)ctx.getSource(), self, IntegerArgumentType.getInteger(ctx, "stage") + ); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> setDestroyedSkinStageCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + IntegerArgumentType.getInteger(ctx, "stage") + ) + ) + ) + ) + ) + ) + )) + .then( + ((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("gender") + .requires(NonCommands::isOpCommandSource)) + .then( + CommandManager.literal("get") + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> getGenderCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")) + ) + ) + )) + .then( + CommandManager.literal("set") + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .then( + CommandManager.argument("gender", StringArgumentType.word()) + .executes( + ctx -> setGenderCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "gender") + ) + ) + ) + ) + )) + .then( + CommandManager.literal("reset") + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes(ctx -> resetGenderCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))) + ) + ) + )) + .then( + ((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("pregnancy") + .then( + CommandManager.literal("cycle") + .then( + CommandManager.literal("set") + .then( + ((RequiredArgumentBuilder)CommandManager.argument("day", IntegerArgumentType.integer(1, 30)) + .executes( + ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError( + Text.literal("控制台用法: /needsofnature pregnancy cycle set ") + ); + return 0; + } else { + return setPregnancyCycleDayCommand( + (ServerCommandSource)ctx.getSource(), self, IntegerArgumentType.getInteger(ctx, "day") + ); + } + } + )) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> setPregnancyCycleDayCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + IntegerArgumentType.getInteger(ctx, "day") + ) + ) + ) + ) + ) + )) + .then( + CommandManager.literal("set") + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .then( + ((RequiredArgumentBuilder)CommandManager.argument("entity", StringArgumentType.string()) + .executes( + ctx -> setPregnancyCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "entity"), + NeedsOfNature.getConfig().getPregnancyDurationMinutes(), + 0 + ) + )) + .then( + ((RequiredArgumentBuilder)CommandManager.argument("minutes", IntegerArgumentType.integer(1, 5256000)) + .executes( + ctx -> setPregnancyCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "entity"), + IntegerArgumentType.getInteger(ctx, "minutes"), + 0 + ) + )) + .then( + CommandManager.argument("spawns", IntegerArgumentType.integer(1, 16)) + .executes( + ctx -> setPregnancyCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "entity"), + IntegerArgumentType.getInteger(ctx, "minutes"), + IntegerArgumentType.getInteger(ctx, "spawns") + ) + ) + ) + ) + ) + ) + )) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("clear").executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature pregnancy clear ")); + return 0; + } else { + return clearPregnancyCommand((ServerCommandSource)ctx.getSource(), self); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> clearPregnancyCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")) + ) + ) + )) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("complete").executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature pregnancy complete ")); + return 0; + } else { + return completePregnancyCommand((ServerCommandSource)ctx.getSource(), self); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> completePregnancyCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player")) + ) + ) + )) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("get").executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()).sendError(Text.literal("控制台用法: /needsofnature pregnancy get ")); + return 0; + } else { + return sendPregnantEntity((ServerCommandSource)ctx.getSource(), self); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes(ctx -> sendPregnantEntity((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))) + ) + ) + ) + ) + ); + } + + private static LiteralArgumentBuilder messCommand() { + return (LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("mess") + .requires(NonCommands::isOpCommandSource)) + .then( + ((LiteralArgumentBuilder)CommandManager.literal("clean").executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()).sendError(Text.literal("控制台用法: /needsofnature skin mess clean ")); + return 0; + } else { + return cleanMessCommand((ServerCommandSource)ctx.getSource(), self); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes(ctx -> cleanMessCommand((ServerCommandSource)ctx.getSource(), EntityArgumentType.getPlayer(ctx, "player"))) + ) + )) + .then( + ((LiteralArgumentBuilder)((LiteralArgumentBuilder)((LiteralArgumentBuilder)CommandManager.literal("set") + .then( + CommandManager.argument("v_value", IntegerArgumentType.integer(0, 10)) + .then( + CommandManager.argument("a_value", IntegerArgumentType.integer(0, 10)) + .then( + ((RequiredArgumentBuilder)CommandManager.argument("m_value", IntegerArgumentType.integer(0, 10)) + .executes( + ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature skin mess set ")); + return 0; + } else { + return setMessCommand( + (ServerCommandSource)ctx.getSource(), + self, + IntegerArgumentType.getInteger(ctx, "v_value"), + IntegerArgumentType.getInteger(ctx, "a_value"), + IntegerArgumentType.getInteger(ctx, "m_value") + ); + } + } + )) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> setMessCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + IntegerArgumentType.getInteger(ctx, "v_value"), + IntegerArgumentType.getInteger(ctx, "a_value"), + IntegerArgumentType.getInteger(ctx, "m_value") + ) + ) + ) + ) + ) + )) + .then(messSlotSetCommand("v"))) + .then(messSlotSetCommand("a"))) + .then(messSlotSetCommand("m")) + ); + } + + private static LiteralArgumentBuilder messSlotSetCommand(String slot) { + return (LiteralArgumentBuilder)CommandManager.literal(slot) + .then( + ((RequiredArgumentBuilder)CommandManager.argument("value", IntegerArgumentType.integer(0, 10)).executes(ctx -> { + ServerPlayerEntity self = ((ServerCommandSource)ctx.getSource()).getPlayer(); + if (self == null) { + ((ServerCommandSource)ctx.getSource()) + .sendError(Text.literal("控制台用法: /needsofnature skin mess set " + slot + " ")); + return 0; + } else { + return setMessSlotCommand((ServerCommandSource)ctx.getSource(), self, slot, IntegerArgumentType.getInteger(ctx, "value")); + } + })) + .then( + CommandManager.argument("player", EntityArgumentType.player()) + .executes( + ctx -> setMessSlotCommand( + (ServerCommandSource)ctx.getSource(), + EntityArgumentType.getPlayer(ctx, "player"), + slot, + IntegerArgumentType.getInteger(ctx, "value") + ) + ) + ) + ); + } + + private static int sendPlayerInfo(ServerCommandSource source, ServerPlayerEntity target) { + int result = sendPlayerEnergy(source, target); + result += sendPlayerCooldowns(source, target); + result += sendPlayerContestInfo(source, target); + source.sendFeedback(() -> Text.literal(NonPregnancyConceptionSystem.describeCycle(target)), false); + return result + 1; + } + + private static int sendPlayerContestInfo(ServerCommandSource source, ServerPlayerEntity target) { + List lines = NonPlayerAttackSystem.getContestDebugLines(target); + + for (String line : lines) { + source.sendFeedback(() -> Text.literal(line), false); + } + + return lines.size(); + } + + private static int sendPlayerEnergy(ServerCommandSource source, ServerPlayerEntity target) { + if (target instanceof EnergyHolder holder) { + int current = holder.getEnergy(); + int max = holder.getMaxEnergy(); + source.sendFeedback(() -> Text.literal(target.getName().getString() + " energy: " + current + "/" + max), false); + return current; + } else { + source.sendError(Text.literal("目标没有能量数据。")); + return 0; + } + } + + private static int sendPlayerCooldowns(ServerCommandSource source, ServerPlayerEntity target) { + if (!(target.getEntityWorld() instanceof ServerWorld world)) { + source.sendError(Text.literal("目标不在服务器世界中。")); + return 0; + } else { + List cooldowns = NeedsOfNature.getActivePlayerCooldownDebugLines(world, target); + if (cooldowns.isEmpty()) { + source.sendFeedback(() -> Text.literal(target.getName().getString() + " cooldowns: none"), false); + return 0; + } + + source.sendFeedback(() -> Text.literal(target.getName().getString() + " cooldowns:"), false); + + for (String line : cooldowns) { + source.sendFeedback(() -> Text.literal("- " + line), false); + } + + return cooldowns.size(); + } + } + + private static int requestRippedSkinInfo(ServerCommandSource source) { + ServerPlayerEntity player = source.getPlayer(); + if (player == null) { + source.sendError(Text.literal("控制台用法: /needsofnature skin ripped info as a player")); + return 0; + } else { + ServerPlayNetworking.send(player, new SkinRippedInfoRequestS2CPayload()); + source.sendFeedback(() -> Text.literal("已请求输出本地撕裂皮肤信息。"), false); + return 1; + } + } + + private static boolean isOpCommandSource(ServerCommandSource source) { + if (source == null) { + return false; + } + + ServerPlayerEntity player = source.getPlayer(); + return player == null ? true : source.getServer().getPlayerManager().isOperator(new PlayerConfigEntry(player.getGameProfile())); + } + + private static int resetMobCooldowns(ServerCommandSource source) { + int failsafeEntries = NeedsOfNature.clearAttackOutcomeFailsafeEntries(); + int mobsReset = 0; + + for (ServerWorld world : source.getServer().getWorlds()) { + for (Entity entity : world.iterateEntities()) { + if (entity instanceof MobEntity mob && mob instanceof EnergyHolder holder) { + holder.onNonAnimationStarted(); + mobsReset++; + } + } + } + + int total = failsafeEntries + mobsReset; + String feedback = "Reset mob cooldowns. Failsafe entries: " + failsafeEntries + ", mob partner cooldown sets: " + mobsReset + "."; + source.sendFeedback(() -> Text.literal(feedback), true); + return total; + } + + private static int cleanMessCommand(ServerCommandSource source, ServerPlayerEntity target) { + NonMessSystem.clearMess(target); + source.sendFeedback(() -> Text.literal(target.getName().getString() + " mess cleared."), true); + return 1; + } + + private static int setMessCommand(ServerCommandSource source, ServerPlayerEntity target, int v, int a, int m) { + if (!NeedsOfNature.isMessSystemEnabled()) { + source.sendError(Text.literal("污渍系统已禁用。")); + return 0; + } else if (target instanceof MessHolder holder) { + holder.setVMess(NonMessSystem.clampMess(v)); + holder.setAMess(NonMessSystem.clampMess(a)); + holder.setMMess(NonMessSystem.clampMess(m)); + NonMessSystem.broadcastMessState(target); + maybeGrantMessedUp(target, holder); + sendMessSetFeedback(source, target, holder); + return 1; + } else { + source.sendError(Text.literal("目标没有污渍数据。")); + return 0; + } + } + + private static int setMessSlotCommand(ServerCommandSource source, ServerPlayerEntity target, String slot, int value) { + if (!NeedsOfNature.isMessSystemEnabled()) { + source.sendError(Text.literal("污渍系统已禁用。")); + return 0; + } + + if (target instanceof MessHolder holder) { + int clamped = NonMessSystem.clampMess(value); + switch (slot) { + case "v": + holder.setVMess(clamped); + break; + case "a": + holder.setAMess(clamped); + break; + case "m": + holder.setMMess(clamped); + break; + default: + source.sendError(Text.literal("未知污渍槽位 '" + slot + "'. Use v, a, or m.")); + return 0; + } + + NonMessSystem.broadcastMessState(target); + maybeGrantMessedUp(target, holder); + sendMessSetFeedback(source, target, holder); + return 1; + } else { + source.sendError(Text.literal("目标没有污渍数据。")); + return 0; + } + } + + private static void sendMessSetFeedback(ServerCommandSource source, ServerPlayerEntity target, MessHolder holder) { + source.sendFeedback( + () -> Text.literal( + target.getName().getString() + + " mess set to V=" + + NonMessSystem.clampMess(holder.getVMess()) + + ", A=" + + NonMessSystem.clampMess(holder.getAMess()) + + ", M=" + + NonMessSystem.clampMess(holder.getMMess()) + + "." + ), + true + ); + } + + private static void maybeGrantMessedUp(ServerPlayerEntity target, MessHolder holder) { + if (NonMessSystem.clampMess(holder.getVMess()) >= 10 + && NonMessSystem.clampMess(holder.getAMess()) >= 10 + && NonMessSystem.clampMess(holder.getMMess()) >= 10) { + NonAdvancementHooks.grantMessedUp(target); + } + } + + private static int getGenderCommand(ServerCommandSource source, ServerPlayerEntity target) { + source.sendFeedback(() -> Text.literal(target.getName().getString() + " gender: " + genderName(currentGenderMask(target))), false); + return 1; + } + + private static int setGenderCommand(ServerCommandSource source, ServerPlayerEntity target, String genderRaw) { + int mask = parseGenderMask(genderRaw); + if (mask == 0) { + source.sendError(Text.literal("未知性别 '" + genderRaw + "'. Use male, female, or both.")); + return 0; + } else { + int applied = NonGenderSystem.setPlayerGenderFromCommand(target, mask); + source.sendFeedback(() -> Text.literal(target.getName().getString() + " gender set to " + genderName(applied) + "."), true); + return 1; + } + } + + private static int resetGenderCommand(ServerCommandSource source, ServerPlayerEntity target) { + NonGenderSystem.resetPlayerGenderSelection(target); + source.sendFeedback(() -> Text.literal(target.getName().getString() + " gender selection reset."), true); + return 1; + } + + private static int currentGenderMask(ServerPlayerEntity target) { + return target instanceof GenderHolder holder ? holder.getGenderMask() & 3 : 0; + } + + private static int parseGenderMask(String raw) { + if (raw == null) { + return 0; + } + + return switch (raw.trim().toLowerCase(Locale.ROOT)) { + case "m", "male" -> 1; + case "f", "female" -> 2; + case "b", "both", "male+female", "female+male" -> 3; + default -> 0; + }; + } + + private static String genderName(int mask) { + return switch (mask & 3) { + case 1 -> "male"; + case 2 -> "female"; + case 3 -> "male+female"; + default -> "none"; + }; + } + + private static int setDestroyedSkinStageCommand(ServerCommandSource source, ServerPlayerEntity target, int stage) { + if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + source.sendError(Text.literal("撕裂皮肤系统已禁用。")); + return 0; + } + + int clamped = Math.max(0, Math.min(4, stage)); + if (target instanceof DestroyedSkinHolder holder) { + holder.setDestroyedSkinDamage(NonDestroyedSkinSystem.destroyedSkinDamageForStage(clamped)); + } + + NonDestroyedSkinSystem.setDestroyedSkinStage(target, clamped); + source.sendFeedback( + () -> Text.literal( + target.getName().getString() + + " ripped skin stage set to " + + clamped + + " (damage " + + NonDestroyedSkinSystem.destroyedSkinDamageForStage(clamped) + + "/10)." + ), + true + ); + return clamped; + } + + private static int resetDestroyedSkinCommand(ServerCommandSource source, ServerPlayerEntity target) { + if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + source.sendError(Text.literal("撕裂皮肤系统已禁用。")); + return 0; + } else { + NonDestroyedSkinSystem.resetDestroyedSkinDamage(target); + source.sendFeedback(() -> Text.literal(target.getName().getString() + " ripped skin damage reset."), true); + return 1; + } + } + + private static int sendPregnantEntity(ServerCommandSource source, ServerPlayerEntity target) { + Identifier pregnantEntity = NeedsOfNature.getPregnantEntityTypeId(target); + if (pregnantEntity == null) { + source.sendFeedback(() -> Text.literal(target.getName().getString() + " is not pregnant."), false); + return 0; + } else { + source.sendFeedback(() -> Text.literal(target.getName().getString() + " pregnant entity: " + pregnantEntity), false); + return 1; + } + } + + private static int setPregnancyCycleDayCommand(ServerCommandSource source, ServerPlayerEntity target, int day) { + if (!NonPregnancyConceptionSystem.setCycleDay(target, day)) { + source.sendError(Text.literal("目标没有孕期周期数据。")); + return 0; + } else { + source.sendFeedback(() -> Text.literal(target.getName().getString() + " pregnancy cycle set to day " + day + "/30."), true); + return 1; + } + } + + private static int setPregnancyCommand(ServerCommandSource source, ServerPlayerEntity target, String entityIdRaw, int minutes, int offspringCountOverride) { + if (NonGenderSystem.isOnlyMale(target)) { + source.sendError(Text.literal("无法为纯男性玩家分娩。")); + return 0; + } + + Identifier entityId = Identifier.tryParse(entityIdRaw); + if (entityId != null && Registries.ENTITY_TYPE.containsId(entityId)) { + if (target.getEntityWorld() instanceof ServerWorld world) { + long var13 = NeedsOfNature.getGlobalTick(world); + long durationTicks = Math.max(1L, Math.min(5256000L, minutes)) * 1200L; + NeedsOfNature.beginPregnancy(target, entityId, var13 + durationTicks, null, offspringCountOverride); + int resolvedDisplaySpawnCount = ((PregnancyHolder)target).getPregnancyOffspringCount(); + if (resolvedDisplaySpawnCount <= 0) { + resolvedDisplaySpawnCount = offspringCountOverride > 0 ? Math.max(1, Math.min(16, offspringCountOverride)) : 1; + } + + int displaySpawnCount = resolvedDisplaySpawnCount; + source.sendFeedback( + () -> Text.literal( + "Set pregnancy for " + + target.getName().getString() + + " to " + + entityId + + " (" + + minutes + + " minutes, " + + displaySpawnCount + + " spawn" + + (displaySpawnCount == 1 ? "" : "s") + + ")." + ), + true + ); + return 1; + } else { + source.sendError(Text.literal("目标不在服务器世界中。")); + return 0; + } + } else { + source.sendError(Text.literal("未知实体 ID: " + entityIdRaw)); + return 0; + } + } + + private static int clearPregnancyCommand(ServerCommandSource source, ServerPlayerEntity target) { + NeedsOfNature.clearPregnancyState(target, true); + source.sendFeedback(() -> Text.literal("已清除 " + target.getName().getString() + "."), true); + return 1; + } + + private static int completePregnancyCommand(ServerCommandSource source, ServerPlayerEntity target) { + Identifier pregnantEntity = NeedsOfNature.getPregnantEntityTypeId(target); + if (pregnantEntity == null) { + source.sendError(Text.literal(target.getName().getString() + " is not pregnant.")); + return 0; + } else if (target.getEntityWorld() instanceof ServerWorld world) { + NeedsOfNature.markPregnancyPending(target, NeedsOfNature.getGlobalTick(world)); + source.sendFeedback(() -> Text.literal("已标记 " + target.getName().getString() + "."), true); + return 1; + } else { + source.sendError(Text.literal("目标不在服务器世界中。")); + return 0; + } + } +} diff --git a/src/main/java/com/nonid/NonConfig.java b/src/main/java/com/nonid/NonConfig.java new file mode 100644 index 0000000..c61ae5a --- /dev/null +++ b/src/main/java/com/nonid/NonConfig.java @@ -0,0 +1,2783 @@ +package com.nonid; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.nonid.client.NonLiquidColors; +import com.nonid.data.NonEntityProfiles; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.util.Identifier; + +public final class NonConfig { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("needsofnature.json"); + private static final int PREGNANCY_DURATION_MINUTES_MIN = 1; + private static final int PREGNANCY_DURATION_MINUTES_MAX = 5256000; + public static final int DEFAULT_GENDER_MALE_CHANCE_PERCENT = 48; + public static final int DEFAULT_GENDER_FEMALE_CHANCE_PERCENT = 47; + public static final int DEFAULT_GENDER_BOTH_CHANCE_PERCENT = 5; + public static final int DEFAULT_LIQUID_TANK_CAPACITY_ML = 200; + public static final int LIQUID_TANK_CAPACITY_MIN_ML = 1; + public static final int LIQUID_TANK_CAPACITY_MAX_ML = 10000; + public static final int DEFAULT_ATTACK_OUTCOME_FAILSAFE_THRESHOLD = 3; + public static final int DEFAULT_ACTION_SOUND_VOLUME_PERCENT = 100; + public static final double DEFAULT_PEAK_XP_PER_ML = 0.06666666666666667; + public static final int DEFAULT_NORMAL_DAMAGE_DESTROYED_SKIN_CHANCE_PERCENT = 10; + public static final int DEFAULT_LAST_DEFEATED_ENERGY_THRESHOLD = 70; + public static final int DEFAULT_LAST_DEFEATED_SEARCH_RADIUS = 6; + public static final int DEFAULT_LAST_DEFEATED_COOLDOWN_SECONDS = 120; + public static final int DEFAULT_ATTACK_ESCAPE_INVULNERABILITY_SECONDS = 3; + public static final int DEFAULT_ATTACK_ESCAPE_ANIMATION_PROTECTION_SECONDS = 3; + public static final int ATTACK_ESCAPE_PROTECTION_SECONDS_MAX = 10; + public static final int DEFAULT_ATTACK_ESCAPE_DAMAGE_DIFFICULTY_PERCENT = 100; + public static final int ATTACK_ESCAPE_DAMAGE_DIFFICULTY_PERCENT_MAX = 200; + public static final int DEFAULT_PVP_FULL_HEALTH_ESCAPE_BONUS_PERCENT = 30; + public static final int DEFAULT_PVP_CRITICAL_HEALTH_MASH_STRENGTH_PERCENT = 25; + public static final int DEFAULT_PVP_MAX_HEAT_ESCAPE_STRENGTH_PERCENT = 70; + public static final int DEFAULT_PVP_ZERO_HEAT_ATTACK_STRENGTH_PERCENT = 70; + public static final int PVP_MASH_PERCENT_MAX = 200; + public static final int DEFAULT_POST_ESCAPE_GATHER_MAX_MOBS = 8; + public static final int DEFAULT_ALLOWED_PLAYER_INTERACTION_PROFILES_MASK = 15; + public static final int DEFAULT_PLAYER_REQUEST_MIN_ENERGY = 70; + public static final boolean DEFAULT_BLOCK_ANIMATIONS_WHILE_RIDING_LIVING_ENTITIES = true; + public static final boolean DEFAULT_BLOCK_ANIMATIONS_WHILE_RIDING_VEHICLES = false; + public static final int PREGNANCY_OFFSPRING_COUNT_MIN = 1; + public static final int PREGNANCY_OFFSPRING_COUNT_MAX = 16; + public static final float ENTITY_ENERGY_GAIN_MULTIPLIER_MIN = 0.1F; + public static final float ENTITY_ENERGY_GAIN_MULTIPLIER_MAX = 10.0F; + public static final int PREGNANCY_CYCLE_DAYS = 30; + public static final boolean DEFAULT_LIQUID_BASED_PREGNANCY = false; + public static final int DEFAULT_PREGNANCY_RISKY_START_DAY = 13; + public static final int DEFAULT_PREGNANCY_OVULATION_END_DAY = 17; + public static final int DEFAULT_PREGNANCY_SAFE_DAY_CHANCE_PERCENT = 10; + public static final int DEFAULT_PREGNANCY_RISKY_CHANCE_PERCENT = 300; + public static final int DEFAULT_PREGNANCY_OVULATION_CHANCE_PERCENT = 500; + public static final int PREGNANCY_CYCLE_CHANCE_PERCENT_MAX = 1000; + public static final float DEFAULT_PREGNANCY_EGG_START_SIZE = 0.5F; + public static final float DEFAULT_PREGNANCY_EGG_END_SIZE = 1.0F; + public static final float DEFAULT_PREGNANCY_EGG_HEALTH = 2.0F; + public static final float PREGNANCY_EGG_SIZE_MIN = 0.05F; + public static final float PREGNANCY_EGG_SIZE_MAX = 4.0F; + public static final float PREGNANCY_EGG_HEALTH_MIN = 0.5F; + public static final float PREGNANCY_EGG_HEALTH_MAX = 100.0F; + public static final String PREGNANCY_BIRTH_MODE_EGG = "egg"; + public static final String PREGNANCY_BIRTH_MODE_DIRECT = "direct"; + public static final String DEFAULT_DESTROYED_SKIN_TINT_HEX = ""; + public static final String DEFAULT_INTIFACE_SERVER_URL = "ws://127.0.0.1:12345"; + public static final int DEFAULT_INTIFACE_REACTIVE_IMPACT_STRENGTH_PERCENT = 60; + public static final int DEFAULT_INTIFACE_REACTIVE_IMPACT_DURATION_MS = 170; + public static final int DEFAULT_INTIFACE_PEAK_REACTIVE_IMPACT_STRENGTH_PERCENT = 20; + public static final int DEFAULT_INTIFACE_PEAK_REACTIVE_IMPACT_DURATION_MS = 500; + public static final int DEFAULT_INTIFACE_COOLDOWN_MS = 80; + public static final int DEFAULT_INTIFACE_LATENCY_COMPENSATION_MS = 0; + public static final int DEFAULT_INTIFACE_ANIMATION_BASELINE_STRENGTH_PERCENT = 1; + public static final int DEFAULT_INTIFACE_OSCILLATOR_REGULAR_SPEED_PERCENT = 100; + public static final int DEFAULT_INTIFACE_OSCILLATOR_PEAK_SPEED_PERCENT = 50; + public static final int DEFAULT_INTIFACE_STROKER_MIN_DISTANCE_PERCENT = 0; + public static final int DEFAULT_INTIFACE_STROKER_MAX_DISTANCE_PERCENT = 100; + public static final int DEFAULT_INTIFACE_STROKER_REGULAR_MOVE_DURATION_MS = 500; + public static final int DEFAULT_INTIFACE_STROKER_PEAK_MOVE_DURATION_MS = 300; + public static final int DEFAULT_INTIFACE_ENERGIZED_ONE_BASE_PERCENT = 1; + public static final int DEFAULT_INTIFACE_ENERGIZED_ONE_PULSE_PERCENT = 5; + public static final int DEFAULT_INTIFACE_ENERGIZED_TWO_BASE_PERCENT = 5; + public static final int DEFAULT_INTIFACE_ENERGIZED_TWO_PULSE_PERCENT = 10; + public static final int DEFAULT_INTIFACE_ENERGIZED_THREE_BASE_PERCENT = 10; + public static final int DEFAULT_INTIFACE_ENERGIZED_THREE_PULSE_PERCENT = 20; + public static final float FEMALE_GENDER_BREAST_SIZE_MAX = 1.6F; + public static final float FEMALE_GENDER_BREAST_HEIGHT_MIN = -2.0F; + private int initialEnergyMax = 90; + private double energyGainRate = 1.0; + private double nearAnimationEnergyGainMult = 3.0; + private double playerEnergyAuraMultLow = 2.0; + private double playerEnergyAuraMultMid = 5.0; + private double playerEnergyAuraMultHigh = 10.0; + private double destroyedSkinAuraMultStage1 = 2.0; + private double destroyedSkinAuraMultStage2 = 5.0; + private double destroyedSkinAuraMultStage3 = 20.0; + private double destroyedSkinAuraMultStage4 = 50.0; + private int normalDamageDestroyedSkinChancePercent = 10; + private boolean lastDefeatedEnabled = true; + private int lastDefeatedEnergyThreshold = 70; + private int lastDefeatedSearchRadius = 6; + private int lastDefeatedCooldownSeconds = 120; + private int playerEnergyAuraRadiusMin = 6; + private int playerEnergyAuraRadiusMax = 20; + private int playerEnergyAuraPulseTicks = 20; + private String debugChatMode = null; + private boolean forceVanillaEntityTextures = false; + private int animationBlockSearchRadius = 3; + private boolean autoSwitchThirdPersonOnAnimationStart = true; + private int scanBudgetPerTick = 32; + private int loopProgressSeconds = 15; + private int peakLoopProgressSeconds = 7; + private int genderMaleChancePercent = 48; + private int genderFemaleChancePercent = 47; + private int genderBothChancePercent = 5; + private int maleMaleChancePercent = 20; + private int femaleFemaleChancePercent = 20; + private int entityEntityChancePercent = 20; + private int multiActorJoinChancePercent = 75; + private String playerGender = NonConfig.PlayerGenderSelection.FEMALE.id(); + private boolean allowPlayerGenderChangeAnytime = true; + private boolean requirePlayerGenderSelectionOnJoin = false; + private int allowedStartingGenderMask = 7; + private int allowedPlayerInteractionProfilesMask = 15; + private int playerRequestMinEnergy = 70; + private boolean pregnancyEnabled = true; + private boolean liquidTankEnabled = true; + private boolean messSystemEnabled = true; + private boolean destroyedSkinSystemEnabled = true; + private boolean allowCraftingTableSkinRepair = false; + private boolean keepMessAfterDeath = false; + private boolean keepRippedSkinAfterDeath = false; + private boolean keepLiquidTankAfterDeath = false; + private boolean keepPregnancyAfterDeath = false; + private boolean armorShedEffectEnabled = true; + private boolean syncFemaleGenderModWithNonGender = true; + private boolean destroyedSkinFemaleGenderOverridesEnabled = true; + private NonConfig.FemaleGenderDestroyedProfile femaleGenderDestroyedLow = NonConfig.FemaleGenderDestroyedProfile.lowDefault(); + private NonConfig.FemaleGenderDestroyedProfile femaleGenderDestroyedHigh = NonConfig.FemaleGenderDestroyedProfile.highDefault(); + private int actionSoundVolumePercent = 100; + private boolean intifaceEnabled = false; + private boolean intifaceAutoConnectOnWorldJoin = true; + private boolean intifaceStopOnDisconnect = true; + private String intifaceServerUrl = "ws://127.0.0.1:12345"; + private int intifaceReactiveImpactStrengthPercent = 60; + private int intifaceReactiveImpactDurationMs = 170; + private int intifacePeakReactiveImpactStrengthPercent = 20; + private int intifacePeakReactiveImpactDurationMs = 500; + private int intifaceCooldownMs = 80; + private int intifaceLatencyCompensationMs = 0; + private int intifaceAnimationBaselineStrengthPercent = 1; + private int intifaceOscillatorRegularSpeedPercent = 100; + private int intifaceOscillatorPeakSpeedPercent = 50; + private int intifaceStrokerMinDistancePercent = 0; + private int intifaceStrokerMaxDistancePercent = 100; + private int intifaceStrokerRegularMoveDurationMs = 500; + private int intifaceStrokerPeakMoveDurationMs = 300; + private int intifaceEnergizedOneBasePercent = 1; + private int intifaceEnergizedOnePulsePercent = 5; + private int intifaceEnergizedTwoBasePercent = 5; + private int intifaceEnergizedTwoPulsePercent = 10; + private int intifaceEnergizedThreeBasePercent = 10; + private int intifaceEnergizedThreePulsePercent = 20; + private int liquidTankCapacityMl = 200; + private boolean vanillaOverridesEnabled = true; + private boolean useAnimationBreeding = true; + private boolean requireMaleFemaleForBreeding = true; + private boolean femaleCowOnlyMilking = true; + private boolean attackCreativePlayers = false; + private boolean convertMaleOnlyVInjectionsToA = true; + private int attackEscapeHits = 12; + private double attackDecayPerSecond = 0.2; + private int attackEscapeDamageDifficultyPercent = 100; + private int pvpFullHealthEscapeBonusPercent = 30; + private int pvpCriticalHealthMashStrengthPercent = 25; + private int pvpMaxHeatEscapeStrengthPercent = 70; + private int pvpZeroHeatAttackStrengthPercent = 70; + private Integer attackEscapeInvulnerabilityTicks = null; + private int attackEscapeInvulnerabilitySeconds = 3; + private int attackEscapeAnimationProtectionSeconds = 3; + private int postEscapeGatherMaxMobs = 8; + private boolean blockAnimationsWhileRidingLivingEntities = true; + private boolean blockAnimationsWhileRidingVehicles = false; + private int attackOutcomeFailsafeThreshold = 3; + private int pregnancyChancePercent = 5; + private int pregnancyDurationMinutes = 3; + private boolean liquidBasedPregnancy = false; + private int pregnancyRiskyStartDay = 13; + private int pregnancyOvulationEndDay = 17; + private int pregnancySafeDayChancePercent = 10; + private Integer pregnancyRiskyChancePercent; + private int pregnancyOvulationChancePercent = 500; + private Map pregnancyChanceByEntity = new LinkedHashMap<>(); + private Map pregnancyOffspringCountByEntity = new LinkedHashMap<>(); + private int pregnancyEggDefaultHatchSeconds = 60; + private Map pregnancyEggEnabledByEntity = new LinkedHashMap<>(); + private Map pregnancyEggHatchSecondsByEntity = new LinkedHashMap<>(); + private boolean pregnancyAutoTameMobs = true; + private boolean pregnancyFriendlyMobsIgnorePlayers = false; + private Map entityProfilesByEntity = new LinkedHashMap<>(); + private double liquidDecayPerSecond = 1.0; + private int liquidPuddleDespawnSeconds = 40; + private double peakXpPerMl = 0.06666666666666667; + private float filledStageOneSpeedMult = 0.8F; + private float filledStageTwoSpeedMult = 0.7F; + private float filledStageThreeSpeedMult = 0.6F; + private float filledStageOneJumpMult = 0.9F; + private float filledStageTwoJumpMult = 0.75F; + private float filledStageThreeJumpMult = 0.6F; + private int uiEnergyOffsetX = 0; + private int uiEnergyOffsetY = 0; + private int uiEnergyHeartOffsetX = 0; + private int uiEnergyHeartOffsetY = 0; + private int uiAttackOffsetX = 0; + private int uiAttackOffsetY = 0; + private int uiPromptOffsetX = 0; + private int uiPromptOffsetY = 0; + private int uiLiquidOffsetX = 0; + private int uiLiquidOffsetY = 0; + private boolean uiEnergyVisible = true; + private boolean uiEnergyHeartVisible = true; + private boolean uiAttackVisible = true; + private boolean uiPromptVisible = true; + private boolean uiLiquidVisible = true; + private boolean debugSpinMode = false; + private String destroyedSkinTintHex = ""; + private Map genderSpawnByEntity = new LinkedHashMap<>(); + private Map liquidGainByEntity = new LinkedHashMap<>(); + private Map liquidColorByEntity = new LinkedHashMap<>(); + private List externalNoNPackLoadOrder = new ArrayList<>(); + private List disabledAnimationPacks = new ArrayList<>(); + private List disabledAnimations = new ArrayList<>(); + private List acknowledgedStartupWarnings = new ArrayList<>(); + + public int getInitialEnergyMax() { + return Math.max(0, Math.min(200, this.initialEnergyMax)); + } + + public void setInitialEnergyMax(int value) { + this.initialEnergyMax = Math.max(0, Math.min(200, value)); + } + + public double getEnergyGainRate() { + return Math.max(0.1, Math.min(10.0, this.energyGainRate)); + } + + public void setEnergyGainRate(double value) { + this.energyGainRate = Math.max(0.1, Math.min(10.0, value)); + } + + public int getActionSoundVolumePercent() { + return Math.max(0, Math.min(200, this.actionSoundVolumePercent)); + } + + public float getActionSoundVolumeMultiplier() { + return this.getActionSoundVolumePercent() / 100.0F; + } + + public void setActionSoundVolumePercent(int value) { + this.actionSoundVolumePercent = Math.max(0, Math.min(200, value)); + } + + public boolean isIntifaceEnabled() { + return this.intifaceEnabled; + } + + public void setIntifaceEnabled(boolean value) { + this.intifaceEnabled = value; + } + + public boolean isIntifaceAutoConnectOnWorldJoin() { + return this.intifaceAutoConnectOnWorldJoin; + } + + public void setIntifaceAutoConnectOnWorldJoin(boolean value) { + this.intifaceAutoConnectOnWorldJoin = value; + } + + public boolean isIntifaceStopOnDisconnect() { + return this.intifaceStopOnDisconnect; + } + + public void setIntifaceStopOnDisconnect(boolean value) { + this.intifaceStopOnDisconnect = value; + } + + public String getIntifaceServerUrl() { + return sanitizeIntifaceServerUrl(this.intifaceServerUrl); + } + + public void setIntifaceServerUrl(String value) { + this.intifaceServerUrl = sanitizeIntifaceServerUrl(value); + } + + public int getIntifaceReactiveImpactStrengthPercent() { + return clampPercent(this.intifaceReactiveImpactStrengthPercent); + } + + public void setIntifaceReactiveImpactStrengthPercent(int value) { + this.intifaceReactiveImpactStrengthPercent = clampPercent(value); + } + + public int getIntifaceReactiveImpactDurationMs() { + return clampInt(valueOrDefault(this.intifaceReactiveImpactDurationMs, 170), 20, 5000); + } + + public void setIntifaceReactiveImpactDurationMs(int value) { + this.intifaceReactiveImpactDurationMs = clampInt(value, 20, 5000); + } + + public int getIntifacePeakReactiveImpactStrengthPercent() { + return clampPercent(this.intifacePeakReactiveImpactStrengthPercent); + } + + public void setIntifacePeakReactiveImpactStrengthPercent(int value) { + this.intifacePeakReactiveImpactStrengthPercent = clampPercent(value); + } + + public int getIntifacePeakReactiveImpactDurationMs() { + return clampInt(valueOrDefault(this.intifacePeakReactiveImpactDurationMs, 500), 20, 5000); + } + + public void setIntifacePeakReactiveImpactDurationMs(int value) { + this.intifacePeakReactiveImpactDurationMs = clampInt(value, 20, 5000); + } + + public int getIntifaceCooldownMs() { + return clampInt(this.intifaceCooldownMs, 0, 5000); + } + + public void setIntifaceCooldownMs(int value) { + this.intifaceCooldownMs = clampInt(value, 0, 5000); + } + + public int getIntifaceLatencyCompensationMs() { + return clampInt(this.intifaceLatencyCompensationMs, 0, 1000); + } + + public void setIntifaceLatencyCompensationMs(int value) { + this.intifaceLatencyCompensationMs = clampInt(value, 0, 1000); + } + + public int getIntifaceAnimationBaselineStrengthPercent() { + return clampPercent(this.intifaceAnimationBaselineStrengthPercent); + } + + public void setIntifaceAnimationBaselineStrengthPercent(int value) { + this.intifaceAnimationBaselineStrengthPercent = clampPercent(value); + } + + public int getIntifaceOscillatorRegularSpeedPercent() { + return clampPercent(this.intifaceOscillatorRegularSpeedPercent); + } + + public void setIntifaceOscillatorRegularSpeedPercent(int value) { + this.intifaceOscillatorRegularSpeedPercent = clampPercent(value); + } + + public int getIntifaceOscillatorPeakSpeedPercent() { + return clampPercent(this.intifaceOscillatorPeakSpeedPercent); + } + + public void setIntifaceOscillatorPeakSpeedPercent(int value) { + this.intifaceOscillatorPeakSpeedPercent = clampPercent(value); + } + + public int getIntifaceStrokerMinDistancePercent() { + return clampPercent(this.intifaceStrokerMinDistancePercent); + } + + public void setIntifaceStrokerMinDistancePercent(int value) { + this.intifaceStrokerMinDistancePercent = Math.min(clampPercent(value), this.getIntifaceStrokerMaxDistancePercent()); + } + + public int getIntifaceStrokerMaxDistancePercent() { + return clampPercent(this.intifaceStrokerMaxDistancePercent); + } + + public void setIntifaceStrokerMaxDistancePercent(int value) { + this.intifaceStrokerMaxDistancePercent = Math.max(clampPercent(value), this.getIntifaceStrokerMinDistancePercent()); + } + + public void setIntifaceStrokerDistancesPercent(int minValue, int maxValue) { + int min = clampPercent(Math.min(minValue, maxValue)); + int max = clampPercent(Math.max(minValue, maxValue)); + this.intifaceStrokerMinDistancePercent = min; + this.intifaceStrokerMaxDistancePercent = max; + } + + public int getIntifaceStrokerRegularMoveDurationMs() { + return clampInt(this.intifaceStrokerRegularMoveDurationMs, 0, 10000); + } + + public void setIntifaceStrokerRegularMoveDurationMs(int value) { + this.intifaceStrokerRegularMoveDurationMs = clampInt(value, 0, 10000); + } + + public int getIntifaceStrokerPeakMoveDurationMs() { + return clampInt(this.intifaceStrokerPeakMoveDurationMs, 0, 10000); + } + + public void setIntifaceStrokerPeakMoveDurationMs(int value) { + this.intifaceStrokerPeakMoveDurationMs = clampInt(value, 0, 10000); + } + + public int getIntifaceEnergizedBasePercent(int level) { + return switch (Math.max(1, Math.min(3, level))) { + case 1 -> clampPercent(this.intifaceEnergizedOneBasePercent); + case 2 -> clampPercent(this.intifaceEnergizedTwoBasePercent); + default -> clampPercent(this.intifaceEnergizedThreeBasePercent); + }; + } + + public void setIntifaceEnergizedBasePercent(int level, int value) { + int clamped = clampPercent(value); + switch (Math.max(1, Math.min(3, level))) { + case 1: + this.intifaceEnergizedOneBasePercent = clamped; + break; + case 2: + this.intifaceEnergizedTwoBasePercent = clamped; + break; + default: + this.intifaceEnergizedThreeBasePercent = clamped; + } + } + + public int getIntifaceEnergizedPulsePercent(int level) { + return switch (Math.max(1, Math.min(3, level))) { + case 1 -> clampPercent(this.intifaceEnergizedOnePulsePercent); + case 2 -> clampPercent(this.intifaceEnergizedTwoPulsePercent); + default -> clampPercent(this.intifaceEnergizedThreePulsePercent); + }; + } + + public void setIntifaceEnergizedPulsePercent(int level, int value) { + int clamped = clampPercent(value); + switch (Math.max(1, Math.min(3, level))) { + case 1: + this.intifaceEnergizedOnePulsePercent = clamped; + break; + case 2: + this.intifaceEnergizedTwoPulsePercent = clamped; + break; + default: + this.intifaceEnergizedThreePulsePercent = clamped; + } + } + + public double getNearAnimationEnergyGainMult() { + return Math.max(1.0, Math.min(10.0, this.nearAnimationEnergyGainMult)); + } + + public void setNearAnimationEnergyGainMult(double value) { + this.nearAnimationEnergyGainMult = Math.max(1.0, Math.min(10.0, value)); + } + + public double getPlayerEnergyAuraMultLow() { + return Math.max(1.0, Math.min(20.0, this.playerEnergyAuraMultLow)); + } + + public void setPlayerEnergyAuraMultLow(double value) { + this.playerEnergyAuraMultLow = Math.max(1.0, Math.min(20.0, value)); + } + + public double getPlayerEnergyAuraMultMid() { + return Math.max(1.0, Math.min(20.0, this.playerEnergyAuraMultMid)); + } + + public void setPlayerEnergyAuraMultMid(double value) { + this.playerEnergyAuraMultMid = Math.max(1.0, Math.min(20.0, value)); + } + + public double getPlayerEnergyAuraMultHigh() { + return Math.max(1.0, Math.min(20.0, this.playerEnergyAuraMultHigh)); + } + + public void setPlayerEnergyAuraMultHigh(double value) { + this.playerEnergyAuraMultHigh = Math.max(1.0, Math.min(20.0, value)); + } + + public double getDestroyedSkinAuraMultStage1() { + return this.clampDestroyedSkinAuraMult(this.destroyedSkinAuraMultStage1); + } + + public void setDestroyedSkinAuraMultStage1(double value) { + this.destroyedSkinAuraMultStage1 = this.clampDestroyedSkinAuraMult(value); + } + + public double getDestroyedSkinAuraMultStage2() { + return this.clampDestroyedSkinAuraMult(this.destroyedSkinAuraMultStage2); + } + + public void setDestroyedSkinAuraMultStage2(double value) { + this.destroyedSkinAuraMultStage2 = this.clampDestroyedSkinAuraMult(value); + } + + public double getDestroyedSkinAuraMultStage3() { + return this.clampDestroyedSkinAuraMult(this.destroyedSkinAuraMultStage3); + } + + public void setDestroyedSkinAuraMultStage3(double value) { + this.destroyedSkinAuraMultStage3 = this.clampDestroyedSkinAuraMult(value); + } + + public double getDestroyedSkinAuraMultStage4() { + return this.clampDestroyedSkinAuraMult(this.destroyedSkinAuraMultStage4); + } + + public void setDestroyedSkinAuraMultStage4(double value) { + this.destroyedSkinAuraMultStage4 = this.clampDestroyedSkinAuraMult(value); + } + + private double clampDestroyedSkinAuraMult(double value) { + return Math.max(1.0, Math.min(50.0, value)); + } + + public int getNormalDamageDestroyedSkinChancePercent() { + return clampPercent(this.normalDamageDestroyedSkinChancePercent); + } + + public void setNormalDamageDestroyedSkinChancePercent(int value) { + this.normalDamageDestroyedSkinChancePercent = clampPercent(value); + } + + public boolean isLastDefeatedEnabled() { + return this.lastDefeatedEnabled; + } + + public void setLastDefeatedEnabled(boolean value) { + this.lastDefeatedEnabled = value; + } + + public int getLastDefeatedEnergyThreshold() { + return Math.max(0, Math.min(200, this.lastDefeatedEnergyThreshold)); + } + + public void setLastDefeatedEnergyThreshold(int value) { + this.lastDefeatedEnergyThreshold = Math.max(0, Math.min(200, value)); + } + + public int getLastDefeatedSearchRadius() { + return Math.max(1, Math.min(32, this.lastDefeatedSearchRadius)); + } + + public void setLastDefeatedSearchRadius(int value) { + this.lastDefeatedSearchRadius = Math.max(1, Math.min(32, value)); + } + + public int getLastDefeatedCooldownSeconds() { + return Math.max(0, Math.min(3600, this.lastDefeatedCooldownSeconds)); + } + + public void setLastDefeatedCooldownSeconds(int value) { + this.lastDefeatedCooldownSeconds = Math.max(0, Math.min(3600, value)); + } + + public int getPlayerEnergyAuraRadiusMin() { + return Math.max(1, Math.min(64, this.playerEnergyAuraRadiusMin)); + } + + public void setPlayerEnergyAuraRadiusMin(int value) { + this.playerEnergyAuraRadiusMin = Math.max(1, Math.min(64, value)); + } + + public int getPlayerEnergyAuraRadiusMax() { + return Math.max(this.getPlayerEnergyAuraRadiusMin(), Math.min(64, this.playerEnergyAuraRadiusMax)); + } + + public void setPlayerEnergyAuraRadiusMax(int value) { + this.playerEnergyAuraRadiusMax = Math.max(1, Math.min(64, value)); + } + + public int getPlayerEnergyAuraPulseTicks() { + return Math.max(1, Math.min(1200, this.playerEnergyAuraPulseTicks)); + } + + public void setPlayerEnergyAuraPulseTicks(int value) { + this.playerEnergyAuraPulseTicks = Math.max(1, Math.min(1200, value)); + } + + public NonDebugChatMode debugChatMode() { + return NonDebugChatMode.fromId(this.debugChatMode, NonDebugChatMode.SETUP_ERRORS); + } + + public void setDebugChatMode(NonDebugChatMode mode) { + NonDebugChatMode resolved = mode == null ? NonDebugChatMode.SETUP_ERRORS : mode; + this.debugChatMode = resolved.id(); + } + + public boolean allowsDebugChat(NonDebugChatCategory category) { + return this.debugChatMode().allows(category); + } + + public boolean forceVanillaEntityTextures() { + return this.forceVanillaEntityTextures; + } + + public void setForceVanillaEntityTextures(boolean value) { + this.forceVanillaEntityTextures = value; + } + + public int blockSearchRadius() { + return Math.max(1, Math.min(16, this.animationBlockSearchRadius)); + } + + public void setBlockSearchRadius(int value) { + this.animationBlockSearchRadius = Math.max(1, Math.min(16, value)); + } + + public boolean autoSwitchThirdPersonOnAnimationStart() { + return this.autoSwitchThirdPersonOnAnimationStart; + } + + public void setAutoSwitchThirdPersonOnAnimationStart(boolean value) { + this.autoSwitchThirdPersonOnAnimationStart = value; + } + + public int getScanBudgetPerTick() { + return Math.max(1, Math.min(512, this.scanBudgetPerTick)); + } + + public void setScanBudgetPerTick(int value) { + this.scanBudgetPerTick = Math.max(1, Math.min(512, value)); + } + + public int getLoopProgressSeconds() { + return Math.max(5, Math.min(300, this.loopProgressSeconds)); + } + + public void setLoopProgressSeconds(int seconds) { + this.loopProgressSeconds = Math.max(5, Math.min(300, seconds)); + } + + public int getPeakLoopProgressSeconds() { + return Math.max(1, Math.min(300, this.peakLoopProgressSeconds)); + } + + public void setPeakLoopProgressSeconds(int seconds) { + this.peakLoopProgressSeconds = Math.max(1, Math.min(300, seconds)); + } + + public int getGenderMaleChancePercent() { + return clampPercent(this.genderMaleChancePercent); + } + + public void setGenderMaleChancePercent(int percent) { + this.genderMaleChancePercent = clampPercent(percent); + } + + public int getGenderFemaleChancePercent() { + return clampPercent(this.genderFemaleChancePercent); + } + + public void setGenderFemaleChancePercent(int percent) { + this.genderFemaleChancePercent = clampPercent(percent); + } + + public int getGenderBothChancePercent() { + return clampPercent(this.genderBothChancePercent); + } + + public void setGenderBothChancePercent(int percent) { + this.genderBothChancePercent = clampPercent(percent); + } + + public NonConfig.GenderSpawnChances getGlobalGenderSpawnChances() { + NonConfig.GenderSpawnChances chances = new NonConfig.GenderSpawnChances( + this.getGenderMaleChancePercent(), this.getGenderFemaleChancePercent(), this.getGenderBothChancePercent() + ); + return chances.isValid() ? chances : defaultGenderSpawnChances(); + } + + public void setGlobalGenderSpawnChances(NonConfig.GenderSpawnChances chances) { + NonConfig.GenderSpawnChances safe = validOrDefault(chances); + this.setGenderMaleChancePercent(safe.male()); + this.setGenderFemaleChancePercent(safe.female()); + this.setGenderBothChancePercent(safe.both()); + } + + public int getMaleMaleChancePercent() { + return Math.max(0, Math.min(100, this.maleMaleChancePercent)); + } + + public void setMaleMaleChancePercent(int percent) { + this.maleMaleChancePercent = Math.max(0, Math.min(100, percent)); + } + + public int getFemaleFemaleChancePercent() { + return Math.max(0, Math.min(100, this.femaleFemaleChancePercent)); + } + + public void setFemaleFemaleChancePercent(int percent) { + this.femaleFemaleChancePercent = Math.max(0, Math.min(100, percent)); + } + + public int getEntityEntityChancePercent() { + return Math.max(0, Math.min(100, this.entityEntityChancePercent)); + } + + public void setEntityEntityChancePercent(int percent) { + this.entityEntityChancePercent = Math.max(0, Math.min(100, percent)); + } + + public int getMultiActorJoinChancePercent() { + return Math.max(0, Math.min(100, this.multiActorJoinChancePercent)); + } + + public void setMultiActorJoinChancePercent(int percent) { + this.multiActorJoinChancePercent = Math.max(0, Math.min(100, percent)); + } + + public NonConfig.PlayerGenderSelection getPlayerGenderSelection() { + return NonConfig.PlayerGenderSelection.fromId(this.playerGender, NonConfig.PlayerGenderSelection.FEMALE); + } + + public void setPlayerGenderSelection(NonConfig.PlayerGenderSelection selection) { + NonConfig.PlayerGenderSelection resolved = selection == null ? NonConfig.PlayerGenderSelection.FEMALE : selection; + this.playerGender = resolved.id(); + } + + public int getPlayerGenderMask() { + return this.getPlayerGenderSelection().mask(); + } + + public boolean allowPlayerGenderChangeAnytime() { + return this.allowPlayerGenderChangeAnytime; + } + + public void setAllowPlayerGenderChangeAnytime(boolean value) { + this.allowPlayerGenderChangeAnytime = value; + } + + public boolean requirePlayerGenderSelectionOnJoin() { + return this.requirePlayerGenderSelectionOnJoin; + } + + public void setRequirePlayerGenderSelectionOnJoin(boolean value) { + this.requirePlayerGenderSelectionOnJoin = value; + } + + public int getAllowedStartingGenderMask() { + int mask = this.allowedStartingGenderMask & 7; + return mask == 0 ? 7 : mask; + } + + public void setAllowedStartingGenderMask(int mask) { + int normalized = mask & 7; + this.allowedStartingGenderMask = normalized == 0 ? 7 : normalized; + } + + public int getAllowedPlayerInteractionProfilesMask() { + int normalized = this.allowedPlayerInteractionProfilesMask & 15; + return normalized == 0 ? 15 : normalized; + } + + public void setAllowedPlayerInteractionProfilesMask(int mask) { + int normalized = mask & 15; + this.allowedPlayerInteractionProfilesMask = normalized == 0 ? 15 : normalized; + } + + public int getPlayerRequestMinEnergy() { + return Math.max(0, Math.min(200, this.playerRequestMinEnergy)); + } + + public void setPlayerRequestMinEnergy(int value) { + this.playerRequestMinEnergy = Math.max(0, Math.min(200, value)); + } + + public boolean isPregnancyEnabled() { + return this.pregnancyEnabled; + } + + public void setPregnancyEnabled(boolean value) { + this.pregnancyEnabled = value; + } + + public boolean isLiquidTankEnabled() { + return this.liquidTankEnabled; + } + + public void setLiquidTankEnabled(boolean value) { + this.liquidTankEnabled = value; + } + + public boolean isMessSystemEnabled() { + return this.messSystemEnabled; + } + + public void setMessSystemEnabled(boolean value) { + this.messSystemEnabled = value; + } + + public boolean isDestroyedSkinSystemEnabled() { + return this.destroyedSkinSystemEnabled; + } + + public void setDestroyedSkinSystemEnabled(boolean value) { + this.destroyedSkinSystemEnabled = value; + } + + public boolean isCraftingTableSkinRepairAllowed() { + return this.allowCraftingTableSkinRepair; + } + + public void setCraftingTableSkinRepairAllowed(boolean value) { + this.allowCraftingTableSkinRepair = value; + } + + public boolean keepMessAfterDeath() { + return this.keepMessAfterDeath; + } + + public void setKeepMessAfterDeath(boolean value) { + this.keepMessAfterDeath = value; + } + + public boolean keepRippedSkinAfterDeath() { + return this.keepRippedSkinAfterDeath; + } + + public void setKeepRippedSkinAfterDeath(boolean value) { + this.keepRippedSkinAfterDeath = value; + } + + public boolean keepLiquidTankAfterDeath() { + return this.keepLiquidTankAfterDeath; + } + + public void setKeepLiquidTankAfterDeath(boolean value) { + this.keepLiquidTankAfterDeath = value; + } + + public boolean keepPregnancyAfterDeath() { + return this.keepPregnancyAfterDeath; + } + + public void setKeepPregnancyAfterDeath(boolean value) { + this.keepPregnancyAfterDeath = value; + } + + public boolean isArmorShedEffectEnabled() { + return this.armorShedEffectEnabled; + } + + public void setArmorShedEffectEnabled(boolean value) { + this.armorShedEffectEnabled = value; + } + + public boolean isSyncFemaleGenderModWithNonGender() { + return this.syncFemaleGenderModWithNonGender; + } + + public void setSyncFemaleGenderModWithNonGender(boolean value) { + this.syncFemaleGenderModWithNonGender = value; + } + + public boolean isDestroyedSkinFemaleGenderOverridesEnabled() { + return this.destroyedSkinFemaleGenderOverridesEnabled; + } + + public void setDestroyedSkinFemaleGenderOverridesEnabled(boolean value) { + this.destroyedSkinFemaleGenderOverridesEnabled = value; + } + + public NonConfig.FemaleGenderDestroyedProfile getFemaleGenderDestroyedLow() { + if (this.femaleGenderDestroyedLow == null) { + this.femaleGenderDestroyedLow = NonConfig.FemaleGenderDestroyedProfile.lowDefault(); + } + + this.femaleGenderDestroyedLow = this.femaleGenderDestroyedLow.sanitized(); + return this.femaleGenderDestroyedLow; + } + + public void setFemaleGenderDestroyedLow(NonConfig.FemaleGenderDestroyedProfile value) { + this.femaleGenderDestroyedLow = value == null ? NonConfig.FemaleGenderDestroyedProfile.lowDefault() : value.sanitized(); + } + + public NonConfig.FemaleGenderDestroyedProfile getFemaleGenderDestroyedHigh() { + if (this.femaleGenderDestroyedHigh == null) { + this.femaleGenderDestroyedHigh = NonConfig.FemaleGenderDestroyedProfile.highDefault(); + } + + this.femaleGenderDestroyedHigh = this.femaleGenderDestroyedHigh.sanitized(); + return this.femaleGenderDestroyedHigh; + } + + public void setFemaleGenderDestroyedHigh(NonConfig.FemaleGenderDestroyedProfile value) { + this.femaleGenderDestroyedHigh = value == null ? NonConfig.FemaleGenderDestroyedProfile.highDefault() : value.sanitized(); + } + + public int getLiquidTankCapacityMl() { + return Math.max(1, Math.min(10000, this.liquidTankCapacityMl)); + } + + public void setLiquidTankCapacityMl(int value) { + this.liquidTankCapacityMl = Math.max(1, Math.min(10000, value)); + } + + public boolean isVanillaOverridesEnabled() { + return this.vanillaOverridesEnabled; + } + + public void setVanillaOverridesEnabled(boolean value) { + this.vanillaOverridesEnabled = value; + } + + public boolean useAnimationBreeding() { + return this.vanillaOverridesEnabled && this.useAnimationBreeding; + } + + public void setUseAnimationBreeding(boolean value) { + this.useAnimationBreeding = value; + } + + public boolean requireMaleFemaleForBreeding() { + return this.vanillaOverridesEnabled && this.requireMaleFemaleForBreeding; + } + + public void setRequireMaleFemaleForBreeding(boolean value) { + this.requireMaleFemaleForBreeding = value; + } + + public boolean femaleCowOnlyMilking() { + return this.vanillaOverridesEnabled && this.femaleCowOnlyMilking; + } + + public void setFemaleCowOnlyMilking(boolean value) { + this.femaleCowOnlyMilking = value; + } + + public boolean canAttackCreativePlayers() { + return this.attackCreativePlayers; + } + + public void setAttackCreativePlayers(boolean value) { + this.attackCreativePlayers = value; + } + + public boolean convertMaleOnlyVInjectionsToA() { + return this.convertMaleOnlyVInjectionsToA; + } + + public void setConvertMaleOnlyVInjectionsToA(boolean value) { + this.convertMaleOnlyVInjectionsToA = value; + } + + public int getAttackEscapeHits() { + return Math.max(1, Math.min(50, this.attackEscapeHits)); + } + + public void setAttackEscapeHits(int hits) { + this.attackEscapeHits = Math.max(1, Math.min(50, hits)); + } + + public double getAttackDecayPerSecond() { + return Math.max(0.0, Math.min(5.0, this.attackDecayPerSecond)); + } + + public void setAttackDecayPerSecond(double decay) { + this.attackDecayPerSecond = Math.max(0.0, Math.min(5.0, decay)); + } + + public int getAttackEscapeDamageDifficultyPercent() { + return clampAttackEscapeDamageDifficultyPercent(this.attackEscapeDamageDifficultyPercent); + } + + public void setAttackEscapeDamageDifficultyPercent(int percent) { + this.attackEscapeDamageDifficultyPercent = clampAttackEscapeDamageDifficultyPercent(percent); + } + + public int getPvpFullHealthEscapeBonusPercent() { + return clampPvpMashPercent(this.pvpFullHealthEscapeBonusPercent); + } + + public void setPvpFullHealthEscapeBonusPercent(int percent) { + this.pvpFullHealthEscapeBonusPercent = clampPvpMashPercent(percent); + } + + public int getPvpCriticalHealthMashStrengthPercent() { + return clampPvpMashPercent(this.pvpCriticalHealthMashStrengthPercent); + } + + public void setPvpCriticalHealthMashStrengthPercent(int percent) { + this.pvpCriticalHealthMashStrengthPercent = clampPvpMashPercent(percent); + } + + public int getPvpMaxHeatEscapeStrengthPercent() { + return clampPvpMashPercent(this.pvpMaxHeatEscapeStrengthPercent); + } + + public void setPvpMaxHeatEscapeStrengthPercent(int percent) { + this.pvpMaxHeatEscapeStrengthPercent = clampPvpMashPercent(percent); + } + + public int getPvpZeroHeatAttackStrengthPercent() { + return clampPvpMashPercent(this.pvpZeroHeatAttackStrengthPercent); + } + + public void setPvpZeroHeatAttackStrengthPercent(int percent) { + this.pvpZeroHeatAttackStrengthPercent = clampPvpMashPercent(percent); + } + + public int getAttackEscapeInvulnerabilitySeconds() { + return clampAttackEscapeProtectionSeconds(this.attackEscapeInvulnerabilitySeconds); + } + + public void setAttackEscapeInvulnerabilitySeconds(int seconds) { + this.attackEscapeInvulnerabilityTicks = null; + this.attackEscapeInvulnerabilitySeconds = clampAttackEscapeProtectionSeconds(seconds); + } + + public int getAttackEscapeInvulnerabilityTicks() { + return secondsToTicks(this.getAttackEscapeInvulnerabilitySeconds()); + } + + public int getAttackEscapeAnimationProtectionSeconds() { + return clampAttackEscapeProtectionSeconds(this.attackEscapeAnimationProtectionSeconds); + } + + public void setAttackEscapeAnimationProtectionSeconds(int seconds) { + this.attackEscapeAnimationProtectionSeconds = clampAttackEscapeProtectionSeconds(seconds); + } + + public int getAttackEscapeAnimationProtectionTicks() { + return secondsToTicks(this.getAttackEscapeAnimationProtectionSeconds()); + } + + private static int clampAttackEscapeProtectionSeconds(int seconds) { + return Math.max(0, Math.min(10, seconds)); + } + + private static int clampAttackEscapeDamageDifficultyPercent(int percent) { + return Math.max(0, Math.min(200, percent)); + } + + private static int clampPvpMashPercent(int percent) { + return Math.max(0, Math.min(200, percent)); + } + + private static int secondsToTicks(int seconds) { + return clampAttackEscapeProtectionSeconds(seconds) * 20; + } + + private static int ticksToSeconds(int ticks) { + int clampedTicks = Math.max(0, ticks); + return clampedTicks == 0 ? 0 : clampAttackEscapeProtectionSeconds((clampedTicks + 19) / 20); + } + + public int getPostEscapeGatherMaxMobs() { + return Math.max(0, Math.min(64, this.postEscapeGatherMaxMobs)); + } + + public void setPostEscapeGatherMaxMobs(int value) { + this.postEscapeGatherMaxMobs = Math.max(0, Math.min(64, value)); + } + + public boolean blockAnimationsWhileRidingLivingEntities() { + return this.blockAnimationsWhileRidingLivingEntities; + } + + public void setBlockAnimationsWhileRidingLivingEntities(boolean value) { + this.blockAnimationsWhileRidingLivingEntities = value; + } + + public boolean blockAnimationsWhileRidingVehicles() { + return this.blockAnimationsWhileRidingVehicles; + } + + public void setBlockAnimationsWhileRidingVehicles(boolean value) { + this.blockAnimationsWhileRidingVehicles = value; + } + + public int getAttackOutcomeFailsafeThreshold() { + return Math.max(1, Math.min(50, this.attackOutcomeFailsafeThreshold)); + } + + public void setAttackOutcomeFailsafeThreshold(int threshold) { + this.attackOutcomeFailsafeThreshold = Math.max(1, Math.min(50, threshold)); + } + + public int getPregnancyChancePercent() { + return !this.pregnancyEnabled ? 0 : Math.max(0, Math.min(100, this.pregnancyChancePercent)); + } + + public void setPregnancyChancePercent(int value) { + this.pregnancyChancePercent = Math.max(0, Math.min(100, value)); + } + + public int getPregnancyDurationMinutes() { + return Math.max(1, Math.min(5256000, this.pregnancyDurationMinutes)); + } + + public void setPregnancyDurationMinutes(int minutes) { + this.pregnancyDurationMinutes = Math.max(1, Math.min(5256000, minutes)); + } + + public long getPregnancyDurationTicks() { + return this.getPregnancyDurationMinutes() * 1200L; + } + + public boolean isLiquidBasedPregnancyEnabled() { + return this.liquidBasedPregnancy; + } + + public void setLiquidBasedPregnancyEnabled(boolean value) { + this.liquidBasedPregnancy = value; + } + + public int getPregnancyRiskyStartDay() { + return clampInt(this.pregnancyRiskyStartDay, 1, 28); + } + + public void setPregnancyRiskyStartDay(int day) { + this.pregnancyRiskyStartDay = clampInt(day, 1, 28); + if (this.pregnancyOvulationEndDay < this.pregnancyRiskyStartDay + 2) { + this.pregnancyOvulationEndDay = this.pregnancyRiskyStartDay + 2; + } + } + + public int getPregnancyOvulationEndDay() { + return Math.max(this.getPregnancyRiskyStartDay() + 2, clampInt(this.pregnancyOvulationEndDay, 1, 30)); + } + + public void setPregnancyOvulationEndDay(int day) { + this.pregnancyOvulationEndDay = Math.max(this.getPregnancyRiskyStartDay() + 2, clampInt(day, 1, 30)); + } + + public int getPregnancySafeDayChancePercent() { + return clampInt(this.pregnancySafeDayChancePercent, 0, 1000); + } + + public void setPregnancySafeDayChancePercent(int percent) { + this.pregnancySafeDayChancePercent = clampInt(percent, 0, 1000); + } + + public int getPregnancyRiskyChancePercent() { + return this.pregnancyRiskyChancePercent == null ? 300 : clampInt(this.pregnancyRiskyChancePercent, 0, 1000); + } + + public void setPregnancyRiskyChancePercent(int percent) { + this.pregnancyRiskyChancePercent = clampInt(percent, 0, 1000); + } + + public int getPregnancyOvulationChancePercent() { + return clampInt(this.pregnancyOvulationChancePercent, 0, 1000); + } + + public void setPregnancyOvulationChancePercent(int percent) { + this.pregnancyOvulationChancePercent = clampInt(percent, 0, 1000); + } + + public Map getPregnancyChanceByEntity() { + return this.pregnancyChanceByEntity == null ? Map.of() : Map.copyOf(this.pregnancyChanceByEntity); + } + + public void setPregnancyChanceByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + if (key != null) { + key = key.trim(); + if (!key.isEmpty()) { + int value = entry.getValue() == null ? 0 : entry.getValue(); + value = Math.max(0, Math.min(100, value)); + cleaned.put(key, value); + } + } + } + } + } + + this.pregnancyChanceByEntity = cleaned; + } + + public int getPregnancyChanceFor(String entityId) { + if (!this.pregnancyEnabled) { + return 0; + } else if (entityId != null && !entityId.isBlank() && this.pregnancyChanceByEntity != null) { + Integer override = this.pregnancyChanceByEntity.get(entityId); + return override == null ? this.getPregnancyChancePercent() : Math.max(0, Math.min(100, override)); + } else { + return this.getPregnancyChancePercent(); + } + } + + public Map getPregnancyOffspringCountByEntity() { + if (this.pregnancyOffspringCountByEntity == null) { + return Map.of(); + } + + Map copy = new LinkedHashMap<>(); + + for (Entry entry : this.pregnancyOffspringCountByEntity.entrySet()) { + if (entry != null && entry.getKey() != null && entry.getValue() != null) { + copy.put(entry.getKey(), entry.getValue().sanitized()); + } + } + + return copy; + } + + public void setPregnancyOffspringCountByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + if (key != null) { + key = key.trim(); + if (!key.isEmpty()) { + NonConfig.OffspringCountRange range = entry.getValue() == null ? new NonConfig.OffspringCountRange() : entry.getValue().sanitized(); + cleaned.put(key, range); + } + } + } + } + } + + this.pregnancyOffspringCountByEntity = cleaned; + } + + public NonConfig.OffspringCountRange getPregnancyOffspringCountRangeFor(String entityId) { + if (entityId != null && !entityId.isBlank() && this.pregnancyOffspringCountByEntity != null) { + NonConfig.OffspringCountRange override = this.pregnancyOffspringCountByEntity.get(entityId); + return override == null ? new NonConfig.OffspringCountRange() : override.sanitized(); + } else { + return new NonConfig.OffspringCountRange(); + } + } + + public int getPregnancyEggDefaultHatchSeconds() { + return Math.max(1, Math.min(72000, this.pregnancyEggDefaultHatchSeconds)); + } + + public void setPregnancyEggDefaultHatchSeconds(int seconds) { + this.pregnancyEggDefaultHatchSeconds = Math.max(1, Math.min(72000, seconds)); + } + + public Map getPregnancyEggEnabledByEntity() { + return this.pregnancyEggEnabledByEntity == null ? Map.of() : Map.copyOf(this.pregnancyEggEnabledByEntity); + } + + public void setPregnancyEggEnabledByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + if (key != null) { + key = key.trim(); + if (!key.isEmpty()) { + boolean enabled = Boolean.TRUE.equals(entry.getValue()); + cleaned.put(key, enabled); + } + } + } + } + } + + this.pregnancyEggEnabledByEntity = cleaned; + } + + public boolean isPregnancyEggEnabledFor(String entityId) { + return entityId != null && !entityId.isBlank() && this.pregnancyEggEnabledByEntity != null + ? Boolean.TRUE.equals(this.pregnancyEggEnabledByEntity.get(entityId)) + : false; + } + + public Map getPregnancyEggHatchSecondsByEntity() { + return this.pregnancyEggHatchSecondsByEntity == null ? Map.of() : Map.copyOf(this.pregnancyEggHatchSecondsByEntity); + } + + public void setPregnancyEggHatchSecondsByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + if (key != null) { + key = key.trim(); + if (!key.isEmpty()) { + int value = entry.getValue() == null ? this.getPregnancyEggDefaultHatchSeconds() : entry.getValue(); + value = Math.max(1, Math.min(72000, value)); + cleaned.put(key, value); + } + } + } + } + } + + this.pregnancyEggHatchSecondsByEntity = cleaned; + } + + public int getPregnancyEggHatchSecondsFor(String entityId) { + int fallback = this.getPregnancyEggDefaultHatchSeconds(); + if (entityId != null && !entityId.isBlank() && this.pregnancyEggHatchSecondsByEntity != null) { + Integer override = this.pregnancyEggHatchSecondsByEntity.get(entityId); + return override == null ? fallback : Math.max(1, Math.min(72000, override)); + } else { + return fallback; + } + } + + public boolean isPregnancyAutoTameMobs() { + return this.pregnancyAutoTameMobs; + } + + public void setPregnancyAutoTameMobs(boolean value) { + this.pregnancyAutoTameMobs = value; + } + + public boolean pregnancyFriendlyMobsIgnorePlayers() { + return this.pregnancyFriendlyMobsIgnorePlayers; + } + + public void setPregnancyFriendlyMobsIgnorePlayers(boolean value) { + this.pregnancyFriendlyMobsIgnorePlayers = value; + } + + public Map getEntityProfilesByEntity() { + if (this.entityProfilesByEntity == null) { + return Map.of(); + } + + Map copy = new LinkedHashMap<>(); + + for (Entry entry : this.entityProfilesByEntity.entrySet()) { + if (entry != null && entry.getKey() != null && entry.getValue() != null) { + String key = entry.getKey().trim(); + if (!key.isEmpty()) { + NonConfig.EntityProfile profile = entry.getValue().sanitized(); + if (!profile.isEmpty()) { + copy.put(key, profile); + } + } + } + } + + return copy; + } + + public void setEntityProfilesByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null && entry.getKey() != null && entry.getValue() != null) { + String key = entry.getKey().trim(); + if (!key.isEmpty()) { + NonConfig.EntityProfile profile = entry.getValue().sanitized(); + if (!profile.isEmpty()) { + cleaned.put(key, profile); + } + } + } + } + } + + this.entityProfilesByEntity = cleaned; + NonEntityProfiles.invalidateEnergyGainMultiplierCache(); + } + + public double getLiquidDecayPerSecond() { + return Math.max(0.0, Math.min(20.0, this.liquidDecayPerSecond)); + } + + public void setLiquidDecayPerSecond(double decay) { + this.liquidDecayPerSecond = Math.max(0.0, Math.min(20.0, decay)); + } + + public int getLiquidPuddleDespawnSeconds() { + return Math.max(1, Math.min(300, this.liquidPuddleDespawnSeconds)); + } + + public void setLiquidPuddleDespawnSeconds(int seconds) { + this.liquidPuddleDespawnSeconds = Math.max(1, Math.min(300, seconds)); + } + + public double getPeakXpPerMl() { + return Math.max(0.0, Math.min(10.0, this.peakXpPerMl)); + } + + public void setPeakXpPerMl(double value) { + this.peakXpPerMl = Math.max(0.0, Math.min(10.0, value)); + } + + public float getFilledStageOneSpeedMult() { + return this.clampMultiplier(this.filledStageOneSpeedMult); + } + + public void setFilledStageOneSpeedMult(float value) { + this.filledStageOneSpeedMult = this.clampMultiplier(value); + } + + public float getFilledStageTwoSpeedMult() { + return this.clampMultiplier(this.filledStageTwoSpeedMult); + } + + public void setFilledStageTwoSpeedMult(float value) { + this.filledStageTwoSpeedMult = this.clampMultiplier(value); + } + + public float getFilledStageThreeSpeedMult() { + return this.clampMultiplier(this.filledStageThreeSpeedMult); + } + + public void setFilledStageThreeSpeedMult(float value) { + this.filledStageThreeSpeedMult = this.clampMultiplier(value); + } + + public float getFilledStageOneJumpMult() { + return this.clampMultiplier(this.filledStageOneJumpMult); + } + + public void setFilledStageOneJumpMult(float value) { + this.filledStageOneJumpMult = this.clampMultiplier(value); + } + + public float getFilledStageTwoJumpMult() { + return this.clampMultiplier(this.filledStageTwoJumpMult); + } + + public void setFilledStageTwoJumpMult(float value) { + this.filledStageTwoJumpMult = this.clampMultiplier(value); + } + + public float getFilledStageThreeJumpMult() { + return this.clampMultiplier(this.filledStageThreeJumpMult); + } + + public void setFilledStageThreeJumpMult(float value) { + this.filledStageThreeJumpMult = this.clampMultiplier(value); + } + + public int getUiEnergyOffsetX() { + return this.clampUiOffset(this.uiEnergyOffsetX); + } + + public void setUiEnergyOffsetX(int value) { + this.uiEnergyOffsetX = this.clampUiOffset(value); + } + + public int getUiEnergyOffsetY() { + return this.clampUiOffset(this.uiEnergyOffsetY); + } + + public void setUiEnergyOffsetY(int value) { + this.uiEnergyOffsetY = this.clampUiOffset(value); + } + + public int getUiEnergyHeartOffsetX() { + return this.clampUiOffset(this.uiEnergyHeartOffsetX); + } + + public void setUiEnergyHeartOffsetX(int value) { + this.uiEnergyHeartOffsetX = this.clampUiOffset(value); + } + + public int getUiEnergyHeartOffsetY() { + return this.clampUiOffset(this.uiEnergyHeartOffsetY); + } + + public void setUiEnergyHeartOffsetY(int value) { + this.uiEnergyHeartOffsetY = this.clampUiOffset(value); + } + + public int getUiAttackOffsetX() { + return this.clampUiOffset(this.uiAttackOffsetX); + } + + public void setUiAttackOffsetX(int value) { + this.uiAttackOffsetX = this.clampUiOffset(value); + } + + public int getUiAttackOffsetY() { + return this.clampUiOffset(this.uiAttackOffsetY); + } + + public void setUiAttackOffsetY(int value) { + this.uiAttackOffsetY = this.clampUiOffset(value); + } + + public int getUiPromptOffsetX() { + return this.clampUiOffset(this.uiPromptOffsetX); + } + + public void setUiPromptOffsetX(int value) { + this.uiPromptOffsetX = this.clampUiOffset(value); + } + + public int getUiPromptOffsetY() { + return this.clampUiOffset(this.uiPromptOffsetY); + } + + public void setUiPromptOffsetY(int value) { + this.uiPromptOffsetY = this.clampUiOffset(value); + } + + public int getUiLiquidOffsetX() { + return this.clampUiOffset(this.uiLiquidOffsetX); + } + + public void setUiLiquidOffsetX(int value) { + this.uiLiquidOffsetX = this.clampUiOffset(value); + } + + public int getUiLiquidOffsetY() { + return this.clampUiOffset(this.uiLiquidOffsetY); + } + + public void setUiLiquidOffsetY(int value) { + this.uiLiquidOffsetY = this.clampUiOffset(value); + } + + public boolean isUiEnergyVisible() { + return this.uiEnergyVisible; + } + + public void setUiEnergyVisible(boolean value) { + this.uiEnergyVisible = value; + } + + public boolean isUiEnergyHeartVisible() { + return this.uiEnergyHeartVisible; + } + + public void setUiEnergyHeartVisible(boolean value) { + this.uiEnergyHeartVisible = value; + } + + public boolean isUiAttackVisible() { + return this.uiAttackVisible; + } + + public void setUiAttackVisible(boolean value) { + this.uiAttackVisible = value; + } + + public boolean isUiPromptVisible() { + return this.uiPromptVisible; + } + + public void setUiPromptVisible(boolean value) { + this.uiPromptVisible = value; + } + + public boolean isUiLiquidVisible() { + return this.uiLiquidVisible; + } + + public void setUiLiquidVisible(boolean value) { + this.uiLiquidVisible = value; + } + + public boolean isDebugSpinMode() { + return this.debugSpinMode; + } + + public void setDebugSpinMode(boolean value) { + this.debugSpinMode = value; + } + + public String getDestroyedSkinTintHex() { + return sanitizeHexColor(this.destroyedSkinTintHex); + } + + public void setDestroyedSkinTintHex(String value) { + this.destroyedSkinTintHex = sanitizeHexColor(value); + } + + public boolean hasDestroyedSkinTintOverride() { + return !this.getDestroyedSkinTintHex().isEmpty(); + } + + public int getDestroyedSkinTintRgb(int fallbackRgb) { + String hex = this.getDestroyedSkinTintHex(); + if (hex.isEmpty()) { + return fallbackRgb & 16777215; + } + + try { + return Integer.parseUnsignedInt(hex, 16) & 16777215; + } catch (NumberFormatException e) { + return fallbackRgb & 16777215; + } + } + + public Map getGenderSpawnByEntity() { + if (this.genderSpawnByEntity == null) { + return Map.of(); + } + + Map out = new LinkedHashMap<>(); + + for (Entry entry : this.genderSpawnByEntity.entrySet()) { + if (entry != null) { + String key = sanitizeKey(entry.getKey()); + if (key != null) { + NonConfig.GenderSpawnChances chances = sanitizeGenderSpawnChances(entry.getValue()); + if (chances != null) { + out.put(key, chances); + } + } + } + } + + return Map.copyOf(out); + } + + public void setGenderSpawnByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = sanitizeKey(entry.getKey()); + if (key != null) { + NonConfig.GenderSpawnChances chances = sanitizeGenderSpawnChances(entry.getValue()); + if (chances != null) { + cleaned.put(key, chances); + } + } + } + } + } + + this.genderSpawnByEntity = cleaned; + } + + public Map getLiquidGainByEntity() { + return this.liquidGainByEntity == null ? Map.of() : Map.copyOf(this.liquidGainByEntity); + } + + public int getLiquidGainFor(String entityId) { + if (entityId != null && this.liquidGainByEntity != null) { + Integer value = this.liquidGainByEntity.get(entityId); + return value == null ? 0 : Math.max(0, Math.min(1000, value)); + } else { + return 0; + } + } + + public void setLiquidGainByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + if (key != null) { + key = key.trim(); + if (!key.isEmpty()) { + int value = entry.getValue() == null ? 0 : entry.getValue(); + value = Math.max(0, Math.min(1000, value)); + cleaned.put(key, value); + } + } + } + } + } + + this.liquidGainByEntity = cleaned; + } + + public Map getLiquidColorByEntity() { + return this.liquidColorByEntity == null ? Map.of() : Map.copyOf(this.liquidColorByEntity); + } + + public void setLiquidColorByEntity(Map entries) { + Map cleaned = new LinkedHashMap<>(); + if (entries != null) { + for (Entry entry : entries.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + if (key != null) { + key = key.trim(); + if (!key.isEmpty()) { + String normalizedHex = NonLiquidColors.normalizeHexColor(entry.getValue()); + if (normalizedHex != null) { + cleaned.put(key, normalizedHex); + } + } + } + } + } + } + + this.liquidColorByEntity = cleaned; + } + + public Integer getLiquidColorOverrideRgb(String entityId) { + return entityId != null && this.liquidColorByEntity != null ? NonLiquidColors.resolveEntityOverrideFromMap(this.liquidColorByEntity, entityId) : null; + } + + public List getExternalNoNPackLoadOrder() { + return this.externalNoNPackLoadOrder == null ? List.of() : List.copyOf(this.externalNoNPackLoadOrder); + } + + public void setExternalNoNPackLoadOrder(List order) { + this.externalNoNPackLoadOrder = sanitizePackLoadOrder(order); + } + + public List getDisabledAnimationPacks() { + return this.disabledAnimationPacks == null ? List.of() : List.copyOf(this.disabledAnimationPacks); + } + + public void setDisabledAnimationPacks(List packIds) { + this.disabledAnimationPacks = sanitizeStringList(packIds); + } + + public List getDisabledAnimations() { + return this.disabledAnimations == null ? List.of() : List.copyOf(this.disabledAnimations); + } + + public void setDisabledAnimations(List animationIds) { + this.disabledAnimations = sanitizeStringList(animationIds); + } + + public List getAcknowledgedStartupWarnings() { + return this.acknowledgedStartupWarnings == null ? List.of() : List.copyOf(this.acknowledgedStartupWarnings); + } + + public void setAcknowledgedStartupWarnings(List warningIds) { + this.acknowledgedStartupWarnings = sanitizeAcknowledgedStartupWarnings(warningIds); + } + + public boolean isStartupWarningAcknowledged(String warningId) { + String normalized = sanitizeWarningId(warningId); + return normalized != null && this.getAcknowledgedStartupWarnings().contains(normalized); + } + + public boolean acknowledgeStartupWarning(String warningId) { + String normalized = sanitizeWarningId(warningId); + if (normalized == null) { + return false; + } + + LinkedHashSet merged = new LinkedHashSet<>(this.getAcknowledgedStartupWarnings()); + if (!merged.add(normalized)) { + return false; + } + + this.setAcknowledgedStartupWarnings(new ArrayList<>(merged)); + return true; + } + + private static List sanitizePackLoadOrder(List order) { + if (order != null && !order.isEmpty()) { + LinkedHashSet cleaned = new LinkedHashSet<>(); + + for (String raw : order) { + if (raw != null) { + String trimmed = raw.trim(); + if (!trimmed.isEmpty()) { + cleaned.add(trimmed); + } + } + } + + return new ArrayList<>(cleaned); + } else { + return new ArrayList<>(); + } + } + + private static List sanitizeStringList(List values) { + if (values != null && !values.isEmpty()) { + LinkedHashSet cleaned = new LinkedHashSet<>(); + + for (String raw : values) { + if (raw != null) { + String trimmed = raw.trim(); + if (!trimmed.isEmpty()) { + cleaned.add(trimmed); + } + } + } + + return new ArrayList<>(cleaned); + } else { + return new ArrayList<>(); + } + } + + private static List sanitizeAcknowledgedStartupWarnings(List warningIds) { + if (warningIds != null && !warningIds.isEmpty()) { + LinkedHashSet cleaned = new LinkedHashSet<>(); + + for (String raw : warningIds) { + String normalized = sanitizeWarningId(raw); + if (normalized != null) { + cleaned.add(normalized); + } + } + + return cleaned.stream().sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.toCollection(ArrayList::new)); + } else { + return new ArrayList<>(); + } + } + + private static String sanitizeWarningId(String raw) { + if (raw == null) { + return null; + } + + String value = raw.trim(); + return value.isEmpty() ? null : value; + } + + private int clampUiOffset(int value) { + return Math.max(-999, Math.min(999, value)); + } + + private float clampMultiplier(float value) { + return !Float.isFinite(value) ? 1.0F : Math.max(0.1F, Math.min(1.0F, value)); + } + + private static int clampPercent(int value) { + return Math.max(0, Math.min(100, value)); + } + + private static int clampInt(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private static int valueOrDefault(int value, int fallback) { + return value <= 0 ? fallback : value; + } + + private static String sanitizeIntifaceServerUrl(String raw) { + if (raw != null && !raw.isBlank()) { + String value = raw.trim(); + + try { + URI uri = new URI(value); + String scheme = uri.getScheme(); + if (scheme == null) { + return "ws://127.0.0.1:12345"; + } else if (!scheme.equalsIgnoreCase("ws") && !scheme.equalsIgnoreCase("wss")) { + return "ws://127.0.0.1:12345"; + } else { + return uri.getHost() != null && !uri.getHost().isBlank() ? value : "ws://127.0.0.1:12345"; + } + } catch (URISyntaxException e) { + return "ws://127.0.0.1:12345"; + } + } else { + return "ws://127.0.0.1:12345"; + } + } + + private static float clampFloat(float value, float min, float max) { + return !Float.isFinite(value) ? min : Math.max(min, Math.min(max, value)); + } + + public static NonConfig.GenderSpawnChances defaultGenderSpawnChances() { + return new NonConfig.GenderSpawnChances(48, 47, 5); + } + + public static NonConfig.GenderSpawnChances validOrDefault(NonConfig.GenderSpawnChances chances) { + NonConfig.GenderSpawnChances safe = sanitizeGenderSpawnChances(chances); + return safe == null ? defaultGenderSpawnChances() : safe; + } + + public static NonConfig.GenderSpawnChances sanitizeGenderSpawnChances(NonConfig.GenderSpawnChances chances) { + if (chances == null) { + return null; + } + + NonConfig.GenderSpawnChances safe = chances.sanitized(); + return safe.isValid() ? safe : null; + } + + public static String normalizeBirthMode(String value) { + if (value == null) { + return null; + } else { + String normalized = value.trim().toLowerCase(Locale.ROOT); + if ("egg".equals(normalized)) { + return "egg"; + } else { + return "direct".equals(normalized) ? "direct" : null; + } + } + } + + public static String normalizeEntityIdString(String value) { + if (value == null) { + return null; + } + + String normalized = value.trim(); + return normalized.isEmpty() ? null : normalized; + } + + public static String normalizeTextureIdString(String value) { + if (value == null) { + return null; + } + + String normalized = value.trim(); + if (normalized.isEmpty()) { + return null; + } + + Identifier parsed = Identifier.tryParse(normalized); + return parsed != null && !parsed.getPath().isBlank() ? normalized : null; + } + + private static String sanitizeKey(String raw) { + if (raw == null) { + return null; + } + + String key = raw.trim(); + return key.isEmpty() ? null : key; + } + + private static String sanitizeHexColor(String raw) { + if (raw == null) { + return ""; + } + + String value = raw.trim(); + if (value.startsWith("#")) { + value = value.substring(1); + } + + value = value.toUpperCase(Locale.ROOT); + if (value.isEmpty()) { + return ""; + } else { + return value.matches("[0-9A-F]{6}") ? value : ""; + } + } + + private static Map defaultPregnancyEggEnabledMap() { + Map map = new LinkedHashMap<>(); + map.put("minecraft:spider", true); + map.put("minecraft:cave_spider", true); + return map; + } + + private static Map defaultPregnancyOffspringCountMap() { + Map map = new LinkedHashMap<>(); + map.put("minecraft:spider", new NonConfig.OffspringCountRange(1, 3)); + map.put("minecraft:cave_spider", new NonConfig.OffspringCountRange(2, 5)); + map.put("minecraft:wolf", new NonConfig.OffspringCountRange(1, 2)); + map.put("minecraft:horse", new NonConfig.OffspringCountRange(1, 1)); + map.put("minecraft:fox", new NonConfig.OffspringCountRange(1, 2)); + map.put("minecraft:dolphin", new NonConfig.OffspringCountRange(1, 1)); + map.put("minecraft:pig", new NonConfig.OffspringCountRange(1, 3)); + return map; + } + + private static Map defaultGenderSpawnByEntityMap() { + Map map = new LinkedHashMap<>(); + NonConfig.GenderSpawnChances maleOnly = new NonConfig.GenderSpawnChances(100, 0, 0); + map.put("minecraft:spider", maleOnly); + map.put("minecraft:cave_spider", maleOnly); + return map; + } + + private void ensureDefaults() { + if (this.debugChatMode == null) { + this.debugChatMode = NonDebugChatMode.SETUP_ERRORS.id(); + } else { + this.debugChatMode = this.debugChatMode().id(); + } + + this.animationBlockSearchRadius = this.blockSearchRadius(); + this.pregnancyChancePercent = this.getPregnancyChancePercent(); + this.pregnancyDurationMinutes = this.getPregnancyDurationMinutes(); + this.pregnancyRiskyStartDay = this.getPregnancyRiskyStartDay(); + this.pregnancyOvulationEndDay = this.getPregnancyOvulationEndDay(); + this.pregnancySafeDayChancePercent = this.getPregnancySafeDayChancePercent(); + if (this.pregnancyRiskyChancePercent == null) { + if (this.pregnancyOvulationChancePercent == 500) { + this.pregnancyRiskyChancePercent = 300; + } else { + this.pregnancyRiskyChancePercent = clampInt(this.pregnancyOvulationChancePercent, 0, 1000); + this.pregnancyOvulationChancePercent = 500; + } + } + + this.pregnancyRiskyChancePercent = this.getPregnancyRiskyChancePercent(); + this.pregnancyOvulationChancePercent = this.getPregnancyOvulationChancePercent(); + this.pregnancyChanceByEntity = new LinkedHashMap<>(); + this.pregnancyOffspringCountByEntity = new LinkedHashMap<>(); + this.pregnancyEggDefaultHatchSeconds = this.getPregnancyEggDefaultHatchSeconds(); + this.pregnancyEggEnabledByEntity = new LinkedHashMap<>(); + this.pregnancyEggHatchSecondsByEntity = new LinkedHashMap<>(); + this.pregnancyAutoTameMobs = this.isPregnancyAutoTameMobs(); + this.pregnancyFriendlyMobsIgnorePlayers = this.pregnancyFriendlyMobsIgnorePlayers(); + this.setEntityProfilesByEntity(this.entityProfilesByEntity); + Map mergedGains = new LinkedHashMap<>(); + if (this.liquidGainByEntity != null) { + this.setLiquidGainByEntity(this.liquidGainByEntity); + mergedGains.putAll(this.liquidGainByEntity); + } + + this.liquidGainByEntity = mergedGains; + Map mergedColors = new LinkedHashMap<>(); + if (this.liquidColorByEntity != null) { + this.setLiquidColorByEntity(this.liquidColorByEntity); + mergedColors.putAll(this.liquidColorByEntity); + } + + this.liquidColorByEntity = mergedColors; + this.setExternalNoNPackLoadOrder(this.externalNoNPackLoadOrder); + this.setDisabledAnimationPacks(this.disabledAnimationPacks); + this.setDisabledAnimations(this.disabledAnimations); + this.destroyedSkinTintHex = this.getDestroyedSkinTintHex(); + this.liquidPuddleDespawnSeconds = this.getLiquidPuddleDespawnSeconds(); + this.peakXpPerMl = this.getPeakXpPerMl(); + this.initialEnergyMax = this.getInitialEnergyMax(); + this.energyGainRate = this.getEnergyGainRate(); + this.nearAnimationEnergyGainMult = this.getNearAnimationEnergyGainMult(); + this.playerEnergyAuraMultLow = this.getPlayerEnergyAuraMultLow(); + this.playerEnergyAuraMultMid = this.getPlayerEnergyAuraMultMid(); + this.playerEnergyAuraMultHigh = this.getPlayerEnergyAuraMultHigh(); + this.destroyedSkinAuraMultStage1 = this.getDestroyedSkinAuraMultStage1(); + this.destroyedSkinAuraMultStage2 = this.getDestroyedSkinAuraMultStage2(); + this.destroyedSkinAuraMultStage3 = this.getDestroyedSkinAuraMultStage3(); + this.destroyedSkinAuraMultStage4 = this.getDestroyedSkinAuraMultStage4(); + this.normalDamageDestroyedSkinChancePercent = this.getNormalDamageDestroyedSkinChancePercent(); + this.lastDefeatedEnabled = this.isLastDefeatedEnabled(); + this.lastDefeatedEnergyThreshold = this.getLastDefeatedEnergyThreshold(); + this.lastDefeatedSearchRadius = this.getLastDefeatedSearchRadius(); + this.lastDefeatedCooldownSeconds = this.getLastDefeatedCooldownSeconds(); + this.playerEnergyAuraRadiusMin = this.getPlayerEnergyAuraRadiusMin(); + this.playerEnergyAuraRadiusMax = this.getPlayerEnergyAuraRadiusMax(); + this.playerEnergyAuraPulseTicks = this.getPlayerEnergyAuraPulseTicks(); + this.genderSpawnByEntity = new LinkedHashMap<>(); + this.setGlobalGenderSpawnChances(this.getGlobalGenderSpawnChances()); + this.maleMaleChancePercent = this.getMaleMaleChancePercent(); + this.femaleFemaleChancePercent = this.getFemaleFemaleChancePercent(); + this.entityEntityChancePercent = this.getEntityEntityChancePercent(); + this.multiActorJoinChancePercent = this.getMultiActorJoinChancePercent(); + this.setPlayerGenderSelection(this.getPlayerGenderSelection()); + this.allowPlayerGenderChangeAnytime = this.allowPlayerGenderChangeAnytime(); + this.requirePlayerGenderSelectionOnJoin = this.requirePlayerGenderSelectionOnJoin(); + this.setAllowedStartingGenderMask(this.allowedStartingGenderMask); + this.setAllowedPlayerInteractionProfilesMask(this.allowedPlayerInteractionProfilesMask); + this.playerRequestMinEnergy = this.getPlayerRequestMinEnergy(); + this.setAcknowledgedStartupWarnings(this.acknowledgedStartupWarnings); + this.messSystemEnabled = this.isMessSystemEnabled(); + this.destroyedSkinSystemEnabled = this.isDestroyedSkinSystemEnabled(); + this.allowCraftingTableSkinRepair = this.isCraftingTableSkinRepairAllowed(); + this.keepMessAfterDeath = this.keepMessAfterDeath(); + this.keepRippedSkinAfterDeath = this.keepRippedSkinAfterDeath(); + this.keepLiquidTankAfterDeath = this.keepLiquidTankAfterDeath(); + this.keepPregnancyAfterDeath = this.keepPregnancyAfterDeath(); + this.armorShedEffectEnabled = this.isArmorShedEffectEnabled(); + this.syncFemaleGenderModWithNonGender = this.isSyncFemaleGenderModWithNonGender(); + this.destroyedSkinFemaleGenderOverridesEnabled = this.isDestroyedSkinFemaleGenderOverridesEnabled(); + this.setFemaleGenderDestroyedLow(this.femaleGenderDestroyedLow); + this.setFemaleGenderDestroyedHigh(this.femaleGenderDestroyedHigh); + this.actionSoundVolumePercent = this.getActionSoundVolumePercent(); + this.intifaceServerUrl = this.getIntifaceServerUrl(); + this.intifaceReactiveImpactStrengthPercent = this.getIntifaceReactiveImpactStrengthPercent(); + this.intifaceReactiveImpactDurationMs = this.getIntifaceReactiveImpactDurationMs(); + this.intifacePeakReactiveImpactStrengthPercent = this.getIntifacePeakReactiveImpactStrengthPercent(); + this.intifacePeakReactiveImpactDurationMs = this.getIntifacePeakReactiveImpactDurationMs(); + this.intifaceCooldownMs = this.getIntifaceCooldownMs(); + this.intifaceLatencyCompensationMs = this.getIntifaceLatencyCompensationMs(); + this.intifaceAnimationBaselineStrengthPercent = this.getIntifaceAnimationBaselineStrengthPercent(); + this.intifaceOscillatorRegularSpeedPercent = this.getIntifaceOscillatorRegularSpeedPercent(); + this.intifaceOscillatorPeakSpeedPercent = this.getIntifaceOscillatorPeakSpeedPercent(); + this.intifaceStrokerMaxDistancePercent = this.getIntifaceStrokerMaxDistancePercent(); + this.intifaceStrokerMinDistancePercent = this.getIntifaceStrokerMinDistancePercent(); + this.intifaceStrokerRegularMoveDurationMs = this.getIntifaceStrokerRegularMoveDurationMs(); + this.intifaceStrokerPeakMoveDurationMs = this.getIntifaceStrokerPeakMoveDurationMs(); + this.intifaceEnergizedOneBasePercent = this.getIntifaceEnergizedBasePercent(1); + this.intifaceEnergizedOnePulsePercent = this.getIntifaceEnergizedPulsePercent(1); + this.intifaceEnergizedTwoBasePercent = this.getIntifaceEnergizedBasePercent(2); + this.intifaceEnergizedTwoPulsePercent = this.getIntifaceEnergizedPulsePercent(2); + this.intifaceEnergizedThreeBasePercent = this.getIntifaceEnergizedBasePercent(3); + this.intifaceEnergizedThreePulsePercent = this.getIntifaceEnergizedPulsePercent(3); + this.attackEscapeHits = this.getAttackEscapeHits(); + this.attackDecayPerSecond = this.getAttackDecayPerSecond(); + this.attackEscapeDamageDifficultyPercent = this.getAttackEscapeDamageDifficultyPercent(); + this.pvpFullHealthEscapeBonusPercent = this.getPvpFullHealthEscapeBonusPercent(); + this.pvpCriticalHealthMashStrengthPercent = this.getPvpCriticalHealthMashStrengthPercent(); + this.pvpMaxHeatEscapeStrengthPercent = this.getPvpMaxHeatEscapeStrengthPercent(); + this.pvpZeroHeatAttackStrengthPercent = this.getPvpZeroHeatAttackStrengthPercent(); + if (this.attackEscapeInvulnerabilityTicks != null) { + this.attackEscapeInvulnerabilitySeconds = ticksToSeconds(this.attackEscapeInvulnerabilityTicks); + this.attackEscapeInvulnerabilityTicks = null; + } + + this.attackEscapeInvulnerabilitySeconds = this.getAttackEscapeInvulnerabilitySeconds(); + this.attackEscapeAnimationProtectionSeconds = this.getAttackEscapeAnimationProtectionSeconds(); + this.postEscapeGatherMaxMobs = this.getPostEscapeGatherMaxMobs(); + this.blockAnimationsWhileRidingLivingEntities = this.blockAnimationsWhileRidingLivingEntities(); + this.blockAnimationsWhileRidingVehicles = this.blockAnimationsWhileRidingVehicles(); + this.attackOutcomeFailsafeThreshold = this.getAttackOutcomeFailsafeThreshold(); + this.filledStageOneSpeedMult = this.clampMultiplier(this.filledStageOneSpeedMult); + this.filledStageTwoSpeedMult = this.clampMultiplier(this.filledStageTwoSpeedMult); + this.filledStageThreeSpeedMult = this.clampMultiplier(this.filledStageThreeSpeedMult); + this.filledStageOneJumpMult = this.clampMultiplier(this.filledStageOneJumpMult); + this.filledStageTwoJumpMult = this.clampMultiplier(this.filledStageTwoJumpMult); + this.filledStageThreeJumpMult = this.clampMultiplier(this.filledStageThreeJumpMult); + } + + public static NonConfig load() { + NonConfig cfg = new NonConfig(); + boolean saveNormalizedConfig = !Files.isRegularFile(PATH); + if (Files.isRegularFile(PATH)) { + try (Reader reader = Files.newBufferedReader(PATH, StandardCharsets.UTF_8)) { + NonConfig fromDisk = (NonConfig)GSON.fromJson(reader, NonConfig.class); + if (fromDisk != null) { + cfg = fromDisk; + } + + saveNormalizedConfig = true; + } catch (JsonParseException | IllegalStateException e) { + Path backup = preserveInvalidConfig(); + NeedsOfNature.LOGGER + .warn( + "[NoN] Invalid config at {}. Using defaults{}.", new Object[]{PATH, backup == null ? "" : " and preserving the invalid file as " + backup, e} + ); + saveNormalizedConfig = backup != null; + } catch (IOException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to read config at {}. Using defaults.", PATH, e); + } + } + + cfg.ensureDefaults(); + if (saveNormalizedConfig) { + cfg.save(); + } + + return cfg; + } + + public NonConfig copy() { + NonConfig copy = (NonConfig)GSON.fromJson(GSON.toJson(this), NonConfig.class); + if (copy == null) { + copy = new NonConfig(); + } + + copy.ensureDefaults(); + return copy; + } + + public void save() { + Path temporary = PATH.resolveSibling(PATH.getFileName() + ".tmp"); + + try { + Files.createDirectories(PATH.getParent()); + + try (Writer writer = Files.newBufferedWriter(temporary, StandardCharsets.UTF_8)) { + GSON.toJson(this, writer); + } + + try { + Files.move(temporary, PATH, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException ignored) { + Files.move(temporary, PATH, StandardCopyOption.REPLACE_EXISTING); + } + + NeedsOfNature.onConfigSaved(this); + } catch (IOException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to save config at {}.", PATH, e); + + try { + Files.deleteIfExists(temporary); + } catch (IOException var5) { + } + } + } + + private static Path preserveInvalidConfig() { + Path backup = PATH.resolveSibling(PATH.getFileName() + ".invalid-" + System.currentTimeMillis() + ".json"); + + try { + Files.move(PATH, backup, StandardCopyOption.REPLACE_EXISTING); + return backup; + } catch (IOException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to preserve invalid config at {}.", PATH, e); + return null; + } + } + + public static final class EggProfile { + private Float startSize; + private Float endSize; + private String texture; + private Float health; + + public EggProfile() { + } + + public EggProfile(Float startSize, Float endSize, String texture, Float health) { + this.startSize = startSize; + this.endSize = endSize; + this.texture = texture; + this.health = health; + } + + public static NonConfig.EggProfile defaults() { + return new NonConfig.EggProfile(0.5F, 1.0F, null, 2.0F); + } + + public Float startSize() { + return this.startSize == null ? null : NonConfig.clampFloat(this.startSize, 0.05F, 4.0F); + } + + public Float endSize() { + return this.endSize == null ? null : NonConfig.clampFloat(this.endSize, 0.05F, 4.0F); + } + + public String texture() { + return NonConfig.normalizeTextureIdString(this.texture); + } + + public Float health() { + return this.health == null ? null : NonConfig.clampFloat(this.health, 0.5F, 100.0F); + } + + public NonConfig.EggProfile sanitized() { + return new NonConfig.EggProfile(this.startSize(), this.endSize(), this.texture(), this.health()); + } + + public NonConfig.EggProfile resolvedOver(NonConfig.EggProfile fallback) { + NonConfig.EggProfile base = fallback == null ? defaults() : fallback.sanitized(); + NonConfig.EggProfile safe = this.sanitized(); + return new NonConfig.EggProfile( + safe.startSize() == null ? base.startSize() : safe.startSize(), + safe.endSize() == null ? base.endSize() : safe.endSize(), + safe.texture() == null ? base.texture() : safe.texture(), + safe.health() == null ? base.health() : safe.health() + ) + .sanitized(); + } + + public boolean isEmpty() { + return this.startSize() == null && this.endSize() == null && this.texture() == null && this.health() == null; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof NonConfig.EggProfile other)) { + return false; + } else { + NonConfig.EggProfile a = this.sanitized(); + NonConfig.EggProfile b = other.sanitized(); + return Objects.equals(a.startSize, b.startSize) + && Objects.equals(a.endSize, b.endSize) + && Objects.equals(a.texture, b.texture) + && Objects.equals(a.health, b.health); + } + } + + @Override + public int hashCode() { + NonConfig.EggProfile safe = this.sanitized(); + return Objects.hash(safe.startSize, safe.endSize, safe.texture, safe.health); + } + } + + public static final class EntityProfile { + private Integer pregnancyChancePercent; + private Integer offspringMin; + private Integer offspringMax; + private String birthEntity; + private String birthMode; + private NonConfig.GenderSpawnChances genderSpawn; + private NonConfig.EggProfile egg; + private Boolean lookAtNearbyAnimations; + private Integer multiActorJoinChancePercent; + private Float energyGainMultiplier; + + public EntityProfile() { + } + + public EntityProfile( + Integer pregnancyChancePercent, + Integer offspringMin, + Integer offspringMax, + String birthEntity, + String birthMode, + NonConfig.GenderSpawnChances genderSpawn + ) { + this(pregnancyChancePercent, offspringMin, offspringMax, birthEntity, birthMode, genderSpawn, null, null, null, null); + } + + public EntityProfile( + Integer pregnancyChancePercent, + Integer offspringMin, + Integer offspringMax, + String birthEntity, + String birthMode, + NonConfig.GenderSpawnChances genderSpawn, + NonConfig.EggProfile egg + ) { + this(pregnancyChancePercent, offspringMin, offspringMax, birthEntity, birthMode, genderSpawn, egg, null, null, null); + } + + public EntityProfile( + Integer pregnancyChancePercent, + Integer offspringMin, + Integer offspringMax, + String birthEntity, + String birthMode, + NonConfig.GenderSpawnChances genderSpawn, + NonConfig.EggProfile egg, + Boolean lookAtNearbyAnimations, + Integer multiActorJoinChancePercent, + Float energyGainMultiplier + ) { + this.pregnancyChancePercent = pregnancyChancePercent; + this.offspringMin = offspringMin; + this.offspringMax = offspringMax; + this.birthEntity = birthEntity; + this.birthMode = birthMode; + this.genderSpawn = genderSpawn; + this.egg = egg; + this.lookAtNearbyAnimations = lookAtNearbyAnimations; + this.multiActorJoinChancePercent = multiActorJoinChancePercent; + this.energyGainMultiplier = energyGainMultiplier; + } + + public static NonConfig.EntityProfile full( + int pregnancyChancePercent, int offspringMin, int offspringMax, String birthEntity, String birthMode, NonConfig.GenderSpawnChances genderSpawn + ) { + return full(pregnancyChancePercent, offspringMin, offspringMax, birthEntity, birthMode, genderSpawn, null, true, null, 1.0F); + } + + public static NonConfig.EntityProfile full( + int pregnancyChancePercent, + int offspringMin, + int offspringMax, + String birthEntity, + String birthMode, + NonConfig.GenderSpawnChances genderSpawn, + NonConfig.EggProfile egg + ) { + return full(pregnancyChancePercent, offspringMin, offspringMax, birthEntity, birthMode, genderSpawn, egg, true, null, 1.0F); + } + + public static NonConfig.EntityProfile full( + int pregnancyChancePercent, + int offspringMin, + int offspringMax, + String birthEntity, + String birthMode, + NonConfig.GenderSpawnChances genderSpawn, + NonConfig.EggProfile egg, + boolean lookAtNearbyAnimations, + Integer multiActorJoinChancePercent, + float energyGainMultiplier + ) { + return new NonConfig.EntityProfile( + NonConfig.clampPercent(pregnancyChancePercent), + NonConfig.clampInt(offspringMin, 1, 16), + NonConfig.clampInt(offspringMax, 1, 16), + NonConfig.normalizeEntityIdString(birthEntity), + NonConfig.normalizeBirthMode(birthMode), + NonConfig.validOrDefault(genderSpawn), + egg == null ? null : egg.sanitized(), + lookAtNearbyAnimations, + multiActorJoinChancePercent, + energyGainMultiplier + ) + .sanitized(); + } + + public Integer pregnancyChancePercent() { + return this.pregnancyChancePercent == null ? null : NonConfig.clampPercent(this.pregnancyChancePercent); + } + + public Integer offspringMin() { + return this.offspringMin == null ? null : NonConfig.clampInt(this.offspringMin, 1, 16); + } + + public Integer offspringMax() { + return this.offspringMax == null ? null : NonConfig.clampInt(this.offspringMax, 1, 16); + } + + public String birthEntity() { + return NonConfig.normalizeEntityIdString(this.birthEntity); + } + + public String birthMode() { + return NonConfig.normalizeBirthMode(this.birthMode); + } + + public NonConfig.GenderSpawnChances genderSpawn() { + return NonConfig.sanitizeGenderSpawnChances(this.genderSpawn); + } + + public NonConfig.EggProfile egg() { + NonConfig.EggProfile safe = this.egg == null ? null : this.egg.sanitized(); + return safe != null && !safe.isEmpty() ? safe : null; + } + + public Boolean lookAtNearbyAnimations() { + return this.lookAtNearbyAnimations; + } + + public Integer multiActorJoinChancePercent() { + return this.multiActorJoinChancePercent == null ? null : NonConfig.clampPercent(this.multiActorJoinChancePercent); + } + + public Float energyGainMultiplier() { + return this.energyGainMultiplier == null ? null : NonConfig.clampFloat(this.energyGainMultiplier, 0.1F, 10.0F); + } + + public NonConfig.EntityProfile sanitized() { + Integer safeMin = this.offspringMin(); + Integer safeMax = this.offspringMax(); + if (safeMin != null && safeMax != null && safeMax < safeMin) { + safeMax = safeMin; + } + + return new NonConfig.EntityProfile( + this.pregnancyChancePercent(), + safeMin, + safeMax, + this.birthEntity(), + this.birthMode(), + this.genderSpawn(), + this.egg(), + this.lookAtNearbyAnimations(), + this.multiActorJoinChancePercent(), + this.energyGainMultiplier() + ); + } + + public boolean isEmpty() { + return this.pregnancyChancePercent() == null + && this.offspringMin() == null + && this.offspringMax() == null + && this.birthEntity() == null + && this.birthMode() == null + && this.genderSpawn() == null + && this.egg() == null + && this.lookAtNearbyAnimations() == null + && this.multiActorJoinChancePercent() == null + && this.energyGainMultiplier() == null; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof NonConfig.EntityProfile other)) { + return false; + } else { + NonConfig.EntityProfile a = this.sanitized(); + NonConfig.EntityProfile b = other.sanitized(); + return Objects.equals(a.pregnancyChancePercent, b.pregnancyChancePercent) + && Objects.equals(a.offspringMin, b.offspringMin) + && Objects.equals(a.offspringMax, b.offspringMax) + && Objects.equals(a.birthEntity, b.birthEntity) + && Objects.equals(a.birthMode, b.birthMode) + && Objects.equals(a.genderSpawn, b.genderSpawn) + && Objects.equals(a.egg, b.egg) + && Objects.equals(a.lookAtNearbyAnimations, b.lookAtNearbyAnimations) + && Objects.equals(a.multiActorJoinChancePercent, b.multiActorJoinChancePercent) + && Objects.equals(a.energyGainMultiplier, b.energyGainMultiplier); + } + } + + @Override + public int hashCode() { + NonConfig.EntityProfile safe = this.sanitized(); + return Objects.hash( + safe.pregnancyChancePercent, + safe.offspringMin, + safe.offspringMax, + safe.birthEntity, + safe.birthMode, + safe.genderSpawn, + safe.egg, + safe.lookAtNearbyAnimations, + safe.multiActorJoinChancePercent, + safe.energyGainMultiplier + ); + } + } + + public static final class FemaleGenderDestroyedProfile { + private float breastSize; + private float separation; + private float height; + private float depth; + private float rotation; + private boolean breastPhysics; + private boolean dualPhysics; + private float intensity; + private float momentum; + + public FemaleGenderDestroyedProfile() { + this(0.8F, 0.0F, 0.5F, -0.5F, 0.0F, true, false, 0.25F, 0.625F); + } + + public FemaleGenderDestroyedProfile( + float breastSize, + float separation, + float height, + float depth, + float rotation, + boolean breastPhysics, + boolean dualPhysics, + float intensity, + float momentum + ) { + this.breastSize = NonConfig.clampFloat(breastSize, 0.0F, 1.6F); + this.separation = NonConfig.clampFloat(separation, -1.0F, 1.0F); + this.height = NonConfig.clampFloat(height, -2.0F, 1.0F); + this.depth = NonConfig.clampFloat(depth, -1.0F, 0.0F); + this.rotation = NonConfig.clampFloat(rotation, 0.0F, 0.1F); + this.breastPhysics = breastPhysics; + this.dualPhysics = dualPhysics; + this.intensity = NonConfig.clampFloat(intensity, 0.0F, 0.5F); + this.momentum = NonConfig.clampFloat(momentum, 0.25F, 1.0F); + } + + public static NonConfig.FemaleGenderDestroyedProfile lowDefault() { + return new NonConfig.FemaleGenderDestroyedProfile(0.8F, 0.0F, 0.5F, -0.5F, 0.0F, true, false, 0.25F, 0.625F); + } + + public static NonConfig.FemaleGenderDestroyedProfile highDefault() { + return new NonConfig.FemaleGenderDestroyedProfile(0.8F, -0.1F, 0.2F, 0.0F, 0.06F, true, true, 0.5F, 1.0F); + } + + public NonConfig.FemaleGenderDestroyedProfile sanitized() { + return new NonConfig.FemaleGenderDestroyedProfile( + this.breastSize, this.separation, this.height, this.depth, this.rotation, this.breastPhysics, this.dualPhysics, this.intensity, this.momentum + ); + } + + public float breastSize() { + return NonConfig.clampFloat(this.breastSize, 0.0F, 1.6F); + } + + public float separation() { + return NonConfig.clampFloat(this.separation, -1.0F, 1.0F); + } + + public float height() { + return NonConfig.clampFloat(this.height, -2.0F, 1.0F); + } + + public float depth() { + return NonConfig.clampFloat(this.depth, -1.0F, 0.0F); + } + + public float rotation() { + return NonConfig.clampFloat(this.rotation, 0.0F, 0.1F); + } + + public boolean breastPhysics() { + return this.breastPhysics; + } + + public boolean dualPhysics() { + return this.dualPhysics; + } + + public float intensity() { + return NonConfig.clampFloat(this.intensity, 0.0F, 0.5F); + } + + public float momentum() { + return NonConfig.clampFloat(this.momentum, 0.25F, 1.0F); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else { + return !(obj instanceof NonConfig.FemaleGenderDestroyedProfile other) + ? false + : Float.compare(this.breastSize(), other.breastSize()) == 0 + && Float.compare(this.separation(), other.separation()) == 0 + && Float.compare(this.height(), other.height()) == 0 + && Float.compare(this.depth(), other.depth()) == 0 + && Float.compare(this.rotation(), other.rotation()) == 0 + && this.breastPhysics() == other.breastPhysics() + && this.dualPhysics() == other.dualPhysics() + && Float.compare(this.intensity(), other.intensity()) == 0 + && Float.compare(this.momentum(), other.momentum()) == 0; + } + } + + @Override + public int hashCode() { + int result = Float.hashCode(this.breastSize()); + result = 31 * result + Float.hashCode(this.separation()); + result = 31 * result + Float.hashCode(this.height()); + result = 31 * result + Float.hashCode(this.depth()); + result = 31 * result + Float.hashCode(this.rotation()); + result = 31 * result + Boolean.hashCode(this.breastPhysics()); + result = 31 * result + Boolean.hashCode(this.dualPhysics()); + result = 31 * result + Float.hashCode(this.intensity()); + return 31 * result + Float.hashCode(this.momentum()); + } + } + + public static final class GenderSpawnChances { + private int male; + private int female; + private int both; + + public GenderSpawnChances() { + this(48, 47, 5); + } + + public GenderSpawnChances(int male, int female, int both) { + this.male = NonConfig.clampPercent(male); + this.female = NonConfig.clampPercent(female); + this.both = NonConfig.clampPercent(both); + } + + public int male() { + return NonConfig.clampPercent(this.male); + } + + public int female() { + return NonConfig.clampPercent(this.female); + } + + public int both() { + return NonConfig.clampPercent(this.both); + } + + public boolean isValid() { + return this.male() + this.female() + this.both() == 100; + } + + public NonConfig.GenderSpawnChances sanitized() { + return new NonConfig.GenderSpawnChances(this.male(), this.female(), this.both()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else { + return !(obj instanceof NonConfig.GenderSpawnChances other) + ? false + : this.male() == other.male() && this.female() == other.female() && this.both() == other.both(); + } + } + + @Override + public int hashCode() { + int result = this.male(); + result = 31 * result + this.female(); + return 31 * result + this.both(); + } + } + + public static final class OffspringCountRange { + private int min; + private int max; + + public OffspringCountRange() { + this(1, 1); + } + + public OffspringCountRange(int min, int max) { + int safeMin = NonConfig.clampInt(min, 1, 16); + int safeMax = NonConfig.clampInt(max, 1, 16); + if (safeMax < safeMin) { + safeMax = safeMin; + } + + this.min = safeMin; + this.max = safeMax; + } + + public int min() { + return NonConfig.clampInt(this.min, 1, 16); + } + + public int max() { + return Math.max(this.min(), NonConfig.clampInt(this.max, 1, 16)); + } + + public NonConfig.OffspringCountRange sanitized() { + return new NonConfig.OffspringCountRange(this.min(), this.max()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else { + return !(obj instanceof NonConfig.OffspringCountRange other) ? false : this.min() == other.min() && this.max() == other.max(); + } + } + + @Override + public int hashCode() { + int result = this.min(); + return 31 * result + this.max(); + } + } + + public enum PlayerGenderSelection { + MALE("male", 1), + FEMALE("female", 2), + BOTH("both", 3); + + private final String id; + private final int mask; + + PlayerGenderSelection(String id, int mask) { + this.id = id; + this.mask = mask; + } + + public String id() { + return this.id; + } + + public int mask() { + return this.mask; + } + + public NonConfig.PlayerGenderSelection next() { + return switch (this) { + case MALE -> FEMALE; + case FEMALE -> BOTH; + case BOTH -> MALE; + }; + } + + public static NonConfig.PlayerGenderSelection fromMask(int mask, NonConfig.PlayerGenderSelection fallback) { + int normalized = mask & 3; + + for (NonConfig.PlayerGenderSelection value : values()) { + if (value.mask == normalized) { + return value; + } + } + + return fallback == null ? FEMALE : fallback; + } + + public static NonConfig.PlayerGenderSelection fromId(String id, NonConfig.PlayerGenderSelection fallback) { + if (id != null) { + String normalized = id.trim().toLowerCase(Locale.ROOT); + + for (NonConfig.PlayerGenderSelection value : values()) { + if (value.id.equals(normalized)) { + return value; + } + } + } + + return fallback == null ? FEMALE : fallback; + } + } +} diff --git a/src/main/java/com/nonid/NonDataAccessoryTrinket.java b/src/main/java/com/nonid/NonDataAccessoryTrinket.java new file mode 100644 index 0000000..8c8abbb --- /dev/null +++ b/src/main/java/com/nonid/NonDataAccessoryTrinket.java @@ -0,0 +1,73 @@ +package com.nonid; + +import com.nonid.data.NonAccessoryBehavior; +import com.nonid.data.NonAccessoryDefinitions; +import dev.emi.trinkets.api.SlotReference; +import dev.emi.trinkets.api.Trinket; +import dev.emi.trinkets.api.TrinketComponent; +import dev.emi.trinkets.api.TrinketsApi; +import java.util.Objects; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.util.Pair; + +public final class NonDataAccessoryTrinket implements Trinket { + public static final NonDataAccessoryTrinket INSTANCE = new NonDataAccessoryTrinket(); + + private NonDataAccessoryTrinket() { + } + + public boolean canEquip(ItemStack stack, SlotReference slot, LivingEntity entity) { + if (stack != null && !stack.isEmpty() && slot != null && entity != null) { + NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem())); + String targetSlot = slot.getId(); + return !NonTrinketsIntegration.isSlotAvailableFor(entity, targetSlot) + ? false + : TrinketsApi.getTrinketComponent(entity).map(component -> canEquipWithCurrent(component, stack, slot, behavior, targetSlot)).orElse(true); + } else { + return false; + } + } + + public void onEquip(ItemStack stack, SlotReference slot, LivingEntity entity) { + NonTrinketsCompat.onAccessoryEquipped(entity, slot); + } + + public void onUnequip(ItemStack stack, SlotReference slot, LivingEntity entity) { + NonTrinketsCompat.onAccessoryUnequipped(entity, slot); + } + + private static boolean canEquipWithCurrent( + TrinketComponent component, ItemStack stack, SlotReference target, NonAccessoryBehavior behavior, String targetSlot + ) { + String exclusiveGroup = behavior.exclusiveGroup(); + + for (Pair pair : component.getAllEquipped()) { + SlotReference otherSlot = (SlotReference)pair.getLeft(); + ItemStack otherStack = (ItemStack)pair.getRight(); + if (otherSlot != null && otherStack != null && !otherStack.isEmpty() && !isSameSlot(otherSlot, target)) { + NonAccessoryBehavior otherBehavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(otherStack.getItem())); + if (otherBehavior.occupiesSlot(targetSlot)) { + return false; + } + + for (String occupied : behavior.occupiesSlots()) { + if (NonAccessoryBehavior.isSlot(otherSlot.getId(), occupied)) { + return false; + } + } + + if (exclusiveGroup != null && !exclusiveGroup.isBlank() && exclusiveGroup.equals(otherBehavior.exclusiveGroup())) { + return false; + } + } + } + + return true; + } + + private static boolean isSameSlot(SlotReference first, SlotReference second) { + return first != null && second != null && first.index() == second.index() && Objects.equals(first.getId(), second.getId()); + } +} diff --git a/src/main/java/com/nonid/NonDebugChatCategory.java b/src/main/java/com/nonid/NonDebugChatCategory.java new file mode 100644 index 0000000..f269502 --- /dev/null +++ b/src/main/java/com/nonid/NonDebugChatCategory.java @@ -0,0 +1,9 @@ +package com.nonid; + +public enum NonDebugChatCategory { + INFO, + WARNING, + SETUP, + ERROR, + ALWAYS; +} diff --git a/src/main/java/com/nonid/NonDebugChatMode.java b/src/main/java/com/nonid/NonDebugChatMode.java new file mode 100644 index 0000000..acdffdc --- /dev/null +++ b/src/main/java/com/nonid/NonDebugChatMode.java @@ -0,0 +1,45 @@ +package com.nonid; + +public enum NonDebugChatMode { + ALL("all"), + SETUP_WARNINGS_ERRORS("setup_warnings_errors"), + SETUP_ERRORS("setup_errors"), + ERRORS_ONLY("errors_only"); + + private final String id; + + NonDebugChatMode(String id) { + this.id = id; + } + + public String id() { + return this.id; + } + + public boolean allows(NonDebugChatCategory category) { + if (category == NonDebugChatCategory.ALWAYS) { + return true; + } + + return switch (this) { + case ALL -> true; + case SETUP_WARNINGS_ERRORS -> category == NonDebugChatCategory.SETUP + || category == NonDebugChatCategory.WARNING + || category == NonDebugChatCategory.ERROR; + case SETUP_ERRORS -> category == NonDebugChatCategory.SETUP || category == NonDebugChatCategory.ERROR; + case ERRORS_ONLY -> category == NonDebugChatCategory.ERROR; + }; + } + + public static NonDebugChatMode fromId(String id, NonDebugChatMode fallback) { + if (id != null) { + for (NonDebugChatMode mode : values()) { + if (mode.id.equalsIgnoreCase(id)) { + return mode; + } + } + } + + return fallback; + } +} diff --git a/src/main/java/com/nonid/NonDefeatedAnimationSelector.java b/src/main/java/com/nonid/NonDefeatedAnimationSelector.java new file mode 100644 index 0000000..f43aa1a --- /dev/null +++ b/src/main/java/com/nonid/NonDefeatedAnimationSelector.java @@ -0,0 +1,142 @@ +package com.nonid; + +import com.nonid.data.NonPeakStages; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.util.WeightedPicker; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; +import net.minecraft.entity.Entity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class NonDefeatedAnimationSelector { + private static final String DEFEATED_TAG_PREFIX = "defeat."; + + private NonDefeatedAnimationSelector() { + } + + @Nullable + static AnimationService.MatchedAnimation find(ServerWorld world, ServerPlayerEntity player, @Nullable Identifier sourceAnimationId) { + if (world != null && player != null && player.getEntityWorld() == world) { + List actors = List.of(player); + List candidates = new ArrayList<>(); + + for (AnimationDefinitions.Definition definition : AnimationDefinitions.findActorMatchingCandidates(actors)) { + if (isDefeatedDefinition(definition) && NonInjectorMatchPolicy.allowsAutomaticNonMatch(world, definition, actors)) { + candidates.add(definition); + } + } + + AnimationDefinitions.Definition picked = pickPreferredCandidate( + candidates, + AnimationDefinitions.Definition::contentTags, + AnimationDefinitions.Definition::weight, + sourceContentTags(sourceAnimationId), + world.getRandom().nextDouble() + ); + if (picked == null) { + return null; + } + + List actorKeys = AnimationDefinitions.resolveActorKeys(picked, actors, world.getRandom()); + return actorKeys.size() != actors.size() + ? null + : new AnimationService.MatchedAnimation(picked.id(), List.copyOf(actorKeys), picked.stages() == null ? List.of() : List.copyOf(picked.stages())); + } else { + return null; + } + } + + @Nullable + static T pickPreferredCandidate( + List candidates, + Function> contentTags, + ToDoubleFunction weight, + Collection sourceContentTags, + double randomValue + ) { + if (candidates != null && !candidates.isEmpty() && contentTags != null && weight != null) { + Set sourceTags = normalizeTags(sourceContentTags); + List preferred = new ArrayList<>(); + + for (T candidate : candidates) { + if (candidate != null) { + Set candidateTags = normalizeTags((Collection)contentTags.apply(candidate)); + if (sharesContentTag(sourceTags, candidateTags)) { + preferred.add(candidate); + } + } + } + + List pool = preferred.isEmpty() ? candidates : preferred; + return WeightedPicker.pick(pool, weight, randomValue); + } else { + return null; + } + } + + private static boolean isDefeatedDefinition(AnimationDefinitions.Definition definition) { + if (definition != null && definition.animationTags() != null) { + for (String tag : definition.animationTags()) { + if (tag != null && tag.startsWith("defeat.")) { + return true; + } + } + + return false; + } else { + return false; + } + } + + private static List sourceContentTags(@Nullable Identifier sourceAnimationId) { + if (sourceAnimationId == null) { + return List.of(); + } + + AnimationDefinitions.Definition source = AnimationDefinitions.getDefinition(NonPeakStages.baseAnimationId(sourceAnimationId)); + return source != null && source.contentTags() != null ? source.contentTags() : List.of(); + } + + private static boolean sharesContentTag(Set first, Set second) { + if (!first.isEmpty() && !second.isEmpty()) { + for (String tag : first) { + if (second.contains(tag)) { + return true; + } + } + + return false; + } else { + return first.isEmpty() && second.isEmpty(); + } + } + + private static Set normalizeTags(Collection tags) { + if (tags != null && !tags.isEmpty()) { + LinkedHashSet normalized = new LinkedHashSet<>(); + + for (String raw : tags) { + if (raw != null) { + String tag = raw.trim().toLowerCase(Locale.ROOT); + if (!tag.isEmpty()) { + normalized.add(tag); + } + } + } + + return normalized.isEmpty() ? Set.of() : Set.copyOf(normalized); + } else { + return Set.of(); + } + } +} diff --git a/src/main/java/com/nonid/NonDestroyedSkinSystem.java b/src/main/java/com/nonid/NonDestroyedSkinSystem.java new file mode 100644 index 0000000..026e7b7 --- /dev/null +++ b/src/main/java/com/nonid/NonDestroyedSkinSystem.java @@ -0,0 +1,697 @@ +package com.nonid; + +import com.nonid.network.DestroyedSkinMaskSyncS2CPayload; +import com.nonid.network.DestroyedSkinMaskUploadC2SPayload; +import com.nonid.network.DestroyedSkinParticlesS2CPayload; +import com.nonid.network.DestroyedSkinStateS2CPayload; +import com.nonid.network.DestroyedSkinSyncS2CPayload; +import com.nonid.network.DestroyedSkinUploadC2SPayload; +import com.nonid.network.ManualDamageDestroyedSkinC2SPayload; +import com.nonid.network.RepairDestroyedSkinC2SPayload; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.registry.Registries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; + +final class NonDestroyedSkinSystem { + private static final int DESTROYED_SKIN_MAX_DAMAGE = 10; + private static final int DESTROYED_SKIN_STAGE_1_DAMAGE = 1; + private static final int DESTROYED_SKIN_STAGE_2_DAMAGE = 4; + private static final int DESTROYED_SKIN_STAGE_3_DAMAGE = 7; + private static final int DESTROYED_SKIN_STAGE_4_DAMAGE = 10; + private static final int DESTROYED_SKIN_REPAIR_COOLDOWN_TICKS = 7; + private static final int UPLOAD_COOLDOWN_TICKS = 20; + private static final Map DESTROYED_SKINS_BY_PLAYER = new ConcurrentHashMap<>(); + private static final Map DESTROYED_SKIN_MASKS_BY_PLAYER = new ConcurrentHashMap<>(); + private static final Map DESTROYED_SKIN_STAGE_BY_PLAYER = new ConcurrentHashMap<>(); + private static final Map DESTROYED_SKIN_REPAIR_COOLDOWNS = new ConcurrentHashMap<>(); + private static final Map NEXT_UPLOAD_TICK = new ConcurrentHashMap<>(); + + private NonDestroyedSkinSystem() { + } + + static void syncDestroyedSkins(ServerPlayerEntity player) { + if (player != null) { + MinecraftServer server = player.getEntityWorld().getServer(); + if (server != null) { + boolean enabled = NeedsOfNature.isDestroyedSkinSystemEnabled(); + + for (ServerPlayerEntity listedPlayer : server.getPlayerManager().getPlayerList()) { + UUID uuid = listedPlayer.getUuid(); + if (!enabled) { + sendDestroyedSkinStage(player, uuid, 0); + } else { + byte[] skin = DESTROYED_SKINS_BY_PLAYER.get(uuid); + if (skin != null && skin.length > 0) { + sendDestroyedSkin(player, uuid, getDestroyedSkinStage(uuid), skin); + } else { + sendDestroyedSkinStage(player, uuid, getDestroyedSkinStage(uuid)); + } + + byte[][] masks = DESTROYED_SKIN_MASKS_BY_PLAYER.get(uuid); + if (masks != null) { + for (int stage = 1; stage < Math.min(masks.length, 4); stage++) { + byte[] bytes = masks[stage]; + if (bytes != null && bytes.length > 0) { + sendDestroyedSkinMask(player, uuid, stage, bytes); + } + } + } + } + } + } + } + } + + static void registerDestroyedSkinNetworking() { + ServerPlayNetworking.registerGlobalReceiver( + DestroyedSkinUploadC2SPayload.ID, + (payload, context) -> context.server() + .execute( + () -> { + ServerPlayerEntity player = context.player(); + if (player != null) { + if (NeedsOfNature.isDestroyedSkinSystemEnabled()) { + byte[] bytes = payload.pngBytes(); + UUID playerUuid = player.getUuid(); + if (acceptUpload(player, new NonDestroyedSkinSystem.UploadKey(playerUuid, 0))) { + if (!isValidDestroyedSkinPayload(bytes)) { + NeedsOfNature.LOGGER + .warn( + "[NoN] Ignoring invalid destroyed skin upload from {} ({} bytes).", + player.getName().getString(), + bytes == null ? 0 : bytes.length + ); + } else { + byte[] previous = DESTROYED_SKINS_BY_PLAYER.get(playerUuid); + if (!Arrays.equals(previous, bytes)) { + DESTROYED_SKINS_BY_PLAYER.put(playerUuid, (byte[])bytes.clone()); + NeedsOfNature.logRuntime( + "[NoN] Received destroyed skin upload from {} ({} bytes, stage {}).", + player.getName().getString(), + bytes.length, + getDestroyedSkinStage(playerUuid) + ); + broadcastDestroyedSkin(context.server(), playerUuid, getDestroyedSkinStage(playerUuid), bytes); + } + } + } + } + } + } + ) + ); + ServerPlayNetworking.registerGlobalReceiver( + DestroyedSkinMaskUploadC2SPayload.ID, + (payload, context) -> context.server() + .execute( + () -> { + ServerPlayerEntity player = context.player(); + if (player != null) { + if (NeedsOfNature.isDestroyedSkinSystemEnabled()) { + int stage = payload.stage(); + byte[] bytes = payload.pngBytes(); + if (stage > 0 && stage < 4 && bytes != null) { + UUID playerUuid = player.getUuid(); + if (acceptUpload(player, new NonDestroyedSkinSystem.UploadKey(playerUuid, stage))) { + byte[][] existingMasks = DESTROYED_SKIN_MASKS_BY_PLAYER.get(playerUuid); + byte[] previous = existingMasks == null ? null : existingMasks[stage]; + if (!Arrays.equals(previous, bytes.length == 0 ? null : bytes)) { + if (bytes.length == 0) { + byte[][] masks = existingMasks; + if (masks != null && stage < masks.length) { + masks[stage] = null; + if (!hasAnyMask(masks)) { + DESTROYED_SKIN_MASKS_BY_PLAYER.remove(playerUuid); + } + } + + broadcastDestroyedSkinMask(context.server(), playerUuid, stage, bytes); + } else if (!isValidDestroyedSkinMaskPayload(bytes)) { + NeedsOfNature.LOGGER + .warn( + "[NoN] Ignoring invalid destroyed skin mask upload from {} (stage {}, {} bytes).", + new Object[]{player.getName().getString(), stage, bytes.length} + ); + } else { + byte[][] masks = DESTROYED_SKIN_MASKS_BY_PLAYER.computeIfAbsent(playerUuid, ignored -> new byte[4][]); + masks[stage] = (byte[])bytes.clone(); + NeedsOfNature.logRuntime( + "[NoN] Received destroyed skin mask upload from {} (stage {}, {} bytes).", + player.getName().getString(), + stage, + bytes.length + ); + broadcastDestroyedSkinMask(context.server(), playerUuid, stage, bytes); + } + } + } + } else { + NeedsOfNature.LOGGER + .warn( + "[NoN] Ignoring invalid destroyed skin mask upload from {} (stage {}, {} bytes).", + new Object[]{player.getName().getString(), stage, bytes == null ? 0 : bytes.length} + ); + } + } + } + } + ) + ); + } + + static void registerDestroyedSkinRepairNetworking() { + ServerPlayNetworking.registerGlobalReceiver( + RepairDestroyedSkinC2SPayload.ID, (payload, context) -> context.server().execute(() -> repairDestroyedSkin(context.player())) + ); + ServerPlayNetworking.registerGlobalReceiver( + ManualDamageDestroyedSkinC2SPayload.ID, (payload, context) -> context.server().execute(() -> manuallyDamageDestroyedSkin(context.player())) + ); + } + + private static void repairDestroyedSkin(ServerPlayerEntity player) { + if (NeedsOfNature.isDestroyedSkinSystemEnabled()) { + if (player != null && player.isAlive() && player instanceof DestroyedSkinHolder holder) { + if (player.getEntityWorld() instanceof ServerWorld world) { + if (NonSkinRepairBlocks.isNearRepairBlock(world, player.getBlockPos(), NeedsOfNature.getConfig())) { + UUID uuid = player.getUuid(); + long now = world.getTime(); + long blockedUntil = DESTROYED_SKIN_REPAIR_COOLDOWNS.getOrDefault(uuid, 0L); + if (now >= blockedUntil) { + int beforeDamage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()); + if (beforeDamage <= 0) { + DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(uuid); + } else { + int afterDamage = clampDestroyedSkinDamage(beforeDamage - 1); + holder.setDestroyedSkinDamage(afterDamage); + setDestroyedSkinStage(player, destroyedSkinStageForDamage(afterDamage)); + playDestroyedSkinRepairSound(world, player); + if (afterDamage > 0) { + DESTROYED_SKIN_REPAIR_COOLDOWNS.put(uuid, now + 7L); + } else { + DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(uuid); + } + } + } + } + } + } + } + } + + private static void manuallyDamageDestroyedSkin(ServerPlayerEntity player) { + if (NeedsOfNature.isDestroyedSkinSystemEnabled()) { + if (player != null && player.isAlive()) { + if (player.getEntityWorld() instanceof ServerWorld world) { + if (hasShearsForManualDestroyedSkinDamage(player)) { + int beforeDamage = player instanceof DestroyedSkinHolder holder ? clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()) : 10; + applyDestroyedSkinAttackDamage(world, player); + int afterDamage = player instanceof DestroyedSkinHolder holder ? clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()) : beforeDamage; + if (afterDamage > beforeDamage) { + playDestroyedSkinShearSound(world, player); + } + } + } + } + } + } + + private static boolean hasShearsForManualDestroyedSkinDamage(ServerPlayerEntity player) { + if (player == null) { + return false; + } + + ItemStack cursorStack = player.currentScreenHandler != null ? player.currentScreenHandler.getCursorStack() : ItemStack.EMPTY; + return cursorStack.isOf(Items.SHEARS) || player.getMainHandStack().isOf(Items.SHEARS) || player.getOffHandStack().isOf(Items.SHEARS); + } + + private static boolean isValidDestroyedSkinPayload(byte[] bytes) { + if (bytes != null && bytes.length > 0 && bytes.length <= 1048576) { + int[] dimensions = readPngDimensions(bytes); + if (dimensions == null) { + return false; + } + + int width = dimensions[0]; + int height = dimensions[1]; + return width == height && width >= 64 && width <= 512 && width % 64 == 0 && hasValidPngChunkStructure(bytes); + } else { + return false; + } + } + + private static boolean isValidDestroyedSkinMaskPayload(byte[] bytes) { + if (bytes != null && bytes.length > 0 && bytes.length <= 65536) { + int[] dimensions = readPngDimensions(bytes); + return dimensions != null && dimensions[0] == 64 && dimensions[1] == 64 && hasValidPngChunkStructure(bytes); + } else { + return false; + } + } + + private static boolean acceptUpload(ServerPlayerEntity player, NonDestroyedSkinSystem.UploadKey key) { + long now = player.getEntityWorld().getTime(); + long next = NEXT_UPLOAD_TICK.getOrDefault(key, Long.MIN_VALUE); + if (now < next) { + return false; + } + + NEXT_UPLOAD_TICK.put(key, now + 20L); + return true; + } + + private static boolean hasValidPngChunkStructure(byte[] bytes) { + int offset = 8; + boolean sawIdat = false; + boolean first = true; + + while (offset + 12 <= bytes.length) { + long length = Integer.toUnsignedLong(readBigEndianInt(bytes, offset)); + if (length > bytes.length - offset - 12L) { + return false; + } + + int typeOffset = offset + 4; + boolean ihdr = chunkTypeEquals(bytes, typeOffset, 'I', 'H', 'D', 'R'); + boolean idat = chunkTypeEquals(bytes, typeOffset, 'I', 'D', 'A', 'T'); + boolean iend = chunkTypeEquals(bytes, typeOffset, 'I', 'E', 'N', 'D'); + if (first && (!ihdr || length != 13L)) { + return false; + } + + first = false; + sawIdat |= idat; + offset += Math.toIntExact(length + 12L); + if (iend) { + return length == 0L && sawIdat && offset == bytes.length; + } + } + + return false; + } + + private static boolean chunkTypeEquals(byte[] bytes, int offset, char a, char b, char c, char d) { + return offset >= 0 + && offset + 4 <= bytes.length + && bytes[offset] == (byte)a + && bytes[offset + 1] == (byte)b + && bytes[offset + 2] == (byte)c + && bytes[offset + 3] == (byte)d; + } + + static void clearPlayerRuntimeState(MinecraftServer server, UUID playerUuid) { + if (playerUuid != null) { + broadcastDestroyedSkin(server, playerUuid, 0, new byte[0]); + + for (int stage = 1; stage < 4; stage++) { + broadcastDestroyedSkinMask(server, playerUuid, stage, new byte[0]); + } + + broadcastDestroyedSkinStage(server, playerUuid, 0); + DESTROYED_SKINS_BY_PLAYER.remove(playerUuid); + DESTROYED_SKIN_MASKS_BY_PLAYER.remove(playerUuid); + DESTROYED_SKIN_STAGE_BY_PLAYER.remove(playerUuid); + DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(playerUuid); + NEXT_UPLOAD_TICK.keySet().removeIf(key -> playerUuid.equals(key.playerUuid())); + } + } + + static void clearRuntimeState() { + DESTROYED_SKINS_BY_PLAYER.clear(); + DESTROYED_SKIN_MASKS_BY_PLAYER.clear(); + DESTROYED_SKIN_STAGE_BY_PLAYER.clear(); + DESTROYED_SKIN_REPAIR_COOLDOWNS.clear(); + NEXT_UPLOAD_TICK.clear(); + } + + private static boolean hasAnyMask(byte[][] masks) { + if (masks == null) { + return false; + } + + for (int stage = 1; stage < masks.length; stage++) { + if (masks[stage] != null && masks[stage].length > 0) { + return true; + } + } + + return false; + } + + private static int[] readPngDimensions(byte[] bytes) { + if (bytes == null || bytes.length < 24) { + return null; + } else if ((bytes[0] & 255) == 137 + && bytes[1] == 80 + && bytes[2] == 78 + && bytes[3] == 71 + && bytes[4] == 13 + && bytes[5] == 10 + && bytes[6] == 26 + && bytes[7] == 10 + && bytes[8] == 0 + && bytes[9] == 0 + && bytes[10] == 0 + && bytes[11] == 13 + && bytes[12] == 73 + && bytes[13] == 72 + && bytes[14] == 68 + && bytes[15] == 82) { + int width = readBigEndianInt(bytes, 16); + int height = readBigEndianInt(bytes, 20); + return width > 0 && height > 0 ? new int[]{width, height} : null; + } else { + return null; + } + } + + private static int readBigEndianInt(byte[] bytes, int offset) { + return (bytes[offset] & 0xFF) << 24 | (bytes[offset + 1] & 0xFF) << 16 | (bytes[offset + 2] & 0xFF) << 8 | bytes[offset + 3] & 0xFF; + } + + private static int getDestroyedSkinStage(UUID playerUuid) { + return playerUuid == null ? 0 : Math.max(0, Math.min(4, DESTROYED_SKIN_STAGE_BY_PLAYER.getOrDefault(playerUuid, 0))); + } + + static int getDestroyedSkinStageForPlayer(ServerPlayerEntity player) { + if (player == null) { + return 0; + } else if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + return 0; + } else { + return player instanceof DestroyedSkinHolder holder + ? Math.max(0, Math.min(4, holder.getDestroyedSkinStage())) + : getDestroyedSkinStage(player.getUuid()); + } + } + + static void broadcastDestroyedSkinStage(ServerPlayerEntity player) { + if (player != null) { + MinecraftServer server = player.getEntityWorld().getServer(); + if (server != null) { + broadcastDestroyedSkinStage(server, player.getUuid(), getDestroyedSkinStageForPlayer(player)); + } + } + } + + static void syncDestroyedSkinStageToOtherPlayers(ServerPlayerEntity player) { + if (player != null) { + MinecraftServer server = player.getEntityWorld().getServer(); + if (server != null) { + int stage = getDestroyedSkinStageForPlayer(player); + + for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) { + if (!target.getUuid().equals(player.getUuid())) { + sendDestroyedSkinStage(target, player.getUuid(), stage); + } + } + } + } + } + + static void cacheDestroyedSkinStage(ServerPlayerEntity player) { + if (player != null) { + if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + DESTROYED_SKIN_STAGE_BY_PLAYER.remove(player.getUuid()); + } else if (player instanceof DestroyedSkinHolder holder) { + int damage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()); + int stage = damage > 0 ? destroyedSkinStageForDamage(damage) : Math.max(0, Math.min(4, holder.getDestroyedSkinStage())); + if (stage == 0) { + DESTROYED_SKIN_STAGE_BY_PLAYER.remove(player.getUuid()); + } else { + DESTROYED_SKIN_STAGE_BY_PLAYER.put(player.getUuid(), stage); + } + } + } + } + + static void setDestroyedSkinStage(ServerPlayerEntity player, int stage) { + if (player != null) { + UUID uuid = player.getUuid(); + if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + clearDestroyedSkinStoredState(player); + if (player.getEntityWorld() instanceof ServerWorld world) { + broadcastDestroyedSkinStage(world.getServer(), uuid, 0); + } + } else { + int clamped = Math.max(0, Math.min(4, stage)); + if (player instanceof DestroyedSkinHolder holder) { + holder.setDestroyedSkinStage(clamped); + } + + if (clamped == 0) { + DESTROYED_SKIN_STAGE_BY_PLAYER.remove(uuid); + } else { + DESTROYED_SKIN_STAGE_BY_PLAYER.put(uuid, clamped); + } + + if (player.getEntityWorld() instanceof ServerWorld world) { + broadcastDestroyedSkinStage(world.getServer(), uuid, clamped); + } + + NonApiInternals.fireDestroyedSkinChanged(player); + NeedsOfNature.checkBadIdeaBeaconAdvancement(player); + } + } + } + + static void applyDestroyedSkinAttackDamage(ServerWorld world, ServerPlayerEntity player) { + if (NeedsOfNature.isDestroyedSkinSystemEnabled()) { + if (world != null && player != null && player instanceof DestroyedSkinHolder holder) { + int damageGain = rollDestroyedSkinDamage(world, player); + applyDestroyedSkinDamage(world, player, holder, damageGain); + } + } + } + + static void applyDestroyedSkinVoluntaryJoinInitialDamage(ServerWorld world, ServerPlayerEntity player) { + if (NeedsOfNature.isDestroyedSkinSystemEnabled()) { + if (world != null && player != null && player instanceof DestroyedSkinHolder holder) { + int beforeDamage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()); + if (beforeDamage == 0) { + applyDestroyedSkinDamage(world, player, holder, 1); + } + } + } + } + + private static void applyDestroyedSkinDamage(ServerWorld world, ServerPlayerEntity player, DestroyedSkinHolder holder, int damageGain) { + if (world != null && player != null && holder != null && damageGain > 0) { + int beforeDamage = clampDestroyedSkinDamage(holder.getDestroyedSkinDamage()); + int afterDamage = clampDestroyedSkinDamage(beforeDamage + damageGain); + if (afterDamage != beforeDamage) { + holder.setDestroyedSkinDamage(afterDamage); + int beforeStage = destroyedSkinStageForDamage(beforeDamage); + int afterStage = destroyedSkinStageForDamage(afterDamage); + setDestroyedSkinStage(player, afterStage); + playDestroyedSkinRipSound(world, player); + broadcastDestroyedSkinRipParticles(world, player); + if (afterStage != beforeStage) { + NeedsOfNature.logRuntime("[NoN] {} destroyed skin advanced to stage {} ({}/{}).", player.getName().getString(), afterStage, afterDamage, 10); + } + } + } + } + + private static int rollDestroyedSkinDamage(ServerWorld world, ServerPlayerEntity player) { + double multiplier = NonTrinketsIntegration.getAccessoryEffects(player).destroyedSkinDamageMultiplier(); + if (Double.isFinite(multiplier) && !(multiplier <= 0.0)) { + int whole = (int)Math.floor(multiplier); + double fractional = multiplier - whole; + if (fractional > 0.0 && world.getRandom().nextDouble() < fractional) { + whole++; + } + + return Math.max(0, whole); + } else { + return 0; + } + } + + static void resetDestroyedSkinDamage(ServerPlayerEntity player) { + if (player != null) { + if (player instanceof DestroyedSkinHolder holder) { + holder.setDestroyedSkinDamage(0); + } + + setDestroyedSkinStage(player, 0); + } + } + + static void clearDestroyedSkinStoredState(ServerPlayerEntity player) { + if (player != null) { + UUID uuid = player.getUuid(); + if (player instanceof DestroyedSkinHolder holder) { + holder.setDestroyedSkinDamage(0); + holder.setDestroyedSkinStage(0); + } + + DESTROYED_SKIN_STAGE_BY_PLAYER.remove(uuid); + DESTROYED_SKIN_REPAIR_COOLDOWNS.remove(uuid); + } + } + + private static int clampDestroyedSkinDamage(int damage) { + return Math.max(0, Math.min(10, damage)); + } + + private static int destroyedSkinStageForDamage(int damage) { + int clamped = clampDestroyedSkinDamage(damage); + if (clamped >= 10) { + return 4; + } else if (clamped >= 7) { + return 3; + } else if (clamped >= 4) { + return 2; + } else { + return clamped >= 1 ? 1 : 0; + } + } + + static int destroyedSkinDamageForStage(int stage) { + return switch (Math.max(0, Math.min(4, stage))) { + case 1 -> 1; + case 2 -> 4; + case 3 -> 7; + case 4 -> 10; + default -> 0; + }; + } + + private static void playDestroyedSkinRipSound(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + int index = 1 + world.getRandom().nextInt(3); + String soundPath = "rip" + String.format(Locale.ROOT, "%02d", index); + float pitch = 0.94F + world.getRandom().nextFloat() * 0.12F; + world.playSound( + null, + player.getX(), + player.getY() + player.getStandingEyeHeight() * 0.5, + player.getZ(), + (SoundEvent)Registries.SOUND_EVENT.get(NeedsOfNature.id(soundPath)), + SoundCategory.PLAYERS, + 0.75F, + pitch + ); + } + } + + private static void playDestroyedSkinShearSound(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + world.playSound( + null, + player.getX(), + player.getY() + player.getStandingEyeHeight() * 0.5, + player.getZ(), + SoundEvents.ENTITY_SHEEP_SHEAR, + SoundCategory.PLAYERS, + 0.8F, + 0.95F + world.getRandom().nextFloat() * 0.1F + ); + } + } + + private static void playDestroyedSkinRepairSound(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + world.playSound( + null, + player.getX(), + player.getY() + player.getStandingEyeHeight() * 0.5, + player.getZ(), + SoundEvents.BLOCK_WOOL_PLACE, + SoundCategory.PLAYERS, + 1.35F, + 1.15F + world.getRandom().nextFloat() * 0.15F + ); + } + } + + private static void broadcastDestroyedSkinRipParticles(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + int count = 3 + world.getRandom().nextInt(4); + long seed = world.getRandom().nextLong(); + double x = player.getX(); + double y = player.getY() + player.getHeight() * 0.55; + double z = player.getZ(); + DestroyedSkinParticlesS2CPayload payload = new DestroyedSkinParticlesS2CPayload(player.getUuid(), x, y, z, count, seed); + + for (ServerPlayerEntity target : world.getPlayers()) { + if (!(target.squaredDistanceTo(player) > 9216.0) && ServerPlayNetworking.canSend(target, DestroyedSkinParticlesS2CPayload.ID)) { + ServerPlayNetworking.send(target, payload); + } + } + } + } + + private static void broadcastDestroyedSkin(MinecraftServer server, UUID playerUuid, int stage, byte[] bytes) { + if (server != null && playerUuid != null && bytes != null) { + for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) { + sendDestroyedSkin(target, playerUuid, stage, bytes); + } + } + } + + private static void broadcastDestroyedSkinMask(MinecraftServer server, UUID playerUuid, int stage, byte[] bytes) { + if (server != null && playerUuid != null && bytes != null) { + for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) { + sendDestroyedSkinMask(target, playerUuid, stage, bytes); + } + } + } + + private static void broadcastDestroyedSkinStage(MinecraftServer server, UUID playerUuid, int stage) { + if (server != null && playerUuid != null) { + for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) { + sendDestroyedSkinStage(target, playerUuid, stage); + } + } + } + + private static void sendDestroyedSkin(ServerPlayerEntity target, UUID playerUuid, int stage, byte[] bytes) { + if (target != null && playerUuid != null && bytes != null) { + if (ServerPlayNetworking.canSend(target, DestroyedSkinSyncS2CPayload.ID)) { + if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + stage = 0; + } + + ServerPlayNetworking.send(target, new DestroyedSkinSyncS2CPayload(playerUuid, stage, bytes)); + } + } + } + + private static void sendDestroyedSkinMask(ServerPlayerEntity target, UUID playerUuid, int stage, byte[] bytes) { + if (target != null && playerUuid != null && bytes != null) { + if (stage > 0 && stage < 4) { + if (ServerPlayNetworking.canSend(target, DestroyedSkinMaskSyncS2CPayload.ID)) { + ServerPlayNetworking.send(target, new DestroyedSkinMaskSyncS2CPayload(playerUuid, stage, bytes)); + } + } + } + } + + private static void sendDestroyedSkinStage(ServerPlayerEntity target, UUID playerUuid, int stage) { + if (target != null && playerUuid != null) { + if (ServerPlayNetworking.canSend(target, DestroyedSkinStateS2CPayload.ID)) { + if (!NeedsOfNature.isDestroyedSkinSystemEnabled()) { + stage = 0; + } + + ServerPlayNetworking.send(target, new DestroyedSkinStateS2CPayload(playerUuid, stage)); + } + } + } + + private record UploadKey(UUID playerUuid, int maskStage) { + } +} diff --git a/src/main/java/com/nonid/NonDiagnostics.java b/src/main/java/com/nonid/NonDiagnostics.java new file mode 100644 index 0000000..a6ef81c --- /dev/null +++ b/src/main/java/com/nonid/NonDiagnostics.java @@ -0,0 +1,166 @@ +package com.nonid; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStopped; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect; +import net.minecraft.entity.Entity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +public final class NonDiagnostics { + private static final boolean RUNTIME_LOGS = Boolean.getBoolean("needsofnature.runtimeLogs"); + private static final int MAX_PENDING_SETUP_CHAT_MESSAGES = 100; + private static final Map DEBUG_CHAT_PREFERENCES = new ConcurrentHashMap<>(); + private static final List PENDING_SETUP_CHAT = new ArrayList<>(); + private static boolean initialized; + + private NonDiagnostics() { + } + + public static synchronized void initialize() { + if (!initialized) { + initialized = true; + ServerTickEvents.END_SERVER_TICK.register(NonDiagnostics::flushPendingSetupChat); + ServerPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, server) -> DEBUG_CHAT_PREFERENCES.remove(handler.player.getUuid())); + ServerLifecycleEvents.SERVER_STOPPED.register((ServerStopped)server -> { + DEBUG_CHAT_PREFERENCES.clear(); + synchronized (PENDING_SETUP_CHAT) { + PENDING_SETUP_CHAT.clear(); + } + }); + } + } + + public static void setDebugChatPreference(UUID playerUuid, @Nullable NonDebugChatMode mode) { + if (playerUuid != null) { + DEBUG_CHAT_PREFERENCES.put(playerUuid, mode == null ? NonDebugChatMode.SETUP_ERRORS : mode); + } + } + + public static boolean shouldSendDebugChat(@Nullable ServerPlayerEntity player, @Nullable NonDebugChatCategory category) { + if (player == null) { + return false; + } + + NonDebugChatMode mode = DEBUG_CHAT_PREFERENCES.getOrDefault(player.getUuid(), NonDebugChatMode.SETUP_ERRORS); + return mode.allows(category == null ? NonDebugChatCategory.INFO : category); + } + + public static boolean shouldSendDebugChat(@Nullable ServerPlayerEntity player, @Nullable NonDebugChatCategory category, boolean force) { + return force || shouldSendDebugChat(player, category); + } + + public static void sendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, Text message, boolean force) { + if (message != null && shouldSendDebugChat(player, category, force)) { + player.sendMessage(message.copy().formatted(chatColor(category)), false); + } + } + + public static void logSetupWarning(String template, Object... args) { + NeedsOfNature.LOGGER.warn(template, args); + queueSetupChat(formatLogTemplate(template, args)); + } + + public static void logSetupError(String template, Object... args) { + NeedsOfNature.LOGGER.error(template, args); + queueSetupChat(formatLogTemplate(template, args)); + } + + public static void logRuntime(String template, Object... args) { + if (RUNTIME_LOGS) { + NeedsOfNature.LOGGER.info(template, args); + } else { + NeedsOfNature.LOGGER.debug(template, args); + } + } + + public static void queueSetupChat(String message) { + if (message != null && !message.isBlank()) { + synchronized (PENDING_SETUP_CHAT) { + if (PENDING_SETUP_CHAT.size() >= 100) { + PENDING_SETUP_CHAT.removeFirst(); + } + + PENDING_SETUP_CHAT.add(new NonDiagnostics.PendingSetupChatMessage(Text.literal(message), new HashSet<>())); + } + } + } + + public static String formatLogTemplate(String template, Object... args) { + if (template == null) { + return ""; + } + + if (args != null && args.length != 0) { + StringBuilder out = new StringBuilder(template.length() + args.length * 8); + int argIndex = 0; + + for (int i = 0; i < template.length(); i++) { + char c = template.charAt(i); + if (c == '{' && i + 1 < template.length() && template.charAt(i + 1) == '}' && argIndex < args.length) { + Object arg = args[argIndex++]; + if (!(arg instanceof Throwable)) { + out.append(String.valueOf(arg)); + } + + i++; + } else { + out.append(c); + } + } + + return out.toString(); + } else { + return template; + } + } + + public static Formatting chatColor(@Nullable NonDebugChatCategory category) { + if (category == null) { + return Formatting.WHITE; + } + + return switch (category) { + case ALWAYS -> Formatting.DARK_GRAY; + case SETUP -> Formatting.YELLOW; + case WARNING -> Formatting.LIGHT_PURPLE; + case ERROR -> Formatting.RED; + case INFO -> Formatting.WHITE; + }; + } + + private static void flushPendingSetupChat(MinecraftServer server) { + if (server != null) { + List players = server.getPlayerManager().getPlayerList(); + if (!players.isEmpty()) { + synchronized (PENDING_SETUP_CHAT) { + PENDING_SETUP_CHAT.removeIf(message -> { + for (ServerPlayerEntity player : players) { + if (player != null && !message.sentTo().contains(player.getUuid())) { + sendDebugChat(player, NonDebugChatCategory.SETUP, message.message(), false); + message.sentTo().add(player.getUuid()); + } + } + + return message.sentTo().containsAll(players.stream().map(Entity::getUuid).toList()); + }); + } + } + } + } + + private record PendingSetupChatMessage(Text message, Set sentTo) { + } +} diff --git a/src/main/java/com/nonid/NonGatherCancelReason.java b/src/main/java/com/nonid/NonGatherCancelReason.java new file mode 100644 index 0000000..adc4d90 --- /dev/null +++ b/src/main/java/com/nonid/NonGatherCancelReason.java @@ -0,0 +1,7 @@ +package com.nonid; + +public enum NonGatherCancelReason { + NO_LOS, + TOO_FAR, + TIMEOUT; +} diff --git a/src/main/java/com/nonid/NonGatherSystem.java b/src/main/java/com/nonid/NonGatherSystem.java new file mode 100644 index 0000000..b50de18 --- /dev/null +++ b/src/main/java/com/nonid/NonGatherSystem.java @@ -0,0 +1,772 @@ +package com.nonid; + +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AnimationStageInfo; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public final class NonGatherSystem { + private static final double MOVE_SPEED = 1.15; + private static final int REPATH_EVERY_TICKS = 5; + private static final int TIMEOUT_TICKS = 3600; + private static final int NO_LOS_TIMEOUT_TICKS = 100; + private static final int TOO_FAR_TIMEOUT_TICKS = 200; + private static final double TIMEOUT_MAX_DIST_SQ = 100.0; + private static final double GATHER_EXTRA_DIST = 0.25; + private static final double GATHER_TARGET_OVERSHOOT = 1.0; + private static final double START_MAX_VERTICAL_DELTA = 2.5; + private static final float LOOK_YAW_SPEED = 30.0F; + private static final float LOOK_PITCH_SPEED = 30.0F; + private static final Map SESSIONS_BY_INITIATOR = new ConcurrentHashMap<>(); + + private NonGatherSystem() { + } + + static void clearRuntimeState() { + SESSIONS_BY_INITIATOR.clear(); + } + + @Nullable + public static NonGatherSystem.GatherCandidate findGatherCandidate( + ServerWorld world, List actors, boolean isAttack, Entity attacker, @Nullable Entity gatherAnchor + ) { + if (world != null && actors != null && !actors.isEmpty() && attacker != null) { + List sortedActors = new ArrayList<>(actors); + sortedActors.sort(Comparator.comparingInt(Entity::getId)); + AnimationService.MatchedAnimation matched = isAttack + ? NonAnimationMatcher.findAutomaticAttack(world, sortedActors, Set.of(), gatherAnchor, true, attacker, Set.of()) + : NonAnimationMatcher.findAutomatic(world, sortedActors, Set.of(), gatherAnchor, true, null, Set.of()); + return toGatherCandidate(matched); + } else { + return null; + } + } + + @Nullable + public static NonGatherSystem.GatherCandidate findUnprotectedGatherCandidate( + ServerWorld world, List actors, boolean isAttack, Entity attacker, @Nullable Entity gatherAnchor + ) { + NonGatherSystem.GatherCandidate candidate = findGatherCandidate(world, actors, isAttack, attacker, gatherAnchor); + if (candidate == null) { + return null; + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId()); + if (definition == null) { + return null; + } + + List sortedActors = new ArrayList<>(actors); + sortedActors.sort(Comparator.comparingInt(Entity::getId)); + return !NeedsOfNature.isProtectedByAccessory(world, definition, sortedActors) + ? candidate + : findUnprotectedFallbackCandidate(world, sortedActors, isAttack, attacker, gatherAnchor, true, candidate.animationId(), null); + } + + @Nullable + static NonGatherSystem.GatherCandidate findUnprotectedFallbackCandidate( + ServerWorld world, + List actors, + boolean isAttack, + Entity attacker, + @Nullable Entity gatherAnchor, + boolean requireStartEligibility, + Identifier blockedAnimationId, + @Nullable Identifier joinPredecessorAnimationId + ) { + if (blockedAnimationId == null) { + return null; + } + + List sortedActors = new ArrayList<>(actors); + sortedActors.sort(Comparator.comparingInt(Entity::getId)); + Set excluded = new HashSet<>(); + excluded.add(blockedAnimationId); + + while (true) { + AnimationService.MatchedAnimation matched = isAttack + ? NonAnimationMatcher.findAutomaticAttack( + world, sortedActors, Set.of(), gatherAnchor, requireStartEligibility, attacker, excluded, null, joinPredecessorAnimationId + ) + : NonAnimationMatcher.findAutomatic( + world, sortedActors, Set.of(), gatherAnchor, requireStartEligibility, null, excluded, null, joinPredecessorAnimationId + ); + NonGatherSystem.GatherCandidate candidate = toGatherCandidate(matched); + if (candidate == null) { + return null; + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId()); + if (definition == null) { + return null; + } + + if (!NeedsOfNature.isProtectedByAccessory(world, definition, sortedActors)) { + return candidate; + } + + excluded.add(candidate.animationId()); + } + } + + @Nullable + private static NonGatherSystem.GatherCandidate toGatherCandidate(@Nullable AnimationService.MatchedAnimation matched) { + return matched == null ? null : new NonGatherSystem.GatherCandidate(matched.animationId(), matched.actorKeys(), matched.stages()); + } + + public static boolean startMobPlayerGather( + ServerWorld world, MobEntity mob, ServerPlayerEntity player, Identifier animationId, List actorKeys, List stages + ) { + if (world != null && mob != null && player != null && animationId != null) { + if (mob.getEntityWorld() != world || player.getEntityWorld() != world) { + return false; + } else if (!NonMobGatherEligibility.canInitiateAutoGather(mob)) { + return false; + } else if (isAutomaticMobPlayerStartBlockedByMount(player)) { + reportAutomaticMobPlayerMountBlock(world, mob, player, animationId, "gather_start"); + return false; + } else { + NonGatherSystem.Session session = new NonGatherSystem.Session( + NonGatherSystem.SessionType.MOB_PLAYER, + mob.getUuid(), + player.getUuid(), + animationId, + List.copyOf(actorKeys), + stages == null ? List.of() : List.copyOf(stages), + true, + null, + world.getRegistryKey(), + world.getTime(), + 0, + 0, + 0 + ); + SESSIONS_BY_INITIATOR.put(mob.getUuid(), session); + NeedsOfNature.trackNoNGatherPair(world, List.of(mob, player)); + return true; + } + } else { + return false; + } + } + + public static boolean isAutomaticMobPlayerStartBlockedByMount(ServerPlayerEntity player) { + if (player != null && player.hasVehicle()) { + Entity vehicle = player.getVehicle(); + if (vehicle != null && !vehicle.isRemoved()) { + NonConfig config = NeedsOfNature.getConfig(); + if (config == null) { + return false; + } else { + return vehicle instanceof LivingEntity ? config.blockAnimationsWhileRidingLivingEntities() : config.blockAnimationsWhileRidingVehicles(); + } + } else { + return false; + } + } else { + return false; + } + } + + public static void reportAutomaticMobPlayerMountBlock( + ServerWorld world, MobEntity mob, ServerPlayerEntity player, @Nullable Identifier animationId, String phase + ) { + if (world != null && mob != null && player != null) { + Entity vehicle = player.getVehicle(); + Identifier mobType = Registries.ENTITY_TYPE.getId(mob.getType()); + Identifier vehicleType = vehicle == null ? null : Registries.ENTITY_TYPE.getId(vehicle.getType()); + NeedsOfNature.logRuntime( + "NoN automatic mob-player start blocked by mounted-player protection: phase={} mobType={} player={} vehicle={} anim={}", + phase, + mobType, + player.getName().getString(), + vehicleType, + animationId + ); + Text vehicleName = (Text)(vehicle == null ? Text.literal("载具") : vehicle.getDisplayName()); + NeedsOfNature.sendDebugChat( + player, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.mounted_animation_blocked", new Object[]{vehicleName}) + ); + } + } + + public static void startMobMobGather( + ServerWorld world, + MobEntity initiator, + MobEntity partner, + Identifier animationId, + List actorKeys, + List stages, + boolean attackMode + ) { + if (world != null && initiator != null && partner != null && animationId != null) { + if (initiator.getEntityWorld() == world && partner.getEntityWorld() == world) { + if (NonMobGatherEligibility.canInitiateAutoGather(initiator) && NonMobGatherEligibility.canBeAutoGatherTarget(partner)) { + NonGatherSystem.Session session = new NonGatherSystem.Session( + NonGatherSystem.SessionType.MOB_MOB, + initiator.getUuid(), + partner.getUuid(), + animationId, + List.copyOf(actorKeys), + stages == null ? List.of() : List.copyOf(stages), + attackMode, + null, + world.getRegistryKey(), + world.getTime(), + 0, + 0, + 0 + ); + SESSIONS_BY_INITIATOR.put(initiator.getUuid(), session); + NeedsOfNature.trackNoNGatherPair(world, List.of(initiator, partner)); + } + } + } + } + + public static void startJoinGather(ServerWorld world, MobEntity joiningMob, ServerPlayerEntity requester, UUID activeInstanceId, Identifier animationId) { + if (world != null && joiningMob != null && requester != null && activeInstanceId != null) { + if (joiningMob.getEntityWorld() == world && requester.getEntityWorld() == world) { + if (NonMobGatherEligibility.canInitiateAutoGather(joiningMob)) { + NonGatherSystem.Session session = new NonGatherSystem.Session( + NonGatherSystem.SessionType.MOB_JOIN_ACTIVE_ANIMATION, + joiningMob.getUuid(), + requester.getUuid(), + animationId, + List.of(), + List.of(), + false, + activeInstanceId, + world.getRegistryKey(), + world.getTime(), + 0, + 0, + 0 + ); + SESSIONS_BY_INITIATOR.put(joiningMob.getUuid(), session); + NeedsOfNature.trackNoNGatherPair(world, List.of(joiningMob, requester)); + } + } + } + } + + public static void tickServerWorld(ServerWorld world) { + if (world != null && !SESSIONS_BY_INITIATOR.isEmpty()) { + for (NonGatherSystem.Session session : List.copyOf(SESSIONS_BY_INITIATOR.values())) { + if (session != null && world.getRegistryKey().equals(session.worldKey())) { + if (!(world.getEntity(session.initiatorUuid()) instanceof MobEntity mob && mob.getEntityWorld() == world)) { + remove(session, world, false, null, "initiator_missing"); + } else if (session.type() == NonGatherSystem.SessionType.MOB_PLAYER) { + tickMobPlayer(world, session, mob); + } else if (session.type() == NonGatherSystem.SessionType.MOB_MOB) { + tickMobMob(world, session, mob); + } else { + tickJoin(world, session, mob); + } + } + } + } + } + + public static boolean isActorGathering(UUID actorUuid) { + if (actorUuid != null && !SESSIONS_BY_INITIATOR.isEmpty()) { + for (NonGatherSystem.Session session : SESSIONS_BY_INITIATOR.values()) { + if (session != null && (actorUuid.equals(session.initiatorUuid()) || actorUuid.equals(session.targetUuid()))) { + return true; + } + } + + return false; + } else { + return false; + } + } + + public static boolean isMobGatheringWithPlayer(ServerWorld world, UUID mobUuid, UUID playerUuid) { + if (world != null && mobUuid != null && playerUuid != null) { + NonGatherSystem.Session session = SESSIONS_BY_INITIATOR.get(mobUuid); + if (session == null || !world.getRegistryKey().equals(session.worldKey())) { + return false; + } else { + return !playerUuid.equals(session.targetUuid()) + ? false + : session.type() == NonGatherSystem.SessionType.MOB_PLAYER || session.type() == NonGatherSystem.SessionType.MOB_JOIN_ACTIVE_ANIMATION; + } + } else { + return false; + } + } + + public static void cancelForActors(ServerWorld world, Collection actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty() && !SESSIONS_BY_INITIATOR.isEmpty()) { + Set actors = new HashSet<>(actorUuids); + + for (NonGatherSystem.Session session : List.copyOf(SESSIONS_BY_INITIATOR.values())) { + if (session != null + && world.getRegistryKey().equals(session.worldKey()) + && (actors.contains(session.initiatorUuid()) || actors.contains(session.targetUuid()))) { + remove(session, world, false, null, "player_request_accepted"); + } + } + } + } + + private static void tickMobPlayer(ServerWorld world, NonGatherSystem.Session session, MobEntity mob) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(session.targetUuid()); + if (player == null || player.getEntityWorld() != world || !player.isAlive() || player.isRemoved() || mob.isRemoved()) { + remove(session, world, false, null, "invalid_actor"); + } else if (!NonMobGatherEligibility.canInitiateAutoGather(mob)) { + remove(session, world, false, null, "mob_not_auto_gatherable"); + } else if (!NeedsOfNature.canMobAttackPlayer(player) || NeedsOfNature.isMobAttackFailsafeActive(world, mob.getUuid())) { + remove(session, world, false, null, "player_not_attackable_or_failsafe"); + } else if (isAutomaticMobPlayerStartBlockedByMount(player)) { + reportAutomaticMobPlayerMountBlock(world, mob, player, session.animationId(), "close_range_start"); + remove(session, world, false, null, "player_mounted_protected"); + } else if (AnimationService.isActorPendingOrActive(world, mob.getUuid()) || AnimationService.isActorPendingOrActive(world, player.getUuid())) { + remove(session, world, false, null, "afw_actor_pending_or_active"); + } else if (timedOut(world, session)) { + remove(session, world, true, NonGatherCancelReason.TIMEOUT, "timeout"); + } else { + int noLosTicks = mob.canSee(player) ? 0 : session.noLosTicks() + 1; + int tooFarTicks = mob.squaredDistanceTo(player) > 100.0 ? session.tooFarTicks() + 1 : 0; + if (!cancelledForDistance(session, world, noLosTicks, tooFarTicks)) { + mob.getLookControl().lookAt(player, 30.0F, 30.0F); + if (NonAttackSystem.hasAnimationStartProtection(world, player)) { + int repathCooldown = Math.max(0, session.repathCooldown() - 1); + if (repathCooldown <= 0) { + startMovingIntoTarget(mob, player); + repathCooldown = 5; + } + + update(session, repathCooldown, noLosTicks, tooFarTicks); + } else if (closeEnoughToStart(mob, player)) { + List actors = sortedActors(mob, player); + NonGatherSystem.StartCandidate startCandidate = resolveProtectedStartCandidate(world, session, actors, true, mob, player); + if (startCandidate == null) { + NeedsOfNature.clearNoNGatherPair(world, List.of(mob.getUuid(), player.getUuid())); + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + } else { + UUID instanceId = NeedsOfNature.startAnimationNowWithContext( + world, + startCandidate.animationId(), + actors, + startCandidate.actorKeys(), + startCandidate.stages(), + NonAnimationDamageBehavior.STOP_ON_DAMAGE, + false, + player, + NeedsOfNature.buildModeContext(true) + ); + NeedsOfNature.clearNoNGatherPair(world, List.of(mob.getUuid(), player.getUuid())); + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + if (instanceId == null) { + NeedsOfNature.logRuntime( + "NoN gather start failed at close range: mobType={} player={} anim={}", + Registries.ENTITY_TYPE.getId(mob.getType()), + player.getName().getString(), + startCandidate.animationId() + ); + } + } + } else { + int repathCooldown = Math.max(0, session.repathCooldown() - 1); + if (repathCooldown <= 0) { + startMovingIntoTarget(mob, player); + repathCooldown = 5; + } + + update(session, repathCooldown, noLosTicks, tooFarTicks); + } + } + } + } + + private static void tickMobMob(ServerWorld world, NonGatherSystem.Session session, MobEntity initiator) { + if (!( + world.getEntity(session.targetUuid()) instanceof MobEntity partner + && partner.getEntityWorld() == world + && partner.isAlive() + && !partner.isRemoved() + && !initiator.isRemoved() + )) { + remove(session, world, false, null, "invalid_actor"); + } else if (!NonMobGatherEligibility.canInitiateAutoGather(initiator) || !NonMobGatherEligibility.canBeAutoGatherTarget(partner)) { + remove(session, world, false, null, "mob_not_auto_gatherable"); + } else if (AnimationService.isActorPendingOrActive(world, initiator.getUuid()) || AnimationService.isActorPendingOrActive(world, partner.getUuid())) { + remove(session, world, false, null, "afw_actor_pending_or_active"); + } else if (timedOut(world, session)) { + remove(session, world, true, NonGatherCancelReason.TIMEOUT, "timeout"); + } else { + boolean hasLos = initiator.canSee(partner) && partner.canSee(initiator); + int noLosTicks = hasLos ? 0 : session.noLosTicks() + 1; + int tooFarTicks = initiator.squaredDistanceTo(partner) > 100.0 ? session.tooFarTicks() + 1 : 0; + if (!cancelledForDistance(session, world, noLosTicks, tooFarTicks)) { + initiator.getLookControl().lookAt(partner, 30.0F, 30.0F); + partner.getLookControl().lookAt(initiator, 30.0F, 30.0F); + if (closeEnoughToStart(initiator, partner)) { + List actors = sortedActors(initiator, partner); + NonGatherSystem.StartCandidate startCandidate = resolveProtectedStartCandidate( + world, session, actors, session.attackMode(), initiator, session.attackMode() ? partner : null + ); + if (startCandidate == null) { + NeedsOfNature.clearNoNGatherPair(world, List.of(initiator.getUuid(), partner.getUuid())); + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + } else { + UUID instanceId = NeedsOfNature.startAnimationNowWithContext( + world, + startCandidate.animationId(), + actors, + startCandidate.actorKeys(), + startCandidate.stages(), + NonAnimationDamageBehavior.STOP_ON_DAMAGE, + false, + session.attackMode() ? partner : null, + NeedsOfNature.buildModeContext(session.attackMode()) + ); + NeedsOfNature.clearNoNGatherPair(world, List.of(initiator.getUuid(), partner.getUuid())); + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + if (instanceId == null) { + NeedsOfNature.logRuntime( + "NoN mob gather start failed at close range: first={} second={} anim={}", + Registries.ENTITY_TYPE.getId(initiator.getType()), + Registries.ENTITY_TYPE.getId(partner.getType()), + startCandidate.animationId() + ); + } + } + } else { + int repathCooldown = Math.max(0, session.repathCooldown() - 1); + if (repathCooldown <= 0) { + startMovingIntoTarget(initiator, partner); + startMovingIntoTarget(partner, initiator); + repathCooldown = 5; + } + + update(session, repathCooldown, noLosTicks, tooFarTicks); + } + } + } + } + + private static void tickJoin(ServerWorld world, NonGatherSystem.Session session, MobEntity joiningMob) { + ServerPlayerEntity requester = world.getServer().getPlayerManager().getPlayer(session.targetUuid()); + if (requester == null + || requester.getEntityWorld() != world + || !requester.isAlive() + || requester.isRemoved() + || joiningMob.isRemoved() + || session.activeInstanceUuid() == null) { + remove(session, world, false, null, "invalid_join_actor"); + } else if (!NonMobGatherEligibility.canInitiateAutoGather(joiningMob)) { + remove(session, world, false, null, "mob_not_auto_gatherable"); + } else if (AnimationService.isActorPendingOrActive(world, joiningMob.getUuid())) { + remove(session, world, false, null, "afw_actor_pending_or_active"); + } else { + AnimationStageInfo stage = AnimationService.getCurrentStage(world, session.activeInstanceUuid()); + if (stage == null) { + remove(session, world, false, null, "join_stage_unavailable"); + } else if (timedOut(world, session)) { + remove(session, world, true, NonGatherCancelReason.TIMEOUT, "timeout"); + } else { + int noLosTicks = joiningMob.canSee(requester) ? 0 : session.noLosTicks() + 1; + int tooFarTicks = joiningMob.squaredDistanceTo(requester) > 100.0 ? session.tooFarTicks() + 1 : 0; + if (!cancelledForDistance(session, world, noLosTicks, tooFarTicks)) { + joiningMob.getLookControl().lookAt(requester, 30.0F, 30.0F); + if (closeEnoughToJoin(joiningMob, requester)) { + Identifier joinAnimationId = resolveProtectedJoinAnimationId(world, session, joiningMob, requester); + if (joinAnimationId == null) { + NeedsOfNature.clearNoNGatherPair(world, List.of(joiningMob.getUuid(), requester.getUuid())); + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + } else { + boolean voluntaryManualPeakJoin = NeedsOfNature.isVoluntaryManualPeakInstance(world, session.activeInstanceUuid()); + boolean accepted = NeedsOfNature.joinAnimationNow(world, session.activeInstanceUuid(), List.of(joiningMob), requester, joinAnimationId); + NeedsOfNature.clearNoNGatherPair(world, List.of(joiningMob.getUuid(), requester.getUuid())); + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + if (!accepted) { + NeedsOfNature.logRuntime( + "NoN join gather failed at close range: mobType={} player={} instance={} anim={}", + Registries.ENTITY_TYPE.getId(joiningMob.getType()), + requester.getName().getString(), + session.activeInstanceUuid(), + session.animationId() + ); + } else if (voluntaryManualPeakJoin) { + NonDestroyedSkinSystem.applyDestroyedSkinVoluntaryJoinInitialDamage(world, requester); + } + } + } else { + int repathCooldown = Math.max(0, session.repathCooldown() - 1); + if (repathCooldown <= 0) { + startMovingIntoTarget(joiningMob, requester); + repathCooldown = 5; + } + + update(session, repathCooldown, noLosTicks, tooFarTicks); + } + } + } + } + } + + private static void startMovingIntoTarget(MobEntity mob, Entity target) { + if (mob != null && target != null) { + double dx = target.getX() - mob.getX(); + double dz = target.getZ() - mob.getZ(); + double length = Math.sqrt(dx * dx + dz * dz); + double targetX = target.getX(); + double targetZ = target.getZ(); + if (length > 1.0E-4) { + targetX += dx / length * 1.0; + targetZ += dz / length * 1.0; + } + + mob.getNavigation().startMovingTo(targetX, target.getY(), targetZ, 1.15); + } + } + + @Nullable + private static Identifier resolveProtectedJoinAnimationId( + ServerWorld world, NonGatherSystem.Session session, MobEntity joiningMob, ServerPlayerEntity requester + ) { + List combinedActors = resolveJoinActors(world, session.activeInstanceUuid(), joiningMob); + if (combinedActors.isEmpty()) { + return session.animationId(); + } + + Identifier predecessorAnimationId = AnimationService.getActiveAnimationId(world, session.activeInstanceUuid()); + if (predecessorAnimationId == null) { + return null; + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(session.animationId()); + if (definition == null) { + return session.animationId(); + } + + if (!NonInjectorMatchPolicy.allowsAutomaticNonMatch(world, definition, combinedActors)) { + boolean isAttack = NeedsOfNature.isInstanceAttack(world, session.activeInstanceUuid()); + NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate( + world, combinedActors, isAttack, joiningMob, requester, true, session.animationId(), predecessorAnimationId + ); + return fallback == null ? null : fallback.animationId(); + } + + if (!NeedsOfNature.isProtectedByAccessory(world, definition, combinedActors)) { + return session.animationId(); + } + + if (!NeedsOfNature.consumeProtectorForAnimation(world, definition, combinedActors)) { + return session.animationId(); + } + + boolean isAttack = NeedsOfNature.isInstanceAttack(world, session.activeInstanceUuid()); + NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate( + world, combinedActors, isAttack, joiningMob, requester, true, session.animationId(), predecessorAnimationId + ); + return fallback == null ? null : fallback.animationId(); + } + + private static List resolveJoinActors(ServerWorld world, @Nullable UUID activeInstanceUuid, MobEntity joiningMob) { + if (world != null && activeInstanceUuid != null && joiningMob != null) { + List actors = new ArrayList<>(); + + for (UUID actorUuid : NeedsOfNature.getInstanceActors(world, activeInstanceUuid)) { + Entity actor = world.getEntity(actorUuid); + if (actor == null) { + actor = world.getServer().getPlayerManager().getPlayer(actorUuid); + } + + if (actor != null && actor.getEntityWorld() == world && !actor.isRemoved()) { + actors.add(actor); + } + } + + if (joiningMob.getEntityWorld() == world && !joiningMob.isRemoved()) { + actors.add(joiningMob); + } + + actors.sort(Comparator.comparingInt(Entity::getId)); + return actors; + } else { + return List.of(); + } + } + + private static boolean timedOut(ServerWorld world, NonGatherSystem.Session session) { + return world.getTime() - session.startTick() > 3600L; + } + + @Nullable + private static NonGatherSystem.StartCandidate resolveProtectedStartCandidate( + ServerWorld world, NonGatherSystem.Session session, List actors, boolean isAttack, Entity attacker, @Nullable Entity gatherAnchor + ) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(session.animationId()); + if (definition == null) { + return new NonGatherSystem.StartCandidate(session.animationId(), session.actorKeys(), session.stages()); + } + + if (!NonInjectorMatchPolicy.allowsAutomaticNonMatch(world, definition, actors)) { + NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate( + world, actors, isAttack, attacker, gatherAnchor, true, session.animationId(), null + ); + return fallback == null ? null : new NonGatherSystem.StartCandidate(fallback.animationId(), fallback.actorKeys(), fallback.stages()); + } + + if (!NeedsOfNature.isProtectedByAccessory(world, definition, actors)) { + return new NonGatherSystem.StartCandidate(session.animationId(), session.actorKeys(), session.stages()); + } + + if (!NeedsOfNature.consumeProtectorForAnimation(world, definition, actors)) { + return new NonGatherSystem.StartCandidate(session.animationId(), session.actorKeys(), session.stages()); + } + + NonGatherSystem.GatherCandidate fallback = findUnprotectedFallbackCandidate( + world, actors, isAttack, attacker, gatherAnchor, true, session.animationId(), null + ); + return fallback == null ? null : new NonGatherSystem.StartCandidate(fallback.animationId(), fallback.actorKeys(), fallback.stages()); + } + + private static boolean cancelledForDistance(NonGatherSystem.Session session, ServerWorld world, int noLosTicks, int tooFarTicks) { + if (noLosTicks > 100) { + remove(session, world, true, NonGatherCancelReason.NO_LOS, "no_los"); + return true; + } else if (tooFarTicks > 200) { + remove(session, world, true, NonGatherCancelReason.TOO_FAR, "too_far"); + return true; + } else { + return false; + } + } + + private static void update(NonGatherSystem.Session session, int repathCooldown, int noLosTicks, int tooFarTicks) { + SESSIONS_BY_INITIATOR.put( + session.initiatorUuid(), + new NonGatherSystem.Session( + session.type(), + session.initiatorUuid(), + session.targetUuid(), + session.animationId(), + session.actorKeys(), + session.stages(), + session.attackMode(), + session.activeInstanceUuid(), + session.worldKey(), + session.startTick(), + repathCooldown, + noLosTicks, + tooFarTicks + ) + ); + } + + private static void remove(NonGatherSystem.Session session, ServerWorld world, boolean applyCooldown, @Nullable NonGatherCancelReason reason) { + remove(session, world, applyCooldown, reason, "unspecified"); + } + + private static void remove(NonGatherSystem.Session session, ServerWorld world, boolean applyCooldown, @Nullable NonGatherCancelReason reason, String detail) { + if (session != null && world != null) { + Entity initiator = world.getEntity(session.initiatorUuid()); + Entity target = world.getEntity(session.targetUuid()); + if (initiator instanceof MobEntity mob) { + mob.getNavigation().stop(); + } + + if (target instanceof MobEntity mob) { + mob.getNavigation().stop(); + } + + List actorUuids = List.of(session.initiatorUuid(), session.targetUuid()); + if (applyCooldown && reason != null) { + NeedsOfNature.applyNoNGatherCancellation(world, session.animationId(), actorUuids, reason); + } else { + NeedsOfNature.clearNoNGatherPair(world, actorUuids); + } + + SESSIONS_BY_INITIATOR.remove(session.initiatorUuid(), session); + } + } + + private static boolean closeEnoughToStart(Entity a, Entity b) { + if (a != null && b != null) { + return Math.abs(a.getY() - b.getY()) > 2.5 ? false : horizontalBoxDistanceSq(a.getBoundingBox(), b.getBoundingBox()) <= 0.0625; + } else { + return false; + } + } + + private static boolean closeEnoughToJoin(Entity a, Entity b) { + return closeEnoughToStart(a, b); + } + + private static double horizontalBoxDistanceSq(Box a, Box b) { + double dx = axisGap(a.minX, a.maxX, b.minX, b.maxX); + double dz = axisGap(a.minZ, a.maxZ, b.minZ, b.maxZ); + return dx * dx + dz * dz; + } + + private static double axisGap(double aMin, double aMax, double bMin, double bMax) { + if (aMax < bMin) { + return bMin - aMax; + } else { + return bMax < aMin ? aMin - bMax : 0.0; + } + } + + private static List sortedActors(Entity first, Entity second) { + List actors = new ArrayList<>(2); + actors.add(first); + actors.add(second); + actors.sort(Comparator.comparingInt(Entity::getId)); + return actors; + } + + public record GatherCandidate(Identifier animationId, List actorKeys, List stages) { + } + + private record Session( + NonGatherSystem.SessionType type, + UUID initiatorUuid, + UUID targetUuid, + Identifier animationId, + List actorKeys, + List stages, + boolean attackMode, + @Nullable UUID activeInstanceUuid, + RegistryKey worldKey, + long startTick, + int repathCooldown, + int noLosTicks, + int tooFarTicks + ) { + } + + private enum SessionType { + MOB_PLAYER, + MOB_MOB, + MOB_JOIN_ACTIVE_ANIMATION; + } + + private record StartCandidate(Identifier animationId, List actorKeys, List stages) { + } +} diff --git a/src/main/java/com/nonid/NonGenderSystem.java b/src/main/java/com/nonid/NonGenderSystem.java new file mode 100644 index 0000000..f56171f --- /dev/null +++ b/src/main/java/com/nonid/NonGenderSystem.java @@ -0,0 +1,408 @@ +package com.nonid; + +import com.nonid.api.NonChangeSource; +import com.nonid.data.NonEntityProfiles; +import com.nonid.network.GenderSelectionPromptS2CPayload; +import com.nonid.network.SetPlayerGenderC2SPayload; +import java.util.Map; +import java.util.Set; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents.Load; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.passive.CowEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; + +final class NonGenderSystem { + private static final String GENDER_TAG_MALE = "gender.male"; + private static final String GENDER_TAG_FEMALE = "gender.female"; + private static final String GENDER_TAG_SELECTED = "gender.selected"; + private static final int DEFAULT_PLAYER_GENDER_MASK = 2; + + private NonGenderSystem() { + } + + static void registerMobGenderTags() { + ServerEntityEvents.ENTITY_LOAD.register((Load)(entity, world) -> { + if (entity instanceof MobEntity) { + if (entity.getType() != NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE) { + if (entity.getType() != NeedsOfNature.PREGNANCY_EGG_ENTITY_TYPE) { + Set tags = entity.getCommandTags(); + boolean hasMale = tags.contains("gender.male"); + boolean hasFemale = tags.contains("gender.female"); + if (!hasMale && !hasFemale) { + NonConfig.GenderSpawnChances chances = resolveGenderSpawnChances(entity); + int roll = world.getRandom().nextInt(100); + if (roll < chances.male()) { + entity.addCommandTag("gender.male"); + hasMale = true; + } else if (roll < chances.male() + chances.female()) { + entity.addCommandTag("gender.female"); + hasFemale = true; + } else { + entity.addCommandTag("gender.male"); + entity.addCommandTag("gender.female"); + hasMale = true; + hasFemale = true; + } + } + + if (entity instanceof GenderHolder holder) { + int mask = 0; + if (hasMale) { + mask |= 1; + } + + if (hasFemale) { + mask |= 2; + } + + holder.setGenderMask(mask); + } + + if (entity instanceof EnergyHolder holder && world instanceof ServerWorld serverWorld) { + holder.ensureEnergyInitialized(serverWorld); + } + } + } + } + }); + } + + static void registerCowMilkingGenderGate() { + UseEntityCallback.EVENT.register((UseEntityCallback)(player, world, hand, entity, hitResult) -> { + NonConfig config = NeedsOfNature.getConfig(); + if (config == null || !config.femaleCowOnlyMilking()) { + return ActionResult.PASS; + } + + if (entity instanceof CowEntity cow) { + if (cow.getType() != EntityType.COW) { + return ActionResult.PASS; + } else { + ItemStack stack = player.getStackInHand(hand); + if (!stack.isEmpty() && stack.isOf(Items.BUCKET)) { + return (ActionResult)(canCowBeMilked(cow) ? ActionResult.PASS : ActionResult.FAIL); + } else { + return ActionResult.PASS; + } + } + } else { + return ActionResult.PASS; + } + }); + } + + static void registerPlayerGenderRequests() { + ServerPlayNetworking.registerGlobalReceiver(SetPlayerGenderC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (player != null) { + handleClientGenderRequest(player, payload.mask(), payload.initialSelection()); + } + })); + } + + static void ensurePlayerGenderInitialized(ServerPlayerEntity player) { + if (player != null) { + int mask = 0; + if (player instanceof GenderHolder holder) { + mask = holder.getGenderMask() & 3; + } + + if (mask == 0) { + mask = resolveGenderMaskFromTags(player); + } + + if (mask != 0) { + markPlayerGenderSelected(player); + } + + if (mask == 0) { + NonConfig config = NeedsOfNature.getConfig(); + if (config.requirePlayerGenderSelectionOnJoin()) { + sendGenderSelectionPrompt(player); + } else { + int var3 = 2; + applyPlayerGenderMask(player, var3, true, NonChangeSource.SYSTEM); + } + } else { + applyGenderMask(player, mask); + if (NeedsOfNature.getConfig().requirePlayerGenderSelectionOnJoin() && !hasPlayerGenderSelected(player)) { + sendGenderSelectionPrompt(player); + } + } + } + } + + static int setPlayerGenderFromCommand(ServerPlayerEntity player, int mask) { + return setPlayerGenderFromCommand(player, mask, NonChangeSource.COMMAND); + } + + static int setPlayerGenderFromCommand(ServerPlayerEntity player, int mask, NonChangeSource source) { + int normalized = normalizeGenderMask(mask); + applyPlayerGenderMask(player, normalized, true, source == null ? NonChangeSource.COMMAND : source); + return normalized; + } + + static void resetPlayerGenderSelection(ServerPlayerEntity player) { + if (player != null) { + clearPlayerGenderSelectionState(player); + sendGenderSelectionPrompt(player); + } + } + + static boolean canClientChangeGenderFreely() { + return NeedsOfNature.getConfig().allowPlayerGenderChangeAnytime(); + } + + static boolean isStartingGenderAllowed(int mask) { + int normalized = normalizeGenderMask(mask); + int allowed = NeedsOfNature.getConfig().getAllowedStartingGenderMask(); + return (allowed & startingGenderChoiceBit(normalized)) != 0; + } + + private static void handleClientGenderRequest(ServerPlayerEntity player, int requestedMask, boolean initialSelection) { + NonConfig config = NeedsOfNature.getConfig(); + int normalized = normalizeGenderMask(requestedMask); + boolean selected = hasPlayerGenderSelected(player); + if (initialSelection) { + if (selected && !config.allowPlayerGenderChangeAnytime()) { + sendCurrentGenderFeedback(player, "Gender has already been selected on this server."); + } else if (!isStartingGenderAllowed(normalized)) { + sendGenderSelectionPrompt(player); + } else { + applyPlayerGenderMask(player, normalized, true, NonChangeSource.JOIN_SELECTION); + } + } else { + if (!config.allowPlayerGenderChangeAnytime()) { + if (!selected && config.requirePlayerGenderSelectionOnJoin()) { + sendGenderSelectionPrompt(player); + return; + } + + if (selected) { + sendCurrentGenderFeedback(player, "Gender changes are disabled by this server."); + return; + } + + if (!isStartingGenderAllowed(normalized)) { + sendGenderSelectionPrompt(player); + return; + } + } + + applyPlayerGenderMask(player, normalized, true, NonChangeSource.CLIENT_PREFERENCE); + } + } + + private static void applyPlayerGenderMask(ServerPlayerEntity player, int mask, boolean markSelected) { + applyPlayerGenderMask(player, mask, markSelected, NonChangeSource.SYSTEM); + } + + private static void applyPlayerGenderMask(ServerPlayerEntity player, int mask, boolean markSelected, NonChangeSource source) { + applyGenderMask(player, mask, source); + if (markSelected) { + markPlayerGenderSelected(player); + } + } + + private static void markPlayerGenderSelected(ServerPlayerEntity player) { + if (player != null) { + player.addCommandTag("gender.selected"); + } + } + + private static void clearPlayerGenderSelectionState(ServerPlayerEntity player) { + if (player != null) { + player.removeCommandTag("gender.selected"); + player.removeCommandTag("gender.male"); + player.removeCommandTag("gender.female"); + if (player instanceof GenderHolder holder) { + holder.setGenderMask(0); + } + } + } + + private static boolean hasPlayerGenderSelected(ServerPlayerEntity player) { + return player != null && player.getCommandTags().contains("gender.selected"); + } + + private static void sendGenderSelectionPrompt(ServerPlayerEntity player) { + if (player != null) { + NonConfig config = NeedsOfNature.getConfig(); + ServerPlayNetworking.send( + player, + new GenderSelectionPromptS2CPayload( + config.getAllowedStartingGenderMask(), currentOrDefaultPlayerMask(player), !config.allowPlayerGenderChangeAnytime() + ) + ); + } + } + + private static void sendCurrentGenderFeedback(ServerPlayerEntity player, String message) { + if (player != null) { + player.sendMessage(Text.literal("[NoN] " + message), false); + } + } + + private static int currentOrDefaultPlayerMask(ServerPlayerEntity player) { + if (player instanceof GenderHolder holder) { + int mask = holder.getGenderMask() & 3; + if (mask != 0) { + return mask; + } + } + + return firstAllowedStartingMask(NeedsOfNature.getConfig().getAllowedStartingGenderMask()); + } + + private static int firstAllowedStartingMask(int allowedMask) { + int allowed = allowedMask & 3; + if ((allowedMask & 2) != 0) { + return 2; + } else if ((allowedMask & 1) != 0) { + return 1; + } else { + return (allowedMask & 4) != 0 ? 3 : 3; + } + } + + private static int startingGenderChoiceBit(int genderMask) { + return switch (genderMask & 3) { + case 1 -> 1; + case 2 -> 2; + case 3 -> 4; + default -> 2; + }; + } + + private static int normalizeGenderMask(int mask) { + int normalized = mask & 3; + return normalized == 0 ? 2 : normalized; + } + + static NonConfig.GenderSpawnChances resolveGenderSpawnChances(Entity entity) { + return NonEntityProfiles.resolve(entity).genderSpawn(); + } + + static Map resolveEffectiveGenderSpawnMap() { + return NonEntityProfiles.resolveEffectiveGenderSpawnMap(); + } + + static boolean hasMaleTag(Entity entity) { + return entity != null && entity.getCommandTags().contains("gender.male"); + } + + static boolean hasFemaleTag(Entity entity) { + return entity != null && entity.getCommandTags().contains("gender.female"); + } + + static boolean isOnlyMale(Entity entity) { + return entity instanceof GenderHolder holder ? (holder.getGenderMask() & 3) == 1 : false; + } + + static boolean hasMaleGender(Entity entity) { + if (entity instanceof GenderHolder holder) { + int mask = holder.getGenderMask() & 3; + if (mask != 0) { + return (mask & 1) != 0; + } + } + + return hasMaleTag(entity); + } + + static boolean hasFemaleGender(Entity entity) { + if (entity instanceof GenderHolder holder) { + int mask = holder.getGenderMask() & 3; + if (mask != 0) { + return (mask & 2) != 0; + } + } + + return hasFemaleTag(entity); + } + + static NonLiquidTankType getLiquidTankType(Entity entity) { + return isOnlyMale(entity) ? NonLiquidTankType.A : NonLiquidTankType.V; + } + + static void applyGenderMask(Entity entity, int mask) { + applyGenderMask(entity, mask, NonChangeSource.SYSTEM); + } + + static boolean isOnlyFemale(Entity entity) { + return entity == null ? false : hasFemaleGender(entity) && !hasMaleGender(entity); + } + + static void applyGenderMask(Entity entity, int mask, NonChangeSource source) { + if (entity != null) { + int beforeMask = entity instanceof GenderHolder existingHolder ? existingHolder.getGenderMask() & 3 : 0; + int normalizedMask = mask & 3; + if (normalizedMask == 0) { + normalizedMask = 2; + } + + entity.removeCommandTag("gender.male"); + entity.removeCommandTag("gender.female"); + if ((normalizedMask & 1) != 0) { + entity.addCommandTag("gender.male"); + } + + if ((normalizedMask & 2) != 0) { + entity.addCommandTag("gender.female"); + } + + if (entity instanceof GenderHolder holder) { + holder.setGenderMask(normalizedMask); + } + + if (normalizedMask == 1 && entity instanceof ServerPlayerEntity player) { + if (NonMessSystem.normalizeMaleOnlyMess(player, false)) { + NonMessSystem.broadcastMessState(player); + } + + NeedsOfNature.clearPregnancyState(player, true); + } + + if (entity instanceof ServerPlayerEntity player && (beforeMask & 3) != (normalizedMask & 3)) { + NonApiInternals.fireGenderChanged(player, normalizedMask, source == null ? NonChangeSource.SYSTEM : source); + NonPregnancyConceptionSystem.syncState(player); + } + } + } + + private static boolean canCowBeMilked(CowEntity cow) { + if (cow == null) { + return true; + } else { + return cow instanceof GenderHolder holder ? holder.hasGender(2) : cow.getCommandTags().contains("gender.female"); + } + } + + private static int resolveGenderMaskFromTags(Entity entity) { + if (entity == null) { + return 0; + } + + Set tags = entity.getCommandTags(); + int mask = 0; + if (tags.contains("gender.male")) { + mask |= 1; + } + + if (tags.contains("gender.female")) { + mask |= 2; + } + + return mask; + } +} diff --git a/src/main/java/com/nonid/NonInjectorMatchPolicy.java b/src/main/java/com/nonid/NonInjectorMatchPolicy.java new file mode 100644 index 0000000..bb73f6f --- /dev/null +++ b/src/main/java/com/nonid/NonInjectorMatchPolicy.java @@ -0,0 +1,47 @@ +package com.nonid; + +import com.nonid.data.NonLiquidRoles; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import java.util.List; +import net.minecraft.entity.Entity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; + +public final class NonInjectorMatchPolicy { + private NonInjectorMatchPolicy() { + } + + public static boolean allowsAutomaticNonMatch(ServerWorld world, AnimationDefinitions.Definition definition, List actors) { + if (world == null || definition == null || actors == null || actors.isEmpty()) { + return false; + } + + if (!hasMaleOnlyPlayer(actors)) { + return true; + } + + NonLiquidRoles.LiquidRoles roles = AnimationGameplayMetadata.liquidRoles(definition.id()); + if (roles != null && !roles.injectorRoles().isEmpty()) { + boolean hasV = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.V); + boolean hasA = roles.injectorRoles().containsValue(NonLiquidRoles.InjectorRole.A); + if (!hasV) { + return true; + } else { + return hasA ? false : NeedsOfNature.getConfig().convertMaleOnlyVInjectionsToA(); + } + } else { + return true; + } + } + + private static boolean hasMaleOnlyPlayer(List actors) { + for (Entity actor : actors) { + if (actor instanceof ServerPlayerEntity && NonGenderSystem.isOnlyMale(actor)) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/nonid/NonItemSystem.java b/src/main/java/com/nonid/NonItemSystem.java new file mode 100644 index 0000000..7812669 --- /dev/null +++ b/src/main/java/com/nonid/NonItemSystem.java @@ -0,0 +1,208 @@ +package com.nonid; + +import com.nonid.item.EnergyStabilizerItem; +import com.nonid.potion.NonPotions; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.FoodComponent; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.component.type.TooltipDisplayComponent; +import net.minecraft.component.type.FoodComponent.Builder; +import net.minecraft.entity.EntityType; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.ItemGroup.Entries; +import net.minecraft.potion.Potion; +import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class NonItemSystem { + private static final FoodComponent LIQUID_BOTTLE_FOOD = new Builder().nutrition(1).saturationModifier(0.0F).alwaysEdible().build(); + private static final FoodComponent FERTILE_NECTAR_FOOD = new Builder().nutrition(7).saturationModifier(0.0F).alwaysEdible().build(); + private static final TooltipDisplayComponent LIQUID_BOTTLE_TOOLTIP = TooltipDisplayComponent.DEFAULT.with(DataComponentTypes.POTION_CONTENTS, true); + + private NonItemSystem() { + } + + static void giveItemOrDrop(ServerPlayerEntity player, ItemStack stack) { + if (player != null && stack != null && !stack.isEmpty()) { + if (!player.getInventory().insertStack(stack)) { + player.dropItem(stack, false); + } + } + } + + static boolean consumeOneGlassBottle(ServerPlayerEntity player, Hand hand) { + if (player != null && hand != null) { + ItemStack stack = player.getStackInHand(hand); + if (!stack.isEmpty() && stack.isOf(Items.GLASS_BOTTLE)) { + stack.decrement(1); + return true; + } else { + return false; + } + } else { + return false; + } + } + + static void refundGlassBottle(ServerPlayerEntity player) { + giveItemOrDrop(player, new ItemStack(Items.GLASS_BOTTLE)); + } + + static ItemStack createBottleStackForTank(LiquidHolder holder) { + Identifier entityTypeId = null; + if (holder != null && holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) { + entityTypeId = holder.getLiquidEntityTypeId(); + } + + return createLiquidBottleStack(entityTypeId); + } + + static ItemStack createLiquidBottleStack(@Nullable Identifier entityTypeId) { + int tint = NonLiquidSystem.resolveBottleTintRgb(entityTypeId); + PotionContentsComponent contents = new PotionContentsComponent( + Optional.of(Registries.POTION.getEntry(NonPotions.LIQUID)), Optional.of(tint), List.of(), Optional.empty() + ); + ItemStack stack = new ItemStack(Items.POTION); + stack.set(DataComponentTypes.POTION_CONTENTS, contents); + stack.set(DataComponentTypes.MAX_STACK_SIZE, 16); + stack.set(DataComponentTypes.FOOD, LIQUID_BOTTLE_FOOD); + NonPotions.applyHoneyBottleDrinkSound(stack); + stack.set(DataComponentTypes.TOOLTIP_DISPLAY, LIQUID_BOTTLE_TOOLTIP); + NonPotions.setLiquidBottleEntityTypeId(stack, entityTypeId); + if (entityTypeId == null) { + stack.set(DataComponentTypes.CUSTOM_NAME, Text.translatable("item.needsofnature.mixed_liquid_bottle").copy().styled(style -> style.withItalic(false))); + } else { + stack.set( + DataComponentTypes.CUSTOM_NAME, + Text.translatable("item.needsofnature.entity_liquid_bottle", new Object[]{resolveEntityNameText(entityTypeId)}) + .copy() + .styled(style -> style.withItalic(false)) + ); + } + + return stack; + } + + static void addEntityLiquidAndStabilizersToItemGroup(Entries entries, @Nullable NonConfig config) { + for (String rawEntityId : NonLiquidSystem.resolveEffectiveLiquidGainMap().keySet()) { + Identifier entityId = Identifier.tryParse(rawEntityId); + if (entityId != null) { + entries.add(createLiquidBottleStack(entityId)); + entries.add(createEnergyStabilizerStack(entityId)); + } + } + } + + static void addNonPotionVariantsToItemGroup(Entries entries) { + List> nonPotions = new ArrayList<>(); + + for (Potion potion : Registries.POTION) { + Identifier potionId = Registries.POTION.getId(potion); + if (potionId != null && "needsofnature".equals(potionId.getNamespace())) { + nonPotions.add(Registries.POTION.getEntry(potion)); + } + } + + entries.add(createLiquidBottleStack(null)); + + for (RegistryEntry potionEntry : nonPotions) { + if (potionEntry.value() != NonPotions.LIQUID) { + entries.add(createPotionVariantStack(Items.POTION, potionEntry)); + } + } + + for (RegistryEntry potionEntry : nonPotions) { + if (potionEntry.value() != NonPotions.LIQUID) { + entries.add(createPotionVariantStack(Items.SPLASH_POTION, potionEntry)); + } + } + + for (RegistryEntry potionEntry : nonPotions) { + if (potionEntry.value() != NonPotions.LIQUID) { + entries.add(createPotionVariantStack(Items.LINGERING_POTION, potionEntry)); + } + } + + for (RegistryEntry potionEntry : nonPotions) { + if (potionEntry.value() != NonPotions.LIQUID) { + entries.add(createPotionVariantStack(Items.TIPPED_ARROW, potionEntry)); + } + } + } + + static ItemStack createPotionVariantStack(Item item, RegistryEntry potionEntry) { + ItemStack stack = new ItemStack(item); + stack.set(DataComponentTypes.POTION_CONTENTS, new PotionContentsComponent(potionEntry)); + if (item == Items.POTION && potionEntry.value() == NonPotions.FERTILE_NECTAR) { + stack.set(DataComponentTypes.FOOD, FERTILE_NECTAR_FOOD); + stack.set(DataComponentTypes.MAX_STACK_SIZE, 16); + NonPotions.applyHoneyBottleDrinkSound(stack); + } + + return stack; + } + + static boolean isNonPotionVariantStack(ItemStack stack) { + if (stack != null && !stack.isEmpty()) { + Item item = stack.getItem(); + boolean potionLike = item == Items.POTION || item == Items.SPLASH_POTION || item == Items.LINGERING_POTION || item == Items.TIPPED_ARROW; + if (!potionLike) { + return false; + } + + PotionContentsComponent contents = (PotionContentsComponent)stack.getOrDefault(DataComponentTypes.POTION_CONTENTS, PotionContentsComponent.DEFAULT); + Optional> potionOpt = contents.potion(); + if (potionOpt.isEmpty()) { + return false; + } + + Identifier potionId = Registries.POTION.getId((Potion)potionOpt.get().value()); + return potionId != null && "needsofnature".equals(potionId.getNamespace()); + } else { + return false; + } + } + + static ItemStack createEnergyStabilizerStack(@Nullable Identifier entityTypeId) { + return EnergyStabilizerItem.createStackForEntity(entityTypeId); + } + + private static Text resolveEntityNameText(Identifier entityTypeId) { + return (Text)(entityTypeId == null + ? Text.literal("未知") + : Registries.ENTITY_TYPE + .getOptionalValue(entityTypeId) + .map(EntityType::getName) + .orElseGet(() -> Text.literal(formatFallbackEntityName(entityTypeId)))); + } + + private static String formatFallbackEntityName(Identifier entityTypeId) { + String[] parts = entityTypeId.getPath().replace('/', '_').split("_"); + StringBuilder out = new StringBuilder(); + + for (String part : parts) { + if (part != null && !part.isEmpty()) { + if (!out.isEmpty()) { + out.append(' '); + } + + out.append(Character.toUpperCase(part.charAt(0))); + if (part.length() > 1) { + out.append(part.substring(1)); + } + } + } + + return out.isEmpty() ? entityTypeId.toString() : out.toString(); + } +} diff --git a/src/main/java/com/nonid/NonLiquidSystem.java b/src/main/java/com/nonid/NonLiquidSystem.java new file mode 100644 index 0000000..cfaf472 --- /dev/null +++ b/src/main/java/com/nonid/NonLiquidSystem.java @@ -0,0 +1,301 @@ +package com.nonid; + +import com.nonid.client.NonLiquidColors; +import com.nonid.data.NonLiquidGainOverrides; +import com.nonid.effect.NonStatusEffects; +import com.nonid.network.LiquidStateS2CPayload; +import com.nonid.particle.NonParticles; +import com.nonid.potion.NonPotions; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +final class NonLiquidSystem { + private static final double LIQUID_SNEAK_DECAY_MULTIPLIER = 16.0; + private static final double LIQUID_WATER_DECAY_MULTIPLIER = 2.0; + private static final double LIQUID_DECAY_MIN_MULT = 0.1; + private static final double LIQUID_DECAY_MAX_MULT = 1.5; + private static final double LIQUID_SNEAK_PARTICLE_BACK_OFFSET = 0.28; + private static final Map LIQUID_DECAY_REMAINDER = new ConcurrentHashMap<>(); + + private NonLiquidSystem() { + } + + static void clearRuntimeState() { + LIQUID_DECAY_REMAINDER.clear(); + } + + static void syncLiquidState(ServerPlayerEntity player) { + if (player != null) { + if (player instanceof LiquidHolder holder) { + enforceLiquidCapacity(holder); + int stored = holder.getLiquidStored(); + int capacity = holder.getLiquidCapacity(); + updateFilledEffect(player, holder); + int tint = resolveLiquidTintRgb(holder); + ServerPlayNetworking.send(player, new LiquidStateS2CPayload(stored, capacity, tint)); + NonApiInternals.fireLiquidChanged(player); + NonMessSystem.broadcastMessState(player); + NonAdvancementHooks.checkLiquidAdvancements(player, holder); + } + } + } + + private static void enforceLiquidCapacity(LiquidHolder holder) { + if (holder != null) { + int capacity = holder.getLiquidCapacity(); + int stored = holder.getLiquidStored(); + if (capacity <= 0) { + if (stored > 0) { + holder.setLiquidStored(0); + } + } else { + if (stored > capacity) { + holder.setLiquidStored(capacity); + } + } + } + } + + static void syncLiquidStateToAll(@Nullable MinecraftServer server) { + if (server != null) { + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + syncLiquidState(player); + } + } + } + + static void refreshLiquidBottleTintsToAll(@Nullable MinecraftServer server) { + if (server != null) { + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + refreshLiquidBottleTints(player); + } + } + } + + private static void refreshLiquidBottleTints(ServerPlayerEntity player) { + if (player != null) { + PlayerInventory inventory = player.getInventory(); + + for (int i = 0; i < inventory.size(); i++) { + refreshLiquidBottleTint(inventory.getStack(i)); + } + + refreshLiquidBottleTint(player.currentScreenHandler.getCursorStack()); + } + } + + private static void refreshLiquidBottleTint(ItemStack stack) { + if (NonPotions.isLiquidBottle(stack)) { + Identifier entityTypeId = NonPotions.getLiquidBottleEntityTypeId(stack); + int tint = resolveBottleTintRgb(entityTypeId); + PotionContentsComponent current = (PotionContentsComponent)stack.getOrDefault(DataComponentTypes.POTION_CONTENTS, PotionContentsComponent.DEFAULT); + PotionContentsComponent updated = new PotionContentsComponent(current.potion(), Optional.of(tint), List.of(), Optional.empty()); + stack.set(DataComponentTypes.POTION_CONTENTS, updated); + } + } + + static int resolveLiquidTintRgb(LiquidHolder holder) { + if (holder != null && holder.getLiquidStored() > 0) { + return holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY + ? resolveEntityLiquidColorRgb(holder.getLiquidEntityTypeId()) + : resolveMixedLiquidColorRgb(); + } else { + return resolveMixedLiquidColorRgb(); + } + } + + static int resolveBottleTintRgb(@Nullable Identifier entityTypeId) { + return entityTypeId == null ? resolveMixedLiquidColorRgb() : resolveEntityLiquidColorRgb(entityTypeId); + } + + private static int resolveEntityLiquidColorRgb(@Nullable Identifier entityTypeId) { + if (entityTypeId == null) { + return 16776427; + } + + String key = entityTypeId.toString(); + String configuredHex = NeedsOfNature.getConfig().getLiquidColorByEntity().get(key); + Integer configuredRgb = NonLiquidColors.parseHexColor(configuredHex); + if (configuredRgb != null) { + return configuredRgb; + } + + Integer packOverride = NonLiquidGainOverrides.getColorRgb(entityTypeId); + return packOverride != null ? packOverride : 16776427; + } + + private static int resolveMixedLiquidColorRgb() { + String configuredHex = NeedsOfNature.getConfig().getLiquidColorByEntity().get("needsofnature:mixed"); + Integer configuredRgb = NonLiquidColors.parseHexColor(configuredHex); + if (configuredRgb != null) { + return configuredRgb; + } + + Integer packOverride = NonLiquidGainOverrides.getMixedColorRgb(); + return packOverride != null ? packOverride : 15920063; + } + + static Map resolveEffectiveLiquidGainMap() { + Map merged = new LinkedHashMap<>(NonLiquidGainOverrides.getGainByEntity()); + merged.putAll(NeedsOfNature.getConfig().getLiquidGainByEntity()); + return Map.copyOf(merged); + } + + static Map resolveEffectiveLiquidColorMap() { + Map merged = new LinkedHashMap<>(NonLiquidGainOverrides.getColorByEntity()); + String packMixedHex = NonLiquidGainOverrides.getMixedColorHex(); + if (packMixedHex != null) { + merged.put("needsofnature:mixed", packMixedHex); + } + + merged.putAll(NeedsOfNature.getConfig().getLiquidColorByEntity()); + return Map.copyOf(merged); + } + + static void tickLiquidDecay(ServerWorld world) { + if (world != null) { + if (!world.getPlayers().isEmpty()) { + NonConfig config = NeedsOfNature.getConfig(); + boolean liquidTankEnabled = config.isLiquidTankEnabled(); + double decayPerSecond = config.getLiquidDecayPerSecond(); + + for (ServerPlayerEntity player : world.getPlayers()) { + if (player instanceof LiquidHolder holder) { + if (!liquidTankEnabled) { + LIQUID_DECAY_REMAINDER.remove(player.getUuid()); + if (holder.getLiquidStored() > 0) { + holder.setLiquidStored(0); + syncLiquidState(player); + } + + updateFilledEffect(player, holder); + } else { + updateFilledEffect(player, holder); + if (!(decayPerSecond <= 0.0) && NeedsOfNature.getActivePlayerInstance(player) == null) { + int stored = holder.getLiquidStored(); + if (stored <= 0) { + LIQUID_DECAY_REMAINDER.remove(player.getUuid()); + updateFilledEffect(player, holder); + } else { + NonAccessoryEffects accessoryEffects = NonTrinketsIntegration.getActiveTankAccessoryEffects(player); + double decayPerTick = decayPerSecond / 20.0; + boolean equalizedDecay = accessoryEffects.equalizeLiquidDecayContext(); + if (!equalizedDecay && player.isSneaking()) { + decayPerTick *= 16.0; + } + + if (!equalizedDecay && (player.isTouchingWater() || player.isSubmergedInWater())) { + decayPerTick *= 2.0; + } + + int capacity = holder.getLiquidCapacity(); + double fillRatio = 0.0; + if (capacity > 0) { + fillRatio = Math.min(1.0, Math.max(0.0, (double)stored / capacity)); + } + + double curve = 1.0 - Math.log1p((1.0 - fillRatio) * 9.0) / Math.log1p(9.0); + double decayMultiplier = 0.1 + 1.4 * curve; + decayPerTick *= decayMultiplier; + decayPerTick *= accessoryEffects.liquidDecayMultiplier(); + double remainder = LIQUID_DECAY_REMAINDER.getOrDefault(player.getUuid(), 0.0); + double accum = remainder + decayPerTick; + int decay = (int)Math.floor(accum); + remainder = accum - decay; + LIQUID_DECAY_REMAINDER.put(player.getUuid(), remainder); + if (decay > 0) { + int next = Math.max(0, stored - decay); + if (next != stored) { + holder.setLiquidStored(next); + syncLiquidState(player); + } + + spawnLiquidDecayParticles(world, player); + } + } + } + } + } + } + } + } + } + + static void updateFilledEffect(ServerPlayerEntity player, LiquidHolder holder) { + if (player != null && holder != null) { + int amplifier = getAmplifier(holder); + RegistryEntry entry = Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FILLED); + StatusEffectInstance current = player.getStatusEffect(entry); + if (amplifier < 0) { + player.removeStatusEffect(entry); + } else { + if (current != null) { + if (current.getAmplifier() == amplifier) { + return; + } + + player.removeStatusEffect(entry); + } + + player.addStatusEffect(new StatusEffectInstance(entry, -1, amplifier, false, false, true)); + } + } + } + + private static int getAmplifier(LiquidHolder holder) { + int capacity = holder.getLiquidCapacity(); + int stored = holder.getLiquidStored(); + int amplifier = -1; + if (capacity > 0) { + float ratio = Math.max(0.0F, Math.min(1.0F, (float)stored / capacity)); + if (ratio >= 0.8F) { + amplifier = 2; + } else if (ratio >= 0.5F) { + amplifier = 1; + } else if (ratio >= 0.3F) { + amplifier = 0; + } + } + + return amplifier; + } + + private static void spawnLiquidDecayParticles(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + int tintRgb = player instanceof LiquidHolder holder ? resolveLiquidTintRgb(holder) : resolveBottleTintRgb(null); + Vec3d spawnPos = resolveLiquidDecayParticlePos(player); + world.spawnParticles(NonParticles.liquidFalling(tintRgb), spawnPos.x, spawnPos.y, spawnPos.z, 1, 0.0, 0.0, 0.0, 0.0); + } + } + + private static Vec3d resolveLiquidDecayParticlePos(ServerPlayerEntity player) { + double x = player.getX(); + double z = player.getZ(); + if (player.isSneaking()) { + double yawRad = Math.toRadians(player.bodyYaw); + x += Math.sin(yawRad) * 0.28; + z -= Math.cos(yawRad) * 0.28; + } + + return new Vec3d(x, player.getBodyY(0.4), z); + } +} diff --git a/src/main/java/com/nonid/NonLiquidTankType.java b/src/main/java/com/nonid/NonLiquidTankType.java new file mode 100644 index 0000000..7a102a2 --- /dev/null +++ b/src/main/java/com/nonid/NonLiquidTankType.java @@ -0,0 +1,22 @@ +package com.nonid; + +import com.nonid.data.NonLiquidRoles; + +public enum NonLiquidTankType { + V("v"), + A("a"); + + private final String slotName; + + NonLiquidTankType(String slotName) { + this.slotName = slotName; + } + + public String trinketsSlotId() { + return "legs/" + this.slotName; + } + + public boolean accepts(NonLiquidRoles.InjectorRole role) { + return this == V && role == NonLiquidRoles.InjectorRole.V || this == A && role == NonLiquidRoles.InjectorRole.A; + } +} diff --git a/src/main/java/com/nonid/NonMessSystem.java b/src/main/java/com/nonid/NonMessSystem.java new file mode 100644 index 0000000..9d97c33 --- /dev/null +++ b/src/main/java/com/nonid/NonMessSystem.java @@ -0,0 +1,264 @@ +package com.nonid; + +import com.nonid.data.NonLiquidRoles; +import com.nonid.network.MessStateS2CPayload; +import com.nonid.particle.NonParticles; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.entity.Entity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; + +final class NonMessSystem { + private static final Map MESS_CLEAN_NEXT_TICKS = new ConcurrentHashMap<>(); + + private NonMessSystem() { + } + + static void clearRuntimeState() { + MESS_CLEAN_NEXT_TICKS.clear(); + } + + static void incrementMessOnPeak( + ServerWorld world, List actorUuids, List actorKeys, Identifier animationId, Set suppliedInjectorRoles + ) { + if (NeedsOfNature.isMessSystemEnabled()) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + if (suppliedInjectorRoles != null && !suppliedInjectorRoles.isEmpty()) { + boolean hasV = suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.V); + boolean hasA = suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.A); + boolean hasM = suppliedInjectorRoles.contains(NonLiquidRoles.InjectorRole.M); + if (hasV || hasA || hasM) { + for (ServerPlayerEntity player : NeedsOfNature.collectPlayerReceiverActors(world, actorUuids, actorKeys, animationId)) { + if (player instanceof MessHolder holder) { + normalizeMaleOnlyMess(player, false); + int messGain = rollMessGain(world, player); + if (messGain > 0) { + if (hasV) { + if (NonGenderSystem.isOnlyMale(player)) { + if (NeedsOfNature.getConfig().convertMaleOnlyVInjectionsToA()) { + holder.setAMess(clampMess(holder.getAMess() + messGain)); + } + } else { + holder.setVMess(clampMess(holder.getVMess() + messGain)); + } + } + + if (hasA) { + holder.setAMess(clampMess(holder.getAMess() + messGain)); + } + + if (hasM) { + holder.setMMess(clampMess(holder.getMMess() + messGain)); + } + + broadcastMessState(player); + if (holder.getVMess() >= 10 && holder.getAMess() >= 10 && holder.getMMess() >= 10) { + NonAdvancementHooks.grantMessedUp(player); + } + } + } + } + } + } + } + } + } + + private static int rollMessGain(ServerWorld world, ServerPlayerEntity player) { + double multiplier = NonTrinketsIntegration.getActiveTankAccessoryEffects(player).messGainMultiplier(); + if (Double.isFinite(multiplier) && !(multiplier <= 0.0)) { + int whole = (int)Math.floor(multiplier); + double fractional = multiplier - whole; + if (fractional > 0.0 && world.getRandom().nextDouble() < fractional) { + whole++; + } + + return Math.max(0, whole); + } else { + return 0; + } + } + + static void clearMess(ServerPlayerEntity player) { + if (player != null && player instanceof MessHolder holder) { + holder.setVMess(0); + holder.setAMess(0); + holder.setMMess(0); + MESS_CLEAN_NEXT_TICKS.remove(player.getUuid()); + broadcastMessState(player); + } + } + + static void clearMessStoredState(ServerPlayerEntity player) { + if (player != null && player instanceof MessHolder holder) { + holder.setVMess(0); + holder.setAMess(0); + holder.setMMess(0); + MESS_CLEAN_NEXT_TICKS.remove(player.getUuid()); + } + } + + static int clampMess(int value) { + return Math.max(0, Math.min(10, value)); + } + + static void tickMessCleaning(ServerWorld world, long now) { + if (NeedsOfNature.isMessSystemEnabled()) { + if (world != null) { + for (ServerPlayerEntity player : world.getPlayers()) { + if (player instanceof MessHolder holder) { + normalizeMaleOnlyMess(player, false); + UUID playerUuid = player.getUuid(); + if (!hasAnyMess(holder)) { + MESS_CLEAN_NEXT_TICKS.remove(playerUuid); + } else { + boolean inWater = player.isTouchingWater() || player.isSubmergedInWater(); + boolean inRain = !inWater && isExposedToRain(world, player); + if (!inWater && !inRain) { + MESS_CLEAN_NEXT_TICKS.remove(playerUuid); + } else { + Long scheduledTick = MESS_CLEAN_NEXT_TICKS.get(playerUuid); + if (scheduledTick == null) { + MESS_CLEAN_NEXT_TICKS.put(playerUuid, now + 20L); + } else { + long nextTick = scheduledTick; + if (now >= nextTick) { + boolean changed = decrementMess(holder); + if (changed) { + broadcastMessState(player); + if (inWater) { + spawnMessCleaningWaterParticles(world, player); + } + } + + if (hasAnyMess(holder)) { + MESS_CLEAN_NEXT_TICKS.put(playerUuid, now + 20L); + } else { + MESS_CLEAN_NEXT_TICKS.remove(playerUuid); + } + } + } + } + } + } + } + } + } + } + + static boolean normalizeMaleOnlyMess(Entity entity, boolean convertVToA) { + if (NonGenderSystem.isOnlyMale(entity) && entity instanceof MessHolder holder) { + int v = clampMess(holder.getVMess()); + if (v <= 0) { + return false; + } + + holder.setVMess(0); + if (convertVToA) { + holder.setAMess(clampMess(holder.getAMess() + v)); + } + + return true; + } else { + return false; + } + } + + static void broadcastMessState(ServerPlayerEntity player) { + if (player != null) { + NonApiInternals.fireMessChanged(player); + MinecraftServer server = player.getEntityWorld().getServer(); + if (server != null) { + for (ServerPlayerEntity target : server.getPlayerManager().getPlayerList()) { + sendMessState(target, player); + } + } + } + } + + static void syncMessStates(ServerPlayerEntity target) { + if (target != null) { + MinecraftServer server = target.getEntityWorld().getServer(); + if (server != null) { + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + sendMessState(target, player); + } + } + } + } + + private static void sendMessState(ServerPlayerEntity target, ServerPlayerEntity player) { + if (target != null && player != null) { + if (ServerPlayNetworking.canSend(target, MessStateS2CPayload.ID)) { + int v = 0; + int a = 0; + int m = 0; + if (NeedsOfNature.isMessSystemEnabled() && player instanceof MessHolder holder) { + normalizeMaleOnlyMess(player, false); + v = clampMess(holder.getVMess()); + a = clampMess(holder.getAMess()); + m = clampMess(holder.getMMess()); + } + + int tint = 16777215; + int stored = 0; + int capacity = 0; + if (player instanceof LiquidHolder liquidHolder) { + tint = NonLiquidSystem.resolveLiquidTintRgb(liquidHolder); + stored = Math.max(0, liquidHolder.getLiquidStored()); + capacity = Math.max(0, liquidHolder.getLiquidCapacity()); + } + + ServerPlayNetworking.send(target, new MessStateS2CPayload(player.getUuid(), v, a, m, tint, stored, capacity)); + } + } + } + + private static boolean hasAnyMess(MessHolder holder) { + return holder != null && (holder.getVMess() > 0 || holder.getAMess() > 0 || holder.getMMess() > 0); + } + + private static boolean decrementMess(MessHolder holder) { + if (holder == null) { + return false; + } + + int beforeV = clampMess(holder.getVMess()); + int beforeA = clampMess(holder.getAMess()); + int beforeM = clampMess(holder.getMMess()); + if (beforeV <= 0 && beforeA <= 0 && beforeM <= 0) { + return false; + } + + holder.setVMess(Math.max(0, beforeV - 1)); + holder.setAMess(Math.max(0, beforeA - 1)); + holder.setMMess(Math.max(0, beforeM - 1)); + return true; + } + + private static boolean isExposedToRain(ServerWorld world, ServerPlayerEntity player) { + return world != null && player != null && world.isRaining() ? world.hasRain(player.getBlockPos()) : false; + } + + private static void spawnMessCleaningWaterParticles(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + int tintRgb = player instanceof LiquidHolder holder ? NonLiquidSystem.resolveLiquidTintRgb(holder) : NonLiquidSystem.resolveBottleTintRgb(null); + int count = 2 + world.getRandom().nextInt(3); + double y = player.getBodyY(0.45); + + for (int i = 0; i < count; i++) { + double ox = (world.getRandom().nextDouble() - 0.5) * player.getWidth(); + double oy = (world.getRandom().nextDouble() - 0.5) * 0.45; + double oz = (world.getRandom().nextDouble() - 0.5) * player.getWidth(); + world.spawnParticles(NonParticles.liquidWater(tintRgb), player.getX() + ox, y + oy, player.getZ() + oz, 1, 0.0, -0.01, 0.0, 0.0); + } + } + } +} diff --git a/src/main/java/com/nonid/NonMobEnergySystem.java b/src/main/java/com/nonid/NonMobEnergySystem.java new file mode 100644 index 0000000..b547d81 --- /dev/null +++ b/src/main/java/com/nonid/NonMobEnergySystem.java @@ -0,0 +1,227 @@ +package com.nonid; + +import com.nonid.data.NonEntityProfiles; +import com.nonid.internal.animation.data.AnimationDefinitions; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.Map.Entry; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.passive.HorseEntity; +import net.minecraft.registry.Registries; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public final class NonMobEnergySystem { + public static final int DEFAULT_TICKS_PER_ENERGY = 120; + public static final int PARTNER_SCAN_INTERVAL_TICKS = 40; + public static final int POST_ATTEMPT_COOLDOWN_TICKS = 100; + public static final int ROLL_FAIL_COOLDOWN_TICKS = 1200; + public static final int ENERGY_JOIN_MIN = 70; + public static final int ENERGY_MOB_MATCH_MIN = 120; + public static final int ENERGY_ATTACK = 200; + public static final double PARTNER_SCAN_RADIUS = 20.0; + public static final double PLAYER_COOLDOWN_BYPASS_DIST_SQ = 64.0; + public static final double PLAYER_ANIMATION_LINGER_RADIUS = 3.0; + public static final double PLAYER_ANIMATION_LINGER_RADIUS_SQ = 9.0; + public static final double PLAYER_ANIMATION_LINGER_MIN_DISTANCE = 1.5; + public static final double PLAYER_ANIMATION_LINGER_MIN_DISTANCE_SQ = 2.25; + public static final double PLAYER_ANIMATION_LINGER_MOVE_SPEED = 1.1; + private static final int GENDER_MIXED = 0; + private static final int GENDER_MALE = 1; + private static final int GENDER_FEMALE = 2; + + private NonMobEnergySystem() { + } + + public static double passiveGainScale(MobEntity mob, float multiplier, float drift, boolean nearAnimation, double nearAnimationMultiplier) { + double rate = NeedsOfNature.getConfig().getEnergyGainRate(); + double profileMultiplier = NonEntityProfiles.resolveEnergyGainMultiplier(mob); + double scale = multiplier * drift * rate * profileMultiplier; + if (nearAnimation) { + scale *= Math.max(1.0, nearAnimationMultiplier); + } + + return Double.isFinite(scale) && scale > 0.0 ? scale : 0.0; + } + + public static int ticksPerGain(MobEntity mob, float multiplier, float drift, boolean nearAnimation, double nearAnimationMultiplier) { + double scale = passiveGainScale(mob, multiplier, drift, nearAnimation, nearAnimationMultiplier); + return scale <= 0.0 ? 120 : Math.max(1, (int)Math.round(120.0 / scale)); + } + + public static float randomGainMultiplier(ServerWorld world) { + float a = world.getRandom().nextFloat(); + float b = world.getRandom().nextFloat(); + float tri = (a + b) * 0.5F; + return 0.4F + tri * 0.6F; + } + + public static float randomGainDrift(ServerWorld world) { + return 0.9F + world.getRandom().nextFloat() * 0.2F; + } + + public static boolean hasGenericAnimationSupport(EntityType type, boolean baby) { + Identifier entityTypeId = Registries.ENTITY_TYPE.getId(type); + return AnimationDefinitions.hasEnabledGenericAnimationForEntityType(entityTypeId, baby); + } + + public static boolean isEnergyAttackStabilized(Entity entity) { + return entity != null && entity.getCommandTags().contains("non.energy_stabilized"); + } + + public static boolean bypassesEntityEntityRoll(Entity self, Entity candidate) { + return self != null && candidate != null + ? self instanceof HorseEntity && candidate.getType() == NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE + || self.getType() == NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE && candidate instanceof HorseEntity + : false; + } + + public static boolean passesSameGenderRoll(ServerWorld world, Entity debugSource, List actors) { + if (world == null) { + return true; + } + + int group = resolveGenderGroup(actors); + int percent; + String label; + if (group == 1) { + percent = NeedsOfNature.getConfig().getMaleMaleChancePercent(); + label = "male+male"; + } else if (group == 2) { + percent = NeedsOfNature.getConfig().getFemaleFemaleChancePercent(); + label = "female+female"; + } else { + percent = 100; + label = "mixed"; + } + + return rollPercent(world, debugSource, percent, label); + } + + public static int resolveMultiActorJoinChancePercent(Entity joiningEntity) { + Integer profileChance = NonEntityProfiles.resolve(joiningEntity).multiActorJoinChancePercent(); + return profileChance == null ? NeedsOfNature.getConfig().getMultiActorJoinChancePercent() : Math.max(0, Math.min(100, profileChance)); + } + + public static boolean passesMultiActorJoinRoll(ServerWorld world, Entity debugSource, int totalActors, int chancePercent) { + return world != null && totalActors > 2 ? rollPercent(world, debugSource, chancePercent, "join 3+ actors (total=" + totalActors + ")") : true; + } + + public static boolean passesEntityEntityRoll(ServerWorld world, Entity debugSource) { + if (world == null) { + return true; + } + + int percent = NeedsOfNature.getConfig().getEntityEntityChancePercent(); + return rollPercent(world, debugSource, percent, "entity+entity"); + } + + public static void appendCooldownMapDebug( + List lines, + ServerWorld world, + long nowTick, + String label, + Map cooldowns, + Map bypassableCooldowns, + boolean includeBypassState + ) { + if (lines != null && world != null && cooldowns != null && !cooldowns.isEmpty()) { + int shown = 0; + + for (Entry entry : cooldowns.entrySet()) { + UUID partnerId = entry.getKey(); + long remaining = entry.getValue() - nowTick; + if (partnerId != null && remaining > 0L) { + String suffix = ""; + if (includeBypassState && bypassableCooldowns != null && bypassableCooldowns.containsKey(partnerId)) { + suffix = " (bypass: close+LOS)"; + } + + lines.add(label + ": " + debugEntityName(world, partnerId) + " " + NeedsOfNature.formatTicksForDebug(remaining) + suffix); + if (++shown >= 5) { + int extra = cooldowns.size() - shown; + if (extra > 0) { + lines.add(label + ": +" + extra + " more"); + } + break; + } + } + } + } + } + + public static String debugEntityName(ServerWorld world, UUID uuid) { + if (uuid == null) { + return "unknown"; + } + + Entity entity = world.getEntity(uuid); + if (entity != null) { + return entity.getDisplayName().getString(); + } + + String raw = uuid.toString(); + return raw.length() <= 8 ? raw : raw.substring(0, 8); + } + + private static int resolveGenderGroup(List actors) { + int group = 0; + if (actors != null && !actors.isEmpty()) { + for (Entity actor : actors) { + if (actor == null) { + return 0; + } + + boolean hasMale = actor.getCommandTags().contains("gender.male"); + boolean hasFemale = actor.getCommandTags().contains("gender.female"); + int actorGroup; + if (hasMale && !hasFemale) { + actorGroup = 1; + } else { + if (!hasFemale || hasMale) { + return 0; + } + + actorGroup = 2; + } + + if (group == 0) { + group = actorGroup; + } else if (group != actorGroup) { + return 0; + } + } + + return group; + } else { + return 0; + } + } + + private static boolean rollPercent(ServerWorld world, Entity debugSource, int percent, String label) { + if (percent >= 100) { + return true; + } + + if (percent <= 0) { + return false; + } + + boolean success = world.getRandom().nextInt(100) < percent; + sendRollDebug(world, debugSource, label, percent, success); + return success; + } + + private static void sendRollDebug(ServerWorld world, Entity debugSource, String label, int percent, boolean success) { + if (world != null && debugSource != null) { + String result = success ? "success" : "fail"; + NeedsOfNature.sendDebugChatToNearby( + world, debugSource, 20.0, NonDebugChatCategory.INFO, Text.translatable("debug.needsofnature.roll_result", new Object[]{label, percent, result}) + ); + } + } +} diff --git a/src/main/java/com/nonid/NonMobGatherEligibility.java b/src/main/java/com/nonid/NonMobGatherEligibility.java new file mode 100644 index 0000000..eb43b50 --- /dev/null +++ b/src/main/java/com/nonid/NonMobGatherEligibility.java @@ -0,0 +1,36 @@ +package com.nonid; + +import com.nonid.entity.HorseLiquidCollectorEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.passive.CamelEntity; +import net.minecraft.entity.passive.FoxEntity; +import net.minecraft.entity.passive.TameableEntity; + +public final class NonMobGatherEligibility { + private NonMobGatherEligibility() { + } + + public static boolean canInitiateAutoGather(MobEntity mob) { + if (mob == null || !mob.isAlive() || mob.isRemoved()) { + return false; + } + + if (!mob.isAiDisabled() && !mob.isSleeping()) { + if (mob instanceof TameableEntity tameable && (tameable.isSitting() || tameable.isInSittingPose())) { + return false; + } else { + return mob instanceof FoxEntity fox && (fox.isSitting() || fox.isSleeping()) ? false : !(mob instanceof CamelEntity camel && camel.isSitting()); + } + } else { + return false; + } + } + + public static boolean canBeAutoGatherTarget(MobEntity mob) { + if (mob == null || !mob.isAlive() || mob.isRemoved()) { + return false; + } else { + return mob instanceof HorseLiquidCollectorEntity ? true : canInitiateAutoGather(mob); + } + } +} diff --git a/src/main/java/com/nonid/NonPlayerAttackSystem.java b/src/main/java/com/nonid/NonPlayerAttackSystem.java new file mode 100644 index 0000000..37fad88 --- /dev/null +++ b/src/main/java/com/nonid/NonPlayerAttackSystem.java @@ -0,0 +1,717 @@ +package com.nonid; + +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.server.AnimationStartContext; +import com.nonid.internal.animation.server.ServerAnimationController; +import com.nonid.network.AttackStateS2CPayload; +import com.nonid.network.PlayerAttackInputC2SPayload; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDamage; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AllowDamage; +import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect; +import net.minecraft.entity.Entity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Items; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +final class NonPlayerAttackSystem { + private static final int INPUT_LEFT = 1; + private static final int INPUT_RIGHT = 2; + private static final long INPUT_MIN_INTERVAL_TICKS = 1L; + private static final float PVP_MASH_RANDOM_MIN = 0.8F; + private static final float PVP_MASH_RANDOM_MAX = 1.2F; + private static final Map CONTESTS = new ConcurrentHashMap<>(); + private static final Map DEFEATED_OWNERSHIP = new ConcurrentHashMap<>(); + private static final Map FINISH_ATTEMPTS = new ConcurrentHashMap<>(); + private static final ThreadLocal VANILLA_PVP_BYPASS = new ThreadLocal<>(); + + private NonPlayerAttackSystem() { + } + + static void register() { + AttackEntityCallback.EVENT.register((AttackEntityCallback)(player, world, hand, entity, hitResult) -> { + if (!(world instanceof ServerWorld serverWorld && player instanceof ServerPlayerEntity attacker && entity instanceof ServerPlayerEntity victim)) { + return ActionResult.PASS; + } else if (hand == Hand.MAIN_HAND && attacker.getMainHandStack().isEmpty()) { + return (ActionResult)(tryStartPlayerAttack(serverWorld, attacker, victim) ? ActionResult.SUCCESS : ActionResult.PASS); + } else { + return ActionResult.PASS; + } + }); + ServerPlayNetworking.registerGlobalReceiver( + PlayerAttackInputC2SPayload.ID, (payload, context) -> context.server().execute(() -> handleInput(context.player(), payload)) + ); + ServerLivingEntityEvents.ALLOW_DAMAGE + .register( + (AllowDamage)(entity, source, amount) -> { + if (entity instanceof ServerPlayerEntity victim && victim.getEntityWorld() instanceof ServerWorld world) { + NonPlayerAttackSystem.DefeatedOwnership ownership = validOwnership(world, victim.getUuid()); + if (ownership == null) { + return true; + } else { + ServerPlayerEntity attributed = resolveAttributedPlayer(source); + if (attributed != null && ownership.ownerUuid().equals(attributed.getUuid())) { + FINISH_ATTEMPTS.put( + victim.getUuid(), + new NonPlayerAttackSystem.FinishAttempt( + world.getRegistryKey(), attributed.getUuid(), ownership.instanceId(), countReadyTotems(victim), world.getTime() + ) + ); + return true; + } else { + return false; + } + } + } else { + return true; + } + } + ); + ServerLivingEntityEvents.AFTER_DAMAGE.register((AfterDamage)(entity, source, baseDamageTaken, damageTaken, blocked) -> { + if (entity instanceof ServerPlayerEntity victim && victim.getEntityWorld() instanceof ServerWorld world) { + NonPlayerAttackSystem.FinishAttempt attempt = FINISH_ATTEMPTS.remove(victim.getUuid()); + if (attempt != null && attempt.worldKey().equals(world.getRegistryKey()) && !blocked) { + ServerPlayerEntity attributed = resolveAttributedPlayer(source); + if (attributed != null && attempt.ownerUuid().equals(attributed.getUuid())) { + if (victim.isAlive() && countReadyTotems(victim) < attempt.totemsBefore()) { + AnimationService.stopAnimation(world, attempt.instanceId()); + } + } + } + } + }); + ServerPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, server) -> server.execute(() -> clearForPlayer(handler.player))); + } + + private static boolean tryStartPlayerAttack(ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity victim) { + if (world != null && attacker != null && victim != null) { + if (attacker == victim || attacker.getEntityWorld() != world || victim.getEntityWorld() != world) { + return false; + } else if (!attacker.isAlive() || attacker.isRemoved() || attacker.isSpectator()) { + return false; + } else if (!victim.isAlive() || victim.isRemoved() || victim.isSpectator()) { + return false; + } else if (isOwnerFinishingDamage(world, victim, attacker)) { + return false; + } else if (victim.isCreative() && !NeedsOfNature.getConfig().canAttackCreativePlayers()) { + return false; + } else if (!(attacker instanceof EnergyHolder holder && holder.getEnergy() >= 200)) { + return false; + } else { + if (!NonPlayerInteractionSystem.areAttacksAllowed(victim, attacker)) { + return false; + } + + if (AnimationService.isActorPendingOrActive(world, attacker.getUuid()) || AnimationService.isActorPendingOrActive(world, victim.getUuid())) { + return false; + } + + if (!NonAttackSystem.hasAnimationStartProtection(world, attacker) && !NonAttackSystem.hasAnimationStartProtection(world, victim)) { + NonPlayerAttackSystem.AttackSelection selection = selectAttack(world, attacker, victim, true, true); + if (selection.match() == null) { + return selection.blockedByProtector(); + } + + AnimationService.MatchedAnimation match = selection.match(); + List actors = sortedActors(attacker, victim); + NonGatherSystem.cancelForActors(world, List.of(attacker.getUuid(), victim.getUuid())); + UUID instanceId = NeedsOfNature.startAnimationNowWithContext( + world, + match.animationId(), + actors, + match.actorKeys(), + match.stages(), + NonAnimationDamageBehavior.STOP_ON_DAMAGE, + false, + victim, + buildPlayerAttackContext(victim.getUuid(), attacker.getUuid(), Set.of(attacker.getUuid())), + attacker, + false, + true + ); + if (instanceId != null) { + NeedsOfNature.logRuntime( + "[NoN] Player attack started: attacker={} victim={} animation={} instance={}", + attacker.getName().getString(), + victim.getName().getString(), + match.animationId(), + instanceId + ); + } + + return true; + } else { + return false; + } + } + } else { + return false; + } + } + + @Nullable + static AnimationService.MatchedAnimation findViableAttack(ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity victim) { + NonPlayerAttackSystem.AttackSelection selection = selectAttack(world, attacker, victim, false, false); + return selection.match(); + } + + private static NonPlayerAttackSystem.AttackSelection selectAttack( + ServerWorld world, ServerPlayerEntity attacker, ServerPlayerEntity victim, boolean requireStartEligibility, boolean consumeProtector + ) { + List actors = sortedActors(attacker, victim); + Set excluded = new LinkedHashSet<>(); + boolean blockedByProtector = false; + boolean protectorConsumed = false; + + while (true) { + AnimationService.MatchedAnimation match = NonAnimationMatcher.findAutomaticAttack( + world, + actors, + Set.of(), + victim, + requireStartEligibility, + attacker, + excluded, + (candidateWorld, definitionx, candidateActors) -> NonPlayerInteractionSystem.isPlayerInteractionDefinition(definitionx), + null + ); + if (match == null) { + return new NonPlayerAttackSystem.AttackSelection(null, blockedByProtector); + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(match.animationId()); + if (definition == null) { + excluded.add(match.animationId()); + } else { + if (!NeedsOfNature.isProtectedByAccessory(world, definition, actors)) { + return new NonPlayerAttackSystem.AttackSelection(match, blockedByProtector); + } + + blockedByProtector = true; + if (consumeProtector && !protectorConsumed) { + protectorConsumed = NeedsOfNature.consumeProtectorForAnimation(world, definition, actors); + } + + excluded.add(match.animationId()); + } + } + } + + static AnimationStartContext buildPlayerAttackContext(UUID victimUuid, UUID ownerUuid, Set attackerUuids) { + return AnimationStartContext.empty() + .withSessionData(NonAnimationSessionData.playerAttack(new NonAnimationSessionData.PlayerAttack(victimUuid, ownerUuid, ownerUuid, attackerUuids))); + } + + static AnimationStartContext prepareDefeatedJoinContext( + ServerWorld world, List existingActorUuids, ServerPlayerEntity requester, AnimationStartContext sourceContext + ) { + AnimationStartContext context = sourceContext == null ? AnimationStartContext.empty() : sourceContext; + NonAnimationSessionData.PlayerAttack existing = playerAttack(context); + UUID victimUuid = existing == null ? firstPlayerUuid(world, existingActorUuids) : existing.victimUuid(); + if (victimUuid == null) { + return context; + } + + UUID finishOwnerUuid = existing == null ? requester.getUuid() : existing.finishOwnerUuid(); + LinkedHashSet attackers = new LinkedHashSet<>(); + if (existing != null) { + attackers.addAll(existing.attackerUuids()); + } + + attackers.add(requester.getUuid()); + return context.withSessionData( + NonAnimationSessionData.playerAttack(new NonAnimationSessionData.PlayerAttack(victimUuid, finishOwnerUuid, requester.getUuid(), attackers)) + ); + } + + static AnimationStartContext defeatedTailContext(AnimationStartContext sourceContext, UUID victimUuid) { + AnimationStartContext context = sourceContext == null ? AnimationStartContext.empty() : sourceContext; + NonAnimationSessionData.PlayerAttack attack = playerAttack(context); + return attack == null + ? context.withSessionData(NonAnimationSessionData.attack()) + : context.withSessionData(NonAnimationSessionData.playerAttack(attack.forDefeatedTail(victimUuid))); + } + + static boolean isPlayerAttackContext(@Nullable AnimationStartContext context) { + return playerAttack(context) != null; + } + + @Nullable + static ServerPlayerEntity resolveAttackVictim(ServerWorld world, UUID instanceId, List actorUuids) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + NonAnimationSessionData.PlayerAttack attack = snapshot == null ? null : playerAttack(snapshot.startContext()); + if (attack != null && actorUuids != null && actorUuids.contains(attack.victimUuid())) { + ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(attack.victimUuid()); + if (victim != null && victim.getEntityWorld() == world) { + return victim; + } + } + + if (actorUuids == null) { + return null; + } + + for (UUID actorUuid : actorUuids) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(actorUuid); + if (player != null && player.getEntityWorld() == world) { + return player; + } + } + + return null; + } + + static void onAnimationStarted( + ServerWorld world, UUID instanceId, List actorUuids, @Nullable NonAnimationSessionData.PlayerAttack attack, long startTick, boolean defeated + ) { + if (attack != null && actorUuids != null && actorUuids.contains(attack.victimUuid())) { + Set actorSet = Set.copyOf(actorUuids); + LinkedHashSet activeAttackers = new LinkedHashSet<>(); + + for (UUID attackerUuid : attack.attackerUuids()) { + if (actorSet.contains(attackerUuid)) { + ServerPlayerEntity attacker = world.getServer().getPlayerManager().getPlayer(attackerUuid); + if (attacker != null && attacker.getEntityWorld() == world) { + activeAttackers.add(attackerUuid); + } + } + } + + if (defeated) { + DEFEATED_OWNERSHIP.put( + attack.victimUuid(), + new NonPlayerAttackSystem.DefeatedOwnership(world.getRegistryKey(), attack.victimUuid(), attack.finishOwnerUuid(), instanceId) + ); + } + + if (!activeAttackers.isEmpty()) { + NonPlayerAttackSystem.Contest contest = new NonPlayerAttackSystem.Contest( + world.getRegistryKey(), instanceId, attack.victimUuid(), attack.finishOwnerUuid(), attack.initiatorUuid(), activeAttackers, startTick, defeated + ); + CONTESTS.put(instanceId, contest); + } + } + } + + static void onAnimationStopped(ServerWorld world, UUID instanceId) { + NonPlayerAttackSystem.Contest contest = CONTESTS.remove(instanceId); + if (contest != null) { + FINISH_ATTEMPTS.remove(contest.victimUuid); + } + + DEFEATED_OWNERSHIP.entrySet().removeIf(entry -> { + NonPlayerAttackSystem.DefeatedOwnership ownership = entry.getValue(); + return ownership.instanceId().equals(instanceId) && ownership.worldKey().equals(world.getRegistryKey()); + }); + } + + static boolean syncEscapeControl(ServerWorld world, UUID instanceId, boolean escapable, boolean defeated) { + NonPlayerAttackSystem.Contest contest = CONTESTS.get(instanceId); + if (contest != null && contest.worldKey.equals(world.getRegistryKey())) { + contest.escapable = escapable; + contest.defeated = defeated; + broadcastState(world, contest); + return true; + } else { + return false; + } + } + + static boolean isContest(UUID instanceId) { + return instanceId != null && CONTESTS.containsKey(instanceId); + } + + static List getContestDebugLines(ServerPlayerEntity target) { + if (target != null && target.getEntityWorld() instanceof ServerWorld world) { + NonPlayerAttackSystem.Contest contest = findContestForPlayer(world, target.getUuid()); + if (contest == null) { + return List.of(); + } else { + ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid); + if (victim != null && victim.getEntityWorld() == world) { + List lines = new ArrayList<>(); + String role = contest.victimUuid.equals(target.getUuid()) ? "defender" : "attacker"; + lines.add( + String.format( + Locale.ROOT, "PvP contest %s | role %s | progress %.1f%%", contest.instanceId.toString().substring(0, 8), role, contest.progress * 100.0F + ) + ); + lines.add(formatContestParticipant(world, contest, victim, true)); + contest.activeAttackerUuids + .stream() + .sorted(Comparator.comparing(UUID::toString)) + .map(uuid -> world.getServer().getPlayerManager().getPlayer(uuid)) + .filter(player -> player != null && player.getEntityWorld() == world) + .map(player -> formatContestParticipant(world, contest, player, false)) + .forEach(lines::add); + return List.copyOf(lines); + } else { + return List.of(); + } + } + } else { + return List.of(); + } + } + + @Nullable + private static NonPlayerAttackSystem.Contest findContestForPlayer(ServerWorld world, UUID playerUuid) { + for (NonPlayerAttackSystem.Contest contest : CONTESTS.values()) { + if (contest.worldKey.equals(world.getRegistryKey()) && (contest.victimUuid.equals(playerUuid) || contest.activeAttackerUuids.contains(playerUuid))) { + return contest; + } + } + + return null; + } + + private static String formatContestParticipant(ServerWorld world, NonPlayerAttackSystem.Contest contest, ServerPlayerEntity participant, boolean defender) { + NonAttackSystem.PvpMashFactors factors = NonAttackSystem.resolveServerPlayerContestStep(world, participant, defender, contest.defeated, contest.startTick); + int energy = 0; + int maxEnergy = 0; + if (participant instanceof EnergyHolder holder) { + energy = holder.getEnergy(); + maxEnergy = holder.getMaxEnergy(); + } + + return String.format( + Locale.ROOT, + "%s %s: %s%.2f-%.2f%%/input | HP %.1f/%.1f | energy %d/%d | health %.2fx, heat %.2fx, healthy %.2fx, ease %.2fx", + defender ? "Defender" : "Attacker", + participant.getName().getString(), + defender ? "+" : "-", + factors.step() * 0.8F * 100.0F, + factors.step() * 1.2F * 100.0F, + participant.getHealth(), + participant.getMaxHealth(), + energy, + maxEnergy, + factors.healthMultiplier(), + factors.heatMultiplier(), + factors.healthyDefenderMultiplier(), + factors.defeatedEaseMultiplier() + ); + } + + static void tick(ServerWorld world) { + long now = world.getTime(); + + for (NonPlayerAttackSystem.Contest contest : List.copyOf(CONTESTS.values())) { + if (contest.worldKey.equals(world.getRegistryKey())) { + if (AnimationService.getCurrentStage(world, contest.instanceId) == null) { + CONTESTS.remove(contest.instanceId, contest); + } else { + ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid); + if (victim == null || victim.getEntityWorld() != world || !victim.isAlive() || victim.isRemoved()) { + CONTESTS.remove(contest.instanceId, contest); + } + } + } + } + + DEFEATED_OWNERSHIP.entrySet().removeIf(entry -> { + NonPlayerAttackSystem.DefeatedOwnership ownership = entry.getValue(); + return ownership.worldKey().equals(world.getRegistryKey()) && AnimationService.getCurrentStage(world, ownership.instanceId()) == null; + }); + FINISH_ATTEMPTS.entrySet().removeIf(entry -> { + NonPlayerAttackSystem.FinishAttempt attempt = entry.getValue(); + return attempt.worldKey().equals(world.getRegistryKey()) && now - attempt.tick() > 1L; + }); + } + + private static void handleInput(ServerPlayerEntity player, PlayerAttackInputC2SPayload payload) { + if (player != null && payload != null && player.getEntityWorld() instanceof ServerWorld world) { + NonPlayerAttackSystem.Contest contest = CONTESTS.get(payload.instanceId()); + if (contest != null && !contest.resolving && contest.escapable) { + if (contest.worldKey.equals(world.getRegistryKey())) { + boolean defender = contest.victimUuid.equals(player.getUuid()); + boolean attacker = contest.activeAttackerUuids.contains(player.getUuid()); + if (defender || attacker) { + int key = payload.keyId(); + if (key == 1 || key == 2) { + long now = world.getTime(); + long lastTick = contest.lastInputTicks.getOrDefault(player.getUuid(), Long.MIN_VALUE); + if (lastTick == Long.MIN_VALUE || now - lastTick >= 1L) { + int lastKey = contest.lastKeys.getOrDefault(player.getUuid(), 0); + if (lastKey != key) { + contest.lastInputTicks.put(player.getUuid(), now); + contest.lastKeys.put(player.getUuid(), key); + ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid); + if (victim != null && victim.getEntityWorld() == world && victim.isAlive()) { + NonAttackSystem.PvpMashFactors factors = NonAttackSystem.resolveServerPlayerContestStep( + world, player, defender, contest.defeated, contest.startTick + ); + float randomMultiplier = 0.8F + world.getRandom().nextFloat() * 0.40000004F; + float step = factors.step() * randomMultiplier; + contest.progress = clampProgress(contest.progress + (defender ? step : -step)); + broadcastState(world, contest); + if (contest.progress >= 1.0F) { + contest.resolving = true; + NeedsOfNature.completeAttackEscape(world, victim, contest.instanceId, contest.initiatorUuid); + } + } + } + } + } + } + } + } + } + } + + private static void broadcastState(ServerWorld world, NonPlayerAttackSystem.Contest contest) { + AttackStateS2CPayload.EscapeProfile profile = contest.defeated + ? AttackStateS2CPayload.EscapeProfile.DEFEATED + : AttackStateS2CPayload.EscapeProfile.NORMAL; + ServerPlayerEntity victim = world.getServer().getPlayerManager().getPlayer(contest.victimUuid); + if (victim != null && victim.getEntityWorld() == world) { + ServerPlayNetworking.send( + victim, new AttackStateS2CPayload(contest.instanceId, true, contest.escapable, profile, AttackStateS2CPayload.PvpRole.DEFENDER, contest.progress) + ); + } + + for (UUID attackerUuid : contest.activeAttackerUuids) { + ServerPlayerEntity attacker = world.getServer().getPlayerManager().getPlayer(attackerUuid); + if (attacker != null && attacker.getEntityWorld() == world) { + ServerPlayNetworking.send( + attacker, + new AttackStateS2CPayload(contest.instanceId, true, contest.escapable, profile, AttackStateS2CPayload.PvpRole.ATTACKER, contest.progress) + ); + } + } + } + + static boolean isOwnerFinishingDamage(ServerWorld world, ServerPlayerEntity victim, @Nullable DamageSource source) { + ServerPlayerEntity attacker = resolveAttributedPlayer(source); + return attacker != null && isOwnerFinishingDamage(world, victim, attacker); + } + + static boolean shouldBypassVanillaPvpCheck(ServerPlayerEntity victim, PlayerEntity attacker) { + if (victim != null && attacker != null && victim.getEntityWorld() instanceof ServerWorld world) { + NonPlayerAttackSystem.DamageBypass bypass = VANILLA_PVP_BYPASS.get(); + if (bypass != null && bypass.victimUuid().equals(victim.getUuid()) && bypass.attackerUuid().equals(attacker.getUuid())) { + return true; + } + + NonPlayerAttackSystem.DefeatedOwnership ownership = validOwnership(world, victim.getUuid()); + return ownership != null && ownership.ownerUuid().equals(attacker.getUuid()); + } else { + return false; + } + } + + static void applyPlayerAttributedEscapeDamage(ServerWorld world, ServerPlayerEntity victim, ServerPlayerEntity attacker, float amount) { + NonPlayerAttackSystem.DamageBypass previous = VANILLA_PVP_BYPASS.get(); + VANILLA_PVP_BYPASS.set(new NonPlayerAttackSystem.DamageBypass(victim.getUuid(), attacker.getUuid())); + + try { + victim.damage(world, victim.getDamageSources().playerAttack(attacker), amount); + } finally { + if (previous == null) { + VANILLA_PVP_BYPASS.remove(); + } else { + VANILLA_PVP_BYPASS.set(previous); + } + } + } + + private static boolean isOwnerFinishingDamage(ServerWorld world, ServerPlayerEntity victim, ServerPlayerEntity attacker) { + NonPlayerAttackSystem.DefeatedOwnership ownership = validOwnership(world, victim.getUuid()); + return ownership != null && ownership.ownerUuid().equals(attacker.getUuid()); + } + + static void onOwnerFinishingDeath(ServerWorld world, ServerPlayerEntity victim) { + if (world != null && victim != null) { + NonPlayerAttackSystem.DefeatedOwnership ownership = DEFEATED_OWNERSHIP.get(victim.getUuid()); + if (ownership != null && ownership.worldKey().equals(world.getRegistryKey())) { + FINISH_ATTEMPTS.remove(victim.getUuid()); + } + } + } + + @Nullable + static ServerPlayerEntity resolveAttributedPlayer(@Nullable DamageSource source) { + if (source == null) { + return null; + } else if (source.getAttacker() instanceof ServerPlayerEntity player) { + return player; + } else { + return source.getSource() instanceof ServerPlayerEntity player ? player : null; + } + } + + static NonAnimationDamageBehavior defeatedDamageBehavior(AnimationStartContext context) { + return isPlayerAttackContext(context) ? NonAnimationDamageBehavior.IGNORE_DAMAGE : NonAnimationDamageBehavior.STOP_ON_DAMAGE; + } + + static void clearForRespawn(ServerPlayerEntity player) { + clearForPlayer(player); + } + + private static void clearForPlayer(ServerPlayerEntity player) { + if (player != null && player.getEntityWorld() instanceof ServerWorld world) { + UUID var6 = player.getUuid(); + LinkedHashSet stopInstances = new LinkedHashSet<>(); + + for (NonPlayerAttackSystem.Contest contest : List.copyOf(CONTESTS.values())) { + if (contest.worldKey.equals(world.getRegistryKey()) + && ( + contest.victimUuid.equals(var6) + || contest.finishOwnerUuid.equals(var6) + || contest.initiatorUuid.equals(var6) + || contest.activeAttackerUuids.contains(var6) + )) { + stopInstances.add(contest.instanceId); + } + } + + for (NonPlayerAttackSystem.DefeatedOwnership ownership : List.copyOf(DEFEATED_OWNERSHIP.values())) { + if (ownership.worldKey().equals(world.getRegistryKey()) && (ownership.victimUuid().equals(var6) || ownership.ownerUuid().equals(var6))) { + stopInstances.add(ownership.instanceId()); + } + } + + for (UUID instanceId : stopInstances) { + AnimationService.stopAnimation(world, instanceId); + } + + CONTESTS.entrySet().removeIf(entry -> stopInstances.contains(entry.getKey())); + DEFEATED_OWNERSHIP.entrySet().removeIf(entry -> entry.getKey().equals(var6) || entry.getValue().ownerUuid().equals(var6)); + FINISH_ATTEMPTS.remove(var6); + } + } + + static void clearRuntimeState() { + CONTESTS.clear(); + DEFEATED_OWNERSHIP.clear(); + FINISH_ATTEMPTS.clear(); + } + + @Nullable + private static NonPlayerAttackSystem.DefeatedOwnership validOwnership(ServerWorld world, UUID victimUuid) { + NonPlayerAttackSystem.DefeatedOwnership ownership = DEFEATED_OWNERSHIP.get(victimUuid); + if (ownership == null || !ownership.worldKey().equals(world.getRegistryKey())) { + return null; + } else if (AnimationService.getCurrentStage(world, ownership.instanceId()) == null) { + DEFEATED_OWNERSHIP.remove(victimUuid, ownership); + return null; + } else { + return ownership; + } + } + + @Nullable + static NonAnimationSessionData.PlayerAttack playerAttack(@Nullable AnimationStartContext context) { + return context != null && context.sessionData() instanceof NonAnimationSessionData data ? data.playerAttack() : null; + } + + @Nullable + private static UUID firstPlayerUuid(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null) { + for (UUID actorUuid : actorUuids) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(actorUuid); + if (player != null && player.getEntityWorld() == world) { + return player.getUuid(); + } + } + + return null; + } else { + return null; + } + } + + private static List sortedActors(Entity first, Entity second) { + ArrayList actors = new ArrayList<>(List.of(first, second)); + actors.sort(Comparator.comparingInt(Entity::getId)); + return List.copyOf(actors); + } + + private static int countReadyTotems(ServerPlayerEntity player) { + int count = 0; + if (player.getMainHandStack().isOf(Items.TOTEM_OF_UNDYING)) { + count += player.getMainHandStack().getCount(); + } + + if (player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { + count += player.getOffHandStack().getCount(); + } + + return count; + } + + private static float clampProgress(float progress) { + return !Float.isFinite(progress) ? 0.0F : Math.max(0.0F, Math.min(1.0F, progress)); + } + + private record AttackSelection(@Nullable AnimationService.MatchedAnimation match, boolean blockedByProtector) { + } + + private static final class Contest { + final RegistryKey worldKey; + final UUID instanceId; + final UUID victimUuid; + final UUID finishOwnerUuid; + final UUID initiatorUuid; + final Set activeAttackerUuids; + final long startTick; + final Map lastKeys = new HashMap<>(); + final Map lastInputTicks = new HashMap<>(); + float progress; + boolean escapable; + boolean defeated; + boolean resolving; + + Contest( + RegistryKey worldKey, + UUID instanceId, + UUID victimUuid, + UUID finishOwnerUuid, + UUID initiatorUuid, + Set activeAttackerUuids, + long startTick, + boolean defeated + ) { + this.worldKey = worldKey; + this.instanceId = instanceId; + this.victimUuid = victimUuid; + this.finishOwnerUuid = finishOwnerUuid; + this.initiatorUuid = initiatorUuid; + this.activeAttackerUuids = Set.copyOf(activeAttackerUuids); + this.startTick = startTick; + this.defeated = defeated; + } + } + + private record DamageBypass(UUID victimUuid, UUID attackerUuid) { + } + + private record DefeatedOwnership(RegistryKey worldKey, UUID victimUuid, UUID ownerUuid, UUID instanceId) { + } + + private record FinishAttempt(RegistryKey worldKey, UUID ownerUuid, UUID instanceId, int totemsBefore, long tick) { + } +} diff --git a/src/main/java/com/nonid/NonPlayerAvoidanceState.java b/src/main/java/com/nonid/NonPlayerAvoidanceState.java new file mode 100644 index 0000000..c291645 --- /dev/null +++ b/src/main/java/com/nonid/NonPlayerAvoidanceState.java @@ -0,0 +1,7 @@ +package com.nonid; + +import net.minecraft.server.network.ServerPlayerEntity; + +public interface NonPlayerAvoidanceState { + boolean shouldSuppressPlayerAvoidance(ServerPlayerEntity var1); +} diff --git a/src/main/java/com/nonid/NonPlayerInteractionProfiles.java b/src/main/java/com/nonid/NonPlayerInteractionProfiles.java new file mode 100644 index 0000000..5ea9248 --- /dev/null +++ b/src/main/java/com/nonid/NonPlayerInteractionProfiles.java @@ -0,0 +1,383 @@ +package com.nonid; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.Map.Entry; +import net.minecraft.datafixer.DataFixTypes; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.PersistentState; +import net.minecraft.world.PersistentStateType; +import org.jetbrains.annotations.Nullable; + +public final class NonPlayerInteractionProfiles extends PersistentState { + private static final Codec CODEC = RecordCodecBuilder.create( + instance -> instance.group( + Codec.unboundedMap(Codec.STRING, NonPlayerInteractionProfiles.PlayerProfiles.CODEC) + .optionalFieldOf("profiles", Map.of()) + .forGetter(state -> state.profilesByOwner), + Codec.unboundedMap(Codec.STRING, Codec.STRING).optionalFieldOf("known_players", Map.of()).forGetter(state -> state.knownPlayerNames) + ) + .apply(instance, NonPlayerInteractionProfiles::new) + ); + private static final PersistentStateType TYPE = new PersistentStateType( + "needsofnature_player_interactions", NonPlayerInteractionProfiles::new, CODEC, DataFixTypes.SAVED_DATA_COMMAND_STORAGE + ); + private final Map profilesByOwner; + private final Map knownPlayerNames; + + private NonPlayerInteractionProfiles() { + this(Map.of(), Map.of()); + } + + private NonPlayerInteractionProfiles(Map profilesByOwner, Map knownPlayerNames) { + this.profilesByOwner = new LinkedHashMap<>(profilesByOwner == null ? Map.of() : profilesByOwner); + this.knownPlayerNames = new LinkedHashMap<>(knownPlayerNames == null ? Map.of() : knownPlayerNames); + } + + public static NonPlayerInteractionProfiles get(ServerWorld world) { + if (world != null && world.getServer() != null && world.getServer().getOverworld() != null) { + return (NonPlayerInteractionProfiles)world.getServer().getOverworld().getPersistentStateManager().getOrCreate(TYPE); + } else { + throw new IllegalArgumentException("A live server world is required"); + } + } + + public synchronized void recordKnownPlayer(ServerPlayerEntity player) { + if (player != null) { + this.recordKnownPlayer(player.getUuid(), player.getGameProfile().name()); + } + } + + public synchronized void recordKnownPlayer(UUID uuid, String name) { + if (uuid != null) { + String safeName = sanitizeName(name); + String uuidKey = uuid.toString(); + boolean changed = !safeName.equals(this.knownPlayerNames.put(uuidKey, safeName)); + Map updatedOwners = new LinkedHashMap<>(); + + for (Entry ownerEntry : this.profilesByOwner.entrySet()) { + NonPlayerInteractionProfiles.PlayerProfiles profiles = ownerEntry.getValue(); + NonPlayerInteractionProfiles.ProfileOverride existing = profiles.overrides().get(uuidKey); + if (existing != null && !safeName.equals(existing.name())) { + Map overrides = new LinkedHashMap<>(profiles.overrides()); + overrides.put(uuidKey, new NonPlayerInteractionProfiles.ProfileOverride(safeName, existing.mode())); + updatedOwners.put(ownerEntry.getKey(), new NonPlayerInteractionProfiles.PlayerProfiles(profiles.defaultMode(), overrides)); + } + } + + if (!updatedOwners.isEmpty()) { + this.profilesByOwner.putAll(updatedOwners); + changed = true; + } + + if (changed) { + this.markDirty(); + } + } + } + + @Nullable + public synchronized NonPlayerInteractionProfiles.KnownPlayer findKnownPlayerByName(String name) { + String normalized = sanitizeName(name); + if (normalized.isEmpty()) { + return null; + } + + for (Entry entry : this.knownPlayerNames.entrySet()) { + if (entry.getValue().equalsIgnoreCase(normalized)) { + try { + return new NonPlayerInteractionProfiles.KnownPlayer(UUID.fromString(entry.getKey()), entry.getValue()); + } catch (IllegalArgumentException ignored) { + return null; + } + } + } + + return null; + } + + @Nullable + public synchronized NonPlayerInteractionProfiles.KnownPlayer findKnownPlayer(UUID uuid) { + if (uuid == null) { + return null; + } + + String name = this.knownPlayerNames.get(uuid.toString()); + return name == null ? null : new NonPlayerInteractionProfiles.KnownPlayer(uuid, name); + } + + public synchronized NonPlayerInteractionProfiles.Mode resolveMode(UUID ownerUuid, UUID otherUuid, int allowedMask) { + NonPlayerInteractionProfiles.PlayerProfiles profiles = this.getOrDefault(ownerUuid); + NonPlayerInteractionProfiles.ProfileOverride override = otherUuid == null ? null : profiles.overrides().get(otherUuid.toString()); + return NonPlayerInteractionProfiles.Mode.normalizeAllowed(override == null ? profiles.defaultMode() : override.mode(), allowedMask); + } + + public synchronized NonPlayerInteractionProfiles.Snapshot snapshot(UUID ownerUuid, int allowedMask) { + this.normalizeOwner(ownerUuid, allowedMask); + NonPlayerInteractionProfiles.PlayerProfiles profiles = this.getOrDefault(ownerUuid); + NonPlayerInteractionProfiles.Mode normalizedDefault = NonPlayerInteractionProfiles.Mode.normalizeAllowed(profiles.defaultMode(), allowedMask); + List overrides = new ArrayList<>(); + + for (Entry entry : profiles.overrides().entrySet()) { + try { + UUID uuid = UUID.fromString(entry.getKey()); + NonPlayerInteractionProfiles.ProfileOverride value = entry.getValue(); + overrides.add(new NonPlayerInteractionProfiles.OverrideView(uuid, value.name(), value.mode())); + } catch (IllegalArgumentException var11) { + } + } + + overrides.sort(Comparator.comparing(NonPlayerInteractionProfiles.OverrideView::name, String.CASE_INSENSITIVE_ORDER)); + List known = new ArrayList<>(); + + for (Entry entry : this.knownPlayerNames.entrySet()) { + try { + UUID uuid = UUID.fromString(entry.getKey()); + if (!uuid.equals(ownerUuid)) { + known.add(new NonPlayerInteractionProfiles.KnownPlayer(uuid, entry.getValue())); + } + } catch (IllegalArgumentException var10) { + } + } + + known.sort(Comparator.comparing(NonPlayerInteractionProfiles.KnownPlayer::name, String.CASE_INSENSITIVE_ORDER)); + return new NonPlayerInteractionProfiles.Snapshot(normalizedDefault, List.copyOf(overrides), List.copyOf(known)); + } + + public synchronized void setDefaultMode(UUID ownerUuid, NonPlayerInteractionProfiles.Mode mode, int allowedMask) { + if (ownerUuid != null) { + NonPlayerInteractionProfiles.PlayerProfiles existing = this.getOrDefault(ownerUuid); + this.profilesByOwner + .put( + ownerUuid.toString(), + new NonPlayerInteractionProfiles.PlayerProfiles(NonPlayerInteractionProfiles.Mode.normalizeAllowed(mode, allowedMask), existing.overrides()) + ); + this.markDirty(); + } + } + + public synchronized void setOverride(UUID ownerUuid, UUID otherUuid, String name, NonPlayerInteractionProfiles.Mode mode, int allowedMask) { + if (ownerUuid != null && otherUuid != null && !ownerUuid.equals(otherUuid)) { + NonPlayerInteractionProfiles.PlayerProfiles existing = this.getOrDefault(ownerUuid); + Map overrides = new LinkedHashMap<>(existing.overrides()); + String safeName = sanitizeName(name); + if (safeName.isEmpty()) { + safeName = this.knownPlayerNames.getOrDefault(otherUuid.toString(), otherUuid.toString()); + } + + overrides.put( + otherUuid.toString(), + new NonPlayerInteractionProfiles.ProfileOverride(safeName, NonPlayerInteractionProfiles.Mode.normalizeAllowed(mode, allowedMask)) + ); + this.profilesByOwner.put(ownerUuid.toString(), new NonPlayerInteractionProfiles.PlayerProfiles(existing.defaultMode(), overrides)); + this.knownPlayerNames.put(otherUuid.toString(), safeName); + this.markDirty(); + } + } + + public synchronized void removeOverride(UUID ownerUuid, UUID otherUuid) { + if (ownerUuid != null && otherUuid != null) { + NonPlayerInteractionProfiles.PlayerProfiles existing = this.getOrDefault(ownerUuid); + if (existing.overrides().containsKey(otherUuid.toString())) { + Map overrides = new LinkedHashMap<>(existing.overrides()); + overrides.remove(otherUuid.toString()); + this.profilesByOwner.put(ownerUuid.toString(), new NonPlayerInteractionProfiles.PlayerProfiles(existing.defaultMode(), overrides)); + this.markDirty(); + } + } + } + + public synchronized void normalizeAll(int allowedMask) { + boolean changed = false; + + for (String ownerKey : List.copyOf(this.profilesByOwner.keySet())) { + changed |= this.normalizeOwnerKey(ownerKey, allowedMask); + } + + if (changed) { + this.markDirty(); + } + } + + private void normalizeOwner(UUID ownerUuid, int allowedMask) { + if (ownerUuid != null && this.normalizeOwnerKey(ownerUuid.toString(), allowedMask)) { + this.markDirty(); + } + } + + private boolean normalizeOwnerKey(String ownerKey, int allowedMask) { + NonPlayerInteractionProfiles.PlayerProfiles existing = this.profilesByOwner.get(ownerKey); + if (existing == null) { + return false; + } + + NonPlayerInteractionProfiles.Mode normalizedDefault = NonPlayerInteractionProfiles.Mode.normalizeAllowed(existing.defaultMode(), allowedMask); + Map overrides = new LinkedHashMap<>(); + boolean changed = normalizedDefault != existing.defaultMode(); + + for (Entry entry : existing.overrides().entrySet()) { + NonPlayerInteractionProfiles.ProfileOverride value = entry.getValue(); + NonPlayerInteractionProfiles.Mode normalized = NonPlayerInteractionProfiles.Mode.normalizeAllowed(value.mode(), allowedMask); + overrides.put(entry.getKey(), new NonPlayerInteractionProfiles.ProfileOverride(value.name(), normalized)); + changed |= normalized != value.mode(); + } + + if (changed) { + this.profilesByOwner.put(ownerKey, new NonPlayerInteractionProfiles.PlayerProfiles(normalizedDefault, overrides)); + } + + return changed; + } + + private NonPlayerInteractionProfiles.PlayerProfiles getOrDefault(UUID ownerUuid) { + return ownerUuid == null + ? new NonPlayerInteractionProfiles.PlayerProfiles(NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS, Map.of()) + : this.profilesByOwner + .getOrDefault( + ownerUuid.toString(), new NonPlayerInteractionProfiles.PlayerProfiles(NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS, Map.of()) + ); + } + + public static int normalizeAllowedMask(int mask) { + int normalized = mask & 15; + return normalized == 0 ? 15 : normalized; + } + + private static String sanitizeName(@Nullable String name) { + if (name == null) { + return ""; + } + + String trimmed = name.trim(); + return trimmed.length() <= 64 ? trimmed : trimmed.substring(0, 64); + } + + public record KnownPlayer(UUID uuid, String name) { + } + + public enum Mode { + DENY_ALL("deny_all", NonPlayerInteractionProfiles.RequestPolicy.DENY, false), + REQUESTS_ONLY("requests_only", NonPlayerInteractionProfiles.RequestPolicy.ASK, false), + REQUESTS_AND_ATTACKS("requests_and_attacks", NonPlayerInteractionProfiles.RequestPolicy.ASK, true), + AUTO_ACCEPT("auto_accept", NonPlayerInteractionProfiles.RequestPolicy.AUTO_ACCEPT, true); + + public static final Codec CODEC = Codec.STRING + .xmap(value -> fromId(value, REQUESTS_AND_ATTACKS), NonPlayerInteractionProfiles.Mode::id); + private final String id; + private final NonPlayerInteractionProfiles.RequestPolicy requestPolicy; + private final boolean attacksAllowed; + + Mode(String id, NonPlayerInteractionProfiles.RequestPolicy requestPolicy, boolean attacksAllowed) { + this.id = id; + this.requestPolicy = requestPolicy; + this.attacksAllowed = attacksAllowed; + } + + public String id() { + return this.id; + } + + public NonPlayerInteractionProfiles.RequestPolicy requestPolicy() { + return this.requestPolicy; + } + + public boolean attacksAllowed() { + return this.attacksAllowed; + } + + public int bit() { + return 1 << this.ordinal(); + } + + public static NonPlayerInteractionProfiles.Mode fromId(@Nullable String value, NonPlayerInteractionProfiles.Mode fallback) { + if (value != null) { + String normalized = value.trim().toLowerCase(Locale.ROOT); + + for (NonPlayerInteractionProfiles.Mode mode : values()) { + if (mode.id.equals(normalized)) { + return mode; + } + } + } + + return fallback == null ? REQUESTS_AND_ATTACKS : fallback; + } + + public static NonPlayerInteractionProfiles.Mode leastPermissiveAllowed(int allowedMask) { + int normalized = NonPlayerInteractionProfiles.normalizeAllowedMask(allowedMask); + + for (NonPlayerInteractionProfiles.Mode mode : values()) { + if ((normalized & mode.bit()) != 0) { + return mode; + } + } + + return REQUESTS_AND_ATTACKS; + } + + public static NonPlayerInteractionProfiles.Mode normalizeAllowed(@Nullable NonPlayerInteractionProfiles.Mode mode, int allowedMask) { + NonPlayerInteractionProfiles.Mode resolved = mode == null ? REQUESTS_AND_ATTACKS : mode; + return (NonPlayerInteractionProfiles.normalizeAllowedMask(allowedMask) & resolved.bit()) != 0 ? resolved : leastPermissiveAllowed(allowedMask); + } + } + + public record OverrideView(UUID uuid, String name, NonPlayerInteractionProfiles.Mode mode) { + } + + public record PlayerProfiles(NonPlayerInteractionProfiles.Mode defaultMode, Map overrides) { + private static final Codec CODEC = RecordCodecBuilder.create( + instance -> instance.group( + NonPlayerInteractionProfiles.Mode.CODEC + .optionalFieldOf("default_mode", NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS) + .forGetter(NonPlayerInteractionProfiles.PlayerProfiles::defaultMode), + Codec.unboundedMap(Codec.STRING, NonPlayerInteractionProfiles.ProfileOverride.CODEC) + .optionalFieldOf("overrides", Map.of()) + .forGetter(NonPlayerInteractionProfiles.PlayerProfiles::overrides) + ) + .apply(instance, NonPlayerInteractionProfiles.PlayerProfiles::new) + ); + + public PlayerProfiles { + defaultMode = defaultMode == null ? NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS : defaultMode; + overrides = overrides == null ? Map.of() : Map.copyOf(overrides); + } + } + + public record ProfileOverride(String name, NonPlayerInteractionProfiles.Mode mode) { + private static final Codec CODEC = RecordCodecBuilder.create( + instance -> instance.group( + Codec.STRING.optionalFieldOf("name", "").forGetter(NonPlayerInteractionProfiles.ProfileOverride::name), + NonPlayerInteractionProfiles.Mode.CODEC + .optionalFieldOf("mode", NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS) + .forGetter(NonPlayerInteractionProfiles.ProfileOverride::mode) + ) + .apply(instance, NonPlayerInteractionProfiles.ProfileOverride::new) + ); + + public ProfileOverride { + name = NonPlayerInteractionProfiles.sanitizeName(name); + mode = mode == null ? NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS : mode; + } + } + + public enum RequestPolicy { + DENY, + ASK, + AUTO_ACCEPT; + } + + public record Snapshot( + NonPlayerInteractionProfiles.Mode defaultMode, + List overrides, + List knownPlayers + ) { + } +} diff --git a/src/main/java/com/nonid/NonPlayerInteractionSystem.java b/src/main/java/com/nonid/NonPlayerInteractionSystem.java new file mode 100644 index 0000000..5169502 --- /dev/null +++ b/src/main/java/com/nonid/NonPlayerInteractionSystem.java @@ -0,0 +1,1101 @@ +package com.nonid; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.network.PlayerAnimationCancelC2SPayload; +import com.nonid.network.PlayerAnimationChoiceC2SPayload; +import com.nonid.network.PlayerAnimationConsentC2SPayload; +import com.nonid.network.PlayerAnimationConsentS2CPayload; +import com.nonid.network.PlayerAnimationRequestStatusS2CPayload; +import com.nonid.network.PlayerAnimationSelectionS2CPayload; +import com.nonid.network.PlayerProfileActionC2SPayload; +import com.nonid.network.PlayerProfileSnapshotS2CPayload; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDamage; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStarted; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStopped; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Join; +import net.minecraft.entity.Entity; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public final class NonPlayerInteractionSystem { + private static final int REQUEST_TIMEOUT_TICKS = 300; + private static final String STATUS_WAITING = "waiting"; + private static final String STATUS_STARTED = "started"; + private static final String STATUS_FAILED = "failed"; + private static final String STATUS_CANCELLED = "cancelled"; + private static final Map SELECTIONS = new HashMap<>(); + private static final Map REQUESTS = new HashMap<>(); + private static final Map SESSION_BY_PLAYER = new HashMap<>(); + @Nullable + private static MinecraftServer activeServer; + + private NonPlayerInteractionSystem() { + } + + public static void register() { + ServerLifecycleEvents.SERVER_STARTED.register((ServerStarted)server -> activeServer = server); + ServerLifecycleEvents.SERVER_STOPPED.register((ServerStopped)server -> { + if (activeServer == server) { + activeServer = null; + } + + SELECTIONS.clear(); + REQUESTS.clear(); + SESSION_BY_PLAYER.clear(); + }); + ServerPlayNetworking.registerGlobalReceiver( + PlayerProfileActionC2SPayload.ID, (payload, context) -> context.server().execute(() -> handleProfileAction(context.player(), payload)) + ); + ServerPlayNetworking.registerGlobalReceiver( + PlayerAnimationChoiceC2SPayload.ID, (payload, context) -> context.server().execute(() -> handleAnimationChoice(context.player(), payload)) + ); + ServerPlayNetworking.registerGlobalReceiver( + PlayerAnimationConsentC2SPayload.ID, (payload, context) -> context.server().execute(() -> handleConsent(context.player(), payload)) + ); + ServerPlayNetworking.registerGlobalReceiver( + PlayerAnimationCancelC2SPayload.ID, (payload, context) -> context.server().execute(() -> handleCancel(context.player(), payload.requestId())) + ); + ServerPlayConnectionEvents.JOIN.register((Join)(handler, sender, server) -> server.execute(() -> { + if (handler.player.getEntityWorld() instanceof ServerWorld world) { + NonPlayerInteractionProfiles var6 = NonPlayerInteractionProfiles.get(world); + + for (ServerPlayerEntity listedPlayer : server.getPlayerManager().getPlayerList()) { + var6.recordKnownPlayer(listedPlayer); + } + + var6.normalizeAll(allowedMask()); + } + })); + ServerPlayConnectionEvents.DISCONNECT + .register( + (Disconnect)(handler, server) -> server.execute(() -> cancelForPlayer(handler.player.getUuid(), "gui.needsofnature.player_request.disconnected")) + ); + ServerLivingEntityEvents.AFTER_DAMAGE.register((AfterDamage)(entity, source, baseDamageTaken, damageTaken, blocked) -> { + if (!blocked && damageTaken > 0.0F && entity instanceof ServerPlayerEntity player) { + cancelForPlayer(player.getUuid(), "gui.needsofnature.player_request.damaged"); + } + }); + } + + public static boolean handleInteraction(ServerWorld world, ServerPlayerEntity requester, Entity clickedEntity) { + if (isUsablePlayer(requester) && world != null && clickedEntity != null && !clickedEntity.isRemoved()) { + if (requester.getEntityWorld() != world || clickedEntity.getEntityWorld() != world) { + return false; + } else if (requester.getUuid().equals(clickedEntity.getUuid())) { + return false; + } else { + UUID activeInstance = NeedsOfNature.findActiveInstanceContainingActor(world, clickedEntity.getUuid()); + if (activeInstance != null) { + return beginJoinRequest(world, requester, clickedEntity, activeInstance); + } else { + return clickedEntity instanceof ServerPlayerEntity target && requester.isSneaking() ? beginDirectSelection(world, requester, target) : false; + } + } + } else { + return false; + } + } + + public static void tick(ServerWorld world) { + if (world != null) { + long now = world.getTime(); + + for (NonPlayerInteractionSystem.SelectionSession selection : List.copyOf(SELECTIONS.values())) { + if (world.getRegistryKey().equals(selection.worldKey())) { + ServerPlayerEntity requester = getPlayer(world, selection.requesterUuid()); + Entity target = resolveEntity(world, selection.targetUuid()); + if (now >= selection.expiresTick()) { + failSelection(selection, "failed", "gui.needsofnature.player_request.timeout"); + } else if (!validInteractionPair(requester, target)) { + failSelection(selection, "cancelled", "gui.needsofnature.player_request.out_of_range"); + } else if (selection.kind() == NonPlayerInteractionSystem.RequestKind.DIRECT && isBusyForDirectRequest(world, requester, target)) { + failSelection(selection, "failed", "gui.needsofnature.player_request.player_busy"); + } else if (selection.kind() == NonPlayerInteractionSystem.RequestKind.JOIN && !isSelectionSourceJoinable(world, selection)) { + failSelection(selection, "failed", "gui.needsofnature.player_request.animation_changed"); + } + } + } + + for (NonPlayerInteractionSystem.PendingRequest request : List.copyOf(REQUESTS.values())) { + if (world.getRegistryKey().equals(request.worldKey())) { + ServerPlayerEntity requester = getPlayer(world, request.requesterUuid()); + Entity target = resolveEntity(world, request.interactionTargetUuid()); + if (now >= request.expiresTick()) { + finish(request, "failed", "gui.needsofnature.player_request.timeout"); + } else if (!validInteractionPair(requester, target)) { + finish(request, "cancelled", "gui.needsofnature.player_request.out_of_range"); + } else if (request.kind() == NonPlayerInteractionSystem.RequestKind.DIRECT && isBusyForDirectRequest(world, requester, target)) { + finish(request, "failed", "gui.needsofnature.player_request.player_busy"); + } else if (request.kind() == NonPlayerInteractionSystem.RequestKind.JOIN + && (request.sourceInstanceUuid() == null || AnimationService.getCurrentStage(world, request.sourceInstanceUuid()) == null)) { + finish(request, "failed", "gui.needsofnature.player_request.animation_changed"); + } + } + } + } + } + + public static void normalizeAndSyncAll(MinecraftServer server) { + if (server != null && server.getOverworld() != null) { + NonPlayerInteractionProfiles profiles = NonPlayerInteractionProfiles.get(server.getOverworld()); + profiles.normalizeAll(allowedMask()); + + for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { + sendProfileSnapshot(player, ""); + } + } + } + + public static boolean areAttacksAllowed(ServerPlayerEntity target, ServerPlayerEntity otherPlayer) { + if (target != null && otherPlayer != null && target.getEntityWorld() instanceof ServerWorld world) { + NonPlayerInteractionProfiles.Mode mode = NonPlayerInteractionProfiles.get(world).resolveMode(target.getUuid(), otherPlayer.getUuid(), allowedMask()); + return mode.attacksAllowed(); + } else { + return false; + } + } + + private static boolean beginDirectSelection(ServerWorld world, ServerPlayerEntity requester, ServerPlayerEntity target) { + if (!validInteractionPair(requester, target)) { + return false; + } + + if (!hasRequestEnergy(requester)) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.energy_low"); + return true; + } + + if (SESSION_BY_PLAYER.containsKey(requester.getUuid())) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.already_pending"); + return true; + } + + NonPlayerInteractionProfiles.Mode targetMode = resolveMode(world, target, requester); + if (targetMode.requestPolicy() == NonPlayerInteractionProfiles.RequestPolicy.DENY) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.not_accepting"); + return true; + } + + if (!NeedsOfNature.hasActivePlayerAnimation(requester) && !NeedsOfNature.hasActivePlayerAnimation(target)) { + List actors = sortedActors(requester, target); + List actorUuids = actors.stream().map(Entity::getUuid).toList(); + List candidates = enumerateCandidates(world, actors, target, null); + if (candidates.isEmpty()) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.no_match"); + return true; + } else { + UUID requestId = UUID.randomUUID(); + NonPlayerInteractionSystem.SelectionSession session = new NonPlayerInteractionSystem.SelectionSession( + requestId, + NonPlayerInteractionSystem.RequestKind.DIRECT, + world.getRegistryKey(), + requester.getUuid(), + target.getUuid(), + null, + world.getTime() + 300L, + List.copyOf(actorUuids), + List.copyOf(candidates) + ); + SELECTIONS.put(requestId, session); + SESSION_BY_PLAYER.put(requester.getUuid(), requestId); + ServerPlayNetworking.send( + requester, + new PlayerAnimationSelectionS2CPayload( + requestId, target.getUuid(), target.getGameProfile().name(), candidatesJson(session, requester.getUuid()), false + ) + ); + return true; + } + } else { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.player_busy"); + return true; + } + } + + private static boolean beginJoinRequest(ServerWorld world, ServerPlayerEntity requester, Entity clickedEntity, UUID instanceId) { + if (!hasRequestEnergy(requester)) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.energy_low"); + return true; + } else if (SESSION_BY_PLAYER.containsKey(requester.getUuid())) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.already_pending"); + return true; + } else if (AnimationService.isActorPendingOrActive(world, requester.getUuid())) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.player_busy"); + return true; + } else { + boolean defeated = NeedsOfNature.isDefeatedInstance(world, instanceId); + if (!defeated && !NeedsOfNature.isVoluntaryJoinableInstance(world, instanceId)) { + return false; + } else { + List actors = entitiesForJoin(world, instanceId, requester); + if (actors.isEmpty()) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.no_match"); + return true; + } else { + Identifier predecessorAnimationId = AnimationService.getActiveAnimationId(world, instanceId); + if (predecessorAnimationId == null) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.animation_changed"); + return true; + } else if (defeated) { + return beginDefeatedJoin(world, requester, clickedEntity, instanceId, actors, predecessorAnimationId); + } else if (resolveConsentPlan(world, requester, actors, false) == null) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.not_accepting"); + return true; + } else { + List candidates = enumerateCandidates(world, actors, clickedEntity, predecessorAnimationId); + if (candidates.isEmpty()) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.no_match"); + return true; + } else { + UUID requestId = UUID.randomUUID(); + NonPlayerInteractionSystem.SelectionSession selection = new NonPlayerInteractionSystem.SelectionSession( + requestId, + NonPlayerInteractionSystem.RequestKind.JOIN, + world.getRegistryKey(), + requester.getUuid(), + clickedEntity.getUuid(), + instanceId, + world.getTime() + 300L, + actors.stream().map(Entity::getUuid).toList(), + List.copyOf(candidates) + ); + SELECTIONS.put(requestId, selection); + SESSION_BY_PLAYER.put(requester.getUuid(), requestId); + ServerPlayNetworking.send( + requester, + new PlayerAnimationSelectionS2CPayload( + requestId, clickedEntity.getUuid(), clickedEntity.getName().getString(), candidatesJson(selection, requester.getUuid()), true + ) + ); + return true; + } + } + } + } + } + } + + private static boolean beginDefeatedJoin( + ServerWorld world, ServerPlayerEntity requester, Entity clickedEntity, UUID instanceId, List actors, Identifier predecessorAnimationId + ) { + AnimationService.MatchedAnimation matched = NonAnimationMatcher.findAutomaticAttack( + world, actors, Set.of(), clickedEntity, true, requester, Set.of(), null, predecessorAnimationId + ); + if (matched == null) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.no_match"); + return true; + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(matched.animationId()); + if (definition != null && isPlayerInteractionDefinition(definition)) { + NonPlayerInteractionSystem.ConsentPlan consentPlan = resolveConsentPlan(world, requester, actors, true); + if (consentPlan == null) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.not_accepting"); + return true; + } else { + NonPlayerInteractionSystem.PendingRequest request = new NonPlayerInteractionSystem.PendingRequest( + UUID.randomUUID(), + NonPlayerInteractionSystem.RequestKind.JOIN, + world.getRegistryKey(), + requester.getUuid(), + clickedEntity.getUuid(), + instanceId, + actors.stream().map(Entity::getUuid).toList(), + matched.animationId(), + definition.displayName(), + List.copyOf(matched.actorKeys()), + List.copyOf(matched.stages()), + definition.contentTags() == null ? List.of() : List.copyOf(definition.contentTags()), + world.getTime() + 300L, + consentPlan.consentPlayers(), + consentPlan.acceptedPlayers() + ); + executeRequest(world, request); + return true; + } + } else { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.no_match"); + return true; + } + } + + private static void handleAnimationChoice(ServerPlayerEntity requester, PlayerAnimationChoiceC2SPayload payload) { + NonPlayerInteractionSystem.SelectionSession selection = SELECTIONS.get(payload.requestId()); + if (selection != null && selection.requesterUuid().equals(requester.getUuid())) { + if (!(requester.getEntityWorld() instanceof ServerWorld world && world.getRegistryKey().equals(selection.worldKey()))) { + failSelection(selection, "failed", "gui.needsofnature.player_request.invalid"); + } else if (payload.choiceIndex() >= 0 && payload.choiceIndex() < selection.candidates().size()) { + Entity target = resolveEntity(world, selection.targetUuid()); + if (!validInteractionPair(requester, target)) { + failSelection(selection, "failed", "gui.needsofnature.player_request.out_of_range"); + } else if (selection.kind() == NonPlayerInteractionSystem.RequestKind.DIRECT && isBusyForDirectRequest(world, requester, target)) { + failSelection(selection, "failed", "gui.needsofnature.player_request.player_busy"); + } else if (selection.kind() == NonPlayerInteractionSystem.RequestKind.JOIN && !isSelectionSourceJoinable(world, selection)) { + failSelection(selection, "failed", "gui.needsofnature.player_request.animation_changed"); + } else { + List actors = resolveActors(world, selection.actorUuids()); + if (actors.size() != selection.actorUuids().size()) { + failSelection(selection, "failed", "gui.needsofnature.player_request.invalid"); + } else { + NonPlayerInteractionSystem.Candidate candidate = selection.candidates().get(payload.choiceIndex()); + if (!candidate.selectable()) { + failSelection(selection, "failed", "gui.needsofnature.player_request.block_requirement_unavailable"); + } else { + NonPlayerInteractionSystem.ConsentPlan consentPlan = resolveConsentPlan(world, requester, actors, false); + if (consentPlan == null) { + failSelection(selection, "failed", "gui.needsofnature.player_request.not_accepting"); + } else { + removeSelection(selection); + NonPlayerInteractionSystem.PendingRequest request = new NonPlayerInteractionSystem.PendingRequest( + selection.id(), + selection.kind(), + selection.worldKey(), + requester.getUuid(), + selection.targetUuid(), + selection.sourceInstanceUuid(), + selection.actorUuids(), + candidate.animationId(), + candidate.displayName(), + candidate.actorKeys(), + candidate.stages(), + candidate.contentTags(), + world.getTime() + 300L, + consentPlan.consentPlayers(), + consentPlan.acceptedPlayers() + ); + if (consentPlan.consentPlayers().isEmpty()) { + executeRequest(world, request); + } else if (!reserveRequest(request)) { + sendStandaloneFailure(requester, "gui.needsofnature.player_request.recipient_busy"); + } else { + promptConsent(world, request); + } + } + } + } + } + } else { + failSelection(selection, "failed", "gui.needsofnature.player_request.invalid"); + } + } + } + + private static void handleConsent(ServerPlayerEntity player, PlayerAnimationConsentC2SPayload payload) { + NonPlayerInteractionSystem.PendingRequest request = REQUESTS.get(payload.requestId()); + if (request != null && request.consentPlayers().contains(player.getUuid())) { + if (!payload.accepted()) { + finish(request, "failed", "gui.needsofnature.player_request.rejected"); + } else { + NonPlayerInteractionSystem.PendingRequest accepted = request.withAccepted(player.getUuid()); + REQUESTS.put(request.id(), accepted); + if (accepted.acceptedPlayers().containsAll(accepted.consentPlayers())) { + if (player.getEntityWorld() instanceof ServerWorld world && world.getRegistryKey().equals(accepted.worldKey())) { + executeRequest(world, accepted); + } else { + finish(accepted, "failed", "gui.needsofnature.player_request.invalid"); + } + } + } + } + } + + private static void handleCancel(ServerPlayerEntity player, UUID requestId) { + NonPlayerInteractionSystem.SelectionSession selection = SELECTIONS.get(requestId); + if (selection != null && selection.requesterUuid().equals(player.getUuid())) { + failSelection(selection, "cancelled", "gui.needsofnature.player_request.cancelled"); + } else { + NonPlayerInteractionSystem.PendingRequest request = REQUESTS.get(requestId); + if (request != null && (request.requesterUuid().equals(player.getUuid()) || request.consentPlayers().contains(player.getUuid()))) { + finish(request, "cancelled", "gui.needsofnature.player_request.cancelled"); + } + } + } + + private static void executeRequest(ServerWorld world, NonPlayerInteractionSystem.PendingRequest request) { + if (world != null && request != null) { + ServerPlayerEntity requester = getPlayer(world, request.requesterUuid()); + Entity interactionTarget = resolveEntity(world, request.interactionTargetUuid()); + if (!validInteractionPair(requester, interactionTarget)) { + finish(request, "failed", "gui.needsofnature.player_request.out_of_range"); + } else { + List actors = resolveActors(world, request.actorUuids()); + if (actors.size() != request.actorUuids().size()) { + finish(request, "failed", "gui.needsofnature.player_request.invalid"); + } else { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(request.animationId()); + if (definition != null && isPlayerInteractionDefinition(definition)) { + NonGatherSystem.cancelForActors(world, request.actorUuids()); + boolean started; + if (request.kind() == NonPlayerInteractionSystem.RequestKind.DIRECT) { + for (UUID actorUuid : request.actorUuids()) { + if (AnimationService.isActorPendingOrActive(world, actorUuid)) { + finish(request, "failed", "gui.needsofnature.player_request.player_busy"); + return; + } + } + + if (!AnimationService.isAnimationStartEligible(world, definition, actors, request.actorKeys(), interactionTarget)) { + finish(request, "failed", "gui.needsofnature.player_request.animation_changed"); + return; + } + + if (NeedsOfNature.isProtectedByAccessory(world, definition, actors) + && NeedsOfNature.consumeProtectorForAnimation(world, definition, actors)) { + finish(request, "failed", "gui.needsofnature.player_request.protected"); + return; + } + + UUID instanceId = NeedsOfNature.startAnimationNowWithContext( + world, + request.animationId(), + actors, + request.actorKeys(), + request.stages(), + NonAnimationDamageBehavior.STOP_ON_DAMAGE, + false, + interactionTarget, + NeedsOfNature.buildModeContext(false), + requester, + false, + true + ); + started = instanceId != null; + } else { + if (request.sourceInstanceUuid() == null || AnimationService.getCurrentStage(world, request.sourceInstanceUuid()) == null) { + finish(request, "failed", "gui.needsofnature.player_request.animation_changed"); + return; + } + + if (NeedsOfNature.isProtectedByAccessory(world, definition, actors) + && NeedsOfNature.consumeProtectorForAnimation(world, definition, actors)) { + finish(request, "failed", "gui.needsofnature.player_request.protected"); + return; + } + + started = NeedsOfNature.joinAnimationNow( + world, request.sourceInstanceUuid(), List.of(requester), requester, request.animationId(), interactionTarget, request.actorKeys() + ); + } + + finish( + request, + started ? "started" : "failed", + started ? "gui.needsofnature.player_request.started" : "gui.needsofnature.player_request.animation_changed" + ); + } else { + finish(request, "failed", "gui.needsofnature.player_request.animation_changed"); + } + } + } + } + } + + private static List enumerateCandidates( + ServerWorld world, List actors, Entity positionAnchor, @Nullable Identifier joinPredecessorAnimationId + ) { + List result = new ArrayList<>(); + + for (AnimationService.EligibleAnimation candidate : NonAnimationMatcher.findEligibleAutomatic( + world, actors, Set.of(), positionAnchor, false, null, Set.of(), joinPredecessorAnimationId + )) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId()); + if (definition != null && isPlayerInteractionDefinition(definition)) { + boolean selectable = AnimationService.isAnimationStartEligible(world, definition, actors, candidate.actorKeys(), positionAnchor); + AnimationDefinitions.BlockRequirements blockRequirements = definition.blockRequirements(); + if (selectable || blockRequirements != null) { + result.add( + new NonPlayerInteractionSystem.Candidate( + candidate.animationId(), + candidate.displayName(), + candidate.actorKeys(), + candidate.stages(), + candidate.contentTags(), + selectable, + summarizeBlockRequirement(blockRequirements), + candidate.packName(), + candidate.packAuthor() + ) + ); + } + } + } + + return List.copyOf(result); + } + + @Nullable + private static NonPlayerInteractionSystem.ConsentPlan resolveConsentPlan( + ServerWorld world, ServerPlayerEntity requester, List actors, boolean attack + ) { + Set consentPlayers = new LinkedHashSet<>(); + Set acceptedPlayers = new LinkedHashSet<>(); + + for (Entity actor : actors) { + if (actor instanceof ServerPlayerEntity existingPlayer && !existingPlayer.getUuid().equals(requester.getUuid())) { + NonPlayerInteractionProfiles.Mode mode = resolveMode(world, existingPlayer, requester); + if (attack) { + if (!mode.attacksAllowed()) { + return null; + } + } else { + if (mode.requestPolicy() == NonPlayerInteractionProfiles.RequestPolicy.DENY) { + return null; + } + + if (mode.requestPolicy() == NonPlayerInteractionProfiles.RequestPolicy.AUTO_ACCEPT) { + acceptedPlayers.add(existingPlayer.getUuid()); + } else { + consentPlayers.add(existingPlayer.getUuid()); + } + } + } + } + + return new NonPlayerInteractionSystem.ConsentPlan(Set.copyOf(consentPlayers), Set.copyOf(acceptedPlayers)); + } + + private static boolean isSelectionSourceJoinable(ServerWorld world, NonPlayerInteractionSystem.SelectionSession selection) { + if (world == null || selection == null || selection.sourceInstanceUuid() == null) { + return false; + } + + if (!NeedsOfNature.isVoluntaryJoinableInstance(world, selection.sourceInstanceUuid())) { + return false; + } + + UUID targetInstance = NeedsOfNature.findActiveInstanceContainingActor(world, selection.targetUuid()); + return selection.sourceInstanceUuid().equals(targetInstance); + } + + static boolean isPlayerInteractionDefinition(AnimationDefinitions.Definition definition) { + if (definition == null) { + return false; + } + + if (definition.animationTags() == null) { + return true; + } + + for (String rawTag : definition.animationTags()) { + String tag = rawTag == null ? "" : rawTag.trim().toLowerCase(Locale.ROOT); + if (tag.equals("manualpeak") || tag.equals("fillbottle") || tag.equals("birth") || tag.startsWith("birth_") || tag.startsWith("defeat.")) { + return false; + } + } + + return true; + } + + private static boolean reserveRequest(NonPlayerInteractionSystem.PendingRequest request) { + Set participants = new LinkedHashSet<>(request.consentPlayers()); + participants.add(request.requesterUuid()); + + for (UUID playerUuid : participants) { + if (SESSION_BY_PLAYER.containsKey(playerUuid)) { + return false; + } + } + + REQUESTS.put(request.id(), request); + + for (UUID playerUuid : participants) { + SESSION_BY_PLAYER.put(playerUuid, request.id()); + } + + return true; + } + + private static void promptConsent(ServerWorld world, NonPlayerInteractionSystem.PendingRequest request) { + ServerPlayerEntity requester = getPlayer(world, request.requesterUuid()); + if (requester == null) { + finish(request, "failed", "gui.needsofnature.player_request.invalid"); + } else { + ServerPlayNetworking.send(requester, new PlayerAnimationRequestStatusS2CPayload(request.id(), "waiting", "gui.needsofnature.player_request.waiting")); + String tagsJson = stringListJson(request.contentTags()); + + for (UUID recipientUuid : request.consentPlayers()) { + ServerPlayerEntity recipient = getPlayer(world, recipientUuid); + if (recipient == null) { + finish(request, "failed", "gui.needsofnature.player_request.disconnected"); + return; + } + + ServerPlayNetworking.send( + recipient, + new PlayerAnimationConsentS2CPayload( + request.id(), + requester.getUuid(), + requester.getGameProfile().name(), + request.animationId().toString(), + request.displayName() == null ? "" : request.displayName(), + tagsJson, + request.kind() == NonPlayerInteractionSystem.RequestKind.JOIN + ) + ); + } + } + } + + private static void handleProfileAction(ServerPlayerEntity player, PlayerProfileActionC2SPayload payload) { + if (player != null && player.getEntityWorld() instanceof ServerWorld world) { + NonPlayerInteractionProfiles profiles = NonPlayerInteractionProfiles.get(world); + profiles.recordKnownPlayer(player); + String action = payload.action() == null ? "" : payload.action().trim().toLowerCase(Locale.ROOT); + if (action.equals("request")) { + sendProfileSnapshot(player, ""); + } else { + NonPlayerInteractionProfiles.Mode mode = NonPlayerInteractionProfiles.Mode.fromId( + payload.mode(), NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS + ); + if (action.equals("default")) { + profiles.setDefaultMode(player.getUuid(), mode, allowedMask()); + sendProfileSnapshot(player, ""); + } else { + NonPlayerInteractionProfiles.KnownPlayer target = findKnownPlayer(profiles, payload.target()); + if (target == null) { + sendProfileSnapshot(player, "gui.needsofnature.player_profiles.unknown_player"); + } else { + if (action.equals("set")) { + profiles.setOverride(player.getUuid(), target.uuid(), target.name(), mode, allowedMask()); + } else if (action.equals("remove")) { + profiles.removeOverride(player.getUuid(), target.uuid()); + } + + sendProfileSnapshot(player, ""); + } + } + } + } + } + + @Nullable + private static NonPlayerInteractionProfiles.KnownPlayer findKnownPlayer(NonPlayerInteractionProfiles profiles, String input) { + if (profiles != null && input != null && !input.isBlank()) { + try { + NonPlayerInteractionProfiles.KnownPlayer byUuid = profiles.findKnownPlayer(UUID.fromString(input.trim())); + if (byUuid != null) { + return byUuid; + } + } catch (IllegalArgumentException var3) { + } + + return profiles.findKnownPlayerByName(input); + } else { + return null; + } + } + + public static void sendProfileSnapshot(ServerPlayerEntity player, String errorKey) { + if (player != null && player.getEntityWorld() instanceof ServerWorld world) { + NonPlayerInteractionProfiles.Snapshot var10 = NonPlayerInteractionProfiles.get(world).snapshot(player.getUuid(), allowedMask()); + JsonObject root = new JsonObject(); + root.addProperty("default_mode", var10.defaultMode().id()); + root.addProperty("allowed_mask", allowedMask()); + JsonArray overrides = new JsonArray(); + + for (NonPlayerInteractionProfiles.OverrideView entry : var10.overrides()) { + JsonObject object = new JsonObject(); + object.addProperty("uuid", entry.uuid().toString()); + object.addProperty("name", entry.name()); + object.addProperty("mode", entry.mode().id()); + overrides.add(object); + } + + root.add("overrides", overrides); + JsonArray knownPlayers = new JsonArray(); + + for (NonPlayerInteractionProfiles.KnownPlayer entry : var10.knownPlayers()) { + JsonObject object = new JsonObject(); + object.addProperty("uuid", entry.uuid().toString()); + object.addProperty("name", entry.name()); + knownPlayers.add(object); + } + + root.add("known_players", knownPlayers); + ServerPlayNetworking.send(player, new PlayerProfileSnapshotS2CPayload(root.toString(), errorKey == null ? "" : errorKey)); + } + } + + private static String candidatesJson(NonPlayerInteractionSystem.SelectionSession session, UUID requesterUuid) { + JsonArray array = new JsonArray(); + int requesterIndex = session.actorUuids().indexOf(requesterUuid); + + for (int index = 0; index < session.candidates().size(); index++) { + NonPlayerInteractionSystem.Candidate candidate = session.candidates().get(index); + JsonObject object = new JsonObject(); + object.addProperty("index", index); + object.addProperty("animation_id", candidate.animationId().toString()); + object.addProperty("display_name", candidate.displayName() == null ? "" : candidate.displayName()); + object.addProperty("requester_role", roleAt(candidate.actorKeys(), requesterIndex)); + object.addProperty("requester_activity", activityAt(candidate, requesterIndex)); + object.addProperty("selectable", candidate.selectable()); + object.addProperty("block_requirement", candidate.blockRequirementSummary()); + object.add("actor_keys", toJsonArray(candidate.actorKeys())); + object.add("content_tags", toJsonArray(candidate.contentTags())); + object.addProperty("pack_name", candidate.packName() == null ? "" : candidate.packName()); + object.addProperty("pack_author", candidate.packAuthor() == null ? "" : candidate.packAuthor()); + array.add(object); + } + + return array.toString(); + } + + private static String activityAt(NonPlayerInteractionSystem.Candidate candidate, int actorIndex) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId()); + if (definition == null) { + return "active"; + } else { + String actorKey = roleAt(candidate.actorKeys(), actorIndex); + int constraintIndex = AnimationDefinitions.resolveActorKeys(definition).indexOf(actorKey); + if (constraintIndex >= 0 && constraintIndex < definition.actors().size()) { + AnimationDefinitions.ActorActivity activity = definition.actors().get(constraintIndex).activity(); + return activity == AnimationDefinitions.ActorActivity.PASSIVE ? "passive" : "active"; + } else { + return "active"; + } + } + } + + private static String summarizeBlockRequirement(@Nullable AnimationDefinitions.BlockRequirements requirements) { + if (requirements == null) { + return ""; + } + + List parts = new ArrayList<>(); + if (requirements.isWall()) { + parts.add("墙"); + AnimationDefinitions.WallHeight height = requirements.height(); + if (height != null) { + parts.add(height.max() == null ? "高度 " + height.min() + "+" : "高度 " + height.min() + "-" + height.max()); + } + + AnimationDefinitions.WallWidth width = requirements.width(); + if (width != null && (width.min() > 1 || width.max() != null)) { + parts.add(width.max() == null ? "宽度 " + width.min() + "+" : "宽度 " + width.min() + "-" + width.max()); + } + + if (requirements.facing() != null) { + parts.add("朝向 " + localizeFacing(requirements.facing().id())); + } + } else if (requirements.isCenterSupport()) { + parts.add("中心支撑"); + if (requirements.support() != null) { + parts.add(requirements.support().id().replace('_', ' ')); + } + + AnimationDefinitions.SurfaceFootprint footprint = requirements.surfaceFootprint(); + if (footprint != null) { + parts.add("占地 " + footprint.width() + "x" + footprint.depth()); + if (footprint.margin() > 0) { + parts.add("边距 " + footprint.margin()); + } + } + } else { + parts.add(localizeBlockType(requirements.type() == null ? "blocks" : requirements.type())); + } + + AnimationDefinitions.Clearance clearance = requirements.clearance(); + if (clearance != null) { + parts.add("净空 " + clearance.width() + "x" + clearance.height() + "x" + clearance.depth()); + } + + AnimationDefinitions.BlockPredicate blocks = requirements.blocks(); + if (blocks != null && !blocks.isEmpty()) { + parts.add("方块 " + summarizeBlockPredicate(blocks)); + } + + return String.join(" | ", parts); + } + + private static String localizeBlockType(String type) { + return switch (type) { + case "wall" -> "墙"; + case "center support" -> "中心支撑"; + case "surface" -> "表面"; + default -> type.replace('_', ' '); + }; + } + + private static String localizeFacing(String facing) { + return switch (facing) { + case "north" -> "北"; + case "south" -> "南"; + case "east" -> "东"; + case "west" -> "西"; + case "up" -> "上"; + case "down" -> "下"; + default -> facing; + }; + } + + private static String summarizeBlockPredicate(AnimationDefinitions.BlockPredicate blocks) { + List values = new ArrayList<>(); + blocks.blockIds().stream().map(Identifier::toString).sorted().forEach(values::add); + blocks.tagIds().stream().map(id -> "#" + id).sorted().forEach(values::add); + return values.size() <= 3 ? String.join(", ", values) : String.join(", ", values.subList(0, 3)) + ", +" + (values.size() - 3); + } + + private static String roleAt(List actorKeys, int index) { + return actorKeys != null && index >= 0 && index < actorKeys.size() ? actorKeys.get(index) : "player"; + } + + private static JsonArray toJsonArray(List values) { + JsonArray result = new JsonArray(); + if (values != null) { + for (String value : values) { + result.add(value == null ? "" : value); + } + } + + return result; + } + + private static String stringListJson(List values) { + return toJsonArray(values).toString(); + } + + private static void failSelection(NonPlayerInteractionSystem.SelectionSession selection, String state, String messageKey) { + ServerPlayerEntity requester = findPlayer(selection.requesterUuid()); + removeSelection(selection); + if (requester != null) { + ServerPlayNetworking.send(requester, new PlayerAnimationRequestStatusS2CPayload(selection.id(), state, messageKey)); + } + } + + private static void removeSelection(NonPlayerInteractionSystem.SelectionSession selection) { + if (selection != null) { + SELECTIONS.remove(selection.id(), selection); + SESSION_BY_PLAYER.remove(selection.requesterUuid(), selection.id()); + } + } + + private static void finish(NonPlayerInteractionSystem.PendingRequest request, String state, String messageKey) { + if (request != null) { + REQUESTS.remove(request.id()); + Set participants = new LinkedHashSet<>(request.consentPlayers()); + participants.add(request.requesterUuid()); + + for (UUID playerUuid : participants) { + SESSION_BY_PLAYER.remove(playerUuid, request.id()); + ServerPlayerEntity player = findPlayer(playerUuid); + if (player != null) { + ServerPlayNetworking.send(player, new PlayerAnimationRequestStatusS2CPayload(request.id(), state, messageKey)); + } + } + } + } + + private static void cancelForPlayer(UUID playerUuid, String messageKey) { + if (playerUuid != null) { + UUID sessionId = SESSION_BY_PLAYER.get(playerUuid); + if (sessionId != null) { + NonPlayerInteractionSystem.SelectionSession selection = SELECTIONS.get(sessionId); + if (selection != null) { + failSelection(selection, "cancelled", messageKey); + } else { + NonPlayerInteractionSystem.PendingRequest request = REQUESTS.get(sessionId); + if (request != null) { + finish(request, "cancelled", messageKey); + } + } + } + } + } + + private static void sendStandaloneFailure(ServerPlayerEntity player, String messageKey) { + if (player != null) { + UUID requestId = UUID.randomUUID(); + ServerPlayNetworking.send(player, new PlayerAnimationRequestStatusS2CPayload(requestId, "failed", messageKey)); + } + } + + private static NonPlayerInteractionProfiles.Mode resolveMode(ServerWorld world, ServerPlayerEntity owner, ServerPlayerEntity other) { + return NonPlayerInteractionProfiles.get(world).resolveMode(owner.getUuid(), other.getUuid(), allowedMask()); + } + + private static int allowedMask() { + return NeedsOfNature.getConfig().getAllowedPlayerInteractionProfilesMask(); + } + + private static boolean hasRequestEnergy(ServerPlayerEntity player) { + return player instanceof EnergyHolder holder && holder.getEnergy() >= NeedsOfNature.getConfig().getPlayerRequestMinEnergy(); + } + + private static boolean isUsablePlayer(@Nullable ServerPlayerEntity player) { + return player != null && !player.isSpectator() && player.isAlive() && !player.isRemoved(); + } + + private static boolean validInteractionPair(@Nullable ServerPlayerEntity requester, @Nullable Entity target) { + if (isUsablePlayer(requester) && target != null && !target.isRemoved()) { + return requester.getEntityWorld() != target.getEntityWorld() ? false : requester.canInteractWithEntity(target, 0.0); + } else { + return false; + } + } + + private static boolean isBusyForDirectRequest(ServerWorld world, @Nullable ServerPlayerEntity requester, @Nullable Entity target) { + return requester != null && target instanceof ServerPlayerEntity targetPlayer + ? AnimationService.isActorPendingOrActive(world, requester.getUuid()) || AnimationService.isActorPendingOrActive(world, targetPlayer.getUuid()) + : true; + } + + private static List sortedActors(Entity first, Entity second) { + List result = new ArrayList<>(List.of(first, second)); + result.sort(Comparator.comparingInt(Entity::getId)); + return List.copyOf(result); + } + + private static List entitiesForJoin(ServerWorld world, UUID instanceId, ServerPlayerEntity requester) { + List actors = resolveActors(world, NeedsOfNature.getInstanceActors(world, instanceId)); + if (actors.isEmpty()) { + return List.of(); + } + + if (actors.stream().noneMatch(actor -> actor.getUuid().equals(requester.getUuid()))) { + actors.add(requester); + } + + actors.sort(Comparator.comparingInt(Entity::getId)); + return List.copyOf(actors); + } + + private static List resolveActors(ServerWorld world, List actorUuids) { + List result = new ArrayList<>(); + if (world != null && actorUuids != null) { + for (UUID actorUuid : actorUuids) { + Entity actor = resolveEntity(world, actorUuid); + if (actor != null && !actor.isRemoved()) { + result.add(actor); + } + } + + result.sort(Comparator.comparingInt(Entity::getId)); + return result; + } else { + return result; + } + } + + @Nullable + private static Entity resolveEntity(ServerWorld world, UUID uuid) { + if (world != null && uuid != null) { + ServerPlayerEntity player = getPlayer(world, uuid); + return (Entity)(player != null ? player : world.getEntity(uuid)); + } else { + return null; + } + } + + @Nullable + private static ServerPlayerEntity getPlayer(ServerWorld world, UUID uuid) { + if (world != null && uuid != null && world.getServer() != null) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(uuid); + return player != null && player.getEntityWorld() == world ? player : null; + } else { + return null; + } + } + + @Nullable + private static ServerPlayerEntity findPlayer(UUID uuid) { + return uuid != null && activeServer != null ? activeServer.getPlayerManager().getPlayer(uuid) : null; + } + + private record Candidate( + Identifier animationId, + @Nullable String displayName, + List actorKeys, + List stages, + List contentTags, + boolean selectable, + String blockRequirementSummary, + @Nullable String packName, + @Nullable String packAuthor + ) { + } + + private record ConsentPlan(Set consentPlayers, Set acceptedPlayers) { + } + + private record PendingRequest( + UUID id, + NonPlayerInteractionSystem.RequestKind kind, + RegistryKey worldKey, + UUID requesterUuid, + UUID interactionTargetUuid, + @Nullable UUID sourceInstanceUuid, + List actorUuids, + Identifier animationId, + @Nullable String displayName, + List actorKeys, + List stages, + List contentTags, + long expiresTick, + Set consentPlayers, + Set acceptedPlayers + ) { + private NonPlayerInteractionSystem.PendingRequest withAccepted(UUID playerUuid) { + Set accepted = new LinkedHashSet<>(this.acceptedPlayers); + accepted.add(playerUuid); + return new NonPlayerInteractionSystem.PendingRequest( + this.id, + this.kind, + this.worldKey, + this.requesterUuid, + this.interactionTargetUuid, + this.sourceInstanceUuid, + this.actorUuids, + this.animationId, + this.displayName, + this.actorKeys, + this.stages, + this.contentTags, + this.expiresTick, + this.consentPlayers, + Set.copyOf(accepted) + ); + } + } + + private enum RequestKind { + DIRECT, + JOIN; + } + + private record SelectionSession( + UUID id, + NonPlayerInteractionSystem.RequestKind kind, + RegistryKey worldKey, + UUID requesterUuid, + UUID targetUuid, + @Nullable UUID sourceInstanceUuid, + long expiresTick, + List actorUuids, + List candidates + ) { + } +} diff --git a/src/main/java/com/nonid/NonPregnancyConceptionSystem.java b/src/main/java/com/nonid/NonPregnancyConceptionSystem.java new file mode 100644 index 0000000..0609e43 --- /dev/null +++ b/src/main/java/com/nonid/NonPregnancyConceptionSystem.java @@ -0,0 +1,226 @@ +package com.nonid; + +import com.nonid.data.NonEntityProfiles; +import com.nonid.network.PregnancyCycleStateS2CPayload; +import java.util.List; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; + +final class NonPregnancyConceptionSystem { + private static final Identifier PLAYER_ENTITY_TYPE_ID = Identifier.of("minecraft", "player"); + private static final long TICKS_PER_DAY = 24000L; + private static final long CYCLE_TICKS = 720000L; + private static final long CHECK_INTERVAL_TICKS = 20L; + private static final double CHECKS_PER_MINUTE = 60.0; + private static final int SAFE_PHASE_FIRST_END_DAY = 5; + private static final int SAFE_PHASE_SECOND_START_DAY = 26; + + private NonPregnancyConceptionSystem() { + } + + static void tick(ServerWorld world) { + if (world != null && !world.getPlayers().isEmpty() && world.getTime() % 20L == 0L) { + for (ServerPlayerEntity player : world.getPlayers()) { + if (isCycleEnabledFor(player)) { + updateCycle(player, world, true); + tryConception(world, player); + } + } + } + } + + static void syncState(ServerPlayerEntity player) { + if (player != null && player.getEntityWorld() instanceof ServerWorld world) { + if (!isCycleEnabledFor(player)) { + ServerPlayNetworking.send(player, new PregnancyCycleStateS2CPayload(false, 0, PregnancyCyclePhase.NORMAL)); + } else { + updateCycle(player, world, false); + int day = getCycleDay(player); + ServerPlayNetworking.send(player, new PregnancyCycleStateS2CPayload(true, day, resolvePhase(day))); + } + } + } + + static String describeCycle(ServerPlayerEntity player) { + if (player != null && isCycleEnabledFor(player)) { + if (player.getEntityWorld() instanceof ServerWorld world) { + updateCycle(player, world, false); + } + + int day = getCycleDay(player); + return "pregnancy cycle: day " + day + "/30 (" + resolvePhase(day).displayName() + ")"; + } else { + return "pregnancy cycle: disabled"; + } + } + + static void copyState(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer) { + if (oldPlayer instanceof PregnancyCycleHolder oldHolder && newPlayer instanceof PregnancyCycleHolder newHolder) { + if (!oldHolder.isPregnancyCycleInitialized()) { + newHolder.clearPregnancyCycleState(); + } else { + newHolder.setPregnancyCycleState( + oldHolder.getPregnancyCycleProgressTicks(), oldHolder.getPregnancyCycleLastGameTick(), oldHolder.getPregnancyCycleLastTimeOfDay() + ); + } + } + } + + static void recordRecentDonor(LiquidHolder holder, Identifier entityTypeId, NeedsOfNature.PregnancyVariantData variantData) { + if (holder != null && holder.getLiquidStored() > 0 && entityTypeId != null && !PLAYER_ENTITY_TYPE_ID.equals(entityTypeId)) { + holder.recordRecentLiquidDonor(entityTypeId, variantData); + } + } + + static boolean setCycleDay(ServerPlayerEntity player, int day) { + if (player instanceof PregnancyCycleHolder holder && player.getEntityWorld() instanceof ServerWorld world) { + ServerWorld clockWorld = world.getServer() != null && world.getServer().getOverworld() != null ? world.getServer().getOverworld() : world; + int clampedDay = Math.max(1, Math.min(30, day)); + long progressTicks = (clampedDay - 1) * 24000L; + holder.setPregnancyCycleState(progressTicks, Math.max(0L, clockWorld.getTime()), Math.max(0L, clockWorld.getTimeOfDay())); + syncState(player); + return true; + } else { + return false; + } + } + + private static void tryConception(ServerWorld world, ServerPlayerEntity player) { + NonConfig config = NeedsOfNature.getConfig(); + if (config != null && config.isPregnancyEnabled() && config.isLiquidBasedPregnancyEnabled()) { + if (config.isLiquidTankEnabled() && player.isAlive() && !player.isRemoved() && !player.isSpectator()) { + if (NeedsOfNature.getPregnantEntityTypeId(player) == null && !NonTrinketsIntegration.blocksPregnancy(player)) { + if (player instanceof LiquidHolder holder) { + if (holder.getLiquidStored() > 0 && holder.getLiquidCapacity() > 0) { + Identifier sourceType; + NeedsOfNature.PregnancyVariantData sourceVariantData; + if (holder.getLiquidComposition() == LiquidHolder.LiquidComposition.ENTITY) { + sourceType = holder.getLiquidEntityTypeId(); + sourceVariantData = holder.getLiquidDonorVariantData(); + } else { + if (holder.getLiquidComposition() != LiquidHolder.LiquidComposition.MIXED) { + return; + } + + List donors = holder.getRecentLiquidDonors(); + if (donors.isEmpty()) { + return; + } + + LiquidDonorHistory.Entry selected = donors.get(world.getRandom().nextInt(donors.size())); + sourceType = selected.entityTypeId(); + sourceVariantData = selected.variantData(); + } + + if (sourceType != null && !PLAYER_ENTITY_TYPE_ID.equals(sourceType)) { + int profileChance = NonEntityProfiles.resolve(sourceType).pregnancyChancePercent(); + if (profileChance > 0) { + NonAccessoryEffects accessoryEffects = NonTrinketsIntegration.getActiveTankAccessoryEffects(player); + double fillRatio = Math.min(1.0, (double)holder.getLiquidStored() / holder.getLiquidCapacity()); + int cycleDay = getCycleDay(player); + PregnancyCyclePhase phase = resolvePhase(cycleDay); + + double cycleMultiplier = switch (phase) { + case SAFE -> config.getPregnancySafeDayChancePercent() / 100.0; + case NORMAL -> 1.0; + case RISKY -> config.getPregnancyRiskyChancePercent() / 100.0; + case OVULATION -> config.getPregnancyOvulationChancePercent() / 100.0; + }; + double accessoryMultiplier = Math.max(0.0, accessoryEffects.pregnancyChanceMultiplier()); + double naturalMinuteChance = profileChance / 100.0 * fillRatio * cycleMultiplier * accessoryMultiplier; + int fertilityFloor = NeedsOfNature.getFertilityIncreaserChanceFloor(player); + double potionMinuteChance = 0.0; + if (fertilityFloor > 0) { + potionMinuteChance = fertilityFloor / 100.0 * fillRatio * cycleMultiplier * accessoryMultiplier; + if (phase == PregnancyCyclePhase.RISKY || phase == PregnancyCyclePhase.OVULATION) { + potionMinuteChance = Math.min(potionMinuteChance, fertilityFloor / 100.0 * fillRatio); + } + } + + double minuteChance = Math.max(naturalMinuteChance, potionMinuteChance); + minuteChance = Math.max(0.0, Math.min(1.0, minuteChance)); + if (!(minuteChance <= 0.0)) { + double checkChance = minuteChance >= 1.0 ? 1.0 : 1.0 - Math.pow(1.0 - minuteChance, 0.016666666666666666); + if (!(world.getRandom().nextDouble() >= checkChance)) { + long durationTicks = Math.max( + 1L, Math.round(config.getPregnancyDurationTicks() * Math.max(0.01, accessoryEffects.pregnancyDurationMultiplier())) + ); + NeedsOfNature.beginPregnancy(player, sourceType, NeedsOfNature.getGlobalTick(world) + durationTicks, sourceVariantData); + if (sourceType.equals(NeedsOfNature.getPregnantEntityTypeId(player))) { + NonStats.incrementPregnancyStarted(player); + NonAdvancementHooks.grantFirstPregnancy(player); + } + } + } + } + } + } + } + } + } + } + } + + private static boolean isCycleEnabledFor(ServerPlayerEntity player) { + NonConfig config = NeedsOfNature.getConfig(); + return player != null && config != null && config.isPregnancyEnabled() && config.isLiquidBasedPregnancyEnabled() && !NonGenderSystem.isOnlyMale(player); + } + + private static void updateCycle(ServerPlayerEntity player, ServerWorld world, boolean syncOnDayChange) { + if (player instanceof PregnancyCycleHolder holder) { + ServerWorld clockWorld = world.getServer() != null && world.getServer().getOverworld() != null ? world.getServer().getOverworld() : world; + long nowGameTick = Math.max(0L, clockWorld.getTime()); + long nowTimeOfDay = Math.max(0L, clockWorld.getTimeOfDay()); + if (!holder.isPregnancyCycleInitialized()) { + long initialProgress = Math.floorMod(world.getRandom().nextLong(), 720000L); + holder.setPregnancyCycleState(initialProgress, nowGameTick, nowTimeOfDay); + if (syncOnDayChange) { + syncState(player); + } + } else { + int oldDay = getCycleDay(holder.getPregnancyCycleProgressTicks()); + long gameDelta = nonNegativeDelta(nowGameTick, holder.getPregnancyCycleLastGameTick()); + long daytimeDelta = nonNegativeDelta(nowTimeOfDay, holder.getPregnancyCycleLastTimeOfDay()); + long elapsed = Math.max(gameDelta, daytimeDelta); + long progress = Math.floorMod(holder.getPregnancyCycleProgressTicks() + Math.floorMod(elapsed, 720000L), 720000L); + holder.setPregnancyCycleState(progress, nowGameTick, nowTimeOfDay); + if (syncOnDayChange && oldDay != getCycleDay(progress)) { + syncState(player); + } + } + } + } + + private static long nonNegativeDelta(long current, long previous) { + return current >= previous ? current - previous : 0L; + } + + private static int getCycleDay(ServerPlayerEntity player) { + return player instanceof PregnancyCycleHolder holder && holder.isPregnancyCycleInitialized() ? getCycleDay(holder.getPregnancyCycleProgressTicks()) : 1; + } + + private static int getCycleDay(long progressTicks) { + long normalized = Math.floorMod(progressTicks, 720000L); + return (int)(normalized / 24000L) + 1; + } + + private static PregnancyCyclePhase resolvePhase(int day) { + NonConfig config = NeedsOfNature.getConfig(); + if (config != null) { + int riskyStartDay = config.getPregnancyRiskyStartDay(); + int ovulationEndDay = config.getPregnancyOvulationEndDay(); + int ovulationStartDay = Math.max(riskyStartDay, ovulationEndDay - 1); + if (day >= ovulationStartDay && day <= ovulationEndDay) { + return PregnancyCyclePhase.OVULATION; + } + + if (day >= riskyStartDay && day < ovulationStartDay) { + return PregnancyCyclePhase.RISKY; + } + } + + return day > 5 && day < 26 ? PregnancyCyclePhase.NORMAL : PregnancyCyclePhase.SAFE; + } +} diff --git a/src/main/java/com/nonid/NonSkinRepairBlocks.java b/src/main/java/com/nonid/NonSkinRepairBlocks.java new file mode 100644 index 0000000..7fb6e00 --- /dev/null +++ b/src/main/java/com/nonid/NonSkinRepairBlocks.java @@ -0,0 +1,33 @@ +package com.nonid; + +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public final class NonSkinRepairBlocks { + private NonSkinRepairBlocks() { + } + + public static boolean isNearRepairBlock(World world, BlockPos center, NonConfig config) { + if (world != null && center != null) { + for (BlockPos pos : BlockPos.iterateOutwards(center, 1, 1, 1)) { + if (isRepairBlock(world.getBlockState(pos), config)) { + return true; + } + } + + return false; + } else { + return false; + } + } + + public static boolean isRepairBlock(BlockState state, NonConfig config) { + if (state == null) { + return false; + } else { + return state.isOf(Blocks.LOOM) ? true : config != null && config.isCraftingTableSkinRepairAllowed() && state.isOf(Blocks.CRAFTING_TABLE); + } + } +} diff --git a/src/main/java/com/nonid/NonStats.java b/src/main/java/com/nonid/NonStats.java new file mode 100644 index 0000000..f3257d2 --- /dev/null +++ b/src/main/java/com/nonid/NonStats.java @@ -0,0 +1,247 @@ +package com.nonid; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.stat.StatFormatter; +import net.minecraft.stat.Stats; +import net.minecraft.util.Identifier; + +public final class NonStats { + private static final String STAT_PATH_PEAKED_TOTAL = "peaked_animations_total"; + private static final String STAT_PATH_LIQUID_GAINED_TOTAL = "liquid_gained_ml_total"; + private static final String STAT_PATH_MULTI_ACTOR_TOTAL = "multi_actor_animations_total"; + private static final String STAT_PATH_WATER_TOTAL = "water_animations_total"; + private static final String STAT_PATH_ANIMATION_START_TOTAL = "animations_started_total"; + private static final String STAT_PATH_ATTACK_ANIMATION_START_TOTAL = "attack_animations_started_total"; + private static final String STAT_PATH_ATTACK_ESCAPE_SUCCESS_TOTAL = "attack_escapes_success_total"; + private static final String STAT_PATH_M_INJECTOR_PEAKS_ML_TOTAL = "m_injector_peaks_ml_total"; + private static final String STAT_PATH_MANUAL_PEAK_TOTAL = "manual_peaks_total"; + private static final String STAT_PATH_FILL_BOTTLE_TOTAL = "bottles_filled_total"; + private static final String STAT_PATH_PREGNANCY_STARTED_TOTAL = "pregnancies_started_total"; + private static final String STAT_PATH_PREGNANCY_COMPLETED_TOTAL = "pregnancies_completed_total"; + private static final String STAT_PATH_OFFSPRING_SPAWNED_TOTAL = "offspring_spawned_total"; + private static final Identifier STAT_PEAKED_TOTAL = NeedsOfNature.id("peaked_animations_total"); + private static final Identifier STAT_LIQUID_GAINED_TOTAL = NeedsOfNature.id("liquid_gained_ml_total"); + private static final Identifier STAT_MULTI_ACTOR_TOTAL = NeedsOfNature.id("multi_actor_animations_total"); + private static final Identifier STAT_WATER_TOTAL = NeedsOfNature.id("water_animations_total"); + private static final Identifier STAT_ANIMATION_START_TOTAL = NeedsOfNature.id("animations_started_total"); + private static final Identifier STAT_ATTACK_ANIMATION_START_TOTAL = NeedsOfNature.id("attack_animations_started_total"); + private static final Identifier STAT_ATTACK_ESCAPE_SUCCESS_TOTAL = NeedsOfNature.id("attack_escapes_success_total"); + private static final Identifier STAT_M_INJECTOR_PEAKS_ML_TOTAL = NeedsOfNature.id("m_injector_peaks_ml_total"); + private static final Identifier STAT_MANUAL_PEAK_TOTAL = NeedsOfNature.id("manual_peaks_total"); + private static final Identifier STAT_FILL_BOTTLE_TOTAL = NeedsOfNature.id("bottles_filled_total"); + private static final Identifier STAT_PREGNANCY_STARTED_TOTAL = NeedsOfNature.id("pregnancies_started_total"); + private static final Identifier STAT_PREGNANCY_COMPLETED_TOTAL = NeedsOfNature.id("pregnancies_completed_total"); + private static final Identifier STAT_OFFSPRING_SPAWNED_TOTAL = NeedsOfNature.id("offspring_spawned_total"); + private static final Map PEAKED_BY_ENTITY = new LinkedHashMap<>(); + private static final Map LIQUID_GAINED_BY_ENTITY = new LinkedHashMap<>(); + private static final Map OFFSPRING_SPAWNED_BY_ENTITY = new LinkedHashMap<>(); + + private NonStats() { + } + + public static void registerBaseStats() { + registerStat(STAT_PEAKED_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_LIQUID_GAINED_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_MULTI_ACTOR_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_WATER_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_ANIMATION_START_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_ATTACK_ANIMATION_START_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_ATTACK_ESCAPE_SUCCESS_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_M_INJECTOR_PEAKS_ML_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_MANUAL_PEAK_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_FILL_BOTTLE_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_PREGNANCY_STARTED_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_PREGNANCY_COMPLETED_TOTAL, StatFormatter.DEFAULT); + registerStat(STAT_OFFSPRING_SPAWNED_TOTAL, StatFormatter.DEFAULT); + } + + public static void registerKnownEntityStats() { + for (Identifier entityId : Registries.ENTITY_TYPE.getIds()) { + ensureEntityStats(entityId); + } + } + + public static void incrementAnimationStarted(ServerPlayerEntity player) { + increase(player, STAT_ANIMATION_START_TOTAL, 1); + } + + public static void incrementMultiActorAnimation(ServerPlayerEntity player) { + increase(player, STAT_MULTI_ACTOR_TOTAL, 1); + } + + public static void incrementWaterAnimation(ServerPlayerEntity player) { + increase(player, STAT_WATER_TOTAL, 1); + } + + public static void incrementAttackAnimationStarted(ServerPlayerEntity player) { + increase(player, STAT_ATTACK_ANIMATION_START_TOTAL, 1); + } + + public static void incrementAttackEscapeSuccess(ServerPlayerEntity player) { + increase(player, STAT_ATTACK_ESCAPE_SUCCESS_TOTAL, 1); + } + + public static void addMInjectorPeaksMl(ServerPlayerEntity player, int amount) { + increase(player, STAT_M_INJECTOR_PEAKS_ML_TOTAL, amount); + } + + public static void incrementManualPeak(ServerPlayerEntity player) { + increase(player, STAT_MANUAL_PEAK_TOTAL, 1); + } + + public static void incrementFillBottle(ServerPlayerEntity player) { + increase(player, STAT_FILL_BOTTLE_TOTAL, 1); + } + + public static int getBottlesFilled(ServerPlayerEntity player) { + return get(player, STAT_FILL_BOTTLE_TOTAL); + } + + public static void incrementPregnancyStarted(ServerPlayerEntity player) { + increase(player, STAT_PREGNANCY_STARTED_TOTAL, 1); + } + + public static void incrementPregnancyCompleted(ServerPlayerEntity player) { + increase(player, STAT_PREGNANCY_COMPLETED_TOTAL, 1); + } + + public static int getPregnanciesCompleted(ServerPlayerEntity player) { + return get(player, STAT_PREGNANCY_COMPLETED_TOTAL); + } + + public static void addOffspringSpawned(ServerPlayerEntity player, Identifier entityTypeId, int amount) { + if (player != null && amount > 0) { + increase(player, STAT_OFFSPRING_SPAWNED_TOTAL, amount); + if (entityTypeId != null) { + ensureEntityStats(entityTypeId); + Identifier stat = OFFSPRING_SPAWNED_BY_ENTITY.get(entityTypeId); + if (stat != null) { + increase(player, stat, amount); + } + } + } + } + + public static void incrementPeakedAnimations(ServerPlayerEntity player, Set entityTypes) { + increase(player, STAT_PEAKED_TOTAL, 1); + if (entityTypes != null && !entityTypes.isEmpty()) { + for (Identifier entityTypeId : entityTypes) { + if (entityTypeId != null) { + ensureEntityStats(entityTypeId); + Identifier stat = PEAKED_BY_ENTITY.get(entityTypeId); + if (stat != null) { + increase(player, stat, 1); + } + } + } + } + } + + public static void addLiquidGained(ServerPlayerEntity player, int amount, Map perEntityAmounts) { + if (amount > 0) { + increase(player, STAT_LIQUID_GAINED_TOTAL, amount); + if (perEntityAmounts != null && !perEntityAmounts.isEmpty()) { + for (Entry entry : perEntityAmounts.entrySet()) { + Identifier entityId = entry.getKey(); + int entityAmount = entry.getValue() == null ? 0 : entry.getValue(); + if (entityId != null && entityAmount > 0) { + ensureEntityStats(entityId); + Identifier stat = LIQUID_GAINED_BY_ENTITY.get(entityId); + if (stat != null) { + increase(player, stat, entityAmount); + } + } + } + } + } + } + + private static void increase(ServerPlayerEntity player, Identifier statId, int amount) { + if (player != null && statId != null && amount > 0) { + if (registerStat(statId, StatFormatter.DEFAULT)) { + player.increaseStat(statId, amount); + } + } + } + + private static int get(ServerPlayerEntity player, Identifier statId) { + if (player == null || statId == null) { + return 0; + } else { + return !registerStat(statId, StatFormatter.DEFAULT) ? 0 : player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(statId)); + } + } + + private static void ensureEntityStats(Identifier entityId) { + synchronized (NonStats.class) { + if (!PEAKED_BY_ENTITY.containsKey(entityId)) { + Identifier peakedStat = NeedsOfNature.id("peaked_animations_entity_" + sanitizeEntityId(entityId)); + if (registerStat(peakedStat, StatFormatter.DEFAULT)) { + PEAKED_BY_ENTITY.put(entityId, peakedStat); + } + } + + if (!LIQUID_GAINED_BY_ENTITY.containsKey(entityId)) { + Identifier liquidStat = NeedsOfNature.id("liquid_gained_ml_entity_" + sanitizeEntityId(entityId)); + if (registerStat(liquidStat, StatFormatter.DEFAULT)) { + LIQUID_GAINED_BY_ENTITY.put(entityId, liquidStat); + } + } + + if (!OFFSPRING_SPAWNED_BY_ENTITY.containsKey(entityId)) { + Identifier offspringStat = NeedsOfNature.id("offspring_spawned_entity_" + sanitizeEntityId(entityId)); + if (registerStat(offspringStat, StatFormatter.DEFAULT)) { + OFFSPRING_SPAWNED_BY_ENTITY.put(entityId, offspringStat); + } + } + } + } + + private static boolean registerStat(Identifier statId, StatFormatter formatter) { + if (statId == null) { + return false; + } + + if (!Registries.CUSTOM_STAT.containsId(statId)) { + try { + Registry.register(Registries.CUSTOM_STAT, statId, statId); + } catch (RuntimeException ex) { + return false; + } + } + + Stats.CUSTOM.getOrCreateStat(statId, formatter); + return true; + } + + private static String sanitizeEntityId(Identifier entityId) { + return sanitizePathPart(entityId.getNamespace()) + "_" + sanitizePathPart(entityId.getPath()); + } + + private static String sanitizePathPart(String value) { + if (value != null && !value.isBlank()) { + StringBuilder out = new StringBuilder(value.length()); + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c >= 'a' && c <= 'z') { + out.append(c); + } else if (c >= '0' && c <= '9') { + out.append(c); + } else { + out.append('_'); + } + } + + return out.isEmpty() ? "unknown" : out.toString(); + } else { + return "unknown"; + } + } +} diff --git a/src/main/java/com/nonid/NonTrinketsCompat.java b/src/main/java/com/nonid/NonTrinketsCompat.java new file mode 100644 index 0000000..db548e5 --- /dev/null +++ b/src/main/java/com/nonid/NonTrinketsCompat.java @@ -0,0 +1,466 @@ +package com.nonid; + +import com.nonid.data.NonAccessoryBehavior; +import com.nonid.data.NonAccessoryDefinitions; +import com.nonid.data.NonAccessoryItemRegistry; +import com.nonid.data.NonAccessoryStackData; +import com.nonid.data.NonLiquidRoles; +import dev.emi.trinkets.api.SlotReference; +import dev.emi.trinkets.api.TrinketComponent; +import dev.emi.trinkets.api.TrinketsApi; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.util.TriState; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.Identifier; +import net.minecraft.util.Pair; + +final class NonTrinketsCompat { + private static final Map LAST_SYNCED_EFFECTS = new ConcurrentHashMap<>(); + private static final Map> PENDING_UNEQUIPPED_SLOTS = new ConcurrentHashMap<>(); + + private NonTrinketsCompat() { + } + + static void registerPredicates() { + TrinketsApi.registerTrinketPredicate( + NeedsOfNature.id("female"), + (stack, slot, entity) -> NonAccessoryBehavior.isSlot(slot.getId(), "legs/v") && !NonGenderSystem.hasFemaleGender(entity) + ? TriState.FALSE + : TriState.DEFAULT + ); + TrinketsApi.registerTrinketPredicate( + NeedsOfNature.id("male"), + (stack, slot, entity) -> NonAccessoryBehavior.isSlot(slot.getId(), "legs/d") && !NonGenderSystem.hasMaleGender(entity) + ? TriState.FALSE + : TriState.DEFAULT + ); + } + + static void registerTrinketItem(Item item) { + if (item != null) { + TrinketsApi.registerTrinket(item, NonDataAccessoryTrinket.INSTANCE); + } + } + + static NonAccessoryEffects getGlobalAccessoryEffects(LivingEntity entity) { + return entity == null + ? NonAccessoryEffects.NEUTRAL + : TrinketsApi.getTrinketComponent(entity) + .map(TrinketComponent::getAllEquipped) + .orElse(List.of()) + .stream() + .filter(pair -> !isPendingUnequippedSlot(entity, (SlotReference)pair.getLeft())) + .filter(pair -> { + Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem()); + return !NonAccessoryShedSystem.isSlotEffectSuppressed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId); + }) + .map(pair -> Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem())) + .map(NonAccessoryDefinitions::getEffects) + .map(NonAccessoryEffects::globalOnly) + .reduce(NonAccessoryEffects.NEUTRAL, NonAccessoryEffects::combine); + } + + static NonAccessoryEffects getActiveTankAccessoryEffects(LivingEntity entity) { + if (entity == null) { + return NonAccessoryEffects.NEUTRAL; + } + + String activeSlotId = NonTrinketsIntegration.getActiveTankSlotId(entity); + return TrinketsApi.getTrinketComponent(entity) + .map(TrinketComponent::getAllEquipped) + .orElse(List.of()) + .stream() + .filter(pair -> !isPendingUnequippedSlot(entity, (SlotReference)pair.getLeft())) + .filter(pair -> NonAccessoryBehavior.isSlot(((SlotReference)pair.getLeft()).getId(), activeSlotId)) + .filter(pair -> { + Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem()); + return !NonAccessoryShedSystem.isSlotEffectSuppressed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId); + }) + .map(pair -> Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem())) + .map(NonAccessoryDefinitions::getEffects) + .map(NonAccessoryEffects::tankScopedOnly) + .reduce(NonAccessoryEffects.NEUTRAL, NonAccessoryEffects::combine); + } + + static boolean blocksPregnancy(ServerPlayerEntity player) { + if (player == null) { + return false; + } + + String activeSlotId = NonTrinketsIntegration.getActiveTankSlotId(player); + return TrinketsApi.getTrinketComponent(player) + .map(TrinketComponent::getAllEquipped) + .orElse(List.of()) + .stream() + .filter(pair -> !isPendingUnequippedSlot(player, (SlotReference)pair.getLeft())) + .anyMatch( + pair -> { + ItemStack stack = (ItemStack)pair.getRight(); + if (stack != null && !stack.isEmpty()) { + String slotId = ((SlotReference)pair.getLeft()).getId(); + if (!NonAccessoryBehavior.isSlot(slotId, activeSlotId)) { + return false; + } + + Identifier itemId = Registries.ITEM.getId(stack.getItem()); + return NonAccessoryShedSystem.isSlotEffectSuppressed(player.getUuid(), slotId, itemId) + ? false + : NonAccessoryDefinitions.getBehavior(itemId).blocksPregnancy(); + } else { + return false; + } + } + ); + } + + static boolean hasProtectorFor(ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) { + return player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE + ? TrinketsApi.getTrinketComponent(player) + .map(TrinketComponent::getAllEquipped) + .orElse(List.of()) + .stream() + .filter(pair -> !isPendingUnequippedSlot(player, (SlotReference)pair.getLeft())) + .anyMatch(pair -> { + ItemStack stack = (ItemStack)pair.getRight(); + return stack != null && !stack.isEmpty() ? NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem())).blocks(role) : false; + }) + : false; + } + + static boolean consumeProtectorFor(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) { + if (world != null && player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE) { + TrinketComponent component = (TrinketComponent)TrinketsApi.getTrinketComponent(player).orElse(null); + if (component == null) { + return false; + } + + for (Pair pair : component.getAllEquipped()) { + if (!isPendingUnequippedSlot(player, (SlotReference)pair.getLeft())) { + ItemStack stack = (ItemStack)pair.getRight(); + if (stack != null && !stack.isEmpty()) { + NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem())); + if (behavior.blocks(role)) { + if (Math.max(stack.getMaxDamage(), behavior.maxDurability()) <= 0) { + return false; + } + + int cost = Math.max(1, behavior.protectionDurabilityCost()); + applyDurabilityCost(world, player, (SlotReference)pair.getLeft(), stack, cost); + world.playSound( + null, + player.getX(), + player.getY(), + player.getZ(), + SoundEvents.ITEM_SHIELD_BLOCK, + SoundCategory.PLAYERS, + 0.75F, + 0.9F + world.getRandom().nextFloat() * 0.2F + ); + world.spawnParticles(ParticleTypes.CRIT, player.getX(), player.getBodyY(0.55), player.getZ(), 8, 0.25, 0.35, 0.25, 0.08); + forceAccessorySync(player); + return true; + } + } + } + } + + return false; + } else { + return false; + } + } + + static void applyInjectionDurabilityCost(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) { + if (world != null && player != null && role != null) { + if (role == NonLiquidRoles.InjectorRole.V || role == NonLiquidRoles.InjectorRole.A) { + TrinketComponent component = (TrinketComponent)TrinketsApi.getTrinketComponent(player).orElse(null); + if (component != null) { + for (Pair pair : component.getAllEquipped()) { + if (!isPendingUnequippedSlot(player, (SlotReference)pair.getLeft())) { + ItemStack stack = (ItemStack)pair.getRight(); + if (stack != null && !stack.isEmpty()) { + String slotId = ((SlotReference)pair.getLeft()).getId(); + if ((role != NonLiquidRoles.InjectorRole.V || NonAccessoryBehavior.isSlot(slotId, "legs/v")) + && (role != NonLiquidRoles.InjectorRole.A || NonAccessoryBehavior.isSlot(slotId, "legs/a"))) { + NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem())); + int cost = role == NonLiquidRoles.InjectorRole.V ? behavior.vInjectionDurabilityCost() : behavior.aInjectionDurabilityCost(); + if (cost > 0) { + applyDurabilityCost(world, player, (SlotReference)pair.getLeft(), stack, cost); + } + } + } + } + } + + forceAccessorySync(player); + } + } + } + } + + private static boolean applyDurabilityCost(ServerWorld world, ServerPlayerEntity player, SlotReference slot, ItemStack stack, int cost) { + if (world != null && player != null && slot != null && stack != null && !stack.isEmpty() && cost > 0) { + NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(Registries.ITEM.getId(stack.getItem())); + int maxDamage = Math.max(stack.getMaxDamage(), behavior.maxDurability()); + if (maxDamage <= 0) { + return false; + } + + if (stack.getMaxDamage() <= 0) { + stack.set(DataComponentTypes.MAX_DAMAGE, maxDamage); + } + + if (stack.get(DataComponentTypes.DAMAGE) == null) { + stack.set(DataComponentTypes.DAMAGE, 0); + } + + stack.damage(cost, world, player, brokenItem -> TrinketsApi.onTrinketBroken(stack, slot, player)); + slot.inventory().markUpdate(); + return true; + } else { + return false; + } + } + + static void onAccessoryEquipped(LivingEntity entity, SlotReference slot) { + clearPendingUnequippedSlot(entity, slot); + if (entity instanceof ServerPlayerEntity player) { + forceAccessorySync(player); + } + } + + static void onAccessoryUnequipped(LivingEntity entity, SlotReference slot) { + markPendingUnequippedSlot(entity, slot); + if (entity instanceof ServerPlayerEntity player) { + forceAccessorySync(player); + } + } + + static void syncAccessoryStateIfChanged(ServerPlayerEntity player) { + if (player != null) { + NonAccessoryEffects effects = getGlobalAccessoryEffects(player); + effects = effects.combine(getActiveTankAccessoryEffects(player)); + NonAccessoryEffects previous = LAST_SYNCED_EFFECTS.put(player.getUuid(), effects); + if (!effects.equals(previous)) { + NeedsOfNature.syncPlayerRuntimeSettings(player); + NeedsOfNature.syncLiquidState(player); + } + } + } + + static void forceAccessorySync(ServerPlayerEntity player) { + if (player != null) { + LAST_SYNCED_EFFECTS.remove(player.getUuid()); + syncAccessoryStateIfChanged(player); + } + } + + static void clearAccessoryState(UUID playerUuid) { + if (playerUuid != null) { + LAST_SYNCED_EFFECTS.remove(playerUuid); + PENDING_UNEQUIPPED_SLOTS.remove(playerUuid); + } + } + + static void clearRuntimeState() { + LAST_SYNCED_EFFECTS.clear(); + PENDING_UNEQUIPPED_SLOTS.clear(); + } + + static List resolveEquippedSkinOverlays(LivingEntity entity) { + return entity == null + ? List.of() + : TrinketsApi.getTrinketComponent(entity) + .map( + component -> { + List overlays = new ArrayList<>(); + + for (Pair pair : component.getAllEquipped()) { + if (pair.getRight() != null && !((ItemStack)pair.getRight()).isEmpty()) { + Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem()); + if (!NonAccessoryShedSystem.isSlotVisuallyShed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId)) { + for (NonAccessoryItemRegistry.VisualLayer layer : NonAccessoryItemRegistry.getSkinOverlays(itemId) + .forSlot(((SlotReference)pair.getLeft()).getId())) { + NonTrinketsIntegration.ResolvedVisualLayer resolved = NonTrinketsIntegration.resolveVisualLayer((ItemStack)pair.getRight(), layer); + if (resolved != null) { + overlays.add(resolved); + } + } + } + } + } + + return overlays.isEmpty() ? List.of() : List.copyOf(overlays); + } + ) + .orElse(List.of()); + } + + static Set resolveVirtualActorTags(LivingEntity entity) { + ItemStack stack = getMaleRoleAccessoryStack(entity); + if (stack.isEmpty()) { + return Set.of(); + } + + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(Registries.ITEM.getId(stack.getItem())); + return definition == null ? Set.of() : definition.actorVisualBehavior().virtualActorTags(); + } + + static ItemStack getMaleRoleAccessoryStack(LivingEntity entity) { + if (entity != null && NonGenderSystem.isOnlyFemale(entity)) { + TrinketComponent component = (TrinketComponent)TrinketsApi.getTrinketComponent(entity).orElse(null); + if (component == null) { + return ItemStack.EMPTY; + } + + for (Pair pair : component.getAllEquipped()) { + ItemStack stack = (ItemStack)pair.getRight(); + if (stack != null && !stack.isEmpty() && !isPendingUnequippedSlot(entity, (SlotReference)pair.getLeft())) { + String slotId = ((SlotReference)pair.getLeft()).getId(); + if (NonAccessoryBehavior.isSlot(slotId, "legs/v")) { + Identifier itemId = Registries.ITEM.getId(stack.getItem()); + if (!NonAccessoryShedSystem.isSlotVisuallyShed(entity.getUuid(), slotId, itemId) + && !NonAccessoryShedSystem.isSlotEffectSuppressed(entity.getUuid(), slotId, itemId)) { + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(itemId); + if (definition != null && definition.actorVisualBehavior().substitutesMaleRole()) { + return stack; + } + } + } + } + } + + return ItemStack.EMPTY; + } else { + return ItemStack.EMPTY; + } + } + + static NonTrinketsIntegration.StoredInjectorLiquid peekStoredInjectorLiquid(ServerPlayerEntity player) { + ItemStack stack = getMaleRoleAccessoryStack(player); + if (stack.isEmpty()) { + return null; + } else { + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(Registries.ITEM.getId(stack.getItem())); + if (definition != null && definition.actorVisualBehavior().storedLiquidCapacityMl() > 0) { + NonAccessoryStackData.StoredLiquid liquid = NonAccessoryStackData.getStoredLiquid(stack); + return liquid == null + ? null + : new NonTrinketsIntegration.StoredInjectorLiquid( + liquid.entityTypeId(), Math.min(liquid.amountMl(), definition.actorVisualBehavior().storedLiquidCapacityMl()) + ); + } else { + return null; + } + } + } + + static NonTrinketsIntegration.StoredInjectorLiquid consumeStoredInjectorLiquid(ServerPlayerEntity player) { + NonTrinketsIntegration.StoredInjectorLiquid liquid = peekStoredInjectorLiquid(player); + if (liquid == null) { + return null; + } + + ItemStack stack = getMaleRoleAccessoryStack(player); + if (stack.isEmpty()) { + return null; + } + + NonAccessoryStackData.setStoredLiquid(stack, null, 0); + TrinketsApi.getTrinketComponent(player).ifPresent(component -> { + for (Pair pair : component.getAllEquipped()) { + if (pair.getRight() == stack) { + ((SlotReference)pair.getLeft()).inventory().markUpdate(); + break; + } + } + }); + forceAccessorySync(player); + return liquid; + } + + static List getVisuallyShedAccessoryStacks(LivingEntity entity) { + return entity == null ? List.of() : TrinketsApi.getTrinketComponent(entity).map(component -> { + List stacks = new ArrayList<>(); + + for (Pair pair : component.getAllEquipped()) { + if (pair.getRight() != null && !((ItemStack)pair.getRight()).isEmpty()) { + Identifier itemId = Registries.ITEM.getId(((ItemStack)pair.getRight()).getItem()); + if (NonAccessoryShedSystem.isSlotVisuallyShed(entity.getUuid(), ((SlotReference)pair.getLeft()).getId(), itemId)) { + stacks.add((ItemStack)pair.getRight()); + } + } + } + + return stacks.isEmpty() ? List.of() : List.copyOf(stacks); + }).orElse(List.of()); + } + + static NonTrinketsIntegration.TankScopeStatus resolveTankScopeStatus(LivingEntity entity, ItemStack stack) { + return entity != null && stack != null && !stack.isEmpty() + ? TrinketsApi.getTrinketComponent(entity) + .map( + component -> { + for (Pair pair : component.getAllEquipped()) { + if (pair.getRight() == stack) { + return NonTrinketsIntegration.isActiveTankSlot(entity, ((SlotReference)pair.getLeft()).getId()) + ? NonTrinketsIntegration.TankScopeStatus.ACTIVE_EQUIPPED + : NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED; + } + } + + return NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED; + } + ) + .orElse(NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED) + : NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED; + } + + private static void markPendingUnequippedSlot(LivingEntity entity, SlotReference slot) { + String slotId = slotId(slot); + if (entity != null && slotId != null) { + PENDING_UNEQUIPPED_SLOTS.computeIfAbsent(entity.getUuid(), ignored -> ConcurrentHashMap.newKeySet()).add(slotId); + } + } + + private static void clearPendingUnequippedSlot(LivingEntity entity, SlotReference slot) { + String slotId = slotId(slot); + if (entity != null && slotId != null) { + Set slots = PENDING_UNEQUIPPED_SLOTS.get(entity.getUuid()); + if (slots != null) { + slots.remove(slotId); + if (slots.isEmpty()) { + PENDING_UNEQUIPPED_SLOTS.remove(entity.getUuid()); + } + } + } + } + + private static boolean isPendingUnequippedSlot(LivingEntity entity, SlotReference slot) { + String slotId = slotId(slot); + if (entity != null && slotId != null) { + Set slots = PENDING_UNEQUIPPED_SLOTS.get(entity.getUuid()); + return slots != null && slots.contains(slotId); + } else { + return false; + } + } + + private static String slotId(SlotReference slot) { + return slot == null ? null : slot.getId(); + } +} diff --git a/src/main/java/com/nonid/NonTrinketsIntegration.java b/src/main/java/com/nonid/NonTrinketsIntegration.java new file mode 100644 index 0000000..0cb4e2b --- /dev/null +++ b/src/main/java/com/nonid/NonTrinketsIntegration.java @@ -0,0 +1,187 @@ +package com.nonid; + +import com.nonid.data.NonAccessoryBehavior; +import com.nonid.data.NonAccessoryItemRegistry; +import com.nonid.data.NonAccessoryStackData; +import com.nonid.data.NonLiquidRoles; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonTrinketsIntegration { + private static final String TRINKETS_MOD_ID = "trinkets"; + private static final boolean TRINKETS_LOADED = FabricLoader.getInstance().isModLoaded("trinkets"); + + private NonTrinketsIntegration() { + } + + public static boolean isTrinketsLoaded() { + return TRINKETS_LOADED; + } + + public static void registerPredicates() { + if (TRINKETS_LOADED) { + NonTrinketsCompat.registerPredicates(); + } + } + + public static void registerTrinketItem(Item item) { + if (TRINKETS_LOADED) { + NonTrinketsCompat.registerTrinketItem(item); + } + } + + public static boolean isSlotAvailableFor(LivingEntity entity, String slotId) { + if (entity == null || slotId == null) { + return false; + } else if (NonAccessoryBehavior.isSlot(slotId, "legs/v")) { + return NonGenderSystem.hasFemaleGender(entity); + } else { + return NonAccessoryBehavior.isSlot(slotId, "legs/d") ? NonGenderSystem.hasMaleGender(entity) : true; + } + } + + public static NonAccessoryEffects getAccessoryEffects(LivingEntity entity) { + return getGlobalAccessoryEffects(entity); + } + + public static NonAccessoryEffects getGlobalAccessoryEffects(LivingEntity entity) { + return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getGlobalAccessoryEffects(entity) : NonAccessoryEffects.NEUTRAL; + } + + public static NonAccessoryEffects getActiveTankAccessoryEffects(LivingEntity entity) { + return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getActiveTankAccessoryEffects(entity) : NonAccessoryEffects.NEUTRAL; + } + + public static String getActiveTankSlotId(LivingEntity entity) { + return NonGenderSystem.getLiquidTankType(entity).trinketsSlotId(); + } + + public static boolean isActiveTankSlot(LivingEntity entity, String slotId) { + return entity != null && slotId != null ? NonAccessoryBehavior.isSlot(slotId, getActiveTankSlotId(entity)) : false; + } + + public static boolean blocksPregnancy(ServerPlayerEntity player) { + return TRINKETS_LOADED && player != null ? NonTrinketsCompat.blocksPregnancy(player) : false; + } + + public static boolean hasProtectorFor(ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) { + return TRINKETS_LOADED && player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE + ? NonTrinketsCompat.hasProtectorFor(player, role) + : false; + } + + public static boolean consumeProtectorFor(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) { + return TRINKETS_LOADED && world != null && player != null && role != null && role != NonLiquidRoles.InjectorRole.NONE + ? NonTrinketsCompat.consumeProtectorFor(world, player, role) + : false; + } + + public static void applyInjectionDurabilityCost(ServerWorld world, ServerPlayerEntity player, NonLiquidRoles.InjectorRole role) { + if (TRINKETS_LOADED && world != null && player != null && role != null) { + NonTrinketsCompat.applyInjectionDurabilityCost(world, player, role); + } + } + + public static void syncAccessoryStateIfChanged(ServerPlayerEntity player) { + if (TRINKETS_LOADED && player != null) { + NonTrinketsCompat.syncAccessoryStateIfChanged(player); + } + } + + public static void forceAccessorySync(ServerPlayerEntity player) { + if (TRINKETS_LOADED && player != null) { + NonTrinketsCompat.forceAccessorySync(player); + } + } + + public static void clearAccessoryState(UUID playerUuid) { + if (TRINKETS_LOADED && playerUuid != null) { + NonTrinketsCompat.clearAccessoryState(playerUuid); + } + } + + static void clearRuntimeState() { + if (TRINKETS_LOADED) { + NonTrinketsCompat.clearRuntimeState(); + } + } + + public static List resolveEquippedSkinOverlays(LivingEntity entity) { + return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.resolveEquippedSkinOverlays(entity) : List.of(); + } + + public static Set resolveVirtualActorTags(LivingEntity entity) { + return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.resolveVirtualActorTags(entity) : Set.of(); + } + + public static ItemStack getMaleRoleAccessoryStack(LivingEntity entity) { + return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getMaleRoleAccessoryStack(entity) : ItemStack.EMPTY; + } + + public static boolean hasMaleRoleAccessory(LivingEntity entity) { + return !getMaleRoleAccessoryStack(entity).isEmpty(); + } + + @Nullable + public static NonTrinketsIntegration.StoredInjectorLiquid peekStoredInjectorLiquid(ServerPlayerEntity player) { + return TRINKETS_LOADED && player != null ? NonTrinketsCompat.peekStoredInjectorLiquid(player) : null; + } + + @Nullable + public static NonTrinketsIntegration.StoredInjectorLiquid consumeStoredInjectorLiquid(ServerPlayerEntity player) { + return TRINKETS_LOADED && player != null ? NonTrinketsCompat.consumeStoredInjectorLiquid(player) : null; + } + + @Nullable + public static NonTrinketsIntegration.ResolvedVisualLayer resolveVisualLayer(ItemStack stack, NonAccessoryItemRegistry.VisualLayer layer) { + if (stack != null && !stack.isEmpty() && layer != null && layer.texture() != null) { + Identifier itemId = Registries.ITEM.getId(stack.getItem()); + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(itemId); + int fallbackDye = definition == null ? 16777215 : definition.defaultDyeColor(); + + int tintRgb = switch (layer.tintMode()) { + case NONE -> 16777215; + case DYE -> NonAccessoryStackData.resolveDyeRgb(stack, fallbackDye); + case STORED_LIQUID -> { + NonAccessoryStackData.StoredLiquid liquid = NonAccessoryStackData.getStoredLiquid(stack); + yield liquid == null ? -1 : NeedsOfNature.resolveBottleLiquidTintRgb(liquid.entityTypeId()); + } + }; + return tintRgb < 0 ? null : new NonTrinketsIntegration.ResolvedVisualLayer(layer.texture(), tintRgb & 16777215); + } else { + return null; + } + } + + public static List getVisuallyShedAccessoryStacks(LivingEntity entity) { + return TRINKETS_LOADED && entity != null ? NonTrinketsCompat.getVisuallyShedAccessoryStacks(entity) : List.of(); + } + + public static NonTrinketsIntegration.TankScopeStatus resolveTankScopeStatus(LivingEntity entity, ItemStack stack) { + return TRINKETS_LOADED && entity != null && stack != null && !stack.isEmpty() + ? NonTrinketsCompat.resolveTankScopeStatus(entity, stack) + : NonTrinketsIntegration.TankScopeStatus.NOT_EQUIPPED; + } + + public record ResolvedVisualLayer(Identifier texture, int tintRgb) { + } + + public record StoredInjectorLiquid(Identifier entityTypeId, int amountMl) { + } + + public enum TankScopeStatus { + NOT_EQUIPPED, + ACTIVE_EQUIPPED, + INACTIVE_EQUIPPED; + } +} diff --git a/src/main/java/com/nonid/PregnancyCycleHolder.java b/src/main/java/com/nonid/PregnancyCycleHolder.java new file mode 100644 index 0000000..08bccae --- /dev/null +++ b/src/main/java/com/nonid/PregnancyCycleHolder.java @@ -0,0 +1,15 @@ +package com.nonid; + +public interface PregnancyCycleHolder { + boolean isPregnancyCycleInitialized(); + + long getPregnancyCycleProgressTicks(); + + long getPregnancyCycleLastGameTick(); + + long getPregnancyCycleLastTimeOfDay(); + + void setPregnancyCycleState(long var1, long var3, long var5); + + void clearPregnancyCycleState(); +} diff --git a/src/main/java/com/nonid/PregnancyCyclePhase.java b/src/main/java/com/nonid/PregnancyCyclePhase.java new file mode 100644 index 0000000..cf3ffc3 --- /dev/null +++ b/src/main/java/com/nonid/PregnancyCyclePhase.java @@ -0,0 +1,22 @@ +package com.nonid; + +public enum PregnancyCyclePhase { + SAFE, + NORMAL, + RISKY, + OVULATION; + + public static PregnancyCyclePhase fromNetworkId(int id) { + PregnancyCyclePhase[] values = values(); + return id >= 0 && id < values.length ? values[id] : NORMAL; + } + + public String displayName() { + return switch (this) { + case SAFE -> "safe"; + case NORMAL -> "normal"; + case RISKY -> "risky"; + case OVULATION -> "ovulation"; + }; + } +} diff --git a/src/main/java/com/nonid/PregnancyHolder.java b/src/main/java/com/nonid/PregnancyHolder.java new file mode 100644 index 0000000..a4aff54 --- /dev/null +++ b/src/main/java/com/nonid/PregnancyHolder.java @@ -0,0 +1,32 @@ +package com.nonid; + +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public interface PregnancyHolder { + @Nullable + Identifier getPregnantEntityTypeId(); + + long getPregnancyEndTick(); + + boolean isPregnancyPendingHatch(); + + @Nullable + NeedsOfNature.PregnancyVariantData getPregnancyVariantData(); + + int getPregnancyOffspringCount(); + + void setPregnancyOffspringCount(int var1); + + void setPregnancyState(@Nullable Identifier var1, long var2, boolean var4, @Nullable NeedsOfNature.PregnancyVariantData var5); + + default void setPregnancyState(@Nullable Identifier entityTypeId, long endTick, boolean pendingHatch) { + this.setPregnancyState(entityTypeId, endTick, pendingHatch, null); + } + + void clearPregnancyState(); + + default boolean hasPregnancyState() { + return this.getPregnantEntityTypeId() != null; + } +} diff --git a/src/main/java/com/nonid/api/NonAccessoryEffectsSnapshot.java b/src/main/java/com/nonid/api/NonAccessoryEffectsSnapshot.java new file mode 100644 index 0000000..3174b50 --- /dev/null +++ b/src/main/java/com/nonid/api/NonAccessoryEffectsSnapshot.java @@ -0,0 +1,19 @@ +package com.nonid.api; + +public record NonAccessoryEffectsSnapshot( + double liquidDecayMultiplier, + boolean equalizeLiquidDecayContext, + double playerEnergyGainMultiplier, + int liquidCapacityAdd, + double liquidGainMultiplier, + double filledEffectMultiplier, + double pregnancyChanceMultiplier, + double pregnancyDurationMultiplier, + double messGainMultiplier, + double destroyedSkinDamageMultiplier, + int attackEscapeHitsAdd, + double attackEscapeDamageMultiplier, + double playerEnergyAuraMultiplier, + double nearAnimationMobEnergyMultiplier +) { +} diff --git a/src/main/java/com/nonid/api/NonChangeSource.java b/src/main/java/com/nonid/api/NonChangeSource.java new file mode 100644 index 0000000..ff376ef --- /dev/null +++ b/src/main/java/com/nonid/api/NonChangeSource.java @@ -0,0 +1,10 @@ +package com.nonid.api; + +public enum NonChangeSource { + API, + COMMAND, + ITEM, + CLIENT_PREFERENCE, + JOIN_SELECTION, + SYSTEM; +} diff --git a/src/main/java/com/nonid/api/NonDestroyedSkinSnapshot.java b/src/main/java/com/nonid/api/NonDestroyedSkinSnapshot.java new file mode 100644 index 0000000..d151ce7 --- /dev/null +++ b/src/main/java/com/nonid/api/NonDestroyedSkinSnapshot.java @@ -0,0 +1,4 @@ +package com.nonid.api; + +public record NonDestroyedSkinSnapshot(int stage, int damage, int maxDamage) { +} diff --git a/src/main/java/com/nonid/api/NonEnergySnapshot.java b/src/main/java/com/nonid/api/NonEnergySnapshot.java new file mode 100644 index 0000000..d00cd5b --- /dev/null +++ b/src/main/java/com/nonid/api/NonEnergySnapshot.java @@ -0,0 +1,7 @@ +package com.nonid.api; + +public record NonEnergySnapshot(int energy, int maxEnergy, float passiveGainMultiplier, float passiveGainDrift, boolean initialized) { + public float fraction() { + return this.maxEnergy <= 0 ? 0.0F : Math.clamp((float)this.energy / this.maxEnergy, 0.0F, 1.0F); + } +} diff --git a/src/main/java/com/nonid/api/NonEntityApi.java b/src/main/java/com/nonid/api/NonEntityApi.java new file mode 100644 index 0000000..56dd9a8 --- /dev/null +++ b/src/main/java/com/nonid/api/NonEntityApi.java @@ -0,0 +1,45 @@ +package com.nonid.api; + +import com.nonid.EnergyHolder; +import net.minecraft.entity.LivingEntity; +import org.jetbrains.annotations.Nullable; + +public final class NonEntityApi { + private NonEntityApi() { + } + + public static boolean hasEnergy(LivingEntity entity) { + return entity instanceof EnergyHolder; + } + + @Nullable + public static NonEnergySnapshot getEnergy(LivingEntity entity) { + return entity instanceof EnergyHolder holder + ? new NonEnergySnapshot( + holder.getEnergy(), holder.getMaxEnergy(), holder.getEnergyGainMultiplier(), holder.getEnergyGainDrift(), holder.isEnergyInitialized() + ) + : null; + } + + public static boolean setEnergy(LivingEntity entity, int energy) { + if (entity instanceof EnergyHolder holder && !entity.getEntityWorld().isClient()) { + int before = holder.getEnergy(); + holder.setEnergy(energy); + return holder.getEnergy() != before; + } else { + return false; + } + } + + public static int addEnergy(LivingEntity entity, int amount) { + if (entity instanceof EnergyHolder holder && !entity.getEntityWorld().isClient() && amount != 0) { + int before = holder.getEnergy(); + long requested = (long)before + amount; + int safeRequested = requested > 2147483647L ? Integer.MAX_VALUE : (requested < -2147483648L ? Integer.MIN_VALUE : (int)requested); + holder.setEnergy(safeRequested); + return holder.getEnergy() - before; + } else { + return 0; + } + } +} diff --git a/src/main/java/com/nonid/api/NonEvents.java b/src/main/java/com/nonid/api/NonEvents.java new file mode 100644 index 0000000..26d7555 --- /dev/null +++ b/src/main/java/com/nonid/api/NonEvents.java @@ -0,0 +1,105 @@ +package com.nonid.api; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonEvents { + public static final Event PLAYER_GENDER_CHANGED = EventFactory.createArrayBacked( + NonEvents.PlayerGenderChanged.class, callbacks -> (player, gender, source) -> { + for (NonEvents.PlayerGenderChanged callback : callbacks) { + callback.onPlayerGenderChanged(player, gender, source); + } + } + ); + public static final Event LIQUID_TANK_CHANGED = EventFactory.createArrayBacked( + NonEvents.LiquidTankChanged.class, callbacks -> (player, snapshot) -> { + for (NonEvents.LiquidTankChanged callback : callbacks) { + callback.onLiquidTankChanged(player, snapshot); + } + } + ); + public static final Event MESS_CHANGED = EventFactory.createArrayBacked( + NonEvents.MessChanged.class, callbacks -> (player, snapshot) -> { + for (NonEvents.MessChanged callback : callbacks) { + callback.onMessChanged(player, snapshot); + } + } + ); + public static final Event DESTROYED_SKIN_CHANGED = EventFactory.createArrayBacked( + NonEvents.DestroyedSkinChanged.class, callbacks -> (player, snapshot) -> { + for (NonEvents.DestroyedSkinChanged callback : callbacks) { + callback.onDestroyedSkinChanged(player, snapshot); + } + } + ); + public static final Event PREGNANCY_CHANGED = EventFactory.createArrayBacked( + NonEvents.PregnancyChanged.class, callbacks -> (player, snapshot) -> { + for (NonEvents.PregnancyChanged callback : callbacks) { + callback.onPregnancyChanged(player, snapshot); + } + } + ); + public static final Event MODIFY_LIQUID_GAIN = EventFactory.createArrayBacked( + NonEvents.ModifyLiquidGain.class, callbacks -> (player, sourceEntityType, amountMl) -> { + int result = Math.max(0, amountMl); + + for (NonEvents.ModifyLiquidGain callback : callbacks) { + result = Math.max(0, callback.modifyLiquidGain(player, sourceEntityType, result)); + } + + return result; + } + ); + public static final Event ALLOW_PREGNANCY = EventFactory.createArrayBacked( + NonEvents.AllowPregnancy.class, callbacks -> (player, entityTypeId) -> { + for (NonEvents.AllowPregnancy callback : callbacks) { + if (!callback.allowPregnancy(player, entityTypeId)) { + return false; + } + } + + return true; + } + ); + + private NonEvents() { + } + + @FunctionalInterface + public interface AllowPregnancy { + boolean allowPregnancy(ServerPlayerEntity var1, Identifier var2); + } + + @FunctionalInterface + public interface DestroyedSkinChanged { + void onDestroyedSkinChanged(ServerPlayerEntity var1, NonDestroyedSkinSnapshot var2); + } + + @FunctionalInterface + public interface LiquidTankChanged { + void onLiquidTankChanged(ServerPlayerEntity var1, NonLiquidTankSnapshot var2); + } + + @FunctionalInterface + public interface MessChanged { + void onMessChanged(ServerPlayerEntity var1, NonMessSnapshot var2); + } + + @FunctionalInterface + public interface ModifyLiquidGain { + int modifyLiquidGain(ServerPlayerEntity var1, @Nullable Identifier var2, int var3); + } + + @FunctionalInterface + public interface PlayerGenderChanged { + void onPlayerGenderChanged(ServerPlayerEntity var1, NonGender var2, NonChangeSource var3); + } + + @FunctionalInterface + public interface PregnancyChanged { + void onPregnancyChanged(ServerPlayerEntity var1, NonPregnancySnapshot var2); + } +} diff --git a/src/main/java/com/nonid/api/NonGender.java b/src/main/java/com/nonid/api/NonGender.java new file mode 100644 index 0000000..e3703dc --- /dev/null +++ b/src/main/java/com/nonid/api/NonGender.java @@ -0,0 +1,27 @@ +package com.nonid.api; + +public enum NonGender { + NONE(0), + MALE(1), + FEMALE(2), + BOTH(3); + + private final int mask; + + NonGender(int mask) { + this.mask = mask; + } + + public int mask() { + return this.mask; + } + + public static NonGender fromMask(int mask) { + return switch (mask & 3) { + case 1 -> MALE; + case 2 -> FEMALE; + case 3 -> BOTH; + default -> NONE; + }; + } +} diff --git a/src/main/java/com/nonid/api/NonLiquidComposition.java b/src/main/java/com/nonid/api/NonLiquidComposition.java new file mode 100644 index 0000000..966d28d --- /dev/null +++ b/src/main/java/com/nonid/api/NonLiquidComposition.java @@ -0,0 +1,7 @@ +package com.nonid.api; + +public enum NonLiquidComposition { + EMPTY, + MIXED, + ENTITY; +} diff --git a/src/main/java/com/nonid/api/NonLiquidTankSnapshot.java b/src/main/java/com/nonid/api/NonLiquidTankSnapshot.java new file mode 100644 index 0000000..71a46ad --- /dev/null +++ b/src/main/java/com/nonid/api/NonLiquidTankSnapshot.java @@ -0,0 +1,10 @@ +package com.nonid.api; + +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonLiquidTankSnapshot(int storedMl, int capacityMl, NonLiquidComposition composition, @Nullable Identifier entityTypeId, int tintRgb) { + public float fillRatio() { + return this.capacityMl <= 0 ? 0.0F : Math.max(0.0F, Math.min(1.0F, (float)this.storedMl / this.capacityMl)); + } +} diff --git a/src/main/java/com/nonid/api/NonMessSnapshot.java b/src/main/java/com/nonid/api/NonMessSnapshot.java new file mode 100644 index 0000000..31b4c06 --- /dev/null +++ b/src/main/java/com/nonid/api/NonMessSnapshot.java @@ -0,0 +1,7 @@ +package com.nonid.api; + +public record NonMessSnapshot(int v, int a, int m) { + public int total() { + return Math.max(0, this.v) + Math.max(0, this.a) + Math.max(0, this.m); + } +} diff --git a/src/main/java/com/nonid/api/NonMessType.java b/src/main/java/com/nonid/api/NonMessType.java new file mode 100644 index 0000000..8b56929 --- /dev/null +++ b/src/main/java/com/nonid/api/NonMessType.java @@ -0,0 +1,7 @@ +package com.nonid.api; + +public enum NonMessType { + V, + A, + M; +} diff --git a/src/main/java/com/nonid/api/NonPlayerApi.java b/src/main/java/com/nonid/api/NonPlayerApi.java new file mode 100644 index 0000000..5bd885b --- /dev/null +++ b/src/main/java/com/nonid/api/NonPlayerApi.java @@ -0,0 +1,100 @@ +package com.nonid.api; + +import com.nonid.NonApiInternals; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonPlayerApi { + private NonPlayerApi() { + } + + public static NonPlayerStateSnapshot getState(PlayerEntity player) { + return NonApiInternals.snapshotPlayerState(player); + } + + public static NonGender getGender(PlayerEntity player) { + return NonApiInternals.getGender(player); + } + + public static boolean setGender(ServerPlayerEntity player, NonGender gender, NonChangeSource source) { + return NonApiInternals.setGender(player, gender, source); + } + + public static boolean canChangeGender(ServerPlayerEntity player, NonChangeSource source) { + return NonApiInternals.canChangeGender(player, source); + } + + public static NonLiquidTankSnapshot getLiquidTank(PlayerEntity player) { + return NonApiInternals.snapshotLiquid(player); + } + + public static int addLiquid(ServerPlayerEntity player, @Nullable Identifier sourceEntityType, int amountMl) { + return NonApiInternals.addLiquid(player, sourceEntityType, amountMl); + } + + public static int drainLiquid(ServerPlayerEntity player, int amountMl) { + return NonApiInternals.drainLiquid(player, amountMl); + } + + public static NonMessSnapshot getMess(PlayerEntity player) { + return NonApiInternals.snapshotMess(player); + } + + public static boolean addMess(ServerPlayerEntity player, NonMessType type, int amount) { + return NonApiInternals.addMess(player, type, amount); + } + + public static boolean cleanMess(ServerPlayerEntity player, NonMessType type, int amount) { + return NonApiInternals.cleanMess(player, type, amount); + } + + public static boolean clearMess(ServerPlayerEntity player) { + return NonApiInternals.clearMess(player); + } + + public static NonDestroyedSkinSnapshot getDestroyedSkin(PlayerEntity player) { + return NonApiInternals.snapshotDestroyedSkin(player); + } + + public static boolean damageDestroyedSkin(ServerPlayerEntity player, int amount) { + return NonApiInternals.damageDestroyedSkin(player, amount); + } + + public static boolean repairDestroyedSkin(ServerPlayerEntity player, int amount) { + return NonApiInternals.repairDestroyedSkin(player, amount); + } + + public static NonPregnancySnapshot getPregnancy(PlayerEntity player) { + return NonApiInternals.snapshotPregnancy(player); + } + + public static boolean beginPregnancy(ServerPlayerEntity player, Identifier entityTypeId, long endTick) { + return NonApiInternals.beginPregnancy(player, entityTypeId, endTick); + } + + public static boolean clearPregnancy(ServerPlayerEntity player) { + return NonApiInternals.clearPregnancy(player); + } + + public static NonAccessoryEffectsSnapshot getAccessoryEffects(PlayerEntity player) { + return NonApiInternals.snapshotAccessoryEffects(player); + } + + public static boolean isMessSystemEnabled() { + return NonApiInternals.isMessSystemEnabled(); + } + + public static boolean isDestroyedSkinSystemEnabled() { + return NonApiInternals.isDestroyedSkinSystemEnabled(); + } + + public static boolean isLiquidTankEnabled() { + return NonApiInternals.isLiquidTankEnabled(); + } + + public static boolean isPregnancyEnabled() { + return NonApiInternals.isPregnancyEnabled(); + } +} diff --git a/src/main/java/com/nonid/api/NonPlayerInteractionApi.java b/src/main/java/com/nonid/api/NonPlayerInteractionApi.java new file mode 100644 index 0000000..889516d --- /dev/null +++ b/src/main/java/com/nonid/api/NonPlayerInteractionApi.java @@ -0,0 +1,155 @@ +package com.nonid.api; + +import com.nonid.NeedsOfNature; +import com.nonid.NonPlayerInteractionProfiles; +import com.nonid.NonPlayerInteractionSystem; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import org.jetbrains.annotations.Nullable; + +public final class NonPlayerInteractionApi { + private NonPlayerInteractionApi() { + } + + @Nullable + public static NonPlayerInteractionApi.Profile getProfile(ServerPlayerEntity owner) { + ServerWorld world = serverWorld(owner); + if (world == null) { + return null; + } + + NonPlayerInteractionProfiles.Snapshot snapshot = NonPlayerInteractionProfiles.get(world).snapshot(owner.getUuid(), allowedMask()); + return new NonPlayerInteractionApi.Profile( + toApi(snapshot.defaultMode()), + snapshot.overrides().stream().map(entry -> new NonPlayerInteractionApi.Override(entry.uuid(), entry.name(), toApi(entry.mode()))).toList(), + snapshot.knownPlayers().stream().map(entry -> new NonPlayerInteractionApi.KnownPlayer(entry.uuid(), entry.name())).toList() + ); + } + + @Nullable + public static NonPlayerInteractionApi.Mode getResolvedMode(ServerPlayerEntity owner, UUID otherPlayer) { + ServerWorld world = serverWorld(owner); + return world != null && otherPlayer != null + ? toApi(NonPlayerInteractionProfiles.get(world).resolveMode(owner.getUuid(), otherPlayer, allowedMask())) + : null; + } + + public static boolean canRequest(ServerPlayerEntity requester, ServerPlayerEntity target) { + NonPlayerInteractionApi.Mode mode = target != null && requester != null ? getResolvedMode(target, requester.getUuid()) : null; + return mode != null && mode.acceptsRequests(); + } + + public static boolean canAttack(ServerPlayerEntity attacker, ServerPlayerEntity target) { + NonPlayerInteractionApi.Mode mode = target != null && attacker != null ? getResolvedMode(target, attacker.getUuid()) : null; + return mode != null && mode.acceptsAttacks(); + } + + public static boolean setDefaultMode(ServerPlayerEntity owner, NonPlayerInteractionApi.Mode mode) { + ServerWorld world = serverWorld(owner); + if (world != null && mode != null) { + NonPlayerInteractionProfiles.get(world).setDefaultMode(owner.getUuid(), toInternal(mode), allowedMask()); + sync(owner); + return true; + } else { + return false; + } + } + + public static boolean setOverride(ServerPlayerEntity owner, ServerPlayerEntity otherPlayer, NonPlayerInteractionApi.Mode mode) { + return otherPlayer == null ? false : setOverride(owner, otherPlayer.getUuid(), otherPlayer.getGameProfile().name(), mode); + } + + public static boolean setOverride(ServerPlayerEntity owner, UUID otherPlayer, String otherPlayerName, NonPlayerInteractionApi.Mode mode) { + ServerWorld world = serverWorld(owner); + if (world != null && otherPlayer != null && mode != null && !otherPlayer.equals(owner.getUuid())) { + NonPlayerInteractionProfiles.get(world).setOverride(owner.getUuid(), otherPlayer, otherPlayerName, toInternal(mode), allowedMask()); + sync(owner); + return true; + } else { + return false; + } + } + + public static boolean removeOverride(ServerPlayerEntity owner, UUID otherPlayer) { + ServerWorld world = serverWorld(owner); + if (world != null && otherPlayer != null) { + NonPlayerInteractionProfiles.get(world).removeOverride(owner.getUuid(), otherPlayer); + sync(owner); + return true; + } else { + return false; + } + } + + @Nullable + private static ServerWorld serverWorld(@Nullable ServerPlayerEntity player) { + return player != null && player.getEntityWorld() instanceof ServerWorld world ? world : null; + } + + private static int allowedMask() { + return NeedsOfNature.getConfig().getAllowedPlayerInteractionProfilesMask(); + } + + private static void sync(ServerPlayerEntity owner) { + NonPlayerInteractionSystem.sendProfileSnapshot(owner, ""); + } + + private static NonPlayerInteractionApi.Mode toApi(NonPlayerInteractionProfiles.Mode mode) { + return switch (mode) { + case DENY_ALL -> NonPlayerInteractionApi.Mode.DENY_ALL; + case REQUESTS_ONLY -> NonPlayerInteractionApi.Mode.REQUESTS_ONLY; + case REQUESTS_AND_ATTACKS -> NonPlayerInteractionApi.Mode.REQUESTS_AND_ATTACKS; + case AUTO_ACCEPT -> NonPlayerInteractionApi.Mode.AUTO_ACCEPT; + }; + } + + private static NonPlayerInteractionProfiles.Mode toInternal(NonPlayerInteractionApi.Mode mode) { + return switch (mode) { + case DENY_ALL -> NonPlayerInteractionProfiles.Mode.DENY_ALL; + case REQUESTS_ONLY -> NonPlayerInteractionProfiles.Mode.REQUESTS_ONLY; + case REQUESTS_AND_ATTACKS -> NonPlayerInteractionProfiles.Mode.REQUESTS_AND_ATTACKS; + case AUTO_ACCEPT -> NonPlayerInteractionProfiles.Mode.AUTO_ACCEPT; + }; + } + + public record KnownPlayer(UUID playerUuid, String playerName) { + } + + public enum Mode { + DENY_ALL, + REQUESTS_ONLY, + REQUESTS_AND_ATTACKS, + AUTO_ACCEPT; + + public String id() { + return this.name().toLowerCase(Locale.ROOT); + } + + public boolean acceptsRequests() { + return this != DENY_ALL; + } + + public boolean automaticallyAcceptsRequests() { + return this == AUTO_ACCEPT; + } + + public boolean acceptsAttacks() { + return this == REQUESTS_AND_ATTACKS || this == AUTO_ACCEPT; + } + } + + public record Override(UUID playerUuid, String playerName, NonPlayerInteractionApi.Mode mode) { + } + + public record Profile( + NonPlayerInteractionApi.Mode defaultMode, List overrides, List knownPlayers + ) { + public Profile { + overrides = List.copyOf(overrides); + knownPlayers = List.copyOf(knownPlayers); + } + } +} diff --git a/src/main/java/com/nonid/api/NonPlayerStateSnapshot.java b/src/main/java/com/nonid/api/NonPlayerStateSnapshot.java new file mode 100644 index 0000000..f7e3d3e --- /dev/null +++ b/src/main/java/com/nonid/api/NonPlayerStateSnapshot.java @@ -0,0 +1,11 @@ +package com.nonid.api; + +public record NonPlayerStateSnapshot( + NonGender gender, + NonLiquidTankSnapshot liquidTank, + NonMessSnapshot mess, + NonDestroyedSkinSnapshot destroyedSkin, + NonPregnancySnapshot pregnancy, + NonAccessoryEffectsSnapshot accessoryEffects +) { +} diff --git a/src/main/java/com/nonid/api/NonPregnancySnapshot.java b/src/main/java/com/nonid/api/NonPregnancySnapshot.java new file mode 100644 index 0000000..d14df90 --- /dev/null +++ b/src/main/java/com/nonid/api/NonPregnancySnapshot.java @@ -0,0 +1,10 @@ +package com.nonid.api; + +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonPregnancySnapshot(@Nullable Identifier entityTypeId, long endTick, boolean pendingHatch) { + public boolean active() { + return this.entityTypeId != null; + } +} diff --git a/src/main/java/com/nonid/api/animation/NonActorEvents.java b/src/main/java/com/nonid/api/animation/NonActorEvents.java new file mode 100644 index 0000000..b03e978 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonActorEvents.java @@ -0,0 +1,55 @@ +package com.nonid.api.animation; + +import java.util.LinkedHashSet; +import java.util.Set; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.entity.Entity; +import org.jetbrains.annotations.NotNull; + +public final class NonActorEvents { + public static final Event PROVIDE = EventFactory.createArrayBacked(NonActorEvents.Provider.class, callbacks -> entity -> { + if (callbacks.length != 0 && entity != null) { + LinkedHashSet tags = new LinkedHashSet<>(); + + for (NonActorEvents.Provider callback : callbacks) { + Set provided = callback.provide(entity); + if (provided != null && !provided.isEmpty()) { + for (String tag : provided) { + if (tag != null && !tag.isBlank()) { + tags.add(tag); + } + } + } + } + + return tags.isEmpty() ? Set.of() : Set.copyOf(tags); + } else { + return Set.of(); + } + }); + + private NonActorEvents() { + } + + public static Set resolve(Entity entity) { + if (entity == null) { + return Set.of(); + } + + Set commandTags = entity.getCommandTags(); + Set provided = ((NonActorEvents.Provider)PROVIDE.invoker()).provide(entity); + if (provided.isEmpty()) { + return commandTags; + } + + LinkedHashSet tags = new LinkedHashSet<>(commandTags); + tags.addAll(provided); + return Set.copyOf(tags); + } + + @FunctionalInterface + public interface Provider { + Set provide(Entity var1); + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationActor.java b/src/main/java/com/nonid/api/animation/NonAnimationActor.java new file mode 100644 index 0000000..66783bf --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationActor.java @@ -0,0 +1,31 @@ +package com.nonid.api.animation; + +import java.util.Set; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationActor( + Set entityTypes, + @Nullable String entityVariant, + Set requiredTags, + Set requiredAnyTags, + String label, + boolean baby, + NonAnimationActor.Activity activity, + @Nullable Identifier leftPropItemId, + @Nullable Identifier rightPropItemId, + @Nullable Identifier floorPropItemId +) { + public NonAnimationActor { + entityTypes = entityTypes == null ? Set.of() : Set.copyOf(entityTypes); + requiredTags = requiredTags == null ? Set.of() : Set.copyOf(requiredTags); + requiredAnyTags = requiredAnyTags == null ? Set.of() : Set.copyOf(requiredAnyTags); + label = label == null ? "" : label; + activity = activity == null ? NonAnimationActor.Activity.ACTIVE : activity; + } + + public enum Activity { + ACTIVE, + PASSIVE; + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationApi.java b/src/main/java/com/nonid/api/animation/NonAnimationApi.java new file mode 100644 index 0000000..6fc5b32 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationApi.java @@ -0,0 +1,358 @@ +package com.nonid.api.animation; + +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.api.NonAnimationViews; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.server.ServerAnimationController; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationApi { + private NonAnimationApi() { + } + + public static List getLoadedDefinitions() { + return AnimationDefinitions.getLoadedDefinitionsSnapshot().stream().map(NonAnimationApi::definitionView).toList(); + } + + @Nullable + public static NonAnimationDefinition getDefinition(Identifier animationId) { + return definitionView(AnimationDefinitions.getDefinition(animationId)); + } + + public static boolean isEnabled(Identifier animationId) { + return AnimationDefinitions.isAnimationEnabled(animationId); + } + + public static boolean hasAnimationForEntityType(@Nullable EntityType type, boolean baby) { + return AnimationService.hasEnabledGenericAnimationForEntityType(type, baby); + } + + public static List findCandidates(ServerWorld world, List actors, @Nullable NonAnimationApi.MatchOptions options) { + if (world != null && actors != null && !actors.isEmpty()) { + NonAnimationApi.MatchOptions safe = options == null ? NonAnimationApi.MatchOptions.defaults() : options; + AnimationService.MatchOptions internalOptions = internalOptions(safe); + List result = new ArrayList<>(); + + for (AnimationService.EligibleAnimation candidate : AnimationService.findEligibleAnimationsForActors(world, actors, internalOptions)) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(candidate.animationId()); + if (definition != null) { + boolean eligible = AnimationService.isAnimationStartEligible(world, definition, actors, candidate.actorKeys(), safe.positionAnchor()); + result.add(new NonAnimationCandidate(definitionView(definition), candidate.actorKeys(), eligible)); + } + } + + result.sort(Comparator.comparing(candidatex -> candidatex.definition().id().toString())); + return List.copyOf(result); + } else { + return List.of(); + } + } + + @Nullable + public static NonAnimationCandidate findMatch(ServerWorld world, List actors, @Nullable NonAnimationApi.MatchOptions options) { + if (world != null && actors != null && !actors.isEmpty()) { + NonAnimationApi.MatchOptions safe = options == null ? NonAnimationApi.MatchOptions.defaults() : options; + AnimationService.MatchedAnimation match = AnimationService.findAnimationForActors(world, actors, internalOptions(safe)); + if (match == null) { + return null; + } + + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(match.animationId()); + if (definition == null) { + return null; + } + + boolean eligible = AnimationService.isAnimationStartEligible(world, definition, actors, match.actorKeys(), safe.positionAnchor()); + return new NonAnimationCandidate(definitionView(definition), match.actorKeys(), eligible); + } else { + return null; + } + } + + public static boolean canStart( + ServerWorld world, Identifier animationId, List actors, @Nullable List actorKeys, @Nullable Entity positionAnchor + ) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId); + if (definition == null) { + return false; + } else { + return actorKeys == null + ? AnimationService.isAnimationStartEligible(world, definition, actors, positionAnchor) + : AnimationService.isAnimationStartEligible(world, definition, actors, actorKeys, positionAnchor); + } + } + + @Nullable + public static UUID start( + ServerWorld world, + Identifier animationId, + List actors, + @Nullable List actorKeys, + @Nullable NonAnimationApi.StartOptions options + ) { + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(animationId); + if (definition == null) { + return null; + } + + NonAnimationApi.StartOptions safe = options == null ? NonAnimationApi.StartOptions.defaults() : options; + return AnimationService.startAnimationNow( + world, + animationId, + actors, + actorKeys, + definition.stages(), + new AnimationService.StartOptions( + safe.damageBehavior(), + safe.ignoreAttackers(), + safe.positionAnchor(), + safe.requester(), + safe.metadata(), + safe.forceDebugChat(), + safe.allowMultiplePlayers() + ) + ); + } + + @Nullable + public static UUID startMatched( + ServerWorld world, + List actors, + @Nullable NonAnimationApi.MatchOptions matchOptions, + @Nullable NonAnimationApi.StartOptions startOptions + ) { + NonAnimationCandidate candidate = findMatch(world, actors, matchOptions); + return candidate != null && candidate.startEligible() ? start(world, candidate.definition().id(), actors, candidate.actorKeys(), startOptions) : null; + } + + @Nullable + public static NonAnimationSession getSession(ServerWorld world, UUID instanceId) { + if (world != null && instanceId != null) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + return snapshot == null ? null : NonAnimationViews.session(snapshot); + } else { + return null; + } + } + + @Nullable + public static NonAnimationStage getCurrentStage(ServerWorld world, UUID instanceId) { + return NonAnimationViews.stage(AnimationService.getCurrentStage(world, instanceId)); + } + + public static boolean advanceStage(ServerWorld world, UUID instanceId) { + return AnimationService.advanceStage(world, instanceId); + } + + public static boolean advanceToStage(ServerWorld world, UUID instanceId, int stageNumber) { + return AnimationService.advanceToStage(world, instanceId, stageNumber); + } + + public static void multiplySpeed(ServerWorld world, UUID instanceId, double multiplier) { + AnimationService.adjustSpeedSilently(world, instanceId, multiplier); + } + + public static boolean stop(ServerWorld world, UUID instanceId) { + return AnimationService.stopAnimation(world, instanceId); + } + + public static void stopAll(ServerWorld world) { + AnimationService.stopAll(world); + } + + public static boolean enqueue( + ServerWorld world, Identifier animationId, List actors, int insertIndex, @Nullable NonAnimationApi.StartOptions options + ) { + NonAnimationApi.StartOptions safe = options == null ? NonAnimationApi.StartOptions.defaults() : options; + return AnimationService.enqueueAnimation( + world, animationId, actors, insertIndex, safe.damageBehavior(), safe.ignoreAttackers(), safe.metadata(), safe.requester() + ); + } + + public static void clearQueue(ServerWorld world, ServerPlayerEntity player) { + AnimationService.clearQueuedAnimations(world, player); + } + + public static boolean isActorPendingOrActive(ServerWorld world, UUID actorUuid) { + return AnimationService.isActorPendingOrActive(world, actorUuid); + } + + public static boolean shouldIgnoreAttackers(ServerWorld world, LivingEntity actor) { + return AnimationService.shouldIgnoreAttackers(world, actor); + } + + public static void setStartProtection(ServerPlayerEntity player, int ticks) { + AnimationService.setAnimationStartProtection(player, ticks); + } + + public static void clearStartProtection(ServerPlayerEntity player) { + AnimationService.clearAnimationStartProtection(player); + } + + public static boolean hasStartProtection(ServerWorld world, ServerPlayerEntity player) { + return AnimationService.hasAnimationStartProtection(world, player); + } + + private static AnimationService.MatchOptions internalOptions(NonAnimationApi.MatchOptions options) { + AnimationService.AnimationCandidateFilter filter = options.candidateFilter() == null + ? null + : (world, definition, actors) -> options.candidateFilter().test(world, definitionView(definition), actors); + return new AnimationService.MatchOptions( + options.requiredAnimationTags(), + options.positionAnchor(), + options.requireStartEligibility(), + options.requiredActiveActor(), + options.excludedAnimationIds(), + filter, + options.joinPredecessorAnimationId() + ); + } + + @Nullable + private static NonAnimationDefinition definitionView(@Nullable AnimationDefinitions.Definition definition) { + return definition == null + ? null + : new NonAnimationDefinition( + definition.id(), + definition.displayName(), + definition.actors().stream().map(NonAnimationApi::actorView).toList(), + definition.requiredUnionTags(), + definition.animationTags(), + definition.contentTags(), + NonAnimationViews.stages(definition.stages()), + definition.weight(), + definition.attackEligible(), + blockView(definition.blockRequirements()), + definition.waterRequirement().name().toLowerCase(Locale.ROOT), + definition.positionAnchorActor(), + packView(definition.packInfo()), + definition.joinAfter(), + AnimationDefinitions.isDefinitionEnabled(definition) + ); + } + + private static NonAnimationActor actorView(AnimationDefinitions.ActorConstraint actor) { + return new NonAnimationActor( + actor.entityTypes(), + actor.entityVariant(), + actor.requiredTags(), + actor.requiredTagsAny(), + actor.label(), + actor.ageRequirement() == AnimationDefinitions.AgeRequirement.BABY, + actor.activity() == AnimationDefinitions.ActorActivity.PASSIVE ? NonAnimationActor.Activity.PASSIVE : NonAnimationActor.Activity.ACTIVE, + actor.propLeftItemId(), + actor.propRightItemId(), + actor.propFloorItemId() + ); + } + + @Nullable + private static NonAnimationPack packView(@Nullable AnimationDefinitions.AnimationPackInfo pack) { + return pack == null + ? null + : new NonAnimationPack( + pack.id(), + pack.resourcePackId(), + pack.name(), + pack.author(), + pack.version(), + pack.description(), + pack.localModelRoot(), + pack.modelImports() + .stream() + .map(modelImport -> new NonAnimationPack.ModelImport(modelImport.packId(), modelImport.modelRoot(), modelImport.entityTypes())) + .toList() + ); + } + + @Nullable + private static NonAnimationBlockRequirement blockView(@Nullable AnimationDefinitions.BlockRequirements requirement) { + if (requirement == null) { + return null; + } + + AnimationDefinitions.BlockPredicate blocks = requirement.blocks(); + return new NonAnimationBlockRequirement( + requirement.type(), + requirement.height() == null ? null : new NonAnimationBlockRequirement.WallHeight(requirement.height().min(), requirement.height().max()), + requirement.width() == null ? null : new NonAnimationBlockRequirement.WallWidth(requirement.width().min(), requirement.width().max()), + requirement.facing() == null ? null : requirement.facing().id(), + requirement.clearance() == null + ? null + : new NonAnimationBlockRequirement.Dimensions(requirement.clearance().width(), requirement.clearance().height(), requirement.clearance().depth()), + requirement.support() == null ? null : requirement.support().id(), + requirement.placement() == null ? null : requirement.placement().id(), + requirement.surfaceRadius(), + requirement.surfaceFootprint() == null + ? null + : new NonAnimationBlockRequirement.Footprint( + requirement.surfaceFootprint().width(), + requirement.surfaceFootprint().depth(), + requirement.surfaceFootprint().height(), + requirement.surfaceFootprint().margin() + ), + blocks == null ? Set.of() : blocks.blockIds(), + blocks == null ? Set.of() : blocks.tagIds() + ); + } + + @FunctionalInterface + public interface CandidateFilter { + boolean test(ServerWorld var1, NonAnimationDefinition var2, List var3); + } + + public record MatchOptions( + Set requiredAnimationTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + Set excludedAnimationIds, + @Nullable NonAnimationApi.CandidateFilter candidateFilter, + @Nullable Identifier joinPredecessorAnimationId + ) { + public MatchOptions { + requiredAnimationTags = requiredAnimationTags == null ? Set.of() : Set.copyOf(requiredAnimationTags); + excludedAnimationIds = excludedAnimationIds == null ? Set.of() : Set.copyOf(excludedAnimationIds); + } + + public static NonAnimationApi.MatchOptions defaults() { + return new NonAnimationApi.MatchOptions(Set.of(), null, false, null, Set.of(), null, null); + } + + public static NonAnimationApi.MatchOptions startEligible(@Nullable Entity positionAnchor) { + return new NonAnimationApi.MatchOptions(Set.of(), positionAnchor, true, null, Set.of(), null, null); + } + } + + public record StartOptions( + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Entity positionAnchor, + @Nullable ServerPlayerEntity requester, + Map metadata, + boolean forceDebugChat, + boolean allowMultiplePlayers + ) { + public StartOptions { + damageBehavior = damageBehavior == null ? NonAnimationDamageBehavior.IGNORE_DAMAGE : damageBehavior; + metadata = metadata == null ? Map.of() : Map.copyOf(metadata); + } + + public static NonAnimationApi.StartOptions defaults() { + return new NonAnimationApi.StartOptions(NonAnimationDamageBehavior.IGNORE_DAMAGE, false, null, null, Map.of(), false, false); + } + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationBlockRequirement.java b/src/main/java/com/nonid/api/animation/NonAnimationBlockRequirement.java new file mode 100644 index 0000000..b9b2365 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationBlockRequirement.java @@ -0,0 +1,36 @@ +package com.nonid.api.animation; + +import java.util.Set; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationBlockRequirement( + String type, + @Nullable NonAnimationBlockRequirement.WallHeight wallHeight, + @Nullable NonAnimationBlockRequirement.WallWidth wallWidth, + @Nullable String facing, + @Nullable NonAnimationBlockRequirement.Dimensions clearance, + @Nullable String support, + @Nullable String placement, + int surfaceRadius, + @Nullable NonAnimationBlockRequirement.Footprint surfaceFootprint, + Set blockIds, + Set blockTagIds +) { + public NonAnimationBlockRequirement { + blockIds = blockIds == null ? Set.of() : Set.copyOf(blockIds); + blockTagIds = blockTagIds == null ? Set.of() : Set.copyOf(blockTagIds); + } + + public record Dimensions(int width, int height, int depth) { + } + + public record Footprint(int width, int depth, int height, int margin) { + } + + public record WallHeight(int minimum, @Nullable Integer maximum) { + } + + public record WallWidth(int minimum, @Nullable Integer maximum) { + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationCandidate.java b/src/main/java/com/nonid/api/animation/NonAnimationCandidate.java new file mode 100644 index 0000000..7eb1b94 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationCandidate.java @@ -0,0 +1,9 @@ +package com.nonid.api.animation; + +import java.util.List; + +public record NonAnimationCandidate(NonAnimationDefinition definition, List actorKeys, boolean startEligible) { + public NonAnimationCandidate { + actorKeys = actorKeys == null ? List.of() : List.copyOf(actorKeys); + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationDamageBehavior.java b/src/main/java/com/nonid/api/animation/NonAnimationDamageBehavior.java new file mode 100644 index 0000000..63234d4 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationDamageBehavior.java @@ -0,0 +1,28 @@ +package com.nonid.api.animation; + +import java.util.Locale; + +public enum NonAnimationDamageBehavior { + STOP_ON_DAMAGE, + IGNORE_DAMAGE, + BLOCK_DAMAGE; + + public String id() { + return this.name().toLowerCase(Locale.ROOT); + } + + public static NonAnimationDamageBehavior fromId(String id, NonAnimationDamageBehavior fallback) { + if (id != null && !id.isBlank()) { + String norm = id.trim().toLowerCase(Locale.ROOT); + + return switch (norm) { + case "stop_on_damage" -> STOP_ON_DAMAGE; + case "ignore_damage" -> IGNORE_DAMAGE; + case "block_damage" -> BLOCK_DAMAGE; + default -> fallback; + }; + } else { + return fallback; + } + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationDefinition.java b/src/main/java/com/nonid/api/animation/NonAnimationDefinition.java new file mode 100644 index 0000000..83fca61 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationDefinition.java @@ -0,0 +1,34 @@ +package com.nonid.api.animation; + +import java.util.List; +import java.util.Set; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationDefinition( + Identifier id, + @Nullable String displayName, + List actors, + Set requiredUnionTags, + Set animationTags, + List contentTags, + List stages, + double weight, + boolean attackEligible, + @Nullable NonAnimationBlockRequirement blockRequirement, + String waterRequirement, + @Nullable String positionAnchorActor, + @Nullable NonAnimationPack pack, + Set joinAfter, + boolean enabled +) { + public NonAnimationDefinition { + actors = actors == null ? List.of() : List.copyOf(actors); + requiredUnionTags = requiredUnionTags == null ? Set.of() : Set.copyOf(requiredUnionTags); + animationTags = animationTags == null ? Set.of() : Set.copyOf(animationTags); + contentTags = contentTags == null ? List.of() : List.copyOf(contentTags); + stages = stages == null ? List.of() : List.copyOf(stages); + waterRequirement = waterRequirement == null ? "none" : waterRequirement; + joinAfter = joinAfter == null ? Set.of() : Set.copyOf(joinAfter); + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationEvents.java b/src/main/java/com/nonid/api/animation/NonAnimationEvents.java new file mode 100644 index 0000000..b2bd2d5 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationEvents.java @@ -0,0 +1,117 @@ +package com.nonid.api.animation; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationEvents { + public static final Event BEFORE_START = EventFactory.createArrayBacked( + NonAnimationEvents.BeforeStart.class, callbacks -> context -> { + for (NonAnimationEvents.BeforeStart callback : callbacks) { + if (!callback.allow(context)) { + return false; + } + } + + return true; + } + ); + public static final Event STARTED = EventFactory.createArrayBacked( + NonAnimationEvents.Started.class, callbacks -> context -> { + for (NonAnimationEvents.Started callback : callbacks) { + callback.onStarted(context); + } + } + ); + public static final Event STOPPED = EventFactory.createArrayBacked( + NonAnimationEvents.Stopped.class, callbacks -> context -> { + for (NonAnimationEvents.Stopped callback : callbacks) { + callback.onStopped(context); + } + } + ); + public static final Event STAGE_CHANGED = EventFactory.createArrayBacked( + NonAnimationEvents.StageChanged.class, callbacks -> context -> { + for (NonAnimationEvents.StageChanged callback : callbacks) { + callback.onStageChanged(context); + } + } + ); + + private NonAnimationEvents() { + } + + @FunctionalInterface + public interface BeforeStart { + boolean allow(NonAnimationEvents.BeforeStartContext var1); + } + + public record BeforeStartContext( + ServerWorld world, + Identifier animationId, + List actorUuids, + List actorKeys, + List stages, + @Nullable ServerPlayerEntity requester, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + Map metadata + ) { + public BeforeStartContext { + actorUuids = List.copyOf(actorUuids); + actorKeys = List.copyOf(actorKeys); + stages = List.copyOf(stages); + metadata = Map.copyOf(metadata); + } + } + + @FunctionalInterface + public interface StageChanged { + void onStageChanged(NonAnimationEvents.StageChangedContext var1); + } + + public record StageChangedContext( + ServerWorld world, + UUID instanceId, + Identifier animationId, + List actorUuids, + List actorKeys, + long advanceTick, + @Nullable NonAnimationStage currentStage + ) { + public StageChangedContext { + actorUuids = List.copyOf(actorUuids); + actorKeys = List.copyOf(actorKeys); + } + } + + @FunctionalInterface + public interface Started { + void onStarted(NonAnimationEvents.StartedContext var1); + } + + public record StartedContext(ServerWorld world, NonAnimationSession session, @Nullable ServerPlayerEntity requester) { + } + + @FunctionalInterface + public interface Stopped { + void onStopped(NonAnimationEvents.StoppedContext var1); + } + + public record StoppedContext( + ServerWorld world, UUID instanceId, Identifier animationId, List actorUuids, List actorKeys, List stages + ) { + public StoppedContext { + actorUuids = List.copyOf(actorUuids); + actorKeys = List.copyOf(actorKeys); + stages = List.copyOf(stages); + } + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationPack.java b/src/main/java/com/nonid/api/animation/NonAnimationPack.java new file mode 100644 index 0000000..131d2df --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationPack.java @@ -0,0 +1,33 @@ +package com.nonid.api.animation; + +import java.util.List; +import java.util.Set; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationPack( + String id, + String resourcePackId, + String name, + String author, + @Nullable String version, + @Nullable String description, + @Nullable Identifier localModelRoot, + List modelImports +) { + public NonAnimationPack { + modelImports = modelImports == null ? List.of() : List.copyOf(modelImports); + } + + public NonAnimationPack( + String id, String resourcePackId, String name, String author, @Nullable String version, @Nullable String description, @Nullable Identifier localModelRoot + ) { + this(id, resourcePackId, name, author, version, description, localModelRoot, List.of()); + } + + public record ModelImport(String packId, Identifier modelRoot, Set entityTypes) { + public ModelImport { + entityTypes = entityTypes == null ? Set.of() : Set.copyOf(entityTypes); + } + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationSession.java b/src/main/java/com/nonid/api/animation/NonAnimationSession.java new file mode 100644 index 0000000..c6db032 --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationSession.java @@ -0,0 +1,22 @@ +package com.nonid.api.animation; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationSession( + UUID instanceId, + Identifier animationId, + List actorUuids, + @Nullable NonAnimationStage currentStage, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + Map metadata +) { + public NonAnimationSession { + actorUuids = actorUuids == null ? List.of() : List.copyOf(actorUuids); + metadata = metadata == null ? Map.of() : Map.copyOf(metadata); + } +} diff --git a/src/main/java/com/nonid/api/animation/NonAnimationStage.java b/src/main/java/com/nonid/api/animation/NonAnimationStage.java new file mode 100644 index 0000000..3d17d2b --- /dev/null +++ b/src/main/java/com/nonid/api/animation/NonAnimationStage.java @@ -0,0 +1,24 @@ +package com.nonid.api.animation; + +import java.util.List; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record NonAnimationStage( + Identifier animationId, + boolean looping, + long lengthTicks, + boolean joinsAllowed, + double speed, + Identifier playbackAnimationId, + List actorProps, + double cycleMidpointOffsetSeconds, + double cycleTicks +) { + public NonAnimationStage { + actorProps = actorProps == null ? List.of() : List.copyOf(actorProps); + } + + public record ActorProp(String actorKey, @Nullable Identifier leftItemId, @Nullable Identifier rightItemId, @Nullable Identifier floorItemId) { + } +} diff --git a/src/main/java/com/nonid/api/client/NonAnimationParticleEvents.java b/src/main/java/com/nonid/api/client/NonAnimationParticleEvents.java new file mode 100644 index 0000000..fb249f4 --- /dev/null +++ b/src/main/java/com/nonid/api/client/NonAnimationParticleEvents.java @@ -0,0 +1,59 @@ +package com.nonid.api.client; + +import java.util.UUID; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.entity.Entity; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationParticleEvents { + public static final Event RESOLVE = EventFactory.createArrayBacked( + NonAnimationParticleEvents.ParticleResolver.class, callbacks -> context -> { + for (NonAnimationParticleEvents.ParticleResolver callback : callbacks) { + NonAnimationParticleEvents.ParticleOverride override = callback.resolve(context); + if (override != null) { + return override; + } + } + + return null; + } + ); + + private NonAnimationParticleEvents() { + } + + public record ParticleContext( + @Nullable Entity actor, + UUID actorUuid, + @Nullable Identifier animationId, + @Nullable Identifier animationResource, + @Nullable String animationKey, + Identifier particleId, + String locator, + String script, + Vec3d position, + @Nullable UUID instanceId + ) { + } + + public record ParticleOverride(@Nullable ParticleEffect particle, boolean cancel) { + public static NonAnimationParticleEvents.ParticleOverride spawn(ParticleEffect particle) { + return new NonAnimationParticleEvents.ParticleOverride(particle, false); + } + + public static NonAnimationParticleEvents.ParticleOverride skip() { + return new NonAnimationParticleEvents.ParticleOverride(null, true); + } + } + + @FunctionalInterface + public interface ParticleResolver { + @Nullable + NonAnimationParticleEvents.ParticleOverride resolve(NonAnimationParticleEvents.ParticleContext var1); + } +} diff --git a/src/main/java/com/nonid/api/client/NonAnimationRenderEvents.java b/src/main/java/com/nonid/api/client/NonAnimationRenderEvents.java new file mode 100644 index 0000000..18f5b58 --- /dev/null +++ b/src/main/java/com/nonid/api/client/NonAnimationRenderEvents.java @@ -0,0 +1,106 @@ +package com.nonid.api.client; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemDisplayContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationRenderEvents { + public static final Event RESOLVE = EventFactory.createArrayBacked( + NonAnimationRenderEvents.ModelResolver.class, callbacks -> (entity, entityTypeId, currentModel, currentTexture) -> { + for (NonAnimationRenderEvents.ModelResolver callback : callbacks) { + NonAnimationRenderEvents.ModelOverride resolved = callback.resolve(entity, entityTypeId, currentModel, currentTexture); + if (resolved != null) { + return resolved; + } + } + + return null; + } + ); + public static final Event RESOLVE_RENDER = EventFactory.createArrayBacked( + NonAnimationRenderEvents.RenderResolver.class, callbacks -> (entity, entityTypeId, currentModel, currentTexture) -> { + for (NonAnimationRenderEvents.RenderResolver callback : callbacks) { + NonAnimationRenderEvents.RenderOverride resolved = callback.resolve(entity, entityTypeId, currentModel, currentTexture); + if (resolved != null) { + return resolved; + } + } + + return null; + } + ); + + private NonAnimationRenderEvents() { + } + + public record BoneItemProp(ItemStack stack, ItemDisplayContext displayContext) { + public BoneItemProp(ItemStack stack) { + this(stack, ItemDisplayContext.THIRD_PERSON_RIGHT_HAND); + } + + public BoneItemProp { + stack = stack == null ? ItemStack.EMPTY : stack.copy(); + displayContext = displayContext == null ? ItemDisplayContext.THIRD_PERSON_RIGHT_HAND : displayContext; + } + } + + public record ModelOverride(@Nullable Identifier model, @Nullable Identifier texture) { + } + + @FunctionalInterface + public interface ModelResolver { + @Nullable + NonAnimationRenderEvents.ModelOverride resolve(LivingEntity var1, @Nullable Identifier var2, Identifier var3, Identifier var4); + } + + public record RenderOverride( + @Nullable Identifier model, + @Nullable Identifier texture, + @Nullable List layerTextures, + @Nullable Map boneTextures, + @Nullable Map boneItems, + @Nullable Map boneVisibility, + @Nullable Map> hiddenBoneCubeIndices + ) { + public RenderOverride(@Nullable Identifier model, @Nullable Identifier texture) { + this(model, texture, null, null, null, null, null); + } + + public RenderOverride(@Nullable List layerTextures, @Nullable Map boneTextures) { + this(null, null, layerTextures, boneTextures, null, null, null); + } + + public RenderOverride( + @Nullable List layerTextures, + @Nullable Map boneTextures, + @Nullable Map boneItems + ) { + this(null, null, layerTextures, boneTextures, boneItems, null, null); + } + + public RenderOverride( + @Nullable Identifier model, + @Nullable Identifier texture, + @Nullable List layerTextures, + @Nullable Map boneTextures, + @Nullable Map boneItems, + @Nullable Map boneVisibility + ) { + this(model, texture, layerTextures, boneTextures, boneItems, boneVisibility, null); + } + } + + @FunctionalInterface + public interface RenderResolver { + @Nullable + NonAnimationRenderEvents.RenderOverride resolve(LivingEntity var1, @Nullable Identifier var2, Identifier var3, Identifier var4); + } +} diff --git a/src/main/java/com/nonid/api/client/NonAnimationSoundEvents.java b/src/main/java/com/nonid/api/client/NonAnimationSoundEvents.java new file mode 100644 index 0000000..8b396e0 --- /dev/null +++ b/src/main/java/com/nonid/api/client/NonAnimationSoundEvents.java @@ -0,0 +1,67 @@ +package com.nonid.api.client; + +import java.util.UUID; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationSoundEvents { + public static final Event RESOLVE = EventFactory.createArrayBacked( + NonAnimationSoundEvents.SoundResolver.class, callbacks -> context -> { + for (NonAnimationSoundEvents.SoundResolver callback : callbacks) { + NonAnimationSoundEvents.SoundOverride override = callback.resolve(context); + if (override != null) { + return override; + } + } + + return null; + } + ); + + private NonAnimationSoundEvents() { + } + + public record SoundContext( + LivingEntity anchor, + Identifier animationId, + Identifier animationResource, + String animationKey, + String effect, + double timeSeconds, + float volume, + float pitch, + UUID instanceId, + int stageIndex, + boolean loop, + double cueTimeTicks, + double cycleTicks, + double animationSpeed, + double nextSameEffectCueDelayTicks, + double nextSameEffectCueDelayMs + ) { + } + + public record SoundOverride(@Nullable Identifier soundId, float volume, float pitch, boolean cancel) { + public static NonAnimationSoundEvents.SoundOverride play(Identifier soundId, float volume, float pitch) { + return new NonAnimationSoundEvents.SoundOverride(soundId, volume, pitch, false); + } + + public static NonAnimationSoundEvents.SoundOverride play(Identifier soundId) { + return new NonAnimationSoundEvents.SoundOverride(soundId, 1.0F, 1.0F, false); + } + + public static NonAnimationSoundEvents.SoundOverride skip() { + return new NonAnimationSoundEvents.SoundOverride(null, 1.0F, 1.0F, true); + } + } + + @FunctionalInterface + public interface SoundResolver { + @Nullable + NonAnimationSoundEvents.SoundOverride resolve(NonAnimationSoundEvents.SoundContext var1); + } +} diff --git a/src/main/java/com/nonid/client/HorseLiquidCollectorGeoModel.java b/src/main/java/com/nonid/client/HorseLiquidCollectorGeoModel.java new file mode 100644 index 0000000..7ac890f --- /dev/null +++ b/src/main/java/com/nonid/client/HorseLiquidCollectorGeoModel.java @@ -0,0 +1,33 @@ +package com.nonid.client; + +import com.nonid.entity.HorseLiquidCollectorEntity; +import com.nonid.internal.animation.AnimationEngine; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.constant.dataticket.DataTicket; +import software.bernie.geckolib.model.GeoModel; +import software.bernie.geckolib.renderer.base.GeoRenderState; + +public class HorseLiquidCollectorGeoModel extends GeoModel { + public static final DataTicket<@NotNull Boolean> COLLECTOR_FULL_TICKET = DataTicket.create("non_horse_liquid_collector_full", Boolean.class); + public static final Identifier MODEL_ID = Identifier.of("needsofnature", "entity/horse_liquid_collector"); + public static final Identifier EMPTY_TEXTURE_ID = Identifier.of("needsofnature", "textures/entity/horse_liquid_collector/horse_liquid_collector_empty.png"); + public static final Identifier FULL_TEXTURE_ID = Identifier.of("needsofnature", "textures/entity/horse_liquid_collector/horse_liquid_collector_full.png"); + private static final Identifier PLACEHOLDER_ANIMATION_ID = AnimationEngine.packId("afw/placeholder"); + + @NotNull + public Identifier getModelResource(@NotNull GeoRenderState renderState) { + return MODEL_ID; + } + + @NotNull + public Identifier getTextureResource(GeoRenderState renderState) { + boolean full = (Boolean)renderState.getOrDefaultGeckolibData(COLLECTOR_FULL_TICKET, Boolean.FALSE); + return full ? FULL_TEXTURE_ID : EMPTY_TEXTURE_ID; + } + + @NotNull + public Identifier getAnimationResource(HorseLiquidCollectorEntity animatable) { + return PLACEHOLDER_ANIMATION_ID; + } +} diff --git a/src/main/java/com/nonid/client/HorseLiquidCollectorItemGeoModel.java b/src/main/java/com/nonid/client/HorseLiquidCollectorItemGeoModel.java new file mode 100644 index 0000000..a67ad8c --- /dev/null +++ b/src/main/java/com/nonid/client/HorseLiquidCollectorItemGeoModel.java @@ -0,0 +1,27 @@ +package com.nonid.client; + +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.item.HorseLiquidCollectorItem; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.model.GeoModel; +import software.bernie.geckolib.renderer.base.GeoRenderState; + +public class HorseLiquidCollectorItemGeoModel extends GeoModel { + private static final Identifier PLACEHOLDER_ANIMATION_ID = AnimationEngine.packId("afw/placeholder"); + + @NotNull + public Identifier getModelResource(@NotNull GeoRenderState renderState) { + return HorseLiquidCollectorGeoModel.MODEL_ID; + } + + @NotNull + public Identifier getTextureResource(@NotNull GeoRenderState renderState) { + return HorseLiquidCollectorGeoModel.EMPTY_TEXTURE_ID; + } + + @NotNull + public Identifier getAnimationResource(HorseLiquidCollectorItem animatable) { + return PLACEHOLDER_ANIMATION_ID; + } +} diff --git a/src/main/java/com/nonid/client/HorseLiquidCollectorItemRenderer.java b/src/main/java/com/nonid/client/HorseLiquidCollectorItemRenderer.java new file mode 100644 index 0000000..e0e5242 --- /dev/null +++ b/src/main/java/com/nonid/client/HorseLiquidCollectorItemRenderer.java @@ -0,0 +1,10 @@ +package com.nonid.client; + +import com.nonid.item.HorseLiquidCollectorItem; +import software.bernie.geckolib.renderer.GeoItemRenderer; + +public class HorseLiquidCollectorItemRenderer extends GeoItemRenderer { + public HorseLiquidCollectorItemRenderer() { + super(new HorseLiquidCollectorItemGeoModel()); + } +} diff --git a/src/main/java/com/nonid/client/HorseLiquidCollectorRenderer.java b/src/main/java/com/nonid/client/HorseLiquidCollectorRenderer.java new file mode 100644 index 0000000..2828b8f --- /dev/null +++ b/src/main/java/com/nonid/client/HorseLiquidCollectorRenderer.java @@ -0,0 +1,28 @@ +package com.nonid.client; + +import com.nonid.entity.HorseLiquidCollectorEntity; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.entity.EntityRendererFactory.Context; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.renderer.GeoEntityRenderer; +import software.bernie.geckolib.renderer.base.GeoRenderState; + +public class HorseLiquidCollectorRenderer extends GeoEntityRenderer { + public HorseLiquidCollectorRenderer(Context context) { + super(context, new HorseLiquidCollectorGeoModel()); + } + + public void addRenderData(HorseLiquidCollectorEntity animatable, Void relatedObject, R renderState, float partialTick) { + super.addRenderData(animatable, relatedObject, renderState, partialTick); + renderState.addGeckolibData(HorseLiquidCollectorGeoModel.COLLECTOR_FULL_TICKET, animatable.isCollectorFull()); + } + + public int getPackedOverlay(HorseLiquidCollectorEntity animatable, Void relatedObject, float u, float partialTick) { + return OverlayTexture.DEFAULT_UV; + } + + protected float getDeathMaxRotation(@NotNull GeoRenderState renderState) { + return 0.0F; + } +} diff --git a/src/main/java/com/nonid/client/NonAccessorySkinOverlays.java b/src/main/java/com/nonid/client/NonAccessorySkinOverlays.java new file mode 100644 index 0000000..09d25b3 --- /dev/null +++ b/src/main/java/com/nonid/client/NonAccessorySkinOverlays.java @@ -0,0 +1,14 @@ +package com.nonid.client; + +import com.nonid.NonTrinketsIntegration; +import java.util.List; +import net.minecraft.entity.LivingEntity; + +public final class NonAccessorySkinOverlays { + private NonAccessorySkinOverlays() { + } + + public static List resolveEquippedOverlays(LivingEntity entity) { + return NonTrinketsIntegration.resolveEquippedSkinOverlays(entity); + } +} diff --git a/src/main/java/com/nonid/client/NonAccessoryTooltips.java b/src/main/java/com/nonid/client/NonAccessoryTooltips.java new file mode 100644 index 0000000..2ba5f69 --- /dev/null +++ b/src/main/java/com/nonid/client/NonAccessoryTooltips.java @@ -0,0 +1,270 @@ +package com.nonid.client; + +import com.nonid.NonAccessoryEffects; +import com.nonid.NonTrinketsIntegration; +import com.nonid.data.NonAccessoryBehavior; +import com.nonid.data.NonAccessoryDefinitions; +import com.nonid.data.NonAccessoryItemRegistry; +import com.nonid.data.NonAccessoryStackData; +import com.nonid.data.NonLiquidRoles; +import java.util.List; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +public final class NonAccessoryTooltips { + private static final Text TANK_SCOPED_ICON = Text.literal("\ud83e\uddea "); + + private NonAccessoryTooltips() { + } + + public static void register() { + ItemTooltipCallback.EVENT.register((ItemTooltipCallback)(stack, context, type, lines) -> appendTooltip(stack, lines)); + } + + private static void appendTooltip(ItemStack stack, List lines) { + if (stack != null && !stack.isEmpty()) { + Identifier itemId = Registries.ITEM.getId(stack.getItem()); + if (NonAccessoryDefinitions.hasDefinition(itemId)) { + NonAccessoryEffects effects = NonAccessoryDefinitions.getEffects(itemId); + NonTrinketsIntegration.TankScopeStatus tankScope = resolveTankScopeStatus(stack); + int effectStartIndex = lines.size(); + boolean warnedInactiveTankSlot = false; + warnedInactiveTankSlot |= addLiquidDecayTooltip(lines, itemId, effects.liquidDecayMultiplier(), tankScope); + if (effects.equalizeLiquidDecayContext()) { + lines.add( + tankScopedText( + Text.translatable("item.needsofnature.accessory.tooltip.equalize_liquid_decay_context"), + itemId, + "equalize_liquid_decay_context", + true, + tankScope + ) + ); + warnedInactiveTankSlot |= tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED; + } + + addMultiplierTooltip(lines, itemId, "player_energy_gain", "player_energy_gain_multiplier", effects.playerEnergyGainMultiplier(), false); + warnedInactiveTankSlot |= addAdditiveTooltip(lines, itemId, "liquid_capacity", "liquid_capacity_add", effects.liquidCapacityAdd(), true, tankScope); + warnedInactiveTankSlot |= addMultiplierTooltip( + lines, itemId, "liquid_gain", "liquid_gain_multiplier", effects.liquidGainMultiplier(), true, tankScope + ); + warnedInactiveTankSlot |= addMultiplierTooltip( + lines, itemId, "filled_effect", "filled_effect_multiplier", effects.filledEffectMultiplier(), false, tankScope + ); + warnedInactiveTankSlot |= addMultiplierTooltip( + lines, itemId, "pregnancy_chance", "pregnancy_chance_multiplier", effects.pregnancyChanceMultiplier(), false, tankScope + ); + warnedInactiveTankSlot |= addMultiplierTooltip( + lines, itemId, "pregnancy_duration", "pregnancy_duration_multiplier", effects.pregnancyDurationMultiplier(), false, tankScope + ); + warnedInactiveTankSlot |= addMultiplierTooltip(lines, itemId, "mess_gain", "mess_gain_multiplier", effects.messGainMultiplier(), false, tankScope); + addMultiplierTooltip(lines, itemId, "destroyed_skin_damage", "destroyed_skin_damage_multiplier", effects.destroyedSkinDamageMultiplier(), false); + addAdditiveTooltip(lines, itemId, "attack_escape_hits", "attack_escape_hits_add", effects.attackEscapeHitsAdd(), false); + addMultiplierTooltip(lines, itemId, "attack_escape_damage", "attack_escape_damage_multiplier", effects.attackEscapeDamageMultiplier(), false); + addMultiplierTooltip(lines, itemId, "player_energy_aura", "player_energy_aura_multiplier", effects.playerEnergyAuraMultiplier(), false); + addMultiplierTooltip( + lines, itemId, "near_animation_mob_energy", "near_animation_mob_energy_multiplier", effects.nearAnimationMobEnergyMultiplier(), false + ); + addBehaviorTooltips(lines, itemId, stack); + if (warnedInactiveTankSlot) { + lines.add(effectStartIndex, Text.translatable("item.needsofnature.accessory.tooltip.inactive_tank_slot").formatted(Formatting.GRAY)); + } + } + } + } + + private static void addBehaviorTooltips(List lines, Identifier itemId, ItemStack stack) { + NonAccessoryBehavior behavior = NonAccessoryDefinitions.getBehavior(itemId); + if (!behavior.blocksInjectorTypes().isEmpty()) { + boolean blocksV = behavior.blocksInjectorTypes().contains(NonLiquidRoles.InjectorRole.V); + boolean blocksA = behavior.blocksInjectorTypes().contains(NonLiquidRoles.InjectorRole.A); + if (blocksV && blocksA) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_injector_av").formatted(Formatting.GREEN)); + } else if (blocksV) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_injector_v").formatted(Formatting.GREEN)); + } else if (blocksA) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_injector_a").formatted(Formatting.GREEN)); + } + } + + if (behavior.occupiesSlots().size() > 1) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.occupies_av").formatted(Formatting.GRAY)); + } + + if (behavior.ignoreInjectorSlotEffectSuppression()) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.ignore_injector_slot_effect_suppression").formatted(Formatting.GRAY)); + } + + if (behavior.blocksPregnancy()) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.blocks_pregnancy").formatted(Formatting.GREEN)); + } + + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(itemId); + if (definition != null && definition.actorVisualBehavior().substitutesMaleRole()) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.male_roles").formatted(Formatting.GREEN)); + int capacity = definition.actorVisualBehavior().storedLiquidCapacityMl(); + if (capacity <= 0) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.no_injected_liquid").formatted(Formatting.GRAY)); + } else { + NonAccessoryStackData.StoredLiquid stored = NonAccessoryStackData.getStoredLiquid(stack); + if (stored == null) { + lines.add(Text.translatable("item.needsofnature.accessory.tooltip.stored_liquid.empty", new Object[]{capacity}).formatted(Formatting.GRAY)); + } else { + lines.add( + Text.translatable( + "item.needsofnature.accessory.tooltip.stored_liquid.filled", new Object[]{stored.amountMl(), stored.entityTypeId().toString()} + ) + .formatted(Formatting.AQUA) + ); + } + } + } + } + + private static boolean addLiquidDecayTooltip(List lines, Identifier itemId, double value, NonTrinketsIntegration.TankScopeStatus tankScope) { + if (isNeutral(value)) { + return false; + } + + boolean beneficial = value < 1.0; + String key = beneficial ? "item.needsofnature.accessory.tooltip.liquid_decay_reduced" : "item.needsofnature.accessory.tooltip.liquid_decay_increased"; + lines.add(tankScopedText(Text.translatable(key, new Object[]{signedPercentDelta(value)}), itemId, "liquid_decay_multiplier", beneficial, tankScope)); + return tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED; + } + + private static void addMultiplierTooltip(List lines, Identifier itemId, String keyPart, String effectKey, double value, boolean higherIsBeneficial) { + if (!isNeutral(value)) { + boolean increased = value > 1.0; + boolean beneficial = increased == higherIsBeneficial; + String direction = increased ? "increased" : "reduced"; + lines.add( + styledText( + Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedPercentDelta(value)}), + itemId, + effectKey, + beneficial + ) + ); + } + } + + private static boolean addMultiplierTooltip( + List lines, + Identifier itemId, + String keyPart, + String effectKey, + double value, + boolean higherIsBeneficial, + NonTrinketsIntegration.TankScopeStatus tankScope + ) { + if (isNeutral(value)) { + return false; + } + + boolean increased = value > 1.0; + boolean beneficial = increased == higherIsBeneficial; + String direction = increased ? "increased" : "reduced"; + lines.add( + tankScopedText( + Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedPercentDelta(value)}), + itemId, + effectKey, + beneficial, + tankScope + ) + ); + return tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED; + } + + private static void addAdditiveTooltip(List lines, Identifier itemId, String keyPart, String effectKey, int value, boolean higherIsBeneficial) { + if (value != 0) { + boolean increased = value > 0; + boolean beneficial = increased == higherIsBeneficial; + String direction = increased ? "increased" : "reduced"; + lines.add( + styledText( + Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedInt(value)}), + itemId, + effectKey, + beneficial + ) + ); + } + } + + private static boolean addAdditiveTooltip( + List lines, + Identifier itemId, + String keyPart, + String effectKey, + int value, + boolean higherIsBeneficial, + NonTrinketsIntegration.TankScopeStatus tankScope + ) { + if (value == 0) { + return false; + } + + boolean increased = value > 0; + boolean beneficial = increased == higherIsBeneficial; + String direction = increased ? "increased" : "reduced"; + lines.add( + tankScopedText( + Text.translatable("item.needsofnature.accessory.tooltip." + keyPart + "." + direction, new Object[]{signedInt(value)}), + itemId, + effectKey, + beneficial, + tankScope + ) + ); + return tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED; + } + + private static Text tankScopedText(Text body, Identifier itemId, String effectKey, boolean beneficial, NonTrinketsIntegration.TankScopeStatus tankScope) { + MutableText text = Text.literal("").append(TANK_SCOPED_ICON).append(body); + return (Text)(tankScope == NonTrinketsIntegration.TankScopeStatus.INACTIVE_EQUIPPED + ? text.formatted(Formatting.GRAY) + : applyColor(text, NonAccessoryDefinitions.getTooltipColor(itemId, effectKey), beneficial)); + } + + private static Text styledText(Text body, Identifier itemId, String effectKey, boolean beneficial) { + return applyColor(body.copy(), NonAccessoryDefinitions.getTooltipColor(itemId, effectKey), beneficial); + } + + private static Text applyColor(MutableText text, NonAccessoryDefinitions.TooltipColor color, boolean beneficial) { + if (color == null) { + return text.formatted(beneficial ? Formatting.GREEN : Formatting.RED); + } else if (color.formatting() != null) { + return text.formatted(color.formatting()); + } else { + return color.rgb() != null ? text.setStyle(text.getStyle().withColor(color.rgb())) : text.formatted(beneficial ? Formatting.GREEN : Formatting.RED); + } + } + + private static boolean isNeutral(double value) { + return Math.abs(value - 1.0) < 1.0E-6; + } + + private static String signedPercentDelta(double value) { + int percent = (int)Math.round(Math.abs(value - 1.0) * 100.0); + String sign = value >= 1.0 ? "+" : "-"; + return sign + percent + "%"; + } + + private static String signedInt(int value) { + return (value > 0 ? "+" : "") + value; + } + + private static NonTrinketsIntegration.TankScopeStatus resolveTankScopeStatus(ItemStack stack) { + MinecraftClient client = MinecraftClient.getInstance(); + ClientPlayerEntity player = client == null ? null : client.player; + return NonTrinketsIntegration.resolveTankScopeStatus(player, stack); + } +} diff --git a/src/main/java/com/nonid/client/NonAnimationPreviewRenderer.java b/src/main/java/com/nonid/client/NonAnimationPreviewRenderer.java new file mode 100644 index 0000000..1aed578 --- /dev/null +++ b/src/main/java/com/nonid/client/NonAnimationPreviewRenderer.java @@ -0,0 +1,94 @@ +package com.nonid.client; + +import com.mojang.blaze3d.textures.GpuTextureView; +import com.nonid.internal.animation.AnimationEngine; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +public final class NonAnimationPreviewRenderer { + private static final Map> TEXTURE_CACHE = new HashMap<>(); + + private NonAnimationPreviewRenderer() { + } + + public static void draw(DrawContext context, TextRenderer textRenderer, String animationId, int x, int y, int size) { + Optional preview = previewTexture(animationId); + if (preview.isPresent()) { + NonAnimationPreviewRenderer.PreviewTexture texture = preview.get(); + int textureWidth = texture.width(); + int textureHeight = texture.height(); + int drawWidth = size; + int drawHeight = size; + if (textureWidth > textureHeight) { + drawHeight = Math.max(1, Math.round(size * ((float)textureHeight / textureWidth))); + } else if (textureHeight > textureWidth) { + drawWidth = Math.max(1, Math.round(size * ((float)textureWidth / textureHeight))); + } + + int drawX = x + (size - drawWidth) / 2; + int drawY = y + (size - drawHeight) / 2; + context.fill(x, y, x + size, y + size, -13948117); + context.drawTexture( + RenderPipelines.GUI_TEXTURED, + texture.id(), + drawX, + drawY, + 0.0F, + 0.0F, + drawWidth, + drawHeight, + textureWidth, + textureHeight, + textureWidth, + textureHeight + ); + } else { + context.fill(x, y, x + size, y + size, -13948117); + context.drawCenteredTextWithShadow(textRenderer, "?", x + size / 2, y + size / 2 - 4, -5592406); + } + } + + public static void clearCache() { + TEXTURE_CACHE.clear(); + } + + private static Optional previewTexture(String animationId) { + return TEXTURE_CACHE.computeIfAbsent(animationId, NonAnimationPreviewRenderer::resolvePreviewTexture); + } + + private static Optional resolvePreviewTexture(String animationId) { + Identifier id = Identifier.tryParse(animationId); + if (id == null) { + return Optional.empty(); + } + + String path = "textures/afw/previews/" + id.getPath() + ".png"; + Identifier namespaced = Identifier.of(id.getNamespace(), path); + MinecraftClient client = MinecraftClient.getInstance(); + Identifier preview = null; + if (client.getResourceManager().getResource(namespaced).isPresent()) { + preview = namespaced; + } else { + Identifier fallback = AnimationEngine.packId(path); + if (client.getResourceManager().getResource(fallback).isPresent()) { + preview = fallback; + } + } + + if (preview == null) { + return Optional.empty(); + } + + GpuTextureView textureView = client.getTextureManager().getTexture(preview).getGlTextureView(); + return Optional.of(new NonAnimationPreviewRenderer.PreviewTexture(preview, Math.max(1, textureView.getWidth(0)), Math.max(1, textureView.getHeight(0)))); + } + + private record PreviewTexture(Identifier id, int width, int height) { + } +} diff --git a/src/main/java/com/nonid/client/NonArmorShedClient.java b/src/main/java/com/nonid/client/NonArmorShedClient.java new file mode 100644 index 0000000..713647e --- /dev/null +++ b/src/main/java/com/nonid/client/NonArmorShedClient.java @@ -0,0 +1,307 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import com.nonid.NonAccessoryShedSystem; +import com.nonid.NonTrinketsIntegration; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.world.WorldRenderEvents; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.entity.ItemEntityRenderer; +import net.minecraft.client.render.entity.state.ItemStackEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.ItemEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RotationAxis; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Direction.Axis; +import net.minecraft.util.shape.VoxelShape; + +public final class NonArmorShedClient { + private static final EquipmentSlot[] ARMOR_SLOTS = new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET}; + private static final int FLYING_TICKS = 24; + private static final int GROUND_TICKS = 40; + private static final int GROUND_FADE_TICKS = 30; + private static final double GRAVITY = 0.035; + private static final double AIR_DRAG = 0.96; + private static final double GROUND_DRAG = 0.78; + private static final float ITEM_SCALE = 1.0F; + private static final long FRESH_START_MAX_AGE_TICKS = 5L; + private static final long QUEUED_HANDOFF_SKIP_TICKS = 12L; + private static final Identifier PLAYER_DEFEATED = Identifier.of("needsofnature", "player_defeated-belly"); + private static final Identifier PLAYER_BIRTH = Identifier.of("needsofnature", "player_birth"); + private static final List PROPS = new ArrayList<>(); + private static final Map LAST_SPAWNED_START_TICK = new HashMap<>(); + private static final Map LAST_ACTIVE_TICK = new HashMap<>(); + + private NonArmorShedClient() { + } + + public static void register() { + WorldRenderEvents.AFTER_ENTITIES.register(NonArmorShedClient::render); + } + + public static void clientTick(MinecraftClient client) { + if (!NeedsOfNature.getConfig().isArmorShedEffectEnabled()) { + clear(); + } else { + tickProps(client); + if (client != null && client.world != null) { + long now = client.world.getTime(); + + for (AbstractClientPlayerEntity player : client.world.getPlayers()) { + if (player != null && !player.isRemoved()) { + UUID uuid = player.getUuid(); + if (!ClientAnimationRuntime.isActorActive(uuid)) { + LAST_SPAWNED_START_TICK.remove(uuid); + } else { + Long previousActiveTick = LAST_ACTIVE_TICK.get(uuid); + Long startTick = ClientAnimationRuntime.findStartTickForActor(uuid); + if (startTick == null) { + LAST_ACTIVE_TICK.put(uuid, now); + } else { + Long lastSpawned = LAST_SPAWNED_START_TICK.get(uuid); + if (lastSpawned != null && lastSpawned.equals(startTick)) { + LAST_ACTIVE_TICK.put(uuid, now); + } else { + if (shouldSpawnForStart(uuid, now, startTick, previousActiveTick)) { + spawnForPlayer(player, startTick); + } + + LAST_SPAWNED_START_TICK.put(uuid, startTick); + LAST_ACTIVE_TICK.put(uuid, now); + } + } + } + } + } + } else { + clear(); + } + } + } + + public static void clear() { + PROPS.clear(); + LAST_SPAWNED_START_TICK.clear(); + LAST_ACTIVE_TICK.clear(); + } + + private static boolean shouldSpawnForStart(UUID playerUuid, long now, long startTick, Long previousActiveTick) { + if (now - startTick > 5L) { + return false; + } + + if (previousActiveTick != null && now - previousActiveTick <= 12L) { + return false; + } + + Identifier activeAnimation = ClientAnimationRuntime.findLatestActiveAnimationIdContaining(playerUuid); + return !isQueuedFollowUpAnimation(activeAnimation); + } + + private static boolean isQueuedFollowUpAnimation(Identifier animationId) { + return animationId == null ? false : PLAYER_DEFEATED.equals(animationId) || PLAYER_BIRTH.equals(animationId); + } + + private static void spawnForPlayer(AbstractClientPlayerEntity player, long startTick) { + Vec3d origin = new Vec3d(player.getX(), player.getY() + player.getHeight() * 0.72, player.getZ()); + long seedBase = player.getUuid().getMostSignificantBits() ^ player.getUuid().getLeastSignificantBits() ^ startTick; + Random random = new Random(seedBase); + float yawRad = player.getYaw() * (float) (Math.PI / 180.0); + Vec3d forward = new Vec3d(-MathHelper.sin(yawRad), 0.0, MathHelper.cos(yawRad)); + Vec3d right = new Vec3d(forward.z, 0.0, -forward.x); + + for (int i = 0; i < ARMOR_SLOTS.length; i++) { + EquipmentSlot slot = ARMOR_SLOTS[i]; + ItemStack stack = player.getEquippedStack(slot); + if (stack != null && !stack.isEmpty()) { + double side = (i - 1.5) * 0.18; + Vec3d spawnPos = origin.add(right.multiply(side)).add(0.0, -0.08 * i, 0.0); + addProp(stack, spawnPos, forward, right, random, seedBase, i, ARMOR_SLOTS.length); + } + } + + spawnShedAccessories(player, origin, forward, right, random, seedBase); + } + + private static void spawnShedAccessories(AbstractClientPlayerEntity player, Vec3d origin, Vec3d forward, Vec3d right, Random random, long seedBase) { + NonAccessoryShedSystem.ShedState shedState = NonAccessoryShedSystem.getShedState(player.getUuid()); + if (!shedState.isEmpty()) { + List stacks = NonTrinketsIntegration.getVisuallyShedAccessoryStacks(player); + + for (int index = 0; index < stacks.size(); index++) { + ItemStack stack = stacks.get(index); + if (stack != null && !stack.isEmpty()) { + double side = (index - 0.5) * 0.24; + Vec3d spawnPos = origin.add(right.multiply(side)).add(0.0, -0.34, 0.0); + addProp(stack, spawnPos, forward, right, random, seedBase, 20 + index, 2); + } + } + } + } + + private static void addProp(ItemStack stack, Vec3d spawnPos, Vec3d forward, Vec3d right, Random random, long seedBase, int index, int count) { + ItemStack visualStack = stack.copy(); + visualStack.setCount(1); + double angle = (Math.PI * 2) * index / Math.max(1, count) + (random.nextDouble() - 0.5) * 0.55; + Vec3d outward = right.multiply(Math.cos(angle)).add(forward.multiply(Math.sin(angle))).normalize(); + double speed = 0.105 + random.nextDouble() * 0.035; + Vec3d velocity = outward.multiply(speed).add(0.0, 0.15 + random.nextDouble() * 0.13, 0.0); + PROPS.add( + new NonArmorShedClient.ArmorProp( + visualStack, + spawnPos, + velocity, + random.nextFloat() * 360.0F, + random.nextFloat() * 360.0F, + 7.0F + random.nextFloat() * 7.0F, + 10.0F + random.nextFloat() * 12.0F, + (int)(seedBase + index * 31L) + ) + ); + } + + private static void tickProps(MinecraftClient client) { + if (!PROPS.isEmpty()) { + if (client != null && client.world != null) { + Iterator iterator = PROPS.iterator(); + + while (iterator.hasNext()) { + NonArmorShedClient.ArmorProp prop = iterator.next(); + prop.age++; + prop.previousPosition = prop.position; + if (prop.landed) { + prop.groundAge++; + prop.velocity = prop.velocity.multiply(0.78, 0.0, 0.78); + if (prop.groundAge > 40) { + iterator.remove(); + } + } else { + prop.velocity = prop.velocity.add(0.0, -0.035, 0.0).multiply(0.96, 0.98, 0.96); + Vec3d next = prop.position.add(prop.velocity); + NonArmorShedClient.GroundHit hit = findGround(client, next); + if (hit.hit() && prop.velocity.y <= 0.0 && prop.age > 3) { + prop.position = new Vec3d(next.x, hit.y(), next.z); + prop.velocity = Vec3d.ZERO; + prop.landed = true; + } else { + prop.position = next; + if (prop.age > 64) { + iterator.remove(); + } + } + } + } + } else { + clear(); + } + } + } + + private static NonArmorShedClient.GroundHit findGround(MinecraftClient client, Vec3d pos) { + if (client != null && client.world != null) { + BlockPos blockPos = BlockPos.ofFloored(pos.x, pos.y - 0.04, pos.z); + BlockState state = client.world.getBlockState(blockPos); + VoxelShape shape = state.getCollisionShape(client.world, blockPos); + if (shape.isEmpty()) { + return NonArmorShedClient.GroundHit.MISS; + } + + double top = blockPos.getY() + shape.getMax(Axis.Y); + return pos.y < top + 0.12 ? new NonArmorShedClient.GroundHit(true, top + 0.03) : NonArmorShedClient.GroundHit.MISS; + } else { + return NonArmorShedClient.GroundHit.MISS; + } + } + + private static void render(WorldRenderContext context) { + if (NeedsOfNature.getConfig().isArmorShedEffectEnabled()) { + if (!PROPS.isEmpty()) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null) { + Vec3d cameraPos = context.worldState().cameraRenderState.pos; + if (cameraPos != null) { + float tickProgress = client.getRenderTickCounter().getTickProgress(false); + MatrixStack matrices = context.matrices(); + + for (NonArmorShedClient.ArmorProp prop : PROPS) { + Vec3d renderPos = prop.previousPosition.lerp(prop.position, tickProgress); + matrices.push(); + matrices.translate(renderPos.x - cameraPos.x, renderPos.y - cameraPos.y, renderPos.z - cameraPos.z); + float smoothAge = prop.age + tickProgress; + float yaw = prop.landed ? prop.groundYaw : prop.baseYaw + smoothAge * prop.yawSpeed; + float pitch = prop.landed ? 90.0F : prop.basePitch + smoothAge * prop.pitchSpeed; + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw)); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch)); + float fadeScale = prop.landed ? prop.groundFadeScale(tickProgress) : 1.0F; + matrices.scale(1.0F * fadeScale, 1.0F * fadeScale, 1.0F * fadeScale); + ItemStackEntityRenderState state = new ItemStackEntityRenderState(); + ItemEntity fake = new ItemEntity(client.world, prop.position.x, prop.position.y, prop.position.z, prop.stack); + state.update(fake, prop.stack, client.getItemModelManager()); + ItemEntityRenderer.renderStack(matrices, context.commandQueue(), 15728880, state, net.minecraft.util.math.random.Random.create(prop.seed)); + matrices.pop(); + } + } + } + } + } + } + + private static final class ArmorProp { + final ItemStack stack; + Vec3d position; + Vec3d previousPosition; + Vec3d velocity; + final float baseYaw; + final float basePitch; + final float yawSpeed; + final float pitchSpeed; + final float groundYaw; + final int seed; + int age; + int groundAge; + boolean landed; + + ArmorProp(ItemStack stack, Vec3d position, Vec3d velocity, float baseYaw, float basePitch, float yawSpeed, float pitchSpeed, int seed) { + this.stack = stack; + this.position = position; + this.previousPosition = position; + this.velocity = velocity; + this.baseYaw = baseYaw; + this.basePitch = basePitch; + this.yawSpeed = yawSpeed; + this.pitchSpeed = pitchSpeed; + this.groundYaw = baseYaw; + this.seed = seed; + } + + float groundFadeScale(float tickProgress) { + int fadeStart = Math.max(0, 10); + float smoothGroundAge = this.groundAge + tickProgress; + if (smoothGroundAge <= fadeStart) { + return 1.0F; + } + + float remaining = (40.0F - smoothGroundAge) / 30.0F; + return MathHelper.clamp(remaining, 0.0F, 1.0F); + } + } + + private record GroundHit(boolean hit, double y) { + private static final NonArmorShedClient.GroundHit MISS = new NonArmorShedClient.GroundHit(false, 0.0); + } +} diff --git a/src/main/java/com/nonid/client/NonClientDiagnostics.java b/src/main/java/com/nonid/client/NonClientDiagnostics.java new file mode 100644 index 0000000..23bfbae --- /dev/null +++ b/src/main/java/com/nonid/client/NonClientDiagnostics.java @@ -0,0 +1,32 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import com.nonid.NonDebugChatCategory; +import com.nonid.NonDiagnostics; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; + +public final class NonClientDiagnostics { + private NonClientDiagnostics() { + } + + public static boolean shouldSend(NonDebugChatCategory category) { + return NeedsOfNature.getConfig().allowsDebugChat(category); + } + + public static void send(NonDebugChatCategory category, Text message, boolean withPrefix) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null && message != null) { + if (shouldSend(category)) { + Text output = (Text)(withPrefix ? Text.translatable("debug.needsofnature.prefix", new Object[]{message}) : message); + client.player.sendMessage(output.copy().formatted(NonDiagnostics.chatColor(category)), false); + } + } + } + + public static void sendSetupWarning(String message, boolean withPrefix) { + if (message != null && !message.isBlank()) { + send(NonDebugChatCategory.SETUP, Text.literal(message), withPrefix); + } + } +} diff --git a/src/main/java/com/nonid/client/NonClientResourceCaches.java b/src/main/java/com/nonid/client/NonClientResourceCaches.java new file mode 100644 index 0000000..8abeeb4 --- /dev/null +++ b/src/main/java/com/nonid/client/NonClientResourceCaches.java @@ -0,0 +1,30 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNatureClient; +import com.nonid.internal.animation.client.render.gecko.AnimationActorAnimatable; +import com.nonid.internal.animation.client.render.gecko.GeckoResourceResolver; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; + +public final class NonClientResourceCaches { + private static final Identifier RELOAD_LISTENER_ID = Identifier.of("needsofnature", "client_resource_caches"); + + private NonClientResourceCaches() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.CLIENT_RESOURCES).registerReloader(RELOAD_LISTENER_ID, (SynchronousResourceReloader)manager -> clear()); + } + + private static void clear() { + NeedsOfNatureClient.clearGeneratedTintTextures(); + NonDestroyedSkinClient.clearResourceCaches(); + NonAnimationPreviewRenderer.clearCache(); + PregnancyEggGeoModel.clearTextureCache(); + NonStatLabelResolver.clearCache(); + AnimationActorAnimatable.clearResourceCaches(); + GeckoResourceResolver.clearResourceCaches(); + } +} diff --git a/src/main/java/com/nonid/client/NonDebugNameHelper.java b/src/main/java/com/nonid/client/NonDebugNameHelper.java new file mode 100644 index 0000000..3801f34 --- /dev/null +++ b/src/main/java/com/nonid/client/NonDebugNameHelper.java @@ -0,0 +1,83 @@ +package com.nonid.client; + +import com.nonid.EnergyHolder; +import com.nonid.GenderHolder; +import com.nonid.NeedsOfNature; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; + +public final class NonDebugNameHelper { + private NonDebugNameHelper() { + } + + public static boolean shouldShowDebugLabel(Entity entity) { + if (!(entity instanceof MobEntity)) { + return false; + } + + if (!(entity instanceof EnergyHolder)) { + return false; + } + + MinecraftClient client = MinecraftClient.getInstance(); + PlayerEntity player = client.player; + if (player == null) { + return false; + } + + ItemStack main = player.getMainHandStack(); + ItemStack off = player.getOffHandStack(); + return main.getItem() == NeedsOfNature.DEBUG_STAFF || off.getItem() == NeedsOfNature.DEBUG_STAFF; + } + + public static String resolveGender(Entity entity) { + if (entity instanceof GenderHolder holder) { + boolean male = holder.hasGender(1); + boolean female = holder.hasGender(2); + if (male && female) { + return "male & female"; + } else if (male) { + return "male"; + } else { + return female ? "female" : "none"; + } + } else { + boolean hasMale = entity.getCommandTags().contains("gender.male"); + boolean hasFemale = entity.getCommandTags().contains("gender.female"); + if (hasMale && hasFemale) { + return "male & female"; + } else if (hasMale) { + return "male"; + } else { + return hasFemale ? "female" : "none"; + } + } + } + + public static String resolveGenderSymbol(Entity entity) { + if (entity instanceof GenderHolder holder) { + boolean male = holder.hasGender(1); + boolean female = holder.hasGender(2); + if (male && female) { + return "♂♀"; + } else if (male) { + return "♂"; + } else { + return female ? "♀" : ""; + } + } else { + boolean hasMale = entity.getCommandTags().contains("gender.male"); + boolean hasFemale = entity.getCommandTags().contains("gender.female"); + if (hasMale && hasFemale) { + return "♂♀"; + } else if (hasMale) { + return "♂"; + } else { + return hasFemale ? "♀" : ""; + } + } + } +} diff --git a/src/main/java/com/nonid/client/NonDebugSpinMode.java b/src/main/java/com/nonid/client/NonDebugSpinMode.java new file mode 100644 index 0000000..1c871f6 --- /dev/null +++ b/src/main/java/com/nonid/client/NonDebugSpinMode.java @@ -0,0 +1,19 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import net.minecraft.util.Identifier; + +public final class NonDebugSpinMode { + public static final Identifier DEBUG_SPIN_ANIMATION_RESOURCE = Identifier.of("needsofnature", "afw/debug_spin/debug_spin"); + public static final Identifier DEBUG_SPIN_MODEL_RESOURCE = Identifier.of("needsofnature", "fish"); + public static final Identifier DEBUG_SPIN_TEXTURE_RESOURCE = Identifier.ofVanilla("textures/entity/fish/salmon.png"); + + private NonDebugSpinMode() { + } + + public static boolean isEnabled() { + NonConfig config = NeedsOfNature.getConfig(); + return config != null && config.isDebugSpinMode(); + } +} diff --git a/src/main/java/com/nonid/client/NonDebugSpinMusicClient.java b/src/main/java/com/nonid/client/NonDebugSpinMusicClient.java new file mode 100644 index 0000000..519c6b9 --- /dev/null +++ b/src/main/java/com/nonid/client/NonDebugSpinMusicClient.java @@ -0,0 +1,171 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.MovingSoundInstance; +import net.minecraft.client.sound.SoundInstance; +import net.minecraft.client.sound.SoundInstance.AttenuationType; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.Registries; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; + +public final class NonDebugSpinMusicClient { + private static final Identifier SOUND_ID = NeedsOfNature.id("funkytown"); + private static final float VOLUME = 0.025F; + private static final float PITCH = 1.0F; + private static final Map ACTIVE = new HashMap<>(); + + private NonDebugSpinMusicClient() { + } + + public static void clientTick(MinecraftClient client) { + if (client != null) { + if (NonDebugSpinMode.isEnabled() && client.world != null) { + pruneStoppedSounds(client); + Map anchors = findActiveInstanceAnchors(client.world); + stopInactiveSounds(client, anchors.keySet()); + + for (Entry entry : anchors.entrySet()) { + playForInstance(client, entry.getKey(), entry.getValue()); + } + } else { + stopAll(client); + } + } + } + + public static void stopAll(MinecraftClient client) { + if (!ACTIVE.isEmpty()) { + for (NonDebugSpinMusicClient.SpinMusicSound sound : ACTIVE.values()) { + sound.finish(); + if (client != null) { + client.getSoundManager().stop(sound); + } + } + + ACTIVE.clear(); + } + } + + private static Map findActiveInstanceAnchors(ClientWorld world) { + if (world == null) { + return Map.of(); + } + + Map anchors = new HashMap<>(); + + for (Entity entity : world.getEntities()) { + if (entity instanceof LivingEntity living) { + UUID actorUuid = living.getUuid(); + UUID instanceId = ClientAnimationRuntime.findLatestActiveInstanceContaining(actorUuid); + if (instanceId != null && !anchors.containsKey(instanceId)) { + UUID anchorUuid = ClientAnimationRuntime.findAnchorUuidForActor(actorUuid); + anchors.put(instanceId, anchorUuid != null ? anchorUuid : actorUuid); + } + } + } + + return anchors.isEmpty() ? Map.of() : Map.copyOf(anchors); + } + + private static void stopInactiveSounds(MinecraftClient client, Set activeInstances) { + Iterator> iterator = ACTIVE.entrySet().iterator(); + + while (iterator.hasNext()) { + Entry entry = iterator.next(); + if (!activeInstances.contains(entry.getKey())) { + NonDebugSpinMusicClient.SpinMusicSound sound = entry.getValue(); + if (sound != null) { + sound.finish(); + client.getSoundManager().stop(sound); + } + + iterator.remove(); + } + } + } + + private static void pruneStoppedSounds(MinecraftClient client) { + Iterator> iterator = ACTIVE.entrySet().iterator(); + + while (iterator.hasNext()) { + NonDebugSpinMusicClient.SpinMusicSound sound = iterator.next().getValue(); + if (sound == null || sound.isDone() || client != null && !client.getSoundManager().isPlaying(sound)) { + iterator.remove(); + } + } + } + + private static void playForInstance(MinecraftClient client, UUID instanceId, UUID anchorUuid) { + if (client != null && client.world != null && instanceId != null && anchorUuid != null) { + NonDebugSpinMusicClient.SpinMusicSound existing = ACTIVE.get(instanceId); + if (existing == null || existing.isDone()) { + Entity anchor = client.world.getEntity(anchorUuid); + if (anchor != null) { + NonDebugSpinMusicClient.SpinMusicSound sound = new NonDebugSpinMusicClient.SpinMusicSound( + instanceId, anchorUuid, anchor instanceof PlayerEntity ? SoundCategory.PLAYERS : SoundCategory.NEUTRAL, 0.025F, 1.0F + ); + ACTIVE.put(instanceId, sound); + client.getSoundManager().play(sound); + } + } + } + } + + private static final class SpinMusicSound extends MovingSoundInstance { + private final UUID instanceId; + private final UUID anchorUuid; + + private SpinMusicSound(UUID instanceId, UUID anchorUuid, SoundCategory category, float volume, float pitch) { + super((SoundEvent)Registries.SOUND_EVENT.get(NonDebugSpinMusicClient.SOUND_ID), category, SoundInstance.createRandom()); + this.instanceId = instanceId; + this.anchorUuid = anchorUuid; + this.volume = volume; + this.pitch = pitch; + this.repeat = true; + this.repeatDelay = 0; + this.relative = false; + this.attenuationType = AttenuationType.LINEAR; + this.updatePosition(); + } + + public void tick() { + if (!NonDebugSpinMode.isEnabled() || ClientAnimationRuntime.findCurrentStage(this.instanceId) == null || !this.updatePosition()) { + this.setDone(); + } + } + + private boolean updatePosition() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null) { + Entity anchor = client.world.getEntity(this.anchorUuid); + if (anchor == null) { + return false; + } + + this.x = anchor.getX(); + this.y = anchor.getY(); + this.z = anchor.getZ(); + return true; + } else { + return false; + } + } + + private void finish() { + this.setDone(); + } + } +} diff --git a/src/main/java/com/nonid/client/NonDestroyedSkinClient.java b/src/main/java/com/nonid/client/NonDestroyedSkinClient.java new file mode 100644 index 0000000..709d5c7 --- /dev/null +++ b/src/main/java/com/nonid/client/NonDestroyedSkinClient.java @@ -0,0 +1,2343 @@ +package com.nonid.client; + +import com.nonid.DestroyedSkinHolder; +import com.nonid.GenderHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NeedsOfNatureClient; +import com.nonid.NonConfig; +import com.nonid.NonTrinketsIntegration; +import com.nonid.client.particle.RippedFabricParticle; +import com.nonid.network.DestroyedSkinMaskUploadC2SPayload; +import com.nonid.network.DestroyedSkinUploadC2SPayload; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.client.texture.NativeImage.Format; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerSkinType; +import net.minecraft.resource.Resource; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.random.Random; +import org.jetbrains.annotations.Nullable; + +public final class NonDestroyedSkinClient { + private static final int MIN_STAGE = 0; + private static final int MAX_STAGE = 4; + private static final int EXTERNAL_SKIN_SELECTION_POLL_TICKS = 20; + private static final long TEXTURE_READ_RETRY_NANOS = 1000000000L; + private static final int FALLBACK_FABRIC_RGB = 15920872; + private static final int FALLBACK_SKIN_TINT_RGB = 13803641; + private static final Identifier RIPPED_FABRIC_COLOR_MASK = Identifier.of("needsofnature", "textures/particle/ripped_fabric_color_mask.png"); + private static final Identifier DEFAULT_DESTROYED_TEXTURE_F = Identifier.of("needsofnature", "textures/player_destroyed/destroyed_texture_f.png"); + private static final Identifier DEFAULT_DESTROYED_TEXTURE_M = Identifier.of("needsofnature", "textures/player_destroyed/destroyed_texture_m.png"); + private static final Identifier DEFAULT_DESTROYED_TEXTURE_F_SLIM = Identifier.of("needsofnature", "textures/player_destroyed/destroyed_texture_f_slim.png"); + private static final Identifier DEFAULT_DESTROYED_TEXTURE_F_WIDE = Identifier.of("needsofnature", "textures/player_destroyed/destroyed_texture_f_wide.png"); + private static final Identifier DEFAULT_DESTROYED_TEXTURE_M_SLIM = Identifier.of("needsofnature", "textures/player_destroyed/destroyed_texture_m_slim.png"); + private static final Identifier DEFAULT_DESTROYED_TEXTURE_M_WIDE = Identifier.of("needsofnature", "textures/player_destroyed/destroyed_texture_m_wide.png"); + private static final String DEFAULT_PLAYER_SKIN_PREFIX = "textures/entity/player/"; + private static final String DEFAULT_DESTROYED_SKIN_PREFIX = "textures/player_destroyed/default/"; + private static final String[] VANILLA_DEFAULT_SKIN_NAMES = new String[]{"steve", "alex", "ari", "efe", "kai", "makena", "noor", "sunny", "zuri"}; + private static final double DEFAULT_SKIN_PIXEL_MATCH_THRESHOLD = 0.9; + private static final Identifier[] MASK_TEXTURES = new Identifier[]{ + null, + Identifier.of("needsofnature", "textures/player_destroyed/mask_1.png"), + Identifier.of("needsofnature", "textures/player_destroyed/mask_2.png"), + Identifier.of("needsofnature", "textures/player_destroyed/mask_3.png") + }; + private static final Identifier[][] MESS_MASK_TEXTURES = new Identifier[][]{ + { + Identifier.of("needsofnature", "textures/entity/player/mess/v_mess1.png"), + Identifier.of("needsofnature", "textures/entity/player/mess/v_mess2.png"), + Identifier.of("needsofnature", "textures/entity/player/mess/v_mess3.png") + }, + { + Identifier.of("needsofnature", "textures/entity/player/mess/a_mess1.png"), + Identifier.of("needsofnature", "textures/entity/player/mess/a_mess2.png"), + Identifier.of("needsofnature", "textures/entity/player/mess/a_mess3.png") + }, + { + Identifier.of("needsofnature", "textures/entity/player/mess/m_mess1.png"), + Identifier.of("needsofnature", "textures/entity/player/mess/m_mess2.png"), + Identifier.of("needsofnature", "textures/entity/player/mess/m_mess3.png") + } + }; + private static final Identifier V_MESS_TANK_TEXTURE = Identifier.of("needsofnature", "textures/entity/player/mess/v_mess_tank.png"); + private static final Identifier A_MESS_TANK_TEXTURE = Identifier.of("needsofnature", "textures/entity/player/mess/a_mess_tank.png"); + private static final Map DESTROYED_SKINS = new HashMap<>(); + private static final Map DESTROYED_SKIN_MASKS = new HashMap<>(); + private static final Map STAGES = new HashMap<>(); + private static final Map MESS_STATES = new HashMap<>(); + private static final Map TEXTURE_CACHE = new HashMap<>(); + private static final Map ORIGINAL_SKIN_COLOR_CACHE = new HashMap<>(); + private static final Map EFFECTIVE_BASE_TEXTURES = new HashMap<>(); + private static final Map OBSERVED_EXTERNAL_SKINS = new HashMap<>(); + private static final Map FAILED_TEXTURE_READS = new HashMap<>(); + private static final Set WARNED_TEXTURE_READS = new HashSet<>(); + private static int uploadRetryTicks = 0; + private static boolean uploadedThisConnection = false; + private static boolean uploadedMasksForSelection = false; + private static boolean automaticSkinUnavailableThisConnection = false; + @Nullable + private static String lastUploadedAutoBaseSkinKey = null; + private static int externalSkinSelectionPollTicks = 0; + private static int skinOverrideCacheVersion = Integer.MIN_VALUE; + + private NonDestroyedSkinClient() { + } + + public static void ensureConfigDirectory() { + Path directory = configRoot(); + + try { + Files.createDirectories(directory); + } catch (IOException e) { + setupWarn("[NoN] Failed to create ripped-skin config directory {}.", directory, e); + } + } + + public static void scheduleLocalSkinUploadRetry() { + uploadRetryTicks = 300; + uploadedThisConnection = false; + uploadedMasksForSelection = false; + automaticSkinUnavailableThisConnection = false; + lastUploadedAutoBaseSkinKey = null; + } + + public static void clientTick(MinecraftClient client) { + if (client != null) { + boolean skinOverrideChanged = refreshSkinOverrideCacheVersion(); + pollExternalSkinSelections(client, skinOverrideChanged); + if (uploadRetryTicks > 0) { + uploadRetryTicks--; + if (!ClientPlayNetworking.canSend(DestroyedSkinUploadC2SPayload.ID)) { + if (uploadRetryTicks == 0) { + NeedsOfNature.LOGGER.info("[NoN] Destroyed skin upload retry expired: upload channel never became sendable."); + } + } else { + boolean localCustomSkin = hasLocalCustomDestroyedSkin(); + if (localCustomSkin && uploadedThisConnection && uploadedMasksForSelection) { + uploadRetryTicks = 0; + } else if (!localCustomSkin && automaticSkinUnavailableThisConnection) { + uploadedMasksForSelection = uploadedMasksForSelection | uploadLocalMasks(); + if (uploadedMasksForSelection) { + uploadRetryTicks = 0; + } + } else { + boolean uploadedSkin = uploadedThisConnection || uploadLocalSkinIfPresent(); + if (!uploadedMasksForSelection) { + uploadedMasksForSelection = uploadLocalMasks(); + } + + uploadedThisConnection |= uploadedSkin; + if (localCustomSkin && uploadedThisConnection && uploadedMasksForSelection) { + uploadRetryTicks = 0; + } else if (uploadRetryTicks == 0 && !uploadedThisConnection && !automaticSkinUnavailableThisConnection) { + NeedsOfNature.LOGGER.info("[NoN] Destroyed skin upload retry expired without upload."); + } + } + } + } + } + } + + public static boolean uploadLocalSkinIfPresent() { + for (Path path : localOverrideCandidates("destroyed_skin.png")) { + if (Files.isRegularFile(path)) { + try { + byte[] bytes = Files.readAllBytes(path); + if (!isValidDestroyedSkinPng(bytes)) { + setupWarn("[NoN] Ignoring destroyed skin {}: expected a square PNG from 64x64 through 512x512 in 64-pixel steps.", path); + } else if (ClientPlayNetworking.canSend(DestroyedSkinUploadC2SPayload.ID)) { + ClientPlayNetworking.send(new DestroyedSkinUploadC2SPayload(bytes)); + return true; + } + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to upload local destroyed skin from {}.", path, e); + } + } + } + + return uploadGeneratedSkinIfPossible(); + } + + public static boolean hasLocalCustomDestroyedSkin() { + return firstExistingLocalOverride("destroyed_skin.png") != null; + } + + public static boolean hasCurrentDefaultDestroyedSkin() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + Identifier baseSkin = client.player.getSkin().body().texturePath(); + if (baseSkin == null) { + return false; + } + + boolean defaultSkinPath = isLikelyDefaultPlayerSkin(baseSkin); + boolean profileHasCustomTexture = hasProfileTextureProperties(client); + if (defaultSkinPath && profileHasCustomTexture) { + return false; + } + + NonDestroyedSkinClient.DefaultSkinKey key = resolveDefaultSkinKey(client, baseSkin); + return key == null ? false : readDefaultDestroyedSkin(client, key) != null; + } else { + return false; + } + } + + private static Path localDestroyedSkinPath() { + List candidates = localOverrideCandidates("destroyed_skin.png"); + Path existing = firstExisting(candidates); + return existing != null ? existing : candidates.getFirst(); + } + + private static List localOverrideCandidates(String fileName) { + Path configRoot = configRoot(); + List candidates = new ArrayList<>(2); + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + UUID playerUuid = client.player.getUuid(); + NonDestroyedSkinClient.EffectiveBaseTexture effective = EFFECTIVE_BASE_TEXTURES.get(playerUuid); + Identifier textureId = effective != null ? effective.id() : client.player.getSkin().body().texturePath(); + NonExternalSkinCompat.SkinContext selection = effective != null ? effective.externalSkin() : null; + if (selection == null) { + selection = NonExternalSkinCompat.resolve(client, playerUuid, textureId); + } + + if (selection == null) { + selection = NonExternalSkinCompat.resolveSelected(client, playerUuid); + } + + if (selection != null && selection.folderName() != null) { + candidates.add(configRoot.resolve(selection.folderName()).resolve(fileName)); + } + } + + candidates.add(configRoot.resolve(fileName)); + return candidates; + } + + private static Path configRoot() { + return FabricLoader.getInstance().getConfigDir().resolve("needsofnature"); + } + + @Nullable + private static Path firstExistingLocalOverride(String fileName) { + return firstExisting(localOverrideCandidates(fileName)); + } + + @Nullable + private static Path firstExisting(List candidates) { + for (Path candidate : candidates) { + if (Files.isRegularFile(candidate)) { + return candidate; + } + } + + return null; + } + + private static void pollExternalSkinSelections(MinecraftClient client, boolean force) { + if ((force || externalSkinSelectionPollTicks-- <= 0) && client.world != null) { + externalSkinSelectionPollTicks = 20; + Set presentPlayers = new HashSet<>(); + + for (AbstractClientPlayerEntity player : client.world.getPlayers()) { + UUID playerUuid = player.getUuid(); + presentPlayers.add(playerUuid); + Identifier textureId = player.getSkin().body().texturePath(); + NonExternalSkinCompat.SkinContext selection = NonExternalSkinCompat.resolveSelected(client, playerUuid); + String currentKey = observedSkinKey(textureId, selection); + String previousKey = OBSERVED_EXTERNAL_SKINS.put(playerUuid, currentKey); + if (previousKey != null && !Objects.equals(previousKey, currentKey)) { + NonDestroyedSkinClient.EffectiveBaseTexture previousEffective = EFFECTIVE_BASE_TEXTURES.remove(playerUuid); + if (previousEffective != null) { + removeOriginalSkinColors(previousEffective.id()); + clearTextureReadFailures(previousEffective.id()); + } + + removeOriginalSkinColors(textureId); + clearCachedTextures(playerUuid); + clearTextureReadFailures(textureId); + if (client.player != null && playerUuid.equals(client.player.getUuid())) { + if (selection != null) { + NeedsOfNature.LOGGER + .info( + "[NoN] {} skin selection changed to '{}'{}.", + new Object[]{ + selection.providerId(), + selection.displayName(), + selection.folderName() != null ? " (config folder '" + selection.folderName() + "')" : "" + } + ); + } else { + NeedsOfNature.LOGGER.info("[NoN] External skin override cleared; using the global ripped-skin configuration."); + } + + scheduleLocalSkinUploadRetry(); + } + } + } + + OBSERVED_EXTERNAL_SKINS.keySet().removeIf(uuid -> !presentPlayers.contains(uuid)); + } + } + + private static boolean refreshSkinOverrideCacheVersion() { + int currentVersion = NonSkinOverrideCompat.cacheVersion(); + if (currentVersion < 0) { + return false; + } + + if (skinOverrideCacheVersion == Integer.MIN_VALUE) { + skinOverrideCacheVersion = currentVersion; + return false; + } + + if (skinOverrideCacheVersion == currentVersion) { + return false; + } + + skinOverrideCacheVersion = currentVersion; + return true; + } + + private static String observedSkinKey(Identifier textureId, @Nullable NonExternalSkinCompat.SkinContext selection) { + return selection != null ? selection.selectionKey() : "minecraft\u0000" + textureId; + } + + private static boolean uploadGeneratedSkinIfPossible() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null || client.player == null) { + return false; + } + + if (!ClientPlayNetworking.canSend(DestroyedSkinUploadC2SPayload.ID)) { + return false; + } + + if (automaticSkinUnavailableThisConnection) { + return false; + } + + Identifier baseSkin = client.player.getSkin().body().texturePath(); + if (baseSkin == null) { + return false; + } + + NonExternalSkinCompat.SkinContext externalSkin = NonExternalSkinCompat.resolve(client, client.player.getUuid(), baseSkin); + String uploadKey = baseSkin + "\u0000" + textureIdentity(client, baseSkin) + "\u0000" + externalSkinKey(externalSkin); + if (uploadKey.equals(lastUploadedAutoBaseSkinKey)) { + return true; + } + + boolean defaultSkinPath = isLikelyDefaultPlayerSkin(baseSkin); + boolean profileHasCustomTexture = hasProfileTextureProperties(client); + if (defaultSkinPath && profileHasCustomTexture) { + return false; + } + + NonDestroyedSkinClient.DefaultSkinKey defaultSkinKey = resolveDefaultSkinKey(client, baseSkin); + if (defaultSkinKey != null) { + byte[] defaultDestroyedSkin = readDefaultDestroyedSkin(client, defaultSkinKey); + if (defaultDestroyedSkin != null && isValidDestroyedSkinPng(defaultDestroyedSkin)) { + ClientPlayNetworking.send(new DestroyedSkinUploadC2SPayload(defaultDestroyedSkin)); + lastUploadedAutoBaseSkinKey = uploadKey; + return true; + } + } + + try { + byte[] bytes = createGeneratedDestroyedSkin(client, baseSkin); + if (bytes != null && isValidDestroyedSkinPng(bytes)) { + ClientPlayNetworking.send(new DestroyedSkinUploadC2SPayload(bytes)); + lastUploadedAutoBaseSkinKey = uploadKey; + return true; + } else { + return false; + } + } catch (NonDestroyedSkinClient.MissingDestroyedSkinFallbackException e) { + markAutomaticSkinUnavailable( + "[NoN] No ripped skin fallback texture was found. Install the default NoN pack or provide {} to enable ripped skin visuals.", + localDestroyedSkinPath() + ); + return false; + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to generate ripped skin fallback. Automatic ripped skin upload is disabled for this connection.", e); + automaticSkinUnavailableThisConnection = true; + uploadRetryTicks = 0; + return false; + } + } + + private static boolean uploadLocalMasks() { + if (!ClientPlayNetworking.canSend(DestroyedSkinMaskUploadC2SPayload.ID)) { + return false; + } + + for (int stage = 1; stage < 4; stage++) { + byte[] selectedBytes = null; + + for (Path path : localOverrideCandidates("destroyed_skin_mask_" + stage + ".png")) { + if (Files.isRegularFile(path)) { + try { + byte[] bytes = Files.readAllBytes(path); + if (isValidMaskPng(bytes)) { + selectedBytes = bytes; + break; + } + + setupWarn("[NoN] Ignoring destroyed skin mask {}: expected a 64x64 PNG.", path); + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to upload local destroyed skin mask from {}.", path, e); + } + } + } + + ClientPlayNetworking.send(new DestroyedSkinMaskUploadC2SPayload(stage, selectedBytes != null ? selectedBytes : new byte[0])); + } + + return true; + } + + public static int detectSkinTintFromCurrentPlayer(int fallbackRgb) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + Identifier baseSkin = client.player.getSkin().body().texturePath(); + if (baseSkin == null) { + return fallbackRgb & 16777215; + } + + try { + NativeImage image = readPlayerTextureImage(client, client.player.getUuid(), baseSkin); + if (image != null && isSupportedStaticSkin(image)) { + NativeImage var4 = image; + + int var6; + try { + NativeImage normalized = scaledCopy(image, 64, 64); + + try { + var6 = detectSkinTint(normalized); + } catch (Throwable var10) { + if (normalized != null) { + try { + normalized.close(); + } catch (Throwable var9) { + var10.addSuppressed(var9); + } + } + + throw var10; + } + + if (normalized != null) { + normalized.close(); + } + } catch (Throwable var11) { + if (var4 != null) { + try { + var4.close(); + } catch (Throwable var8) { + var11.addSuppressed(var8); + } + } + + throw var11; + } + + if (var4 != null) { + var4.close(); + } + + return var6; + } else { + if (image != null) { + image.close(); + } + + return fallbackRgb & 16777215; + } + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to auto-detect destroyed skin tint.", e); + return fallbackRgb & 16777215; + } + } else { + return fallbackRgb & 16777215; + } + } + + public static void clear() { + DESTROYED_SKINS.clear(); + DESTROYED_SKIN_MASKS.clear(); + STAGES.clear(); + MESS_STATES.clear(); + clearResourceCaches(); + uploadRetryTicks = 0; + uploadedThisConnection = false; + uploadedMasksForSelection = false; + automaticSkinUnavailableThisConnection = false; + lastUploadedAutoBaseSkinKey = null; + externalSkinSelectionPollTicks = 0; + skinOverrideCacheVersion = Integer.MIN_VALUE; + OBSERVED_EXTERNAL_SKINS.clear(); + } + + public static void clearResourceCaches() { + MinecraftClient client = MinecraftClient.getInstance(); + List texturesToDestroy = new ArrayList<>(TEXTURE_CACHE.values()); + TEXTURE_CACHE.clear(); + ORIGINAL_SKIN_COLOR_CACHE.clear(); + EFFECTIVE_BASE_TEXTURES.clear(); + FAILED_TEXTURE_READS.clear(); + WARNED_TEXTURE_READS.clear(); + destroyTextures(client, texturesToDestroy); + } + + public static void setSkin(UUID playerUuid, int stage, byte[] pngBytes) { + if (playerUuid != null && pngBytes != null) { + if (pngBytes.length == 0) { + clearCachedTextures(playerUuid); + DESTROYED_SKINS.remove(playerUuid); + EFFECTIVE_BASE_TEXTURES.remove(playerUuid); + setStage(playerUuid, stage); + } else if (!isValidDestroyedSkinPng(pngBytes)) { + setupWarn("[NoN] Ignoring synced destroyed skin for {}: expected a square PNG from 64x64 through 512x512 in 64-pixel steps.", playerUuid); + } else { + clearCachedTextures(playerUuid); + DESTROYED_SKINS.put(playerUuid, (byte[])pngBytes.clone()); + setStage(playerUuid, stage); + } + } + } + + public static void setMask(UUID playerUuid, int stage, byte[] pngBytes) { + if (playerUuid != null && pngBytes != null) { + if (stage > 0 && stage < 4) { + if (pngBytes.length == 0) { + byte[][] masks = DESTROYED_SKIN_MASKS.get(playerUuid); + if (masks != null && stage < masks.length) { + masks[stage] = null; + boolean hasAny = false; + + for (int index = 1; index < masks.length; index++) { + if (masks[index] != null) { + hasAny = true; + break; + } + } + + if (!hasAny) { + DESTROYED_SKIN_MASKS.remove(playerUuid); + } + } + + clearCachedTextures(playerUuid); + } else if (!isValidMaskPng(pngBytes)) { + setupWarn("[NoN] Ignoring synced destroyed skin mask for {} stage {}: expected a 64x64 PNG.", playerUuid, stage); + } else { + byte[][] masks = DESTROYED_SKIN_MASKS.computeIfAbsent(playerUuid, ignored -> new byte[4][]); + masks[stage] = (byte[])pngBytes.clone(); + clearCachedTextures(playerUuid); + } + } + } + } + + public static void setStage(UUID playerUuid, int stage) { + if (playerUuid != null) { + int clamped = clampStage(stage); + if (clamped <= 0) { + STAGES.remove(playerUuid); + } else { + STAGES.put(playerUuid, clamped); + } + + MinecraftClient client = MinecraftClient.getInstance(); + if (clamped > 0 && client != null && client.player != null && playerUuid.equals(client.player.getUuid()) && !DESTROYED_SKINS.containsKey(playerUuid)) { + uploadLocalSkinIfPresent(); + } + + if (client != null && client.player != null && playerUuid.equals(client.player.getUuid())) { + NonWildfireGenderSync.syncDestroyedSkinOverrides(NeedsOfNature.getLocalConfig(), clamped); + } + + clearCachedTextures(playerUuid); + } + } + + public static int getStage(UUID playerUuid) { + return playerUuid == null ? 0 : clampStage(STAGES.getOrDefault(playerUuid, 0)); + } + + @Nullable + public static Integer getLiquidTintRgb(UUID playerUuid) { + if (playerUuid == null) { + return null; + } + + NonDestroyedSkinClient.MessState state = MESS_STATES.get(playerUuid); + return state == null ? null : state.tintRgb(); + } + + public static void setMess(UUID playerUuid, int vMess, int aMess, int mMess, int tintRgb, int liquidStored, int liquidCapacity) { + if (playerUuid != null) { + NonDestroyedSkinClient.MessState state = new NonDestroyedSkinClient.MessState( + clampMess(vMess), clampMess(aMess), clampMess(mMess), tintRgb & 16777215, Math.max(0, liquidStored), Math.max(0, liquidCapacity) + ); + if (!state.hasAnyVisual()) { + MESS_STATES.remove(playerUuid); + } else { + MESS_STATES.put(playerUuid, state); + } + + clearCachedTextures(playerUuid); + } + } + + @Nullable + public static Identifier resolveBaseTexture(LivingEntity entity, Identifier currentBaseTexture) { + int stage = getDestroyedStage(entity); + NonDestroyedSkinClient.MessState messState = getMessState(entity); + List accessoryOverlays = NonAccessorySkinOverlays.resolveEquippedOverlays(entity); + return stage <= 0 && !messState.hasAnyVisual() && accessoryOverlays.isEmpty() ? null : getOrCreateTexture(entity, stage, currentBaseTexture, false); + } + + public static Identifier resolveRenderedTexture(LivingEntity entity, Identifier renderedTexture) { + if (entity != null && renderedTexture != null && !isGeneratedTexture(renderedTexture)) { + rememberEffectiveBaseTexture(entity, renderedTexture); + Identifier replacement = resolveBaseTexture(entity, renderedTexture); + return replacement != null ? replacement : renderedTexture; + } else { + return renderedTexture; + } + } + + @Nullable + public static Identifier resolveOverlayTexture(LivingEntity entity) { + int stage = getDestroyedStage(entity); + return stage > 0 && stage < 4 ? getOrCreateTexture(entity, stage, null, true) : null; + } + + public static void spawnRippedFabricParticles(UUID playerUuid, double x, double y, double z, int count, long seed) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null && playerUuid != null && count > 0) { + AbstractClientPlayerEntity player = null; + + for (AbstractClientPlayerEntity candidate : client.world.getPlayers()) { + if (playerUuid.equals(candidate.getUuid())) { + player = candidate; + break; + } + } + + if (player != null) { + Identifier baseSkin = player.getSkin().body().texturePath(); + int[] colors = getOriginalSkinColors(client, playerUuid, baseSkin); + Random random = Random.create(seed); + int particleCount = Math.max(1, Math.min(12, count)); + + for (int i = 0; i < particleCount; i++) { + int rgb = colors[random.nextInt(colors.length)]; + rgb = varyColor(rgb, random); + double px = x + (random.nextDouble() - 0.5) * 0.16; + double py = y + (random.nextDouble() - 0.5) * 0.2; + double pz = z + (random.nextDouble() - 0.5) * 0.16; + double vx = (random.nextDouble() - 0.5) * 0.001; + double vy = -0.006 - random.nextDouble() * 0.004; + double vz = (random.nextDouble() - 0.5) * 0.001; + Particle particle = RippedFabricParticle.create(client.world, px, py, pz, vx, vy, vz, rgb, random); + if (particle != null) { + client.particleManager.addParticle(particle); + } + } + } + } + } + + private static int getDestroyedStage(LivingEntity entity) { + if (!(entity instanceof PlayerEntity)) { + return 0; + } + + if (entity == null) { + return 0; + } + + if (entity instanceof DestroyedSkinHolder holder) { + int tracked = clampStage(holder.getDestroyedSkinStage()); + if (tracked > 0) { + return tracked; + } + } + + return STAGES.getOrDefault(entity.getUuid(), 0); + } + + private static NonDestroyedSkinClient.MessState getMessState(@Nullable LivingEntity entity) { + return entity == null ? NonDestroyedSkinClient.MessState.EMPTY : MESS_STATES.getOrDefault(entity.getUuid(), NonDestroyedSkinClient.MessState.EMPTY); + } + + @Nullable + private static Identifier getOrCreateTexture(LivingEntity entity, int stage, @Nullable Identifier currentBaseTexture, boolean overlayFallback) { + if (entity == null) { + return null; + } + + UUID playerUuid = entity.getUuid(); + byte[] skin = DESTROYED_SKINS.get(playerUuid); + NonDestroyedSkinClient.MessState messState = MESS_STATES.getOrDefault(playerUuid, NonDestroyedSkinClient.MessState.EMPTY); + List accessoryOverlays = overlayFallback + ? List.of() + : NonAccessorySkinOverlays.resolveEquippedOverlays(entity); + if ((skin == null || skin.length == 0) && !messState.hasAnyVisual() && accessoryOverlays.isEmpty()) { + return null; + } + + NonDestroyedSkinClient.TankMaskType tankMaskType = resolveTankMaskType(entity); + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return null; + } + + int baseTextureIdentity = textureIdentity(client, overlayFallback ? null : currentBaseTexture); + NonExternalSkinCompat.SkinContext externalSkin = overlayFallback ? null : effectiveExternalSkin(client, playerUuid, currentBaseTexture); + String externalSkinKey = externalSkinKey(externalSkin); + NonDestroyedSkinClient.TextureKey key = new NonDestroyedSkinClient.TextureKey( + playerUuid, stage, overlayFallback ? null : currentBaseTexture, baseTextureIdentity, externalSkinKey, messState, tankMaskType, accessoryOverlays + ); + Identifier cached = TEXTURE_CACHE.get(key); + if (cached != null) { + return cached; + } + + try { + NativeImage image; + if (overlayFallback) { + image = createMaskedOverlay(client, playerUuid, skin, stage); + } else { + image = createCompositedSkin(client, playerUuid, skin, stage, currentBaseTexture, externalSkin, messState, tankMaskType, accessoryOverlays); + } + + if (image == null) { + return null; + } + + Identifier id = Identifier.of( + "needsofnature", + "textures/dynamic/destroyed_skin/" + + playerUuid.toString().replace("-", "") + + "_" + + stage + + "_" + + messState.cacheKey() + + "_" + + tankMaskType.id + + "_" + + Integer.toHexString(accessoryOverlays.hashCode()) + + "_" + + Integer.toHexString(Objects.hashCode(currentBaseTexture)) + + "_" + + Integer.toHexString(baseTextureIdentity) + + "_" + + Integer.toHexString(externalSkinKey.hashCode()) + + (overlayFallback ? "_overlay" : "_base") + + ".png" + ); + client.getTextureManager().registerTexture(id, new NativeImageBackedTexture(() -> "non_destroyed_skin", image)); + TEXTURE_CACHE.put(key, id); + return id; + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to create destroyed skin texture for {} stage {}.", playerUuid, stage, e); + return null; + } + } + + @Nullable + private static NativeImage createCompositedSkin( + MinecraftClient client, + UUID playerUuid, + byte[] skin, + int stage, + @Nullable Identifier currentBaseTexture, + @Nullable NonExternalSkinCompat.SkinContext externalSkin, + NonDestroyedSkinClient.MessState messState, + NonDestroyedSkinClient.TankMaskType tankMaskType, + List accessoryOverlays + ) throws IOException { + if (currentBaseTexture == null) { + return null; + } + + NativeImage destroyed = stage > 0 && skin != null && skin.length > 0 ? NativeImage.read(skin) : null; + if (destroyed != null && !isSupportedStaticSkin(destroyed)) { + destroyed.close(); + return null; + } + + if (stage >= 4 && destroyed != null) { + NativeImage var27 = destroyed; + + NativeImage var29; + try { + NativeImage out = new NativeImage(Format.RGBA, destroyed.getWidth(), destroyed.getHeight(), false); + copyScaled(destroyed, out); + applyMessOverlays(client, out, messState); + applyTankOverlay(client, out, messState, tankMaskType); + applyAccessoryOverlays(client, out, accessoryOverlays); + var29 = out; + } catch (Throwable var24) { + if (var27 != null) { + try { + var27.close(); + } catch (Throwable var23) { + var24.addSuppressed(var23); + } + } + + throw var24; + } + + if (var27 != null) { + var27.close(); + } + + return var29; + } else { + NativeImage base = readTextureImage(client, currentBaseTexture, externalSkin); + if (base == null) { + if (destroyed != null) { + destroyed.close(); + } + + return null; + } else if (!isSupportedStaticSkin(base)) { + base.close(); + if (destroyed != null) { + destroyed.close(); + } + + return null; + } else { + NativeImage out = base; + + NativeImage mask; + try { + NativeImage var12 = destroyed; + + try { + int targetSize = destroyed != null ? Math.max(base.getWidth(), destroyed.getWidth()) : base.getWidth(); + NativeImage outx = new NativeImage(Format.RGBA, targetSize, targetSize, false); + copyScaled(base, outx); + if (destroyed != null) { + mask = readMask(client, playerUuid, stage); + if (mask != null) { + for (int y = 0; y < outx.getHeight(); y++) { + for (int x = 0; x < outx.getWidth(); x++) { + int factor = maskFactor(sampleScaled(mask, x, y, outx.getWidth(), outx.getHeight())); + if (factor > 0) { + int baseArgb = outx.getColorArgb(x, y); + int destroyedArgb = sampleScaled(destroyed, x, y, outx.getWidth(), outx.getHeight()); + outx.setColorArgb(x, y, blendArgb(baseArgb, destroyedArgb, factor)); + } + } + } + + mask.close(); + } + } + + applyMessOverlays(client, outx, messState); + applyTankOverlay(client, outx, messState, tankMaskType); + applyAccessoryOverlays(client, outx, accessoryOverlays); + mask = outx; + } catch (Throwable var25) { + if (var12 != null) { + try { + var12.close(); + } catch (Throwable var22) { + var25.addSuppressed(var22); + } + } + + throw var25; + } + + if (var12 != null) { + var12.close(); + } + } catch (Throwable var26) { + if (out != null) { + try { + out.close(); + } catch (Throwable var21) { + var26.addSuppressed(var21); + } + } + + throw var26; + } + + if (out != null) { + out.close(); + } + + return mask; + } + } + } + + @Nullable + public static NativeImage readTextureImage(MinecraftClient client, Identifier textureId) throws IOException { + UUID playerUuid = client != null && client.player != null ? client.player.getUuid() : null; + NonExternalSkinCompat.SkinContext externalSkin = playerUuid != null ? effectiveExternalSkin(client, playerUuid, textureId) : null; + return readTextureImage(client, textureId, externalSkin); + } + + @Nullable + private static NativeImage readPlayerTextureImage(MinecraftClient client, UUID playerUuid, Identifier textureId) throws IOException { + return readTextureImage(client, textureId, effectiveExternalSkin(client, playerUuid, textureId)); + } + + @Nullable + private static NativeImage readTextureImage(MinecraftClient client, Identifier textureId, @Nullable NonExternalSkinCompat.SkinContext externalSkin) throws IOException { + if (client != null && textureId != null) { + Resource resource = (Resource)client.getResourceManager().getResource(textureId).orElse(null); + if (resource != null) { + try (InputStream in = resource.getInputStream()) { + return NativeImage.read(in); + } + } else { + int identity = textureIdentity(client, textureId); + NonDestroyedSkinClient.TextureReadKey readKey = new NonDestroyedSkinClient.TextureReadKey(textureId, identity, externalSkinKey(externalSkin)); + Long retryAt = FAILED_TEXTURE_READS.get(readKey); + if (retryAt != null && System.nanoTime() < retryAt) { + return null; + } + + Throwable sourceFailure = null; + Path sourcePath = externalSkin != null ? externalSkin.sourcePath() : null; + if (sourcePath != null && Files.isRegularFile(sourcePath)) { + try (InputStream in = Files.newInputStream(sourcePath)) { + NativeImage image = NativeImage.read(in); + clearTextureReadFailure(readKey); + return image; + } catch (IOException | RuntimeException e) { + sourceFailure = e; + } + } + + if (!(client.getTextureManager().getTexture(textureId) instanceof NativeImageBackedTexture backedTexture)) { + if (sourceFailure != null) { + recordTextureReadFailure(readKey, sourcePath, sourceFailure); + } + + return null; + } else { + NativeImage image = backedTexture.getImage(); + if (image == null) { + recordTextureReadFailure(readKey, sourcePath, sourceFailure); + return null; + } + + NativeImage copy = null; + + try { + copy = new NativeImage(Format.RGBA, image.getWidth(), image.getHeight(), false); + copy.copyFrom(image); + clearTextureReadFailure(readKey); + return copy; + } catch (RuntimeException e) { + if (copy != null) { + copy.close(); + } + + if (sourceFailure != null) { + e.addSuppressed(sourceFailure); + } + + recordTextureReadFailure(readKey, sourcePath, e); + return null; + } + } + } + } else { + return null; + } + } + + private static boolean isGeneratedTexture(Identifier textureId) { + return "needsofnature".equals(textureId.getNamespace()) && textureId.getPath().startsWith("textures/dynamic/destroyed_skin/"); + } + + private static boolean isSupportedStaticSkin(NativeImage image) { + int width = image.getWidth(); + return width >= 64 && width == image.getHeight() && width % 64 == 0; + } + + private static int textureIdentity(MinecraftClient client, @Nullable Identifier textureId) { + return textureId == null ? 0 : System.identityHashCode(client.getTextureManager().getTexture(textureId)); + } + + @Nullable + private static NonExternalSkinCompat.SkinContext effectiveExternalSkin(MinecraftClient client, UUID playerUuid, @Nullable Identifier textureId) { + if (client != null && playerUuid != null && textureId != null) { + NonDestroyedSkinClient.EffectiveBaseTexture effective = EFFECTIVE_BASE_TEXTURES.get(playerUuid); + return effective != null && textureId.equals(effective.id()) ? effective.externalSkin() : NonExternalSkinCompat.resolve(client, playerUuid, textureId); + } else { + return null; + } + } + + private static String externalSkinKey(@Nullable NonExternalSkinCompat.SkinContext externalSkin) { + return externalSkin != null ? externalSkin.cacheKey() : ""; + } + + private static void recordTextureReadFailure(NonDestroyedSkinClient.TextureReadKey key, @Nullable Path sourcePath, @Nullable Throwable error) { + FAILED_TEXTURE_READS.put(key, System.nanoTime() + 1000000000L); + if (error != null && WARNED_TEXTURE_READS.add(key)) { + setupWarn( + "[NoN] Failed to read skin texture {}{}; ripped-skin composition will retry after a short delay.", + key.textureId(), + sourcePath != null ? " from " + sourcePath : "", + error + ); + } + } + + private static void clearTextureReadFailure(NonDestroyedSkinClient.TextureReadKey key) { + FAILED_TEXTURE_READS.remove(key); + } + + private static void clearTextureReadFailures(Identifier textureId) { + FAILED_TEXTURE_READS.keySet().removeIf(key -> key.textureId().equals(textureId)); + WARNED_TEXTURE_READS.removeIf(key -> key.textureId().equals(textureId)); + } + + private static void removeOriginalSkinColors(Identifier textureId) { + ORIGINAL_SKIN_COLOR_CACHE.keySet().removeIf(key -> key.textureId().equals(textureId)); + } + + private static void rememberEffectiveBaseTexture(LivingEntity entity, Identifier textureId) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + UUID playerUuid = entity.getUuid(); + int identity = textureIdentity(client, textureId); + NonDestroyedSkinClient.EffectiveBaseTexture previous = EFFECTIVE_BASE_TEXTURES.get(playerUuid); + if (previous == null || !previous.id().equals(textureId) || previous.textureIdentity() != identity) { + NonExternalSkinCompat.SkinContext externalSkin = NonExternalSkinCompat.resolve(client, playerUuid, textureId); + NonDestroyedSkinClient.EffectiveBaseTexture current = new NonDestroyedSkinClient.EffectiveBaseTexture(textureId, identity, externalSkin); + previous = EFFECTIVE_BASE_TEXTURES.put(playerUuid, current); + if (previous != null && !previous.equals(current)) { + removeOriginalSkinColors(previous.id()); + clearTextureReadFailures(previous.id()); + clearCachedTextures(playerUuid); + if (client.player != null && playerUuid.equals(client.player.getUuid()) && shouldRetryLocalSkinUpload(previous, current)) { + scheduleLocalSkinUploadRetry(); + } + } + } + } + } + + private static boolean shouldRetryLocalSkinUpload(NonDestroyedSkinClient.EffectiveBaseTexture previous, NonDestroyedSkinClient.EffectiveBaseTexture current) { + String previousSelection = externalSelectionKey(previous.externalSkin()); + String currentSelection = externalSelectionKey(current.externalSkin()); + return !previousSelection.equals(currentSelection) + ? true + : previousSelection.isEmpty() && (!previous.id().equals(current.id()) || previous.textureIdentity() != current.textureIdentity()); + } + + private static String externalSelectionKey(@Nullable NonExternalSkinCompat.SkinContext externalSkin) { + return externalSkin != null ? externalSkin.selectionKey() : ""; + } + + private static boolean hasProfileTextureProperties(MinecraftClient client) { + if (client != null && client.player != null && client.player.getGameProfile() != null) { + try { + if (client.player.getGameProfile().getClass().getMethod("getProperties").invoke(client.player.getGameProfile()) instanceof Map map) { + return map.containsKey("textures"); + } + } catch (ReflectiveOperationException | RuntimeException var4) { + } + + try { + if (client.player.getGameProfile().getClass().getMethod("properties").invoke(client.player.getGameProfile()) instanceof Map map) { + return map.containsKey("textures"); + } + } catch (ReflectiveOperationException | RuntimeException var3) { + } + + return false; + } else { + return false; + } + } + + private static boolean isLikelyDefaultPlayerSkin(Identifier textureId) { + if (textureId != null && "minecraft".equals(textureId.getNamespace())) { + String path = textureId.getPath(); + return path != null && path.startsWith("textures/entity/player/"); + } else { + return false; + } + } + + public static int sendDetectedRippedSkinInfo() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + Identifier baseSkin = client.player.getSkin().body().texturePath(); + boolean defaultSkinPath = isLikelyDefaultPlayerSkin(baseSkin); + boolean profileHasCustomTexture = hasProfileTextureProperties(client); + NonDestroyedSkinClient.DefaultSkinKey key = parseDefaultSkinKey(baseSkin); + NonDestroyedSkinClient.DefaultSkinMatch pixelMatch = detectDefaultSkinMatchByPixels(client, baseSkin); + NonDestroyedSkinClient.DefaultSkinKey pixelKey = pixelMatch == null ? null : pixelMatch.key(); + NonDestroyedSkinClient.DefaultSkinKey resolvedKey = key != null ? key : (pixelMatch != null && pixelMatch.accepted() ? pixelMatch.key() : null); + PlayerSkinType model = client.player.getSkin() == null ? null : client.player.getSkin().model(); + String profileName = client.player.getName().getString(); + UUID uuid = client.player.getUuid(); + NonDestroyedSkinClient.DefaultSkinCandidate defaultDestroyed = resolvedKey == null ? null : describeDefaultDestroyedSkinCandidate(client, resolvedKey); + String message = "[NoN RippedSkinInfo] profile=" + + profileName + + " uuid=" + + uuid + + " base=" + + baseSkin + + " model=" + + model + + " defaultPath=" + + defaultSkinPath + + " profileTextures=" + + profileHasCustomTexture + + " parsed=" + + (key == null ? "" : key.model() + "/" + key.name()) + + " pixel=" + + (pixelKey == null ? "" : pixelKey.model() + "/" + pixelKey.name()) + + " confidence=" + + (pixelMatch == null ? "" : String.format(Locale.ROOT, "%.1f%%", pixelMatch.confidence() * 100.0)) + + " accepted=" + + (pixelMatch != null && pixelMatch.accepted()) + + " resolved=" + + (resolvedKey == null ? "" : resolvedKey.model() + "/" + resolvedKey.name()) + + " candidate=" + + (defaultDestroyed == null ? "" : defaultDestroyed.logString()); + NeedsOfNature.LOGGER.info(message); + sendRippedSkinInfoLine(client, "NoN Ripped Skin Info", Formatting.GOLD); + sendRippedSkinInfoLine(client, "Player: " + profileName + " (" + uuid + ")", Formatting.GRAY); + sendRippedSkinInfoLine(client, "Base texture: " + baseSkin, Formatting.WHITE); + sendRippedSkinInfoLine(client, "Model: " + model, Formatting.WHITE); + sendRippedSkinInfoLine(client, "Default path: " + defaultSkinPath + " | Profile textures: " + profileHasCustomTexture, Formatting.WHITE); + sendRippedSkinInfoLine(client, "Parsed key: " + formatDefaultSkinKey(key), Formatting.WHITE); + sendRippedSkinInfoLine( + client, + "Pixel match: " + + formatDefaultSkinKey(pixelKey) + + " | Confidence: " + + (pixelMatch == null ? "" : String.format(Locale.ROOT, "%.1f%%", pixelMatch.confidence() * 100.0)) + + " | Accepted: " + + (pixelMatch != null && pixelMatch.accepted()), + pixelMatch != null && pixelMatch.accepted() ? Formatting.GREEN : Formatting.YELLOW + ); + sendRippedSkinInfoLine(client, "Resolved key: " + formatDefaultSkinKey(resolvedKey), resolvedKey == null ? Formatting.RED : Formatting.GREEN); + if (defaultDestroyed == null) { + sendRippedSkinInfoLine(client, "Ripped candidates: ", Formatting.RED); + } else { + sendRippedSkinInfoLine( + client, + "Ripped candidate: " + defaultDestroyed.genderSpecific() + " = " + defaultDestroyed.genderFound(), + defaultDestroyed.genderFound() ? Formatting.GREEN : Formatting.YELLOW + ); + sendRippedSkinInfoLine( + client, + "Fallback candidate: " + defaultDestroyed.generic() + " = " + defaultDestroyed.genericFound(), + defaultDestroyed.genericFound() ? Formatting.GREEN : Formatting.GRAY + ); + } + + return 1; + } else { + return 0; + } + } + + private static void sendRippedSkinInfoLine(MinecraftClient client, String message, Formatting color) { + client.player.sendMessage(Text.literal(message).formatted(color), false); + } + + private static String formatDefaultSkinKey(@Nullable NonDestroyedSkinClient.DefaultSkinKey key) { + return key == null ? "" : key.model() + "/" + key.name(); + } + + private static NonDestroyedSkinClient.DefaultSkinCandidate describeDefaultDestroyedSkinCandidate( + MinecraftClient client, NonDestroyedSkinClient.DefaultSkinKey key + ) { + String genderSuffix = NeedsOfNature.getLocalConfig().getPlayerGenderSelection() == NonConfig.PlayerGenderSelection.MALE ? "_m" : "_f"; + Identifier genderSpecific = defaultDestroyedSkinId(key, genderSuffix); + Identifier generic = defaultDestroyedSkinId(key, ""); + boolean genderFound = client.getResourceManager().getResource(genderSpecific).isPresent(); + boolean genericFound = client.getResourceManager().getResource(generic).isPresent(); + return new NonDestroyedSkinClient.DefaultSkinCandidate(genderSpecific, genderFound, generic, genericFound); + } + + @Nullable + private static byte[] readDefaultDestroyedSkin(MinecraftClient client, NonDestroyedSkinClient.DefaultSkinKey key) { + if (client != null && key != null) { + String genderSuffix = NeedsOfNature.getLocalConfig().getPlayerGenderSelection() == NonConfig.PlayerGenderSelection.MALE ? "_m" : "_f"; + byte[] genderSpecific = readDefaultDestroyedSkin(client, key, genderSuffix); + return genderSpecific != null ? genderSpecific : readDefaultDestroyedSkin(client, key, ""); + } else { + return null; + } + } + + @Nullable + private static byte[] readDefaultDestroyedSkin(MinecraftClient client, NonDestroyedSkinClient.DefaultSkinKey key, String suffix) { + Identifier destroyedId = defaultDestroyedSkinId(key, suffix); + Resource resource = (Resource)client.getResourceManager().getResource(destroyedId).orElse(null); + if (resource == null) { + return null; + } + + try (InputStream in = resource.getInputStream()) { + return in.readAllBytes(); + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to read default destroyed skin {}.", destroyedId, e); + return null; + } + } + + private static Identifier defaultDestroyedSkinId(NonDestroyedSkinClient.DefaultSkinKey key, String suffix) { + return Identifier.of("needsofnature", "textures/player_destroyed/default/" + key.model() + "/" + key.name() + suffix + ".png"); + } + + @Nullable + private static NonDestroyedSkinClient.DefaultSkinKey resolveDefaultSkinKey(MinecraftClient client, Identifier baseSkin) { + NonDestroyedSkinClient.DefaultSkinKey parsed = parseDefaultSkinKey(baseSkin); + if (parsed != null) { + return parsed; + } + + NonDestroyedSkinClient.DefaultSkinMatch match = detectDefaultSkinMatchByPixels(client, baseSkin); + return match != null && match.accepted() ? match.key() : null; + } + + @Nullable + private static NonDestroyedSkinClient.DefaultSkinKey parseDefaultSkinKey(Identifier baseSkin) { + if (baseSkin != null && "minecraft".equals(baseSkin.getNamespace())) { + String path = baseSkin.getPath(); + if (path != null && path.startsWith("textures/entity/player/")) { + String suffix = path.substring("textures/entity/player/".length()); + if (suffix.endsWith(".png")) { + suffix = suffix.substring(0, suffix.length() - ".png".length()); + } + + String[] parts = suffix.split("/"); + if (parts.length != 2) { + return null; + } else { + String model = parts[0]; + String name = parts[1]; + if (!"slim".equals(model) && !"wide".equals(model)) { + return null; + } else { + return name.isBlank() ? null : new NonDestroyedSkinClient.DefaultSkinKey(model, name); + } + } + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static NonDestroyedSkinClient.DefaultSkinMatch detectDefaultSkinMatchByPixels(MinecraftClient client, Identifier baseSkin) { + if (client != null && baseSkin != null) { + PlayerSkinType currentModel = client.player != null && client.player.getSkin() != null ? client.player.getSkin().model() : PlayerSkinType.SLIM; + String preferredModel = currentModel == PlayerSkinType.WIDE ? "wide" : "slim"; + String fallbackModel = "wide".equals(preferredModel) ? "slim" : "wide"; + UUID playerUuid = client.player != null ? client.player.getUuid() : null; + + try { + NativeImage base = playerUuid != null ? readPlayerTextureImage(client, playerUuid, baseSkin) : readTextureImage(client, baseSkin, null); + + NonDestroyedSkinClient.DefaultSkinMatch var17; + label151: { + label152: { + label153: { + label154: { + label155: { + NonDestroyedSkinClient.DefaultSkinMatch preferred; + try { + if (base != null && base.getWidth() == 64 && base.getHeight() == 64) { + preferred = detectDefaultSkinMatchByPixels(client, base, preferredModel); + NonDestroyedSkinClient.DefaultSkinMatch fallback = detectDefaultSkinMatchByPixels(client, base, fallbackModel); + if (preferred == null) { + var17 = fallback; + break label151; + } + + if (fallback == null) { + var17 = preferred; + break label152; + } + + if (preferred.accepted() && !fallback.accepted()) { + var17 = preferred; + break label153; + } + + if (fallback.accepted() && !preferred.accepted()) { + var17 = fallback; + break label154; + } + + var17 = preferred.confidence() >= fallback.confidence() ? preferred : fallback; + break label155; + } + + preferred = null; + } catch (Throwable var11) { + if (base != null) { + try { + base.close(); + } catch (Throwable var10) { + var11.addSuppressed(var10); + } + } + + throw var11; + } + + if (base != null) { + base.close(); + } + + return preferred; + } + + if (base != null) { + base.close(); + } + + return var17; + } + + if (base != null) { + base.close(); + } + + return var17; + } + + if (base != null) { + base.close(); + } + + return var17; + } + + if (base != null) { + base.close(); + } + + return var17; + } + + if (base != null) { + base.close(); + } + + return var17; + } catch (IOException | RuntimeException e) { + return null; + } + } else { + return null; + } + } + + @Nullable + private static NonDestroyedSkinClient.DefaultSkinMatch detectDefaultSkinMatchByPixels(MinecraftClient client, NativeImage base, String model) { + NonDestroyedSkinClient.DefaultSkinMatch best = null; + + for (String name : VANILLA_DEFAULT_SKIN_NAMES) { + Identifier vanillaId = Identifier.ofVanilla("textures/entity/player/" + model + "/" + name + ".png"); + + try { + NativeImage vanilla = readTextureImage(client, vanillaId); + + try { + if (vanilla != null) { + double confidence = imageMatchConfidence(base, vanilla); + if (best == null || confidence > best.confidence()) { + best = new NonDestroyedSkinClient.DefaultSkinMatch(new NonDestroyedSkinClient.DefaultSkinKey(model, name), confidence); + } + } + } catch (Throwable var13) { + if (vanilla != null) { + try { + vanilla.close(); + } catch (Throwable var12) { + var13.addSuppressed(var12); + } + } + + throw var13; + } + + if (vanilla != null) { + vanilla.close(); + } + } catch (IOException | RuntimeException var14) { + } + } + + return best; + } + + private static double imageMatchConfidence(NativeImage first, NativeImage second) { + if (first != null && second != null) { + if (first.getWidth() == second.getWidth() && first.getHeight() == second.getHeight()) { + int checked = 0; + int matched = 0; + + for (int y = 0; y < first.getHeight(); y++) { + for (int x = 0; x < first.getWidth(); x++) { + int firstArgb = first.getColorArgb(x, y); + int secondArgb = second.getColorArgb(x, y); + int firstAlpha = firstArgb >>> 24 & 0xFF; + int secondAlpha = secondArgb >>> 24 & 0xFF; + if (firstAlpha != 0 || secondAlpha != 0) { + checked++; + if (firstArgb == secondArgb) { + matched++; + } + } + } + } + + return checked <= 0 ? 0.0 : (double)matched / checked; + } else { + return 0.0; + } + } else { + return 0.0; + } + } + + @Nullable + private static NativeImage createMaskedOverlay(MinecraftClient client, UUID playerUuid, byte[] skin, int stage) throws IOException { + NativeImage destroyed = NativeImage.read(skin); + NativeImage mask = readMask(client, playerUuid, stage); + if (mask == null) { + destroyed.close(); + return null; + } + + NativeImage out = new NativeImage(Format.RGBA, destroyed.getWidth(), destroyed.getHeight(), false); + + for (int y = 0; y < destroyed.getHeight(); y++) { + for (int x = 0; x < destroyed.getWidth(); x++) { + int skinArgb = destroyed.getColorArgb(x, y); + int maskArgb = sampleScaled(mask, x, y, destroyed.getWidth(), destroyed.getHeight()); + int factor = maskFactor(maskArgb); + int alpha = (skinArgb >>> 24 & 0xFF) * factor / 255; + out.setColorArgb(x, y, skinArgb & 16777215 | alpha << 24); + } + } + + destroyed.close(); + mask.close(); + return out; + } + + private static byte[] createGeneratedDestroyedSkin(MinecraftClient client, Identifier baseSkin) throws IOException { + NativeImage base = client != null && client.player != null + ? readPlayerTextureImage(client, client.player.getUuid(), baseSkin) + : readTextureImage(client, baseSkin, null); + if (base == null) { + return null; + } + + PlayerSkinType model = client != null && client.player != null && client.player.getSkin() != null ? client.player.getSkin().model() : PlayerSkinType.SLIM; + NativeImage var4 = base; + + Object normalizedBase; + label154: { + byte[] var25; + try { + NativeImage destroyedOverlay = readDestroyedBaseTexture(client, model); + + label156: { + try { + if (isSupportedStaticSkin(base)) { + NativeImage normalizedBasex = scaledCopy(base, 64, 64); + + try { + NativeImage out = new NativeImage(Format.RGBA, 64, 64, false); + + try { + int detectedTint = detectSkinTint(normalizedBasex); + NonConfig config = NeedsOfNature.getLocalConfig(); + int tint = config.getDestroyedSkinTintRgb(detectedTint); + if (!config.hasDestroyedSkinTintOverride()) { + config.setDestroyedSkinTintHex(formatRgb(tint)); + config.save(); + } + + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + int overlayArgb = sampleScaled(destroyedOverlay, x, y, 64, 64); + int overlayAlpha = overlayArgb >>> 24 & 0xFF; + int brightness = ((overlayArgb >>> 16 & 0xFF) + (overlayArgb >>> 8 & 0xFF) + (overlayArgb & 0xFF)) / 3; + out.setColorArgb(x, y, tintByBrightness(tint, brightness, overlayAlpha)); + } + } + + copyRegion(normalizedBasex, out, 0, 0, 64, 16); + var25 = writeImageToBytes(out); + } catch (Throwable var20) { + try { + out.close(); + } catch (Throwable var19) { + var20.addSuppressed(var19); + } + + throw var20; + } + + out.close(); + } catch (Throwable var21) { + if (normalizedBasex != null) { + try { + normalizedBasex.close(); + } catch (Throwable var18) { + var21.addSuppressed(var18); + } + } + + throw var21; + } + + if (normalizedBasex != null) { + normalizedBasex.close(); + } + break label156; + } + + normalizedBase = null; + } catch (Throwable var22) { + if (destroyedOverlay != null) { + try { + destroyedOverlay.close(); + } catch (Throwable var17) { + var22.addSuppressed(var17); + } + } + + throw var22; + } + + if (destroyedOverlay != null) { + destroyedOverlay.close(); + } + break label154; + } + + if (destroyedOverlay != null) { + destroyedOverlay.close(); + } + } catch (Throwable var23) { + if (var4 != null) { + try { + var4.close(); + } catch (Throwable var16) { + var23.addSuppressed(var16); + } + } + + throw var23; + } + + if (var4 != null) { + var4.close(); + } + + return var25; + } + + if (var4 != null) { + var4.close(); + } + + return (byte[])normalizedBase; + } + + private static NativeImage readDestroyedBaseTexture(MinecraftClient client, PlayerSkinType model) throws IOException { + boolean useMaleTexture = NeedsOfNature.getLocalConfig().getPlayerGenderSelection() == NonConfig.PlayerGenderSelection.MALE; + boolean wide = model == PlayerSkinType.WIDE; + Identifier bundledModel = defaultDestroyedTexture(useMaleTexture, wide); + NativeImage bundledModelImage = readBundledDestroyedTexture(client, bundledModel); + if (bundledModelImage != null) { + return bundledModelImage; + } else { + Identifier bundledGender = useMaleTexture ? DEFAULT_DESTROYED_TEXTURE_M : DEFAULT_DESTROYED_TEXTURE_F; + NativeImage bundledGenderImage = readBundledDestroyedTexture(client, bundledGender); + if (bundledGenderImage != null) { + return bundledGenderImage; + } else { + throw new NonDestroyedSkinClient.MissingDestroyedSkinFallbackException( + "Missing ripped skin fallback texture " + bundledModel + " and fallback " + bundledGender + ); + } + } + } + + @Nullable + private static NativeImage readBundledDestroyedTexture(MinecraftClient client, Identifier id) throws IOException { + Resource resource = (Resource)client.getResourceManager().getResource(id).orElse(null); + if (resource == null) { + return null; + } + + try (InputStream in = resource.getInputStream()) { + return NativeImage.read(in); + } + } + + private static Identifier defaultDestroyedTexture(boolean useMaleTexture, boolean wide) { + if (useMaleTexture) { + return wide ? DEFAULT_DESTROYED_TEXTURE_M_WIDE : DEFAULT_DESTROYED_TEXTURE_M_SLIM; + } else { + return wide ? DEFAULT_DESTROYED_TEXTURE_F_WIDE : DEFAULT_DESTROYED_TEXTURE_F_SLIM; + } + } + + private static void copyRegion(NativeImage source, NativeImage target, int minX, int minY, int maxX, int maxY) { + int endX = Math.min(Math.min(source.getWidth(), target.getWidth()), maxX); + int endY = Math.min(Math.min(source.getHeight(), target.getHeight()), maxY); + + for (int y = Math.max(0, minY); y < endY; y++) { + for (int x = Math.max(0, minX); x < endX; x++) { + target.setColorArgb(x, y, source.getColorArgb(x, y)); + } + } + } + + private static byte[] writeImageToBytes(NativeImage image) throws IOException { + Path temp = Files.createTempFile("non_destroyed_skin", ".png"); + + try { + image.writeTo(temp); + return Files.readAllBytes(temp); + } finally { + try { + Files.deleteIfExists(temp); + } catch (IOException var9) { + } + } + } + + private static int detectSkinTint(NativeImage skin) { + Map buckets = new HashMap<>(); + addTintSamples(skin, buckets, 9, 12, 15, 16, 5); + addTintSamples(skin, buckets, 8, 12, 16, 16, 2); + addTintSamples(skin, buckets, 20, 17, 28, 24, 3); + addTintSamples(skin, buckets, 40, 20, 54, 32, 1); + addTintSamples(skin, buckets, 32, 56, 46, 64, 1); + if (buckets.isEmpty()) { + return 13803641; + } + + List topBuckets = new ArrayList<>(buckets.values()); + topBuckets.sort((a, bx) -> Integer.compare(bx.count, a.count)); + int limit = Math.min(3, topBuckets.size()); + long r = 0L; + long g = 0L; + long b = 0L; + int totalWeight = 0; + + for (int i = 0; i < limit; i++) { + NonDestroyedSkinClient.ColorBucket bucket = topBuckets.get(i); + int weight = Math.max(1, bucket.count); + r += (long)bucket.averageR() * weight; + g += (long)bucket.averageG() * weight; + b += (long)bucket.averageB() * weight; + totalWeight += weight; + } + + return totalWeight <= 0 ? 13803641 : (int)(r / totalWeight) << 16 | (int)(g / totalWeight) << 8 | (int)(b / totalWeight); + } + + private static void addTintSamples( + NativeImage skin, Map out, int minX, int minY, int maxX, int maxY, int weight + ) { + int clampedMaxX = Math.min(maxX, skin.getWidth()); + int clampedMaxY = Math.min(maxY, skin.getHeight()); + + for (int y = Math.max(0, minY); y < clampedMaxY; y++) { + for (int x = Math.max(0, minX); x < clampedMaxX; x++) { + int argb = skin.getColorArgb(x, y); + if (isSkinTintCandidate(argb)) { + int rgb = argb & 16777215; + int key = colorBucketKey(rgb); + out.computeIfAbsent(key, ignored -> new NonDestroyedSkinClient.ColorBucket()).add(rgb, Math.max(1, weight)); + } + } + } + } + + private static int colorBucketKey(int rgb) { + int r = (rgb >>> 16 & 0xFF) / 8; + int g = (rgb >>> 8 & 0xFF) / 8; + int b = (rgb & 0xFF) / 8; + return r << 10 | g << 5 | b; + } + + private static boolean isSkinTintCandidate(int argb) { + if ((argb >>> 24 & 0xFF) < 32) { + return false; + } else { + int r = argb >>> 16 & 0xFF; + int g = argb >>> 8 & 0xFF; + int b = argb & 0xFF; + int max = Math.max(r, Math.max(g, b)); + int min = Math.min(r, Math.min(g, b)); + int brightness = (r + g + b) / 3; + if (brightness >= 35 && brightness <= 245) { + float saturation = max == 0 ? 0.0F : (float)(max - min) / max; + return saturation > 0.7F ? false : r >= b * 0.72F && g >= b * 0.5F; + } else { + return false; + } + } + } + + private static int brightness(int rgb) { + return ((rgb >>> 16 & 0xFF) + (rgb >>> 8 & 0xFF) + (rgb & 0xFF)) / 3; + } + + private static int tintByBrightness(int tintRgb, int brightness, int alpha) { + float shade = Math.max(0.0F, Math.min(1.0F, brightness / 255.0F)); + int r = Math.round((tintRgb >>> 16 & 0xFF) * shade); + int g = Math.round((tintRgb >>> 8 & 0xFF) * shade); + int b = Math.round((tintRgb & 0xFF) * shade); + return Math.max(0, Math.min(255, alpha)) << 24 | r << 16 | g << 8 | b; + } + + private static String formatRgb(int rgb) { + return String.format(Locale.ROOT, "%06X", rgb & 16777215); + } + + @Nullable + private static NativeImage readMask(MinecraftClient client, UUID playerUuid, int stage) { + if (stage > 0 && stage < 4) { + byte[][] syncedMasks = DESTROYED_SKIN_MASKS.get(playerUuid); + if (syncedMasks != null && stage < syncedMasks.length) { + byte[] synced = syncedMasks[stage]; + if (synced != null && synced.length > 0) { + try { + return NativeImage.read(synced); + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to read synced destroyed skin mask for {} stage {}.", playerUuid, stage, e); + } + } + } + + Identifier maskId = MASK_TEXTURES[stage]; + if (maskId == null) { + return null; + } + + try { + Resource resource = (Resource)client.getResourceManager().getResource(maskId).orElse(null); + if (resource == null) { + return null; + } + + try (InputStream in = resource.getInputStream()) { + return NativeImage.read(in); + } + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to read destroyed skin mask {}.", maskId, e); + return null; + } + } else { + return null; + } + } + + private static int maskFactor(int maskArgb) { + int maskAlpha = maskArgb >>> 24 & 0xFF; + int maskBrightness = ((maskArgb >>> 16 & 0xFF) + (maskArgb >>> 8 & 0xFF) + (maskArgb & 0xFF)) / 3; + return maskAlpha * maskBrightness / 255; + } + + private static int blendArgb(int baseArgb, int overlayArgb, int factor) { + int inv = 255 - factor; + int a = ((baseArgb >>> 24 & 0xFF) * inv + (overlayArgb >>> 24 & 0xFF) * factor) / 255; + int r = ((baseArgb >>> 16 & 0xFF) * inv + (overlayArgb >>> 16 & 0xFF) * factor) / 255; + int g = ((baseArgb >>> 8 & 0xFF) * inv + (overlayArgb >>> 8 & 0xFF) * factor) / 255; + int b = ((baseArgb & 0xFF) * inv + (overlayArgb & 0xFF) * factor) / 255; + return a << 24 | r << 16 | g << 8 | b; + } + + private static void applyMessOverlays(MinecraftClient client, NativeImage out, NonDestroyedSkinClient.MessState state) { + if (client != null && out != null && state != null && state.hasAnyMess()) { + applyMessRole(client, out, state.vMess(), state.tintRgb(), MESS_MASK_TEXTURES[0]); + applyMessRole(client, out, state.aMess(), state.tintRgb(), MESS_MASK_TEXTURES[1]); + applyMessRole(client, out, state.mMess(), state.tintRgb(), MESS_MASK_TEXTURES[2]); + } + } + + private static void applyTankOverlay( + MinecraftClient client, NativeImage out, NonDestroyedSkinClient.MessState state, NonDestroyedSkinClient.TankMaskType tankMaskType + ) { + if (client != null && out != null && state != null && state.hasTankOverlay()) { + Identifier maskId = tankMaskType == NonDestroyedSkinClient.TankMaskType.A ? A_MESS_TANK_TEXTURE : V_MESS_TANK_TEXTURE; + NativeImage mask = readMessMask(client, maskId); + if (mask != null) { + applyTintMask(out, mask, state.tintRgb(), state.tankAlpha()); + mask.close(); + } + } + } + + private static void applyAccessoryOverlays(MinecraftClient client, NativeImage out, List overlays) { + if (client != null && out != null && overlays != null && !overlays.isEmpty()) { + for (NonTrinketsIntegration.ResolvedVisualLayer layer : overlays) { + if (layer != null && layer.texture() != null) { + NativeImage overlay = readAccessoryOverlay(client, layer.texture()); + if (overlay != null) { + applyRawOverlay(out, overlay, layer.tintRgb()); + overlay.close(); + } + } + } + } + } + + @Nullable + private static NativeImage readAccessoryOverlay(MinecraftClient client, Identifier overlayId) { + if (client != null && overlayId != null) { + try { + Resource resource = (Resource)client.getResourceManager().getResource(overlayId).orElse(null); + if (resource == null) { + return null; + } + + try (InputStream in = resource.getInputStream()) { + NativeImage image = NativeImage.read(in); + if (image.getWidth() == 64 && image.getHeight() == 64) { + return image; + } + + image.close(); + setupWarn("[NoN] Ignoring accessory skin overlay {}: expected 64x64 PNG.", overlayId); + return null; + } + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to read accessory skin overlay {}.", overlayId, e); + return null; + } + } else { + return null; + } + } + + private static void applyRawOverlay(NativeImage out, NativeImage overlay, int tintRgb) { + int tintR = tintRgb >>> 16 & 0xFF; + int tintG = tintRgb >>> 8 & 0xFF; + int tintB = tintRgb & 0xFF; + + for (int y = 0; y < out.getHeight(); y++) { + for (int x = 0; x < out.getWidth(); x++) { + int overlayArgb = sampleScaled(overlay, x, y, out.getWidth(), out.getHeight()); + int alpha = overlayArgb >>> 24 & 0xFF; + if (alpha > 0) { + int red = (overlayArgb >>> 16 & 0xFF) * tintR / 255; + int green = (overlayArgb >>> 8 & 0xFF) * tintG / 255; + int blue = (overlayArgb & 0xFF) * tintB / 255; + overlayArgb = alpha << 24 | red << 16 | green << 8 | blue; + out.setColorArgb(x, y, blendArgb(out.getColorArgb(x, y), overlayArgb, alpha)); + } + } + } + } + + private static void applyMessRole(MinecraftClient client, NativeImage out, int mess, int tintRgb, Identifier[] masks) { + if (mess > 0 && masks != null && masks.length >= 3) { + for (NonDestroyedSkinClient.MessLayer layer : messLayers(mess)) { + if (layer.index() >= 0 && layer.index() < masks.length) { + NativeImage mask = readMessMask(client, masks[layer.index()]); + if (mask != null) { + applyTintMask(out, mask, tintRgb, layer.alpha()); + mask.close(); + } + } + } + } + } + + private static List messLayers(int mess) { + return switch (clampMess(mess)) { + case 1 -> List.of(new NonDestroyedSkinClient.MessLayer(0, 85)); + case 2 -> List.of(new NonDestroyedSkinClient.MessLayer(0, 170)); + case 3 -> List.of(new NonDestroyedSkinClient.MessLayer(0, 255)); + case 4 -> List.of(new NonDestroyedSkinClient.MessLayer(0, 170), new NonDestroyedSkinClient.MessLayer(1, 85)); + case 5 -> List.of(new NonDestroyedSkinClient.MessLayer(0, 85), new NonDestroyedSkinClient.MessLayer(1, 170)); + case 6 -> List.of(new NonDestroyedSkinClient.MessLayer(1, 255)); + case 7 -> List.of(new NonDestroyedSkinClient.MessLayer(1, 170), new NonDestroyedSkinClient.MessLayer(2, 85)); + case 8 -> List.of(new NonDestroyedSkinClient.MessLayer(1, 85), new NonDestroyedSkinClient.MessLayer(2, 170)); + case 9, 10 -> List.of(new NonDestroyedSkinClient.MessLayer(2, 255)); + default -> List.of(); + }; + } + + private static void applyTintMask(NativeImage out, NativeImage mask, int tintRgb, int alpha) { + int overlayBase = 0xFF000000 | tintRgb & 16777215; + + for (int y = 0; y < out.getHeight(); y++) { + for (int x = 0; x < out.getWidth(); x++) { + int factor = maskFactor(sampleScaled(mask, x, y, out.getWidth(), out.getHeight())) * alpha / 255; + if (factor > 0) { + out.setColorArgb(x, y, blendArgb(out.getColorArgb(x, y), overlayBase, factor)); + } + } + } + } + + @Nullable + private static NativeImage readMessMask(MinecraftClient client, Identifier maskId) { + if (client != null && maskId != null) { + try { + Resource resource = (Resource)client.getResourceManager().getResource(maskId).orElse(null); + if (resource == null) { + return null; + } + + try (InputStream in = resource.getInputStream()) { + return NativeImage.read(in); + } + } catch (IOException | RuntimeException e) { + setupWarn("[NoN] Failed to read mess mask {}.", maskId, e); + return null; + } + } else { + return null; + } + } + + private static boolean isValidDestroyedSkinPng(byte[] bytes) { + if (bytes != null && bytes.length != 0 && bytes.length <= 1048576) { + try { + NativeImage image = NativeImage.read(bytes); + + boolean var3; + try { + int width = image.getWidth(); + var3 = width == image.getHeight() && width >= 64 && width <= 512 && width % 64 == 0; + } catch (Throwable var5) { + if (image != null) { + try { + image.close(); + } catch (Throwable var4) { + var5.addSuppressed(var4); + } + } + + throw var5; + } + + if (image != null) { + image.close(); + } + + return var3; + } catch (IOException | RuntimeException e) { + return false; + } + } else { + return false; + } + } + + private static boolean isValidMaskPng(byte[] bytes) { + if (bytes != null && bytes.length != 0 && bytes.length <= 65536) { + try { + NativeImage image = NativeImage.read(bytes); + + boolean var2; + try { + var2 = image.getWidth() == 64 && image.getHeight() == 64; + } catch (Throwable var5) { + if (image != null) { + try { + image.close(); + } catch (Throwable var4) { + var5.addSuppressed(var4); + } + } + + throw var5; + } + + if (image != null) { + image.close(); + } + + return var2; + } catch (IOException | RuntimeException e) { + return false; + } + } else { + return false; + } + } + + private static int[] getOriginalSkinColors(MinecraftClient client, UUID playerUuid, @Nullable Identifier baseSkin) { + if (baseSkin == null) { + return new int[]{15920872}; + } + + NonExternalSkinCompat.SkinContext externalSkin = effectiveExternalSkin(client, playerUuid, baseSkin); + NonDestroyedSkinClient.SkinColorKey key = new NonDestroyedSkinClient.SkinColorKey( + baseSkin, textureIdentity(client, baseSkin), externalSkinKey(externalSkin) + ); + int[] cached = ORIGINAL_SKIN_COLOR_CACHE.get(key); + if (cached != null && cached.length > 0) { + return cached; + } + + int[] colors = readOriginalSkinColors(client, baseSkin, externalSkin); + ORIGINAL_SKIN_COLOR_CACHE.put(key, colors); + return colors; + } + + private static int[] readOriginalSkinColors(MinecraftClient client, Identifier baseSkin, @Nullable NonExternalSkinCompat.SkinContext externalSkin) { + try { + Resource maskResource = (Resource)client.getResourceManager().getResource(RIPPED_FABRIC_COLOR_MASK).orElse(null); + if (maskResource == null) { + return new int[]{15920872}; + } + + NativeImage image = readTextureImage(client, baseSkin, externalSkin); + if (image == null) { + return new int[]{15920872}; + } + + NativeImage var5 = image; + + int[] result; + label144: { + int[] var23; + try (InputStream maskIn = maskResource.getInputStream()) { + label147: { + NativeImage mask = NativeImage.read(maskIn); + + label125: { + try { + int[] colors = new int[image.getWidth() * image.getHeight()]; + int count = 0; + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int argb = image.getColorArgb(x, y); + if ((argb >>> 24 & 0xFF) >= 16) { + int maskArgb = sampleScaled(mask, x, y, image.getWidth(), image.getHeight()); + if (maskFactor(maskArgb) > 0) { + colors[count++] = argb & 16777215; + } + } + } + } + + if (count <= 0) { + result = new int[]{15920872}; + break label125; + } + + result = new int[count]; + System.arraycopy(colors, 0, result, 0, count); + var23 = result; + } catch (Throwable var17) { + if (mask != null) { + try { + mask.close(); + } catch (Throwable var16) { + var17.addSuppressed(var16); + } + } + + throw var17; + } + + if (mask != null) { + mask.close(); + } + break label147; + } + + if (mask != null) { + mask.close(); + } + break label144; + } + } catch (Throwable var19) { + if (var5 != null) { + try { + var5.close(); + } catch (Throwable var14) { + var19.addSuppressed(var14); + } + } + + throw var19; + } + + if (var5 != null) { + var5.close(); + } + + return var23; + } + + if (var5 != null) { + var5.close(); + } + + return result; + } catch (IOException | RuntimeException e) { + return new int[]{15920872}; + } + } + + private static int varyColor(int rgb, Random random) { + float factor = 0.85F + random.nextFloat() * 0.3F; + int r = Math.min(255, Math.max(0, Math.round((rgb >> 16 & 0xFF) * factor))); + int g = Math.min(255, Math.max(0, Math.round((rgb >> 8 & 0xFF) * factor))); + int b = Math.min(255, Math.max(0, Math.round((rgb & 0xFF) * factor))); + return r << 16 | g << 8 | b; + } + + private static void clearCachedTextures(UUID playerUuid) { + MinecraftClient client = MinecraftClient.getInstance(); + List texturesToDestroy = new ArrayList<>(); + TEXTURE_CACHE.entrySet().removeIf(entry -> { + if (!entry.getKey().playerUuid().equals(playerUuid)) { + return false; + } + + texturesToDestroy.add(entry.getValue()); + return true; + }); + destroyTextures(client, texturesToDestroy); + } + + private static void destroyTextures(@Nullable MinecraftClient client, List textureIds) { + if (client != null && textureIds != null && !textureIds.isEmpty()) { + List ids = List.copyOf(textureIds); + client.execute(() -> { + for (Identifier id : ids) { + client.getTextureManager().destroyTexture(id); + } + }); + } + } + + private static int sampleScaled(NativeImage source, int x, int y, int targetWidth, int targetHeight) { + int sourceX = Math.min(source.getWidth() - 1, Math.max(0, x * source.getWidth() / targetWidth)); + int sourceY = Math.min(source.getHeight() - 1, Math.max(0, y * source.getHeight() / targetHeight)); + return source.getColorArgb(sourceX, sourceY); + } + + private static void copyScaled(NativeImage source, NativeImage target) { + for (int y = 0; y < target.getHeight(); y++) { + for (int x = 0; x < target.getWidth(); x++) { + target.setColorArgb(x, y, sampleScaled(source, x, y, target.getWidth(), target.getHeight())); + } + } + } + + private static NativeImage scaledCopy(NativeImage source, int width, int height) { + NativeImage target = new NativeImage(Format.RGBA, width, height, false); + copyScaled(source, target); + return target; + } + + private static int clampStage(int stage) { + return Math.max(0, Math.min(4, stage)); + } + + private static int clampMess(int value) { + return Math.max(0, Math.min(10, value)); + } + + private static NonDestroyedSkinClient.TankMaskType resolveTankMaskType(@Nullable LivingEntity entity) { + if (entity instanceof GenderHolder holder) { + int mask = holder.getGenderMask() & 3; + if (mask == 1) { + return NonDestroyedSkinClient.TankMaskType.A; + } + } + + return NonDestroyedSkinClient.TankMaskType.V; + } + + private static void setupWarn(String template, Object... args) { + NeedsOfNature.LOGGER.warn(template, args); + NeedsOfNatureClient.sendClientSetupWarning(NeedsOfNature.formatLogTemplate(template, args)); + } + + private static void markAutomaticSkinUnavailable(String template, Object... args) { + if (!automaticSkinUnavailableThisConnection) { + setupWarn(template, args); + } + + automaticSkinUnavailableThisConnection = true; + uploadRetryTicks = 0; + } + + private static final class ColorBucket { + long r; + long g; + long b; + int count; + + void add(int rgb, int weight) { + this.r += (long)(rgb >>> 16 & 0xFF) * weight; + this.g += (long)(rgb >>> 8 & 0xFF) * weight; + this.b += (long)(rgb & 0xFF) * weight; + this.count += weight; + } + + int averageR() { + return this.count <= 0 ? 0 : (int)(this.r / this.count); + } + + int averageG() { + return this.count <= 0 ? 0 : (int)(this.g / this.count); + } + + int averageB() { + return this.count <= 0 ? 0 : (int)(this.b / this.count); + } + } + + private record DefaultSkinCandidate(Identifier genderSpecific, boolean genderFound, Identifier generic, boolean genericFound) { + String logString() { + return this.genderSpecific + "=" + this.genderFound + ", " + this.generic + "=" + this.genericFound; + } + } + + private record DefaultSkinKey(String model, String name) { + } + + private record DefaultSkinMatch(NonDestroyedSkinClient.DefaultSkinKey key, double confidence) { + boolean accepted() { + return this.confidence >= 0.9; + } + } + + private record EffectiveBaseTexture(Identifier id, int textureIdentity, @Nullable NonExternalSkinCompat.SkinContext externalSkin) { + } + + private record MessLayer(int index, int alpha) { + } + + private record MessState(int vMess, int aMess, int mMess, int tintRgb, int liquidStored, int liquidCapacity) { + static final NonDestroyedSkinClient.MessState EMPTY = new NonDestroyedSkinClient.MessState(0, 0, 0, 16777215, 0, 0); + + boolean hasAnyMess() { + return this.vMess > 0 || this.aMess > 0 || this.mMess > 0; + } + + boolean hasTankOverlay() { + return this.liquidStored > 0 && this.liquidCapacity > 0; + } + + boolean hasAnyVisual() { + return this.hasAnyMess() || this.hasTankOverlay(); + } + + int tankAlpha() { + if (!this.hasTankOverlay()) { + return 0; + } + + float ratio = Math.max(0.0F, Math.min(1.0F, (float)this.liquidStored / this.liquidCapacity)); + return Math.max(3, Math.min(255, Math.round(3.0F + ratio * 252.0F))); + } + + String cacheKey() { + return NonDestroyedSkinClient.clampMess(this.vMess) + + "_" + + NonDestroyedSkinClient.clampMess(this.aMess) + + "_" + + NonDestroyedSkinClient.clampMess(this.mMess) + + "_" + + Math.max(0, this.liquidStored) + + "_" + + Math.max(0, this.liquidCapacity) + + "_" + + Integer.toHexString(this.tintRgb & 16777215); + } + } + + private static final class MissingDestroyedSkinFallbackException extends IOException { + private MissingDestroyedSkinFallbackException(String message) { + super(message); + } + } + + private record SkinColorKey(Identifier textureId, int textureIdentity, String externalSkinKey) { + } + + private enum TankMaskType { + V("v"), + A("a"); + + private final String id; + + TankMaskType(String id) { + this.id = id; + } + } + + private record TextureKey( + UUID playerUuid, + int stage, + @Nullable Identifier baseTexture, + int baseTextureIdentity, + String externalSkinKey, + NonDestroyedSkinClient.MessState messState, + NonDestroyedSkinClient.TankMaskType tankMaskType, + List accessoryOverlays + ) { + } + + private record TextureReadKey(Identifier textureId, int textureIdentity, String externalSkinKey) { + } +} diff --git a/src/main/java/com/nonid/client/NonEnergizedEffectTextures.java b/src/main/java/com/nonid/client/NonEnergizedEffectTextures.java new file mode 100644 index 0000000..b5673a7 --- /dev/null +++ b/src/main/java/com/nonid/client/NonEnergizedEffectTextures.java @@ -0,0 +1,39 @@ +package com.nonid.client; + +import com.nonid.effect.NonStatusEffects; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; + +public final class NonEnergizedEffectTextures { + private static final Identifier ENERGIZED_SPRITE_ID = Identifier.of("needsofnature", "mob_effect/energized"); + private static final Identifier ENERGIZED_STAGE_ONE_TEXTURE = Identifier.of("needsofnature", "textures/mob_effects/energized1.png"); + private static final Identifier ENERGIZED_STAGE_TWO_TEXTURE = Identifier.of("needsofnature", "textures/mob_effects/energized2.png"); + private static final Identifier ENERGIZED_STAGE_THREE_TEXTURE = Identifier.of("needsofnature", "textures/mob_effects/energized3.png"); + + private NonEnergizedEffectTextures() { + } + + public static boolean isEnergizedSprite(Identifier sprite) { + return ENERGIZED_SPRITE_ID.equals(sprite); + } + + public static Identifier resolveTexture(ClientPlayerEntity player) { + if (player == null) { + return ENERGIZED_STAGE_ONE_TEXTURE; + } else { + StatusEffectInstance effect = player.getStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGIZED)); + if (effect == null) { + return ENERGIZED_STAGE_ONE_TEXTURE; + } else { + int amplifier = Math.max(0, effect.getAmplifier()); + if (amplifier >= 2) { + return ENERGIZED_STAGE_THREE_TEXTURE; + } else { + return amplifier == 1 ? ENERGIZED_STAGE_TWO_TEXTURE : ENERGIZED_STAGE_ONE_TEXTURE; + } + } + } + } +} diff --git a/src/main/java/com/nonid/client/NonExternalSkinCompat.java b/src/main/java/com/nonid/client/NonExternalSkinCompat.java new file mode 100644 index 0000000..6e698d9 --- /dev/null +++ b/src/main/java/com/nonid/client/NonExternalSkinCompat.java @@ -0,0 +1,116 @@ +package com.nonid.client; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class NonExternalSkinCompat { + private NonExternalSkinCompat() { + } + + @Nullable + static NonExternalSkinCompat.SkinContext resolve(MinecraftClient client, UUID playerUuid, @Nullable Identifier expectedTexture) { + return client != null && playerUuid != null ? selectMatching(expectedTexture, candidates(client, playerUuid)) : null; + } + + @Nullable + static NonExternalSkinCompat.SkinContext resolveSelected(MinecraftClient client, UUID playerUuid) { + if (client != null && playerUuid != null) { + List candidates = candidates(client, playerUuid); + return candidates.isEmpty() ? null : candidates.getFirst(); + } else { + return null; + } + } + + private static List candidates(MinecraftClient client, UUID playerUuid) { + String playerName = playerName(client, playerUuid); + List candidates = new ArrayList<>(2); + NonSkinOverrideCompat.SkinSelection skinOverride = NonSkinOverrideCompat.getSelectedSkin(playerName, playerUuid); + if (skinOverride != null) { + candidates.add( + new NonExternalSkinCompat.SkinContext( + "skin_overrides", + skinOverride.textureId(), + skinOverride.selectionId(), + skinOverride.friendlyName(), + skinOverride.folderName(), + skinOverride.sourcePath(), + skinOverride.sourceFingerprint() + ) + ); + } + + NonQuickSkinCompat.SkinSelection quickSkin = NonQuickSkinCompat.getSelectedSkin(playerUuid); + if (quickSkin != null) { + candidates.add( + new NonExternalSkinCompat.SkinContext( + "quickskin", quickSkin.textureId(), quickSkin.skinId(), quickSkin.friendlyName(), quickSkin.folderName(), null, "skin=" + quickSkin.skinId() + ) + ); + } + + return candidates; + } + + @Nullable + static NonExternalSkinCompat.SkinContext selectMatching(@Nullable Identifier expectedTexture, List candidates) { + if (candidates != null && !candidates.isEmpty()) { + if (expectedTexture == null) { + return candidates.getFirst(); + } + + for (NonExternalSkinCompat.SkinContext candidate : candidates) { + if (expectedTexture.equals(candidate.textureId())) { + return candidate; + } + } + + return null; + } else { + return null; + } + } + + private static String playerName(MinecraftClient client, UUID playerUuid) { + if (client.player != null && playerUuid.equals(client.player.getUuid())) { + return client.player.getGameProfile().name(); + } + + if (client.world != null) { + PlayerEntity player = client.world.getPlayerByUuid(playerUuid); + if (player != null) { + return player.getGameProfile().name(); + } + } + + return ""; + } + + record SkinContext( + String providerId, + Identifier textureId, + String selectionId, + @Nullable String friendlyName, + @Nullable String folderName, + @Nullable Path sourcePath, + String sourceFingerprint + ) { + String cacheKey() { + return this.providerId + "\u0000" + this.textureId + "\u0000" + this.selectionId + "\u0000" + this.sourceFingerprint; + } + + String selectionKey() { + return this.providerId + "\u0000" + this.selectionId + "\u0000" + this.sourceFingerprint; + } + + String displayName() { + return this.friendlyName != null ? this.friendlyName : this.selectionId; + } + } +} diff --git a/src/main/java/com/nonid/client/NonFilledEffectTextures.java b/src/main/java/com/nonid/client/NonFilledEffectTextures.java new file mode 100644 index 0000000..729d7b8 --- /dev/null +++ b/src/main/java/com/nonid/client/NonFilledEffectTextures.java @@ -0,0 +1,39 @@ +package com.nonid.client; + +import com.nonid.effect.NonStatusEffects; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; + +public final class NonFilledEffectTextures { + private static final Identifier FILLED_SPRITE_ID = Identifier.of("needsofnature", "mob_effect/filled"); + private static final Identifier FILLED_STAGE_ONE_TEXTURE = Identifier.of("needsofnature", "textures/mob_effects/filled1.png"); + private static final Identifier FILLED_STAGE_TWO_TEXTURE = Identifier.of("needsofnature", "textures/mob_effects/filled2.png"); + private static final Identifier FILLED_STAGE_THREE_TEXTURE = Identifier.of("needsofnature", "textures/mob_effects/filled3.png"); + + private NonFilledEffectTextures() { + } + + public static boolean isFilledSprite(Identifier sprite) { + return FILLED_SPRITE_ID.equals(sprite); + } + + public static Identifier resolveTexture(ClientPlayerEntity player) { + if (player == null) { + return FILLED_STAGE_ONE_TEXTURE; + } else { + StatusEffectInstance effect = player.getStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FILLED)); + if (effect == null) { + return FILLED_STAGE_ONE_TEXTURE; + } else { + int amplifier = Math.max(0, effect.getAmplifier()); + if (amplifier >= 2) { + return FILLED_STAGE_THREE_TEXTURE; + } else { + return amplifier == 1 ? FILLED_STAGE_TWO_TEXTURE : FILLED_STAGE_ONE_TEXTURE; + } + } + } + } +} diff --git a/src/main/java/com/nonid/client/NonGenderSelectionScreen.java b/src/main/java/com/nonid/client/NonGenderSelectionScreen.java new file mode 100644 index 0000000..851e356 --- /dev/null +++ b/src/main/java/com/nonid/client/NonGenderSelectionScreen.java @@ -0,0 +1,122 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.network.SetPlayerGenderC2SPayload; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public final class NonGenderSelectionScreen extends Screen { + private final int allowedMask; + private final int currentMask; + private final boolean permanent; + private int panelX; + private int panelY; + private int panelW; + private int panelH; + + public NonGenderSelectionScreen(int allowedMask, int currentMask, boolean permanent) { + super(Text.translatable("screen.needsofnature.gender_selection.title")); + this.allowedMask = (allowedMask & 7) == 0 ? 7 : allowedMask & 7; + this.currentMask = currentMask & 3; + this.permanent = permanent; + } + + protected void init() { + this.panelW = Math.min(340, Math.max(260, this.width - 40)); + this.panelH = 176; + this.panelX = (this.width - this.panelW) / 2; + this.panelY = Math.max(34, (this.height - this.panelH) / 2); + int buttonW = this.panelW - 56; + int buttonX = this.panelX + 28; + int y = this.panelY + 72; + this.addGenderButton(buttonX, y, buttonW, NonConfig.PlayerGenderSelection.MALE); + this.addGenderButton(buttonX, y + 26, buttonW, NonConfig.PlayerGenderSelection.FEMALE); + this.addGenderButton(buttonX, y + 52, buttonW, NonConfig.PlayerGenderSelection.BOTH); + } + + private void addGenderButton(int x, int y, int w, NonConfig.PlayerGenderSelection selection) { + ButtonWidget button = ButtonWidget.builder(this.genderText(selection), ignored -> this.choose(selection)).dimensions(x, y, w, 22).build(); + button.active = this.isAllowed(selection.mask()); + this.addDrawableChild(button); + } + + private void choose(NonConfig.PlayerGenderSelection selection) { + if (this.isAllowed(selection.mask())) { + if (ClientPlayNetworking.canSend(SetPlayerGenderC2SPayload.ID)) { + ClientPlayNetworking.send(new SetPlayerGenderC2SPayload(selection.mask(), true)); + } + + NonConfig config = NeedsOfNature.getLocalConfig(); + if (config != null) { + config.setPlayerGenderSelection(selection); + config.save(); + NonWildfireGenderSync.syncFromMask(config, selection.mask()); + } + + MinecraftClient.getInstance().setScreen(null); + } + } + + private boolean isAllowed(int mask) { + int bit = switch (mask & 3) { + case 1 -> 1; + case 2 -> 2; + case 3 -> 4; + default -> 0; + }; + return bit != 0 && (this.allowedMask & bit) != 0; + } + + private Text genderText(NonConfig.PlayerGenderSelection selection) { + Text base = switch (selection) { + case MALE -> Text.translatable("screen.needsofnature.gender_selection.option.male").copy().formatted(Formatting.AQUA); + case FEMALE -> Text.translatable("screen.needsofnature.gender_selection.option.female").copy().formatted(Formatting.LIGHT_PURPLE); + case BOTH -> Text.translatable("screen.needsofnature.gender_selection.option.both").copy().formatted(Formatting.GOLD); + }; + return (Text)(selection.mask() == this.currentMask ? Text.translatable("screen.needsofnature.gender_selection.current", new Object[]{base}) : base); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.drawPanel(context); + super.render(context, mouseX, mouseY, deltaTicks); + this.drawTitle(context); + } + + public void renderInGameBackground(DrawContext context) { + } + + private void drawTitle(DrawContext context) { + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, this.panelY + 18, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.translatable(this.permanent ? "screen.needsofnature.gender_selection.permanent" : "screen.needsofnature.gender_selection.changeable"), + this.width / 2, + this.panelY + 40, + this.permanent ? -10630 : -5708033 + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("screen.needsofnature.gender_selection.subtitle"), this.width / 2, this.panelY + 54, -5197648 + ); + } + + private void drawPanel(DrawContext context) { + context.fillGradient(0, 0, this.width, this.height, -871888629, -585427182); + context.fill(this.panelX - 2, this.panelY - 2, this.panelX + this.panelW + 2, this.panelY + this.panelH + 2, -1442840576); + context.fill(this.panelX, this.panelY, this.panelX + this.panelW, this.panelY + this.panelH, -300345324); + context.fill(this.panelX, this.panelY, this.panelX + this.panelW, this.panelY + 2, -12166); + context.fill(this.panelX, this.panelY + this.panelH - 2, this.panelX + this.panelW, this.panelY + this.panelH, -8760784); + context.fill(this.panelX, this.panelY, this.panelX + 2, this.panelY + this.panelH, -8760784); + context.fill(this.panelX + this.panelW - 2, this.panelY, this.panelX + this.panelW, this.panelY + this.panelH, -8760784); + context.fill(this.panelX + 18, this.panelY + 64, this.panelX + this.panelW - 18, this.panelY + 65, 1716207668); + context.fill(this.panelX + 18, this.panelY + this.panelH - 28, this.panelX + this.panelW - 18, this.panelY + this.panelH - 27, 1716207668); + } + + public void close() { + } +} diff --git a/src/main/java/com/nonid/client/NonHudOverlay.java b/src/main/java/com/nonid/client/NonHudOverlay.java new file mode 100644 index 0000000..c3f050a --- /dev/null +++ b/src/main/java/com/nonid/client/NonHudOverlay.java @@ -0,0 +1,1051 @@ +package com.nonid.client; + +import com.nonid.EnergyHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.effect.NonStatusEffects; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.network.AttackStateS2CPayload; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.texture.SpriteAtlasTexture; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Atlases; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.joml.Matrix3x2fStack; + +@Environment(EnvType.CLIENT) +public final class NonHudOverlay { + private static final Identifier ENERGY_BAR_BG = Identifier.of("needsofnature", "hud/progress_background"); + private static final Identifier ENERGY_BAR_FILL = Identifier.of("needsofnature", "hud/progress_full"); + private static final Identifier ATTACK_BAR_BG = Identifier.of("needsofnature", "hud/attack_background"); + private static final Identifier ATTACK_BAR_FILL = Identifier.of("needsofnature", "hud/attack_full"); + private static final Identifier ATTACK_PROMPT = Identifier.of("needsofnature", "hud/ad_mash"); + private static final Identifier LIQUID_TANK_BG = Identifier.of("needsofnature", "hud/liquid_background"); + private static final Identifier LIQUID_TANK_FILL = Identifier.of("needsofnature", "hud/liquid_full"); + private static final Identifier ENERGY_HEART = Identifier.of("needsofnature", "textures/particle/smallheart.png"); + private static final Identifier HUD_TEXTURE_SIZE_RELOADER_ID = NeedsOfNature.id("hud_texture_sizes"); + private static final int ENERGY_BAR_DEFAULT_W = 182; + private static final int ENERGY_BAR_DEFAULT_H = 5; + private static final int ENERGY_BAR_BOTTOM_MARGIN = 27; + private static final int ATTACK_BAR_DEFAULT_W = 81; + private static final int ATTACK_BAR_DEFAULT_H = 5; + private static final int ATTACK_BAR_DEFAULT_Y_OFFSET = -4; + private static final int ATTACK_PROMPT_DEFAULT_TEX_W = 68; + private static final int ATTACK_PROMPT_DEFAULT_TEX_H = 128; + private static final int ENERGY_HEART_DEFAULT_W = 7; + private static final int ENERGY_HEART_DEFAULT_H = 7; + private static final int LIQUID_DEFAULT_W = 8; + private static final float LOOP_PROGRESS_HIDDEN = -1.0F; + private static final int LIQUID_DEFAULT_H = 31; + private static final float ATTACK_ESCAPE_MIN_HEALTH_FACTOR = 0.3F; + private static final float ATTACK_PROGRESS_EASE_RATE = 18.0F; + private static final float ATTACK_PROGRESS_EASE_SNAP_EPSILON = 5.0E-4F; + private static final float ATTACK_PROGRESS_EASE_MAX_FRAME_SECONDS = 0.1F; + private static final long DEFEATED_EASE_DURATION_TICKS = 300L; + private static final float DEFEATED_MAX_STEP_MULTIPLIER = 4.0F; + private static boolean hideVanillaHud = false; + private static float loopProgress = 0.0F; + private static long lastUpdateTick = -1L; + private static UUID attackInstance = null; + private static float attackProgress = 0.0F; + private static float displayedAttackProgress = 0.0F; + private static NonHudOverlay.AttackKey lastAttackKey = NonHudOverlay.AttackKey.NONE; + private static long lastAttackDecayTick = -1L; + private static boolean attackInputClearPending = false; + private static boolean attackEscapable = true; + private static AttackStateS2CPayload.EscapeProfile attackEscapeProfile = AttackStateS2CPayload.EscapeProfile.NONE; + private static AttackStateS2CPayload.PvpRole attackPvpRole = AttackStateS2CPayload.PvpRole.NONE; + private static int liquidStored = 0; + private static int liquidCapacity = 200; + private static int liquidTintRgb = 15920063; + private static int runtimeAttackEscapeHits = -1; + private static boolean uiPreviewEnabled = false; + private static boolean uiPreviewOverrideActive = false; + private static int previewEnergyX = 0; + private static int previewEnergyY = 0; + private static int previewEnergyHeartX = 0; + private static int previewEnergyHeartY = 0; + private static int previewAttackX = 0; + private static int previewAttackY = 0; + private static int previewPromptX = 0; + private static int previewPromptY = 0; + private static int previewLiquidX = 0; + private static int previewLiquidY = 0; + private static boolean previewEnergyVisible = true; + private static boolean previewEnergyHeartVisible = true; + private static boolean previewAttackVisible = true; + private static boolean previewPromptVisible = true; + private static boolean previewLiquidVisible = true; + private static volatile NonHudOverlay.SpriteSize energyHeartSize = new NonHudOverlay.SpriteSize(7, 7); + + private NonHudOverlay() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.CLIENT_RESOURCES) + .registerReloader(HUD_TEXTURE_SIZE_RELOADER_ID, (SynchronousResourceReloader)NonHudOverlay::reloadDirectTextureSizes); + } + + public static void clientTick(MinecraftClient client) { + if (client != null) { + ClientPlayerEntity player = client.player; + if (player == null || client.world == null) { + clearAnimationHudState(); + liquidStored = 0; + liquidTintRgb = 15920063; + } else if (player.isSpectator()) { + clearAnimationHudState(); + } else { + boolean animActive = ClientAnimationRuntime.isActorPendingOrActive(player.getUuid()); + UUID activeInstance = ClientAnimationRuntime.findLatestActiveInstanceContaining(player.getUuid()); + if (!animActive) { + clearAnimationHudState(); + } else { + hideVanillaHud = true; + if (attackInstance != null && activeInstance != null && !activeInstance.equals(attackInstance) && !attackInputClearPending) { + resetAttackState(); + } + + long now = client.world.getTime(); + if (lastUpdateTick != now) { + lastUpdateTick = now; + loopProgress = computeLoopProgress(player.getUuid(), now); + tickAttackDecay(now); + } + } + } + } + } + + public static void onDisconnect(MinecraftClient client) { + clearAnimationHudState(); + liquidStored = 0; + liquidTintRgb = 15920063; + clearPlayerRuntimeSettings(); + } + + private static void clearAnimationHudState() { + hideVanillaHud = false; + loopProgress = 0.0F; + resetAttackState(); + } + + public static boolean shouldHideVanillaHud() { + return hideVanillaHud; + } + + public static boolean shouldRenderGameplayHud() { + MinecraftClient client = MinecraftClient.getInstance(); + return client != null && client.player != null && client.options != null && !client.options.hudHidden && !client.player.isSpectator(); + } + + public static void renderOverlay(DrawContext ctx) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && ctx != null) { + if (shouldRenderGameplayHud()) { + updateDisplayedAttackProgress(client); + NonHudOverlay.UiOffsets offsets = resolveUiOffsets(false); + NonHudOverlay.UiVisibility visibility = resolveUiVisibility(false); + NonHudOverlay.LayeredSprites energySprites = resolveLayeredSprites(ENERGY_BAR_BG, ENERGY_BAR_FILL, 182, 5); + NonHudOverlay.Rect baseEnergy = energyBarRect(ctx, energySprites, 0, 0); + NonHudOverlay.Rect energy = offsetRect(baseEnergy, offsets.energyX(), offsets.energyY()); + NonHudOverlay.LayeredSprites attackSprites = resolveLayeredSprites(ATTACK_BAR_BG, ATTACK_BAR_FILL, 81, 5); + NonHudOverlay.Rect baseAttack = attackBarRect(baseEnergy, attackSprites, 0, 0); + float displayedLoopProgress = loopProgress; + if (client.player != null && client.world != null) { + float tickProgress = client.getRenderTickCounter().getTickProgress(false); + displayedLoopProgress = computeLoopProgress(client.player.getUuid(), (float)client.world.getTime() + Math.max(0.0F, tickProgress)); + } + + float progress = displayedLoopProgress < 0.0F ? -1.0F : Math.max(0.0F, Math.min(1.0F, displayedLoopProgress)); + if (visibility.energy() && progress >= 0.0F) { + drawHorizontalBar(ctx, ENERGY_BAR_BG, ENERGY_BAR_FILL, energySprites, energy, progress); + } + + if (client.player != null && attackInstance != null && ClientAnimationRuntime.isActorPendingOrActive(client.player.getUuid())) { + UUID inst = ClientAnimationRuntime.findLatestActiveInstanceContaining(client.player.getUuid()); + if ((inst == null || inst.equals(attackInstance)) && attackEscapable) { + NonHudOverlay.Rect attack = attackBarDisplayRect(baseAttack, offsets); + if (visibility.attack()) { + drawHorizontalBar(ctx, ATTACK_BAR_BG, ATTACK_BAR_FILL, attackSprites, attack, displayedAttackProgress); + } + + if (visibility.prompt()) { + NonHudOverlay.AttackPromptInfo promptInfo = resolveAttackPromptInfo(); + NonHudOverlay.Rect prompt = attackPromptRect(ctx, baseAttack, promptInfo, offsets.promptX(), offsets.promptY()); + int v = lastAttackKey == NonHudOverlay.AttackKey.LEFT ? promptInfo.frameHeight() : 0; + drawAttackPrompt(ctx, promptInfo, prompt, v); + } + } + } + + renderLiquidOverlay(ctx); + } + } + } + + public static void renderLiquidOverlay(DrawContext ctx) { + if (shouldRenderGameplayHud()) { + renderLiquidOverlayInternal(ctx, false); + renderEnergyHeartOverlay(ctx, false); + } + } + + public static void renderUiPreview(DrawContext ctx) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && ctx != null) { + NonHudOverlay.UiOffsets offsets = resolveUiOffsets(true); + NonHudOverlay.UiVisibility visibility = resolveUiVisibility(true); + NonHudOverlay.LayeredSprites energySprites = resolveLayeredSprites(ENERGY_BAR_BG, ENERGY_BAR_FILL, 182, 5); + NonHudOverlay.Rect baseEnergy = energyBarRect(ctx, energySprites, 0, 0); + NonHudOverlay.Rect energy = offsetRect(baseEnergy, offsets.energyX(), offsets.energyY()); + NonHudOverlay.LayeredSprites attackSprites = resolveLayeredSprites(ATTACK_BAR_BG, ATTACK_BAR_FILL, 81, 5); + NonHudOverlay.Rect baseAttack = attackBarRect(baseEnergy, attackSprites, 0, 0); + if (visibility.energy()) { + drawHorizontalBar(ctx, ENERGY_BAR_BG, ENERGY_BAR_FILL, energySprites, energy, 0.6F); + } + + NonHudOverlay.Rect attack = attackBarDisplayRect(baseAttack, offsets); + if (visibility.attack()) { + drawHorizontalBar(ctx, ATTACK_BAR_BG, ATTACK_BAR_FILL, attackSprites, attack, 0.4F); + } + + if (visibility.prompt()) { + NonHudOverlay.AttackPromptInfo promptInfo = resolveAttackPromptInfo(); + NonHudOverlay.Rect prompt = attackPromptRect(ctx, baseAttack, promptInfo, offsets.promptX(), offsets.promptY()); + drawAttackPrompt(ctx, promptInfo, prompt, 0); + } + + renderLiquidOverlayInternal(ctx, true); + renderEnergyHeartOverlay(ctx, true); + } + } + + public static NonHudOverlay.UiPreviewBounds getUiPreviewBounds(DrawContext ctx) { + if (ctx == null) { + return NonHudOverlay.UiPreviewBounds.empty(); + } + + NonHudOverlay.UiOffsets offsets = resolveUiOffsets(true); + NonHudOverlay.LayeredSprites energySprites = resolveLayeredSprites(ENERGY_BAR_BG, ENERGY_BAR_FILL, 182, 5); + NonHudOverlay.Rect baseEnergy = energyBarRect(ctx, energySprites, 0, 0); + NonHudOverlay.Rect energy = offsetRect(baseEnergy, offsets.energyX(), offsets.energyY()); + NonHudOverlay.LayeredSprites attackSprites = resolveLayeredSprites(ATTACK_BAR_BG, ATTACK_BAR_FILL, 81, 5); + NonHudOverlay.Rect baseAttack = attackBarRect(baseEnergy, attackSprites, 0, 0); + NonHudOverlay.Rect attack = attackBarDisplayRect(baseAttack, offsets); + NonHudOverlay.AttackPromptInfo promptInfo = resolveAttackPromptInfo(); + NonHudOverlay.Rect prompt = attackPromptRect(ctx, baseAttack, promptInfo, offsets.promptX(), offsets.promptY()); + NonHudOverlay.LayeredSprites liquidSprites = resolveLiquidSprites(); + NonHudOverlay.Rect liquid = liquidTankRect(ctx, liquidSprites, offsets.liquidX(), offsets.liquidY()); + NonHudOverlay.Rect heart = energyHeartRect(ctx, true); + return new NonHudOverlay.UiPreviewBounds(energy, heart, attack, prompt, liquid); + } + + private static void renderLiquidOverlayInternal(DrawContext ctx, boolean preview) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && ctx != null) { + if (resolveUiVisibility(preview).liquid()) { + int capacity = liquidCapacity; + int stored = liquidStored; + if (preview) { + capacity = Math.max(1, capacity); + stored = Math.max(1, capacity / 2); + } + + NonHudOverlay.UiOffsets offsets = resolveUiOffsets(preview); + NonHudOverlay.LayeredSprites sprites = resolveLiquidSprites(); + NonHudOverlay.Rect tank = liquidTankRect(ctx, sprites, offsets.liquidX(), offsets.liquidY()); + if (stored > 0 && capacity > 0) { + float progress = Math.max(0.0F, Math.min(1.0F, (float)stored / capacity)); + NonHudOverlay.Rect background = bottomLeftAlignedRect(tank, sprites.background()); + NonHudOverlay.Rect fill = bottomLeftAlignedRect(tank, sprites.fill()); + int filled = Math.max(0, Math.min(fill.height(), Math.round(progress * fill.height()))); + drawGuiSprite(ctx, LIQUID_TANK_BG, background); + if (filled > 0) { + int fillY = fill.y() + fill.height() - filled; + ctx.enableScissor(fill.x(), fillY, fill.x() + fill.width(), fill.y() + fill.height()); + int fillColor = preview ? -1 : NonLiquidColors.toOpaqueArgb(liquidTintRgb); + drawGuiSprite(ctx, LIQUID_TANK_FILL, fill, fillColor); + ctx.disableScissor(); + } + } + } + } + } + + private static void renderEnergyHeartOverlay(DrawContext ctx, boolean preview) { + if (resolveUiVisibility(preview).energyHeart()) { + NonHudOverlay.Rect rect = energyHeartRect(ctx, preview); + renderEnergyHeart(ctx, rect, preview); + } + } + + private static NonHudOverlay.Rect energyHeartRect(DrawContext ctx, boolean preview) { + NonHudOverlay.LayeredSprites liquidSprites = resolveLiquidSprites(); + NonHudOverlay.Rect tank = liquidTankRect(ctx, liquidSprites, 0, 0); + NonHudOverlay.UiOffsets offsets = resolveUiOffsets(preview); + NonHudOverlay.SpriteSize heart = energyHeartSize; + int centerX = tank.x() + tank.width() / 2 + offsets.energyHeartX(); + int x = centerX - heart.width() / 2; + int y = tank.y() - 1 - heart.height() + offsets.energyHeartY(); + return new NonHudOverlay.Rect(x, y, heart.width(), heart.height()); + } + + private static NonHudOverlay.Rect liquidTankRect(DrawContext ctx, NonHudOverlay.LayeredSprites sprites, int offsetX, int offsetY) { + int heartsX = ctx.getScaledWindowWidth() / 2 - 91; + int heartsY = ctx.getScaledWindowHeight() - 39; + int x = heartsX + 192 - 8 + offsetX; + int bottom = heartsY + 10 + 29 + offsetY; + return new NonHudOverlay.Rect(x, bottom - sprites.height(), sprites.width(), sprites.height()); + } + + private static void renderEnergyHeart(DrawContext ctx, NonHudOverlay.Rect rect, boolean preview) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && ctx != null) { + int energizedLevel = preview ? 2 : getEnergizedLevel(client.player); + if (energizedLevel > 0) { + double pulse = computeHeartPulse(energizedLevel); + + float scale = switch (energizedLevel) { + case 1 -> 1.0F + (float)pulse * 0.35F; + case 2 -> 1.0F + (float)pulse * 0.45F; + default -> 1.0F + (float)pulse * 0.85F; + }; + int centerX = rect.x() + rect.width() / 2; + int centerY = rect.y() + rect.height() / 2; + + int alpha = switch (energizedLevel) { + case 1 -> 150 + (int)Math.round(pulse * 70.0); + case 2 -> 185 + (int)Math.round(pulse * 65.0); + default -> 200 + (int)Math.round(pulse * 55.0); + }; + int color = Math.max(0, Math.min(255, alpha)) << 24 | 16777215; + Matrix3x2fStack matrices = ctx.getMatrices(); + matrices.pushMatrix(); + matrices.translate(centerX, centerY); + matrices.scale(scale, scale); + matrices.translate(-centerX, -centerY); + ctx.drawTexture( + RenderPipelines.GUI_TEXTURED, ENERGY_HEART, rect.x(), rect.y(), 0.0F, 0.0F, rect.width(), rect.height(), rect.width(), rect.height(), color + ); + matrices.popMatrix(); + } + } + } + + public static int getEnergizedLevel(ClientPlayerEntity player) { + if (player == null) { + return 0; + } + + for (StatusEffectInstance effect : player.getStatusEffects()) { + if (effect.getEffectType().value() == NonStatusEffects.ENERGIZED) { + return Math.max(1, Math.min(3, effect.getAmplifier() + 1)); + } + } + + return 0; + } + + public static double computeHeartPulse(int energizedLevel) { + return computeHeartPulse(energizedLevel, 0L); + } + + public static double computeHeartPulse(int energizedLevel, long timeOffsetMs) { + double periodMs = switch (energizedLevel) { + case 1 -> 900.0; + case 2 -> 450.0; + default -> 225.0; + }; + + double attackMs = switch (energizedLevel) { + case 1 -> 120.0; + case 2 -> 55.0; + default -> 30.0; + }; + + double decayMs = switch (energizedLevel) { + case 1 -> 200.0; + case 2 -> 150.0; + default -> 100.0; + }; + double elapsedMs = Math.floorMod(System.currentTimeMillis() + Math.max(0L, timeOffsetMs), (long)periodMs); + if (elapsedMs >= attackMs + decayMs) { + return 0.0; + } else if (elapsedMs < attackMs) { + double attack = elapsedMs / attackMs; + return attack * attack; + } else { + double decay = (elapsedMs - attackMs) / decayMs; + double remaining = Math.max(0.0, 1.0 - decay); + return remaining * remaining; + } + } + + private static void drawHorizontalBar( + DrawContext ctx, Identifier backgroundId, Identifier fillId, NonHudOverlay.LayeredSprites sprites, NonHudOverlay.Rect bounds, float progress + ) { + float clamped = Math.max(0.0F, Math.min(1.0F, progress)); + NonHudOverlay.Rect background = bottomLeftAlignedRect(bounds, sprites.background()); + NonHudOverlay.Rect fill = bottomLeftAlignedRect(bounds, sprites.fill()); + int filled = Math.max(0, Math.min(fill.width(), Math.round(clamped * fill.width()))); + drawGuiSprite(ctx, backgroundId, background); + if (filled > 0) { + ctx.enableScissor(fill.x(), fill.y(), fill.x() + filled, fill.y() + fill.height()); + drawGuiSprite(ctx, fillId, fill); + ctx.disableScissor(); + } + } + + private static void drawAttackPrompt(DrawContext ctx, NonHudOverlay.AttackPromptInfo promptInfo, NonHudOverlay.Rect bounds, int v) { + ctx.drawGuiTexture( + RenderPipelines.GUI_TEXTURED, + ATTACK_PROMPT, + promptInfo.textureWidth(), + promptInfo.textureHeight(), + 0, + v, + bounds.x(), + bounds.y(), + bounds.width(), + bounds.height() + ); + } + + private static void drawGuiSprite(DrawContext ctx, Identifier id, NonHudOverlay.Rect bounds) { + ctx.drawGuiTexture(RenderPipelines.GUI_TEXTURED, id, bounds.x(), bounds.y(), bounds.width(), bounds.height()); + } + + private static void drawGuiSprite(DrawContext ctx, Identifier id, NonHudOverlay.Rect bounds, int color) { + ctx.drawGuiTexture(RenderPipelines.GUI_TEXTURED, id, bounds.x(), bounds.y(), bounds.width(), bounds.height(), color); + } + + private static NonHudOverlay.Rect energyBarRect(DrawContext ctx, NonHudOverlay.LayeredSprites sprites, int offsetX, int offsetY) { + int x = (ctx.getScaledWindowWidth() - sprites.width()) / 2 + offsetX; + int bottom = ctx.getScaledWindowHeight() - 27 + offsetY; + return new NonHudOverlay.Rect(x, bottom - sprites.height(), sprites.width(), sprites.height()); + } + + private static NonHudOverlay.Rect attackBarRect(NonHudOverlay.Rect baseEnergy, NonHudOverlay.LayeredSprites sprites, int offsetX, int offsetY) { + int x = baseEnergy.x() + (baseEnergy.width() - sprites.width()) / 2 + offsetX; + int y = baseEnergy.y() - 8 - sprites.height() + offsetY; + return new NonHudOverlay.Rect(x, y, sprites.width(), sprites.height()); + } + + private static NonHudOverlay.Rect attackBarDisplayRect(NonHudOverlay.Rect baseAttack, NonHudOverlay.UiOffsets offsets) { + return offsetRect(baseAttack, offsets.attackX(), offsets.attackY() + -4); + } + + private static NonHudOverlay.Rect attackPromptRect( + DrawContext ctx, NonHudOverlay.Rect baseAttack, NonHudOverlay.AttackPromptInfo promptInfo, int offsetX, int offsetY + ) { + int frameW = Math.max(1, promptInfo.frameWidth()); + int frameH = Math.max(1, promptInfo.frameHeight()); + int x = (ctx.getScaledWindowWidth() - frameW) / 2 + offsetX; + int y = Math.max(4, baseAttack.y() - frameH - 6 + offsetY); + return new NonHudOverlay.Rect(x, y, frameW, frameH); + } + + private static NonHudOverlay.Rect bottomLeftAlignedRect(NonHudOverlay.Rect bounds, NonHudOverlay.SpriteSize size) { + return new NonHudOverlay.Rect(bounds.x(), bounds.y() + bounds.height() - size.height(), size.width(), size.height()); + } + + private static NonHudOverlay.Rect offsetRect(NonHudOverlay.Rect rect, int offsetX, int offsetY) { + return new NonHudOverlay.Rect(rect.x() + offsetX, rect.y() + offsetY, rect.width(), rect.height()); + } + + public static double getCurrentAnimationStageProgress(UUID playerUuid) { + MinecraftClient client = MinecraftClient.getInstance(); + if (playerUuid != null && client != null && client.world != null) { + float progress = computeLoopProgress(playerUuid, client.world.getTime()); + return progress == -1.0F ? Double.NaN : clampProgress(progress); + } else { + return Double.NaN; + } + } + + private static float computeLoopProgress(UUID playerUuid, double nowTick) { + UUID inst = ClientAnimationRuntime.findLatestActiveInstanceContaining(playerUuid); + if (inst == null) { + return 0.0F; + } else { + long stageStartTick = ClientAnimationRuntime.findStageStartTick(inst); + long stageEndTick = ClientAnimationRuntime.findStageEndTick(inst); + AnimationStageInfo currentStage = ClientAnimationRuntime.findCurrentStage(inst); + if (stageStartTick < 0L || currentStage == null) { + return 0.0F; + } else { + return stageEndTick <= stageStartTick ? -1.0F : clampProgress((nowTick - stageStartTick) / (stageEndTick - stageStartTick)); + } + } + } + + private static float clampProgress(double progress) { + return (float)Math.max(0.0, Math.min(1.0, progress)); + } + + private static NonHudOverlay.AttackPromptInfo resolveAttackPromptInfo() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return new NonHudOverlay.AttackPromptInfo(68, 128); + } + + SpriteAtlasTexture atlas = client.getAtlasManager().getAtlasTexture(Atlases.GUI); + Sprite sprite = atlas.getSprite(ATTACK_PROMPT); + if (sprite == null) { + return new NonHudOverlay.AttackPromptInfo(68, 128); + } + + int texW = sprite.getContents().getWidth(); + int texH = sprite.getContents().getHeight(); + return texW > 0 && texH > 0 ? new NonHudOverlay.AttackPromptInfo(texW, texH) : new NonHudOverlay.AttackPromptInfo(68, 128); + } + + private static NonHudOverlay.LayeredSprites resolveLiquidSprites() { + return resolveLayeredSprites(LIQUID_TANK_BG, LIQUID_TANK_FILL, 8, 31); + } + + private static NonHudOverlay.LayeredSprites resolveLayeredSprites(Identifier background, Identifier fill, int fallbackW, int fallbackH) { + return new NonHudOverlay.LayeredSprites(resolveSpriteSize(background, fallbackW, fallbackH), resolveSpriteSize(fill, fallbackW, fallbackH)); + } + + private static NonHudOverlay.SpriteSize resolveSpriteSize(Identifier id, int fallbackW, int fallbackH) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + + SpriteAtlasTexture atlas = client.getAtlasManager().getAtlasTexture(Atlases.GUI); + Sprite sprite = atlas.getSprite(id); + if (sprite == null) { + return new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + + int texW = sprite.getContents().getWidth(); + int texH = sprite.getContents().getHeight(); + return texW > 0 && texH > 0 ? new NonHudOverlay.SpriteSize(texW, texH) : new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + + private static void reloadDirectTextureSizes(ResourceManager manager) { + energyHeartSize = readDirectTextureSize(manager, ENERGY_HEART, 7, 7); + } + + private static NonHudOverlay.SpriteSize readDirectTextureSize(ResourceManager manager, Identifier id, int fallbackW, int fallbackH) { + if (manager == null) { + return new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + + try { + Resource resource = (Resource)manager.getResource(id).orElse(null); + if (resource == null) { + return new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + + try (InputStream input = resource.getInputStream()) { + NativeImage image = NativeImage.read(input); + + label106: { + NonHudOverlay.SpriteSize var9; + try { + int width = image.getWidth(); + int height = image.getHeight(); + if (width <= 0 || height <= 0) { + break label106; + } + + var9 = new NonHudOverlay.SpriteSize(width, height); + } catch (Throwable var12) { + if (image != null) { + try { + image.close(); + } catch (Throwable var11) { + var12.addSuppressed(var11); + } + } + + throw var12; + } + + if (image != null) { + image.close(); + } + + return var9; + } + + if (image != null) { + image.close(); + } + + return new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + } catch (IOException | RuntimeException var14) { + return new NonHudOverlay.SpriteSize(fallbackW, fallbackH); + } + } + + public static void setRuntimeAttackEscapeHits(int attackEscapeHits) { + runtimeAttackEscapeHits = Math.max(1, Math.min(50, attackEscapeHits)); + } + + public static int getRuntimeLoopSeconds() { + return resolveRuntimeLoopSeconds(); + } + + public static int getRuntimePeakLoopSeconds() { + return resolveRuntimePeakLoopSeconds(); + } + + public static int getRuntimeAttackEscapeHits() { + return resolveRuntimeAttackEscapeHits(); + } + + public static double getRuntimeAttackDecayPerSecond() { + return resolveRuntimeAttackDecayPerSecond(); + } + + public static int getRuntimeAttackEscapeDamageDifficultyPercent() { + return resolveRuntimeAttackEscapeDamageDifficultyPercent(); + } + + public static boolean getRuntimeAttackCreativePlayers() { + NonConfig config = NeedsOfNature.getConfig(); + return config != null && config.canAttackCreativePlayers(); + } + + private static int resolveRuntimeLoopSeconds() { + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 15 : config.getLoopProgressSeconds(); + } + + private static int resolveRuntimePeakLoopSeconds() { + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 7 : config.getPeakLoopProgressSeconds(); + } + + private static int resolveRuntimeAttackEscapeHits() { + if (runtimeAttackEscapeHits >= 0) { + return Math.max(1, Math.min(50, runtimeAttackEscapeHits)); + } + + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 12 : config.getAttackEscapeHits(); + } + + private static double resolveRuntimeAttackDecayPerSecond() { + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 0.2 : config.getAttackDecayPerSecond(); + } + + private static int resolveRuntimeAttackEscapeDamageDifficultyPercent() { + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 100 : config.getAttackEscapeDamageDifficultyPercent(); + } + + private static void clearPlayerRuntimeSettings() { + runtimeAttackEscapeHits = -1; + } + + public static void setLiquidState(int stored, int capacity, int tintRgb) { + liquidStored = Math.max(0, stored); + liquidCapacity = Math.max(1, capacity); + liquidTintRgb = tintRgb & 16777215; + if (liquidStored > liquidCapacity) { + liquidStored = liquidCapacity; + } + } + + public static float getLiquidFillRatio() { + if (liquidCapacity <= 0) { + return 0.0F; + } + + float ratio = (float)liquidStored / liquidCapacity; + return Math.max(0.0F, Math.min(1.0F, ratio)); + } + + public static int getLiquidTintRgb() { + return liquidTintRgb; + } + + public static void setEscapeControlState( + UUID instanceId, + boolean attack, + boolean escapable, + AttackStateS2CPayload.EscapeProfile escapeProfile, + AttackStateS2CPayload.PvpRole pvpRole, + float progress + ) { + if (instanceId != null) { + if (attack) { + if (!instanceId.equals(attackInstance)) { + resetAttackState(); + attackInstance = instanceId; + attackInputClearPending = true; + } + + attackEscapable = escapable; + attackEscapeProfile = sanitizeEscapeProfile(escapeProfile, attack); + attackPvpRole = pvpRole == null ? AttackStateS2CPayload.PvpRole.NONE : pvpRole; + if (attackPvpRole != AttackStateS2CPayload.PvpRole.NONE) { + attackProgress = MathHelper.clamp(progress, 0.0F, 1.0F); + } + } else if (instanceId.equals(attackInstance)) { + resetAttackState(); + } + } + } + + private static AttackStateS2CPayload.EscapeProfile sanitizeEscapeProfile(AttackStateS2CPayload.EscapeProfile profile, boolean attack) { + if (!attack) { + return AttackStateS2CPayload.EscapeProfile.NONE; + } else { + return profile != null && profile != AttackStateS2CPayload.EscapeProfile.NONE ? profile : AttackStateS2CPayload.EscapeProfile.NORMAL; + } + } + + public static boolean isAttackInstance(UUID instanceId) { + return instanceId != null && instanceId.equals(attackInstance); + } + + public static void recordAttackInput(UUID instanceId, NonHudOverlay.AttackKey key) { + if (isAttackInstance(instanceId)) { + if (key != NonHudOverlay.AttackKey.NONE) { + if (attackEscapable) { + if (lastAttackKey != key) { + if (attackPvpRole == AttackStateS2CPayload.PvpRole.NONE) { + float step = resolveAttackProgressStep(); + attackProgress = Math.min(1.0F, attackProgress + step); + } + + lastAttackKey = key; + } + } + } + } + } + + public static boolean isServerControlledAttack() { + return attackPvpRole != AttackStateS2CPayload.PvpRole.NONE; + } + + public static boolean isAttackProgressComplete() { + return !isServerControlledAttack() && attackProgress >= 1.0F; + } + + public static void resetAttackProgress() { + attackProgress = 0.0F; + displayedAttackProgress = 0.0F; + lastAttackKey = NonHudOverlay.AttackKey.NONE; + lastAttackDecayTick = -1L; + } + + private static void updateDisplayedAttackProgress(MinecraftClient client) { + if (attackInstance == null) { + displayedAttackProgress = attackProgress; + } else { + float difference = attackProgress - displayedAttackProgress; + if (Math.abs(difference) <= 5.0E-4F) { + displayedAttackProgress = attackProgress; + } else { + float frameSeconds = MathHelper.clamp(client.getRenderTickCounter().getDynamicDeltaTicks() / 20.0F, 0.0F, 0.1F); + float alpha = 1.0F - (float)Math.exp(-18.0F * frameSeconds); + displayedAttackProgress = MathHelper.clamp(MathHelper.lerp(alpha, displayedAttackProgress, attackProgress), 0.0F, 1.0F); + } + } + } + + private static void resetAttackState() { + attackInstance = null; + resetAttackProgress(); + attackInputClearPending = false; + attackEscapable = true; + attackEscapeProfile = AttackStateS2CPayload.EscapeProfile.NONE; + attackPvpRole = AttackStateS2CPayload.PvpRole.NONE; + } + + public static void setUiPreviewEnabled(boolean enabled) { + uiPreviewEnabled = enabled; + if (!enabled) { + uiPreviewOverrideActive = false; + } + } + + public static boolean isUiPreviewEnabled() { + return uiPreviewEnabled; + } + + public static void setUiPreviewOffsets( + int energyX, + int energyY, + int attackX, + int attackY, + int promptX, + int promptY, + int liquidX, + int liquidY, + int energyHeartX, + int energyHeartY, + boolean energyVisible, + boolean energyHeartVisible, + boolean attackVisible, + boolean promptVisible, + boolean liquidVisible + ) { + uiPreviewOverrideActive = true; + previewEnergyX = energyX; + previewEnergyY = energyY; + previewAttackX = attackX; + previewAttackY = attackY; + previewPromptX = promptX; + previewPromptY = promptY; + previewLiquidX = liquidX; + previewLiquidY = liquidY; + previewEnergyHeartX = energyHeartX; + previewEnergyHeartY = energyHeartY; + previewEnergyVisible = energyVisible; + previewEnergyHeartVisible = energyHeartVisible; + previewAttackVisible = attackVisible; + previewPromptVisible = promptVisible; + previewLiquidVisible = liquidVisible; + } + + private static NonHudOverlay.UiOffsets resolveUiOffsets(boolean preview) { + NonConfig config = NeedsOfNature.getConfig(); + int energyX = config == null ? 0 : config.getUiEnergyOffsetX(); + int energyY = config == null ? 0 : config.getUiEnergyOffsetY(); + int energyHeartX = config == null ? 0 : config.getUiEnergyHeartOffsetX(); + int energyHeartY = config == null ? 0 : config.getUiEnergyHeartOffsetY(); + int attackX = config == null ? 0 : config.getUiAttackOffsetX(); + int attackY = config == null ? 0 : config.getUiAttackOffsetY(); + int promptX = config == null ? 0 : config.getUiPromptOffsetX(); + int promptY = config == null ? 0 : config.getUiPromptOffsetY(); + int liquidX = config == null ? 0 : config.getUiLiquidOffsetX(); + int liquidY = config == null ? 0 : config.getUiLiquidOffsetY(); + return preview && uiPreviewOverrideActive + ? new NonHudOverlay.UiOffsets( + previewEnergyX, + previewEnergyY, + previewEnergyHeartX, + previewEnergyHeartY, + previewAttackX, + previewAttackY, + previewPromptX, + previewPromptY, + previewLiquidX, + previewLiquidY + ) + : new NonHudOverlay.UiOffsets(energyX, energyY, energyHeartX, energyHeartY, attackX, attackY, promptX, promptY, liquidX, liquidY); + } + + private static NonHudOverlay.UiVisibility resolveUiVisibility(boolean preview) { + NonConfig config = NeedsOfNature.getConfig(); + boolean energy = config == null || config.isUiEnergyVisible(); + boolean energyHeart = config == null || config.isUiEnergyHeartVisible(); + boolean attack = config == null || config.isUiAttackVisible(); + boolean prompt = config == null || config.isUiPromptVisible(); + boolean liquid = config == null || config.isUiLiquidVisible(); + return preview && uiPreviewOverrideActive + ? new NonHudOverlay.UiVisibility(previewEnergyVisible, previewEnergyHeartVisible, previewAttackVisible, previewPromptVisible, previewLiquidVisible) + : new NonHudOverlay.UiVisibility(energy, energyHeart, attack, prompt, liquid); + } + + public static boolean consumeAttackInputClear() { + if (!attackInputClearPending) { + return false; + } + + attackInputClearPending = false; + return true; + } + + private static float resolveAttackProgressStep() { + int hits = resolveRuntimeAttackEscapeHits(); + if (hits <= 0) { + return 1.0F; + } + + float base = 1.0F / hits; + float healthFactor = resolveAttackHealthFactor(); + if (healthFactor <= 0.0F) { + return 0.0F; + } + + float energyFactor = resolveAttackEnergyFactor(); + if (energyFactor <= 0.0F) { + return 0.0F; + } + + float rngScale = healthFactor >= 0.999F ? 1.0F : randomAttackScale(); + float defeatedEase = resolveDefeatedEaseProgress(); + if (defeatedEase <= 0.0F) { + return base * healthFactor * rngScale * energyFactor; + } + + float adjustedHealth = MathHelper.lerp(defeatedEase, healthFactor, 1.0F); + float adjustedEnergy = MathHelper.lerp(defeatedEase, energyFactor, 1.0F); + float adjustedRng = MathHelper.lerp(defeatedEase, rngScale, 1.0F); + float multiplier = MathHelper.lerp(defeatedEase, 1.0F, 4.0F); + return base * adjustedHealth * adjustedRng * adjustedEnergy * multiplier; + } + + private static float resolveDefeatedEaseProgress() { + if (attackInstance == null) { + return 0.0F; + } + + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null || client.player == null || client.world == null) { + return 0.0F; + } + + if (attackEscapeProfile != AttackStateS2CPayload.EscapeProfile.DEFEATED) { + return 0.0F; + } + + Long startTick = ClientAnimationRuntime.findStartTickForActor(client.player.getUuid()); + if (startTick == null) { + return 0.0F; + } + + long elapsed = Math.max(0L, client.world.getTime() - startTick); + return MathHelper.clamp((float)elapsed / 300.0F, 0.0F, 1.0F); + } + + private static float resolveAttackHealthFactor() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + float health = client.player.getHealth(); + float maxHealth = client.player.getMaxHealth(); + float minHealth = 0.5F; + if (maxHealth <= minHealth) { + return 0.3F; + } + + float rawFactor = (health - minHealth) / (maxHealth - minHealth); + rawFactor = MathHelper.clamp(rawFactor, 0.0F, 1.0F); + float impact = resolveRuntimeAttackEscapeDamageDifficultyPercent() / 100.0F; + float factor = 1.0F - (1.0F - rawFactor) * impact; + return MathHelper.clamp(factor, 0.3F, 1.0F); + } else { + return 1.0F; + } + } + + private static float resolveAttackEnergyFactor() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player instanceof EnergyHolder holder) { + int var6 = Math.max(1, holder.getMaxEnergy()); + float energy = Math.max(0.0F, Math.min(var6, holder.getEnergy())); + if (energy <= 100.0F) { + float t = energy / 100.0F; + return 1.0F - 0.1F * t; + } else { + float t = Math.min(1.0F, (energy - 100.0F) / 100.0F); + float factor = 0.9F - 0.82F * t * t; + return Math.max(0.08F, factor); + } + } else { + return 1.0F; + } + } + + private static float randomAttackScale() { + return 0.5F + ThreadLocalRandom.current().nextFloat(); + } + + private static void tickAttackDecay(long nowTick) { + if (attackInstance == null) { + lastAttackDecayTick = nowTick; + } else if (isServerControlledAttack()) { + lastAttackDecayTick = nowTick; + } else if (lastAttackDecayTick < 0L) { + lastAttackDecayTick = nowTick; + } else { + long elapsedTicks = nowTick - lastAttackDecayTick; + if (elapsedTicks > 0L) { + lastAttackDecayTick = nowTick; + double decayPerSecond = resolveRuntimeAttackDecayPerSecond(); + if (!(decayPerSecond <= 0.0)) { + float decayPerTick = (float)(decayPerSecond / 20.0); + attackProgress = Math.max(0.0F, attackProgress - decayPerTick * (float)elapsedTicks); + } + } + } + } + + public enum AttackKey { + NONE, + LEFT, + RIGHT; + } + + private record AttackPromptInfo(int textureWidth, int textureHeight) { + int frameWidth() { + return this.textureWidth; + } + + int frameHeight() { + return this.textureHeight / 2; + } + } + + private record LayeredSprites(NonHudOverlay.SpriteSize background, NonHudOverlay.SpriteSize fill) { + int width() { + return Math.max(this.background.width(), this.fill.width()); + } + + int height() { + return Math.max(this.background.height(), this.fill.height()); + } + } + + public record Rect(int x, int y, int width, int height) { + public boolean contains(double mouseX, double mouseY) { + return mouseX >= this.x && mouseX < this.x + this.width && mouseY >= this.y && mouseY < this.y + this.height; + } + + public NonHudOverlay.Rect padded(int padding) { + int safe = Math.max(0, padding); + return new NonHudOverlay.Rect(this.x - safe, this.y - safe, this.width + safe * 2, this.height + safe * 2); + } + } + + private record SpriteSize(int width, int height) { + } + + private record UiOffsets( + int energyX, int energyY, int energyHeartX, int energyHeartY, int attackX, int attackY, int promptX, int promptY, int liquidX, int liquidY + ) { + } + + public record UiPreviewBounds( + NonHudOverlay.Rect energy, NonHudOverlay.Rect energyHeart, NonHudOverlay.Rect attack, NonHudOverlay.Rect prompt, NonHudOverlay.Rect liquid + ) { + public static NonHudOverlay.UiPreviewBounds empty() { + NonHudOverlay.Rect empty = new NonHudOverlay.Rect(0, 0, 0, 0); + return new NonHudOverlay.UiPreviewBounds(empty, empty, empty, empty, empty); + } + } + + private record UiVisibility(boolean energy, boolean energyHeart, boolean attack, boolean prompt, boolean liquid) { + } +} diff --git a/src/main/java/com/nonid/client/NonIconButtonTextures.java b/src/main/java/com/nonid/client/NonIconButtonTextures.java new file mode 100644 index 0000000..c379b58 --- /dev/null +++ b/src/main/java/com/nonid/client/NonIconButtonTextures.java @@ -0,0 +1,14 @@ +package com.nonid.client; + +import net.minecraft.util.Identifier; + +public final class NonIconButtonTextures { + public static final Identifier SKIN_REPAIR = Identifier.of("needsofnature", "textures/gui/skin_repair.png"); + public static final Identifier PREGNANCY_CYCLE_SAFE = Identifier.of("needsofnature", "textures/gui/pregnancy_cycle_safe.png"); + public static final Identifier PREGNANCY_CYCLE_NORMAL = Identifier.of("needsofnature", "textures/gui/pregnancy_cycle_normal.png"); + public static final Identifier PREGNANCY_CYCLE_RISKY = Identifier.of("needsofnature", "textures/gui/pregnancy_cycle_risky.png"); + public static final Identifier PREGNANCY_CYCLE_OVULATION = Identifier.of("needsofnature", "textures/gui/pregnancy_cycle_ovulation.png"); + + private NonIconButtonTextures() { + } +} diff --git a/src/main/java/com/nonid/client/NonInventoryScreenHooks.java b/src/main/java/com/nonid/client/NonInventoryScreenHooks.java new file mode 100644 index 0000000..11f6a7b --- /dev/null +++ b/src/main/java/com/nonid/client/NonInventoryScreenHooks.java @@ -0,0 +1,270 @@ +package com.nonid.client; + +import com.mojang.blaze3d.textures.GpuTextureView; +import com.nonid.DestroyedSkinHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonSkinRepairBlocks; +import com.nonid.PregnancyCyclePhase; +import com.nonid.mixin.client.HandledScreenAccessor; +import com.nonid.network.ManualDamageDestroyedSkinC2SPayload; +import com.nonid.network.RepairDestroyedSkinC2SPayload; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents.AfterInit; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents.BeforeRender; +import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents.AllowMouseClick; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public final class NonInventoryScreenHooks { + private static final int SURVIVAL_ICON_SIZE = 14; + private static final int SURVIVAL_BUTTON_SIZE = 18; + private static final int CREATIVE_ICON_SIZE = 12; + private static final int CREATIVE_BUTTON_SIZE = 16; + private static final int CYCLE_ICON_OFFSET_X = -2; + private static final int CYCLE_ICON_OFFSET_Y = -1; + private static final int SURVIVAL_CYCLE_X = 27; + private static final int SURVIVAL_REPAIR_X = 58; + private static final int SURVIVAL_BUTTON_Y = 61; + private static final int SURVIVAL_PREVIEW_MIN_X = 20; + private static final int SURVIVAL_PREVIEW_MAX_X = 82; + private static final int SURVIVAL_PREVIEW_MIN_Y = 4; + private static final int SURVIVAL_PREVIEW_MAX_Y = 82; + private static final int CREATIVE_CYCLE_X = 72; + private static final int CREATIVE_REPAIR_X = 90; + private static final int CREATIVE_BUTTON_Y = 34; + private static final int CREATIVE_PREVIEW_MIN_X = 73; + private static final int CREATIVE_PREVIEW_MAX_X = 105; + private static final int CREATIVE_PREVIEW_MIN_Y = 6; + private static final int CREATIVE_PREVIEW_MAX_Y = 49; + private static boolean registered; + + private NonInventoryScreenHooks() { + } + + public static void register() { + if (!registered) { + registered = true; + ScreenEvents.AFTER_INIT.register((AfterInit)(client, screen, scaledWidth, scaledHeight) -> { + if (screen instanceof HandledScreen handledScreen && isPlayerInventoryScreen(screen)) { + NonInventoryScreenHooks.InventoryControls controls = new NonInventoryScreenHooks.InventoryControls(screen, handledScreen); + Screens.getButtons(screen).add(controls.repairButton); + Screens.getButtons(screen).add(controls.cycleButton); + controls.update(); + ScreenEvents.beforeRender(screen).register((BeforeRender)(current, context, mouseX, mouseY, delta) -> controls.update()); + ScreenMouseEvents.allowMouseClick(screen).register((AllowMouseClick)(current, click) -> click.button() != 0 || !controls.tryManualDamage(click)); + } + }); + } + } + + private static boolean isPlayerInventoryScreen(Screen screen) { + return screen instanceof InventoryScreen || screen instanceof CreativeInventoryScreen; + } + + private static boolean isInventoryViewVisible(Screen screen) { + return !(screen instanceof CreativeInventoryScreen creative && !creative.isInventoryTabSelected()); + } + + private static final class InventoryControls { + private final Screen screen; + private final HandledScreen handledScreen; + private final ButtonWidget repairButton; + private final NonInventoryScreenHooks.PregnancyCycleButton cycleButton; + + private InventoryControls(Screen screen, HandledScreen handledScreen) { + this.screen = screen; + this.handledScreen = handledScreen; + boolean creative = screen instanceof CreativeInventoryScreen; + int buttonSize = creative ? 16 : 18; + int iconSize = creative ? 12 : 14; + this.repairButton = new NonInventoryScreenHooks.SkinRepairButton(buttonSize, iconSize); + this.cycleButton = new NonInventoryScreenHooks.PregnancyCycleButton(buttonSize, iconSize); + } + + private void update() { + boolean inventoryVisible = NonInventoryScreenHooks.isInventoryViewVisible(this.screen); + this.positionButtons(); + ClientPlayerEntity player = MinecraftClient.getInstance().player; + int damage = player instanceof DestroyedSkinHolder holder ? holder.getDestroyedSkinDamage() : 0; + boolean repairVisible = inventoryVisible && damage > 0; + boolean nearRepairBlock = repairVisible + && player != null + && NonSkinRepairBlocks.isNearRepairBlock(player.getEntityWorld(), player.getBlockPos(), NeedsOfNature.getConfig()); + boolean allowCraftingTable = NeedsOfNature.getConfig().isCraftingTableSkinRepairAllowed(); + this.repairButton.visible = repairVisible; + this.repairButton.active = nearRepairBlock; + this.repairButton + .setTooltip( + Tooltip.of( + Text.translatable( + nearRepairBlock + ? "gui.needsofnature.repair_skin" + : (allowCraftingTable ? "gui.needsofnature.repair_skin.needs_loom_or_crafting_table" : "gui.needsofnature.repair_skin.needs_loom") + ) + ) + ); + boolean cycleVisible = inventoryVisible && NonPregnancyClientState.isCycleEnabled(); + this.cycleButton.visible = cycleVisible; + if (cycleVisible) { + PregnancyCyclePhase phase = NonPregnancyClientState.getCyclePhase(); + + Text phaseText = Text.translatable(switch (phase) { + case SAFE -> "config.needsofnature.pregnancy_cycle.safe"; + case NORMAL -> "config.needsofnature.pregnancy_cycle.normal"; + case RISKY -> "config.needsofnature.pregnancy_cycle.risky"; + case OVULATION -> "config.needsofnature.pregnancy_cycle.ovulation"; + }); + this.cycleButton.setPhase(phase); + this.cycleButton + .setTooltip( + Tooltip.of(Text.translatable("gui.needsofnature.pregnancy_cycle.tooltip", new Object[]{NonPregnancyClientState.getCycleDay(), 30, phaseText})) + ); + } + } + + private void positionButtons() { + HandledScreenAccessor accessor = (HandledScreenAccessor)this.handledScreen; + int left = accessor.non$getLeft(); + int top = accessor.non$getTop(); + if (this.screen instanceof CreativeInventoryScreen) { + this.cycleButton.setPosition(left + 72, top + 34); + this.repairButton.setPosition(left + 90, top + 34); + } else { + this.cycleButton.setPosition(left + 27, top + 61); + this.repairButton.setPosition(left + 58, top + 61); + } + } + + private boolean tryManualDamage(Click click) { + if (NonInventoryScreenHooks.isInventoryViewVisible(this.screen) && this.isInsidePlayerPreview(click.x(), click.y())) { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player == null || !this.hasShearsForManualDamage(player)) { + return false; + } + + if (!ClientPlayNetworking.canSend(ManualDamageDestroyedSkinC2SPayload.ID)) { + return false; + } + + ClientPlayNetworking.send(new ManualDamageDestroyedSkinC2SPayload()); + return true; + } else { + return false; + } + } + + private boolean hasShearsForManualDamage(ClientPlayerEntity player) { + ItemStack cursorStack = this.handledScreen.getScreenHandler().getCursorStack(); + return cursorStack.isOf(Items.SHEARS) || player.getMainHandStack().isOf(Items.SHEARS) || player.getOffHandStack().isOf(Items.SHEARS); + } + + private boolean isInsidePlayerPreview(double mouseX, double mouseY) { + HandledScreenAccessor accessor = (HandledScreenAccessor)this.handledScreen; + int left = accessor.non$getLeft(); + int top = accessor.non$getTop(); + return this.screen instanceof CreativeInventoryScreen + ? mouseX >= left + 73 && mouseX <= left + 105 && mouseY >= top + 6 && mouseY <= top + 49 + : mouseX >= left + 20 && mouseX <= left + 82 && mouseY >= top + 4 && mouseY <= top + 82; + } + } + + private static final class PregnancyCycleButton extends ButtonWidget { + private final int iconSize; + private PregnancyCyclePhase phase = PregnancyCyclePhase.NORMAL; + + private PregnancyCycleButton(int buttonSize, int iconSize) { + super(0, 0, buttonSize, buttonSize, net.minecraft.text.Text.translatable(""), button -> {}, DEFAULT_NARRATION_SUPPLIER); + this.iconSize = iconSize; + } + + private void setPhase(PregnancyCyclePhase phase) { + this.phase = phase == null ? PregnancyCyclePhase.NORMAL : phase; + } + + protected void drawIcon(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + Identifier texture = switch (this.phase) { + case SAFE -> NonIconButtonTextures.PREGNANCY_CYCLE_SAFE; + case NORMAL -> NonIconButtonTextures.PREGNANCY_CYCLE_NORMAL; + case RISKY -> NonIconButtonTextures.PREGNANCY_CYCLE_RISKY; + case OVULATION -> NonIconButtonTextures.PREGNANCY_CYCLE_OVULATION; + }; + GpuTextureView textureView = MinecraftClient.getInstance().getTextureManager().getTexture(texture).getGlTextureView(); + int textureWidth = Math.max(1, textureView.getWidth(0)); + int textureHeight = Math.max(1, textureView.getHeight(0)); + float displayScale = this.iconSize / 14.0F; + int drawWidth = Math.max(1, Math.round(textureWidth * displayScale)); + int drawHeight = Math.max(1, Math.round(textureHeight * displayScale)); + int anchorX = this.getX() + (this.getWidth() - this.iconSize) / 2 + -2; + int anchorBottom = this.getY() + (this.getHeight() - this.iconSize) / 2 + this.iconSize + -1; + context.drawTexture( + RenderPipelines.GUI_TEXTURED, + texture, + anchorX, + anchorBottom - drawHeight, + 0.0F, + 0.0F, + drawWidth, + drawHeight, + textureWidth, + textureHeight, + textureWidth, + textureHeight + ); + } + + public void playDownSound(SoundManager soundManager) { + } + } + + private static final class SkinRepairButton extends ButtonWidget { + private final int iconSize; + + private SkinRepairButton(int buttonSize, int iconSize) { + super(0, 0, buttonSize, buttonSize, net.minecraft.text.Text.translatable(""), button -> { + if (ClientPlayNetworking.canSend(RepairDestroyedSkinC2SPayload.ID)) { + ClientPlayNetworking.send(new RepairDestroyedSkinC2SPayload()); + } + }, DEFAULT_NARRATION_SUPPLIER); + this.iconSize = iconSize; + } + + protected void drawIcon(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + int iconX = this.getX() + (this.getWidth() - this.iconSize) / 2; + int iconY = this.getY() + (this.getHeight() - this.iconSize) / 2; + int color = this.active ? -1 : -2130706433; + context.drawTexture( + RenderPipelines.GUI_TEXTURED, + NonIconButtonTextures.SKIN_REPAIR, + iconX, + iconY, + 0.0F, + 0.0F, + this.iconSize, + this.iconSize, + this.iconSize, + this.iconSize, + color + ); + } + + public void playDownSound(SoundManager soundManager) { + } + } +} diff --git a/src/main/java/com/nonid/client/NonLiquidColors.java b/src/main/java/com/nonid/client/NonLiquidColors.java new file mode 100644 index 0000000..57526bf --- /dev/null +++ b/src/main/java/com/nonid/client/NonLiquidColors.java @@ -0,0 +1,82 @@ +package com.nonid.client; + +import com.nonid.NonConfig; +import java.util.Locale; +import java.util.Map; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonLiquidColors { + public static final int MIXED_RGB = 15920063; + public static final int ENTITY_RGB = 16776427; + + private NonLiquidColors() { + } + + public static int toOpaqueArgb(int rgb) { + return 0xFF000000 | rgb & 16777215; + } + + public static int resolveEntityRgb(@Nullable Identifier entityTypeId, @Nullable NonConfig config) { + if (entityTypeId != null && config != null) { + Integer override = config.getLiquidColorOverrideRgb(entityTypeId.toString()); + return override != null ? override : 16776427; + } else { + return 16776427; + } + } + + @Nullable + public static String normalizeHexColor(@Nullable String raw) { + if (raw == null) { + return null; + } + + String text = raw.trim(); + if (text.isEmpty()) { + return null; + } + + if (text.startsWith("#")) { + text = text.substring(1); + } + + if (text.length() != 6) { + return null; + } + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + boolean isHex = c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'; + if (!isHex) { + return null; + } + } + + return text.toUpperCase(Locale.ROOT); + } + + @Nullable + public static Integer parseHexColor(@Nullable String raw) { + String normalized = normalizeHexColor(raw); + if (normalized == null) { + return null; + } + + try { + return Integer.parseInt(normalized, 16) & 16777215; + } catch (NumberFormatException e) { + return null; + } + } + + @Nullable + public static Integer resolveEntityOverrideFromMap(@Nullable Map map, @Nullable String entityId) { + if (map != null && entityId != null) { + String hex = map.get(entityId); + return hex == null ? null : parseHexColor(hex); + } else { + return null; + } + } +} diff --git a/src/main/java/com/nonid/client/NonPackStartupWarnings.java b/src/main/java/com/nonid/client/NonPackStartupWarnings.java new file mode 100644 index 0000000..4525032 --- /dev/null +++ b/src/main/java/com/nonid/client/NonPackStartupWarnings.java @@ -0,0 +1,229 @@ +package com.nonid.client; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.pack.NonExternalPackProvider; +import com.nonid.pack.NonPackRootResolver; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +public final class NonPackStartupWarnings { + private static final String WARNING_PATH = "assets/needsofnature/non_warnings.json"; + private static boolean checkedThisSession = false; + + private NonPackStartupWarnings() { + } + + public static void clientTick(MinecraftClient client) { + if (client != null && !checkedThisSession) { + if (client.getOverlay() == null) { + if (client.currentScreen instanceof TitleScreen) { + checkedThisSession = true; + NonPackStartupWarnings.Warning warning = findHighestPriorityWarning(); + if (warning != null && !isAcknowledged(warning.id())) { + client.setScreen(new NonPackWarningScreen(client.currentScreen, warning)); + } + } + } + } + } + + public static void acknowledge(String warningId) { + if (warningId != null && !warningId.isBlank()) { + NonConfig config = NeedsOfNature.getLocalConfig(); + if (config != null && config.acknowledgeStartupWarning(warningId)) { + config.save(); + } + } + } + + @Nullable + private static NonPackStartupWarnings.Warning findHighestPriorityWarning() { + Path packsDir = NonExternalPackProvider.resolveDefaultPacksDir(); + List discovered = NonExternalPackProvider.listExternalPackFileNames(packsDir); + if (discovered.isEmpty()) { + return null; + } + + NonConfig config = NeedsOfNature.getLocalConfig(); + List configuredOrder = config == null ? List.of() : config.getExternalNoNPackLoadOrder(); + + for (String packName : NonExternalPackProvider.normalizeConfiguredOrder(configuredOrder, discovered)) { + NonPackStartupWarnings.Warning warning = readWarning(packsDir.resolve(packName), packName); + if (warning != null) { + return warning; + } + } + + return null; + } + + @Nullable + private static NonPackStartupWarnings.Warning readWarning(Path packPath, String packName) { + if (packPath != null && packName != null) { + try { + Optional resolved = NonPackRootResolver.resolve(packPath); + if (resolved.isEmpty()) { + return null; + } + + NonPackRootResolver.ResolvedRoot root = resolved.get(); + if (!root.isZip()) { + Path warningPath = root.rootPath() + .resolve("assets/needsofnature/non_warnings.json".replace("/", root.rootPath().getFileSystem().getSeparator())); + if (!Files.isRegularFile(warningPath)) { + return null; + } + + try (Reader reader = Files.newBufferedReader(warningPath, StandardCharsets.UTF_8)) { + return parseWarning(reader, packName); + } + } else { + try (ZipFile zip = new ZipFile(packPath.toFile())) { + ZipEntry entry = zip.getEntry(root.prefixPath("assets/needsofnature/non_warnings.json")); + if (entry == null || entry.isDirectory()) { + return null; + } + + try (Reader reader = new InputStreamReader(zip.getInputStream(entry), StandardCharsets.UTF_8)) { + return parseWarning(reader, packName); + } + } + } + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to load startup warning from pack {}.", packName, e); + return null; + } + } else { + return null; + } + } + + @Nullable + private static NonPackStartupWarnings.Warning parseWarning(Reader reader, String packName) { + JsonElement root = JsonParser.parseReader(reader); + if (!root.isJsonObject()) { + warnInvalid(packName, "root must be an object"); + return null; + } + + JsonObject obj = root.getAsJsonObject(); + String id = stringOrNull(obj, "id"); + if (id != null && !id.isBlank()) { + Text title = readText(obj, "title", "title_key"); + Text body = readText(obj, "text", "text_key"); + if (title != null && body != null) { + return new NonPackStartupWarnings.Warning(packName, id.trim(), title, body); + } + + warnInvalid(packName, "missing title/text or title_key/text_key"); + return null; + } else { + warnInvalid(packName, "missing id"); + return null; + } + } + + @Nullable + private static Text readText(JsonObject obj, String literalKey, String translationKey) { + String key = stringOrNull(obj, translationKey); + if (key != null && !key.isBlank()) { + return Text.translatable(key.trim()); + } + + String literal = stringOrNull(obj, literalKey); + return literal != null && !literal.isBlank() ? parseFormattedLiteral(literal) : null; + } + + private static MutableText parseFormattedLiteral(String raw) { + MutableText out = Text.literal(""); + List active = new ArrayList<>(); + StringBuilder segment = new StringBuilder(); + + for (int i = 0; i < raw.length(); i++) { + char c = raw.charAt(i); + if (c == '&' && i + 1 < raw.length()) { + Formatting formatting = Formatting.byCode(raw.charAt(i + 1)); + if (formatting == null) { + segment.append(c); + } else { + appendSegment(out, segment, active); + i++; + if (formatting == Formatting.RESET) { + active.clear(); + } else if (formatting.isColor()) { + active.removeIf(Formatting::isColor); + active.add(formatting); + } else if (!active.contains(formatting)) { + active.add(formatting); + } + } + } else { + segment.append(c); + } + } + + appendSegment(out, segment, active); + return out; + } + + private static void appendSegment(MutableText out, StringBuilder segment, List active) { + if (!segment.isEmpty()) { + MutableText text = Text.literal(segment.toString()); + if (!active.isEmpty()) { + text.formatted(active.toArray(Formatting[]::new)); + } + + out.append(text); + segment.setLength(0); + } + } + + @Nullable + private static String stringOrNull(JsonObject obj, String key) { + if (obj != null && key != null && obj.has(key)) { + JsonElement element = obj.get(key); + if (element != null && element.isJsonPrimitive()) { + try { + return element.getAsString(); + } catch (RuntimeException e) { + return null; + } + } else { + return null; + } + } else { + return null; + } + } + + private static boolean isAcknowledged(String warningId) { + NonConfig config = NeedsOfNature.getLocalConfig(); + return config != null && config.isStartupWarningAcknowledged(warningId); + } + + private static void warnInvalid(String packName, String reason) { + NeedsOfNature.LOGGER.warn("[NoN] Ignoring startup warning in pack {}: {}.", packName, reason); + } + + public record Warning(String packName, String id, Text title, Text body) { + } +} diff --git a/src/main/java/com/nonid/client/NonPackWarningScreen.java b/src/main/java/com/nonid/client/NonPackWarningScreen.java new file mode 100644 index 0000000..373c43c --- /dev/null +++ b/src/main/java/com/nonid/client/NonPackWarningScreen.java @@ -0,0 +1,126 @@ +package com.nonid.client; + +import java.util.List; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.CheckboxWidget; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; + +public final class NonPackWarningScreen extends Screen { + private static final long PROCEED_DELAY_MS = 10000L; + private static final int CONTENT_WIDTH = 340; + private final Screen parent; + private final NonPackStartupWarnings.Warning warning; + private final long openedAtMs; + private ButtonWidget proceedButton; + private CheckboxWidget dontShowAgainCheckbox; + + public NonPackWarningScreen(Screen parent, NonPackStartupWarnings.Warning warning) { + super(warning.title()); + this.parent = parent; + this.warning = warning; + this.openedAtMs = System.currentTimeMillis(); + } + + protected void init() { + int centerX = this.width / 2; + int buttonY = Math.min(this.height - 38, 246); + int checkboxY = buttonY - 28; + this.dontShowAgainCheckbox = CheckboxWidget.builder(Text.translatable("screen.needsofnature.warning.dont_show_again"), this.textRenderer) + .pos(centerX - 170, checkboxY) + .maxWidth(340) + .build(); + this.addDrawableChild(this.dontShowAgainCheckbox); + this.proceedButton = ButtonWidget.builder(this.proceedText(), button -> this.proceed()).dimensions(centerX - 165, buttonY, 160, 20).build(); + this.addDrawableChild(this.proceedButton); + this.addDrawableChild(ButtonWidget.builder(Text.translatable("screen.needsofnature.warning.close_game"), button -> { + MinecraftClient client = this.client; + if (client != null) { + client.scheduleStop(); + } + }).dimensions(centerX + 5, buttonY, 160, 20).build()); + this.updateProceedButton(); + } + + public void tick() { + this.updateProceedButton(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + context.fill(0, 0, this.width, this.height, -267382768); + int centerX = this.width / 2; + int top = 34; + context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, top, -43691); + int boxX = centerX - 170 - 12; + int boxY = top + 22; + int boxWidth = 364; + int boxHeight = 118; + context.fill(boxX, boxY, boxX + boxWidth, boxY + boxHeight, -1442840576); + context.fill(boxX, boxY, boxX + boxWidth, boxY + 1, 1728053247); + context.fill(boxX, boxY + boxHeight - 1, boxX + boxWidth, boxY + boxHeight, 1711276032); + List lines = this.textRenderer.wrapLines(this.warning.body(), 340); + int y = boxY + 14; + + for (OrderedText line : lines) { + if (y > boxY + boxHeight - 14) { + break; + } + + context.drawTextWithShadow(this.textRenderer, line, centerX - 170, y, -1644826); + y += 11; + } + + Text packText = Text.translatable("screen.needsofnature.warning.pack", new Object[]{this.warning.packName()}); + int packTextWidth = this.textRenderer.getWidth(packText); + context.drawTextWithShadow(this.textRenderer, packText, Math.max(4, this.width - packTextWidth - 6), this.height - 12, -5592406); + super.render(context, mouseX, mouseY, deltaTicks); + } + + public boolean shouldCloseOnEsc() { + return false; + } + + public void close() { + } + + private void proceed() { + if (this.canProceed()) { + if (this.dontShowAgainCheckbox != null && this.dontShowAgainCheckbox.isChecked()) { + NonPackStartupWarnings.acknowledge(this.warning.id()); + } + + MinecraftClient client = this.client; + if (client != null) { + client.setScreen(this.parent); + } + } + } + + private void updateProceedButton() { + if (this.proceedButton != null) { + this.proceedButton.active = this.canProceed(); + this.proceedButton.setMessage(this.proceedText()); + } + } + + private Text proceedText() { + long remainingMs = Math.max(0L, 10000L - this.elapsedMs()); + if (remainingMs <= 0L) { + return Text.translatable("screen.needsofnature.warning.proceed"); + } + + long remainingSeconds = Math.max(1L, (remainingMs + 999L) / 1000L); + return Text.translatable("screen.needsofnature.warning.proceed_countdown", new Object[]{remainingSeconds}); + } + + private boolean canProceed() { + return this.elapsedMs() >= 10000L; + } + + private long elapsedMs() { + return System.currentTimeMillis() - this.openedAtMs; + } +} diff --git a/src/main/java/com/nonid/client/NonPlayerInteractionClient.java b/src/main/java/com/nonid/client/NonPlayerInteractionClient.java new file mode 100644 index 0000000..3bb1d62 --- /dev/null +++ b/src/main/java/com/nonid/client/NonPlayerInteractionClient.java @@ -0,0 +1,848 @@ +package com.nonid.client; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.nonid.NeedsOfNature; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import com.nonid.network.PlayerAnimationCancelC2SPayload; +import com.nonid.network.PlayerAnimationChoiceC2SPayload; +import com.nonid.network.PlayerAnimationConsentC2SPayload; +import com.nonid.network.PlayerAnimationConsentS2CPayload; +import com.nonid.network.PlayerAnimationRequestStatusS2CPayload; +import com.nonid.network.PlayerAnimationSelectionS2CPayload; +import com.nonid.network.PlayerProfileActionC2SPayload; +import com.nonid.network.PlayerProfileSnapshotS2CPayload; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.PlayerSkinDrawer; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.gui.widget.ElementListWidget.Entry; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; + +public final class NonPlayerInteractionClient { + private static final Gson GSON = new Gson(); + private static final Identifier DEFAULT_PLAYER_SKIN = Identifier.ofVanilla("textures/entity/player/wide/steve.png"); + private static int profileRevision; + private static NonPlayerInteractionClient.ProfileSnapshot profileSnapshot = new NonPlayerInteractionClient.ProfileSnapshot( + "requests_and_attacks", 15, List.of(), List.of() + ); + + private NonPlayerInteractionClient() { + } + + public static void register() { + UseEntityCallback.EVENT + .register( + (UseEntityCallback)(player, world, hand, entity, hitResult) -> { + if (world.isClient() + && hand == Hand.MAIN_HAND + && player.getMainHandStack().isEmpty() + && !player.isSpectator() + && entity != null + && !entity.isRemoved() + && !entity.getUuid().equals(player.getUuid())) { + boolean directRequest = player.isSneaking() && entity instanceof PlayerEntity; + boolean joinRequest = ClientAnimationRuntime.findLatestActiveInstanceContaining(entity.getUuid()) != null; + return (ActionResult)(!directRequest && !joinRequest ? ActionResult.PASS : ActionResult.SUCCESS); + } else { + return ActionResult.PASS; + } + } + ); + ClientPlayNetworking.registerGlobalReceiver( + PlayerProfileSnapshotS2CPayload.ID, (payload, context) -> context.client().execute(() -> applyProfileSnapshot(payload)) + ); + ClientPlayNetworking.registerGlobalReceiver( + PlayerAnimationSelectionS2CPayload.ID, (payload, context) -> context.client().execute(() -> openSelection(payload)) + ); + ClientPlayNetworking.registerGlobalReceiver( + PlayerAnimationConsentS2CPayload.ID, (payload, context) -> context.client().execute(() -> openConsent(payload)) + ); + ClientPlayNetworking.registerGlobalReceiver( + PlayerAnimationRequestStatusS2CPayload.ID, (payload, context) -> context.client().execute(() -> applyRequestStatus(payload)) + ); + } + + public static void clear() { + profileRevision++; + profileSnapshot = new NonPlayerInteractionClient.ProfileSnapshot("requests_and_attacks", 15, List.of(), List.of()); + } + + public static int profileRevision() { + return profileRevision; + } + + public static NonPlayerInteractionClient.ProfileSnapshot profileSnapshot() { + return profileSnapshot; + } + + public static void requestProfiles() { + sendProfileAction("request", "", ""); + } + + public static void setDefaultProfile(String mode) { + sendProfileAction("default", "", mode); + } + + public static void setPlayerProfile(String playerNameOrUuid, String mode) { + sendProfileAction("set", playerNameOrUuid, mode); + } + + public static void removePlayerProfile(String playerUuid) { + sendProfileAction("remove", playerUuid, ""); + } + + private static void sendProfileAction(String action, String target, String mode) { + if (ClientPlayNetworking.canSend(PlayerProfileActionC2SPayload.ID)) { + ClientPlayNetworking.send(new PlayerProfileActionC2SPayload(action, target, mode)); + } + } + + private static void applyProfileSnapshot(PlayerProfileSnapshotS2CPayload payload) { + try { + NonPlayerInteractionClient.ProfileSnapshot parsed = (NonPlayerInteractionClient.ProfileSnapshot)GSON.fromJson( + payload.json(), NonPlayerInteractionClient.ProfileSnapshot.class + ); + if (parsed != null) { + profileSnapshot = parsed; + profileRevision++; + } + } catch (JsonParseException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to parse player interaction profile snapshot.", e); + } + + if (payload.errorKey() != null && !payload.errorKey().isBlank()) { + sendLocalMessage(payload.errorKey()); + } + } + + private static void openSelection(PlayerAnimationSelectionS2CPayload payload) { + NonPlayerInteractionClient.Candidate[] parsed; + try { + parsed = (NonPlayerInteractionClient.Candidate[])GSON.fromJson(payload.candidatesJson(), NonPlayerInteractionClient.Candidate[].class); + } catch (JsonParseException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to parse player animation candidates.", e); + sendLocalMessage("gui.needsofnature.player_request.invalid"); + return; + } + + List candidates = parsed == null ? List.of() : List.of(parsed); + if (candidates.isEmpty()) { + sendLocalMessage("gui.needsofnature.player_request.no_match"); + } else { + MinecraftClient client = MinecraftClient.getInstance(); + client.setScreen(new NonPlayerInteractionClient.AnimationSelectionScreen(payload.requestId(), payload.targetName(), candidates, payload.joinRequest())); + } + } + + private static void openConsent(PlayerAnimationConsentS2CPayload payload) { + List tags; + try { + String[] parsed = (String[])GSON.fromJson(payload.contentTagsJson(), String[].class); + tags = parsed == null ? List.of() : List.of(parsed); + } catch (JsonParseException e) { + tags = List.of(); + } + + MinecraftClient.getInstance() + .setScreen( + new NonPlayerInteractionClient.ConsentScreen( + payload.requestId(), payload.requesterUuid(), payload.requesterName(), payload.animationId(), payload.displayName(), tags, payload.joinRequest() + ) + ); + } + + private static void applyRequestStatus(PlayerAnimationRequestStatusS2CPayload payload) { + MinecraftClient client = MinecraftClient.getInstance(); + Screen current = client.currentScreen; + if ("waiting".equals(payload.state())) { + client.setScreen(new NonPlayerInteractionClient.WaitingScreen(payload.requestId())); + } else { + if (current instanceof NonPlayerInteractionClient.RequestBoundScreen requestScreen && requestScreen.requestId().equals(payload.requestId())) { + requestScreen.markResolved(); + client.setScreen(null); + } + + if (payload.messageKey() != null && !payload.messageKey().isBlank() && !"started".equals(payload.state())) { + if (usesRequestActionBar(payload.messageKey())) { + showRequestActionBar(payload.messageKey()); + } else { + sendLocalMessage(payload.messageKey()); + } + } + } + } + + private static boolean usesRequestActionBar(String translationKey) { + return "gui.needsofnature.player_request.rejected".equals(translationKey) || "gui.needsofnature.player_request.energy_low".equals(translationKey); + } + + private static void showRequestActionBar(String translationKey) { + MinecraftClient client = MinecraftClient.getInstance(); + if (translationKey != null && !translationKey.isBlank()) { + Text message = "gui.needsofnature.player_request.energy_low".equals(translationKey) + ? Text.translatable(translationKey, new Object[]{NeedsOfNature.getConfig().getPlayerRequestMinEnergy()}) + : Text.translatable(translationKey); + client.inGameHud.setOverlayMessage(message, false); + } + } + + private static void sendLocalMessage(String translationKey) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null && translationKey != null && !translationKey.isBlank()) { + client.player.sendMessage(Text.translatable(translationKey), false); + } + } + + public static void drawPlayerFace(DrawContext context, UUID playerUuid, int x, int y, int size) { + MinecraftClient client = MinecraftClient.getInstance(); + Identifier texture = DEFAULT_PLAYER_SKIN; + if (client.getNetworkHandler() != null && playerUuid != null) { + PlayerListEntry entry = client.getNetworkHandler().getPlayerListEntry(playerUuid); + if (entry != null && entry.getSkinTextures() != null && entry.getSkinTextures().body() != null) { + PlayerSkinDrawer.draw(context, entry.getSkinTextures(), x, y, size); + return; + } + } + + drawTextureRegion(context, texture, x, y, 8, 8, size, size, 8, 8); + drawTextureRegion(context, texture, x, y, 40, 8, size, size, 8, 8); + } + + private static void drawTextureRegion( + DrawContext context, Identifier texture, int x, int y, int u, int v, int width, int height, int regionWidth, int regionHeight + ) { + context.drawTexture(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 64, 64, regionWidth, regionHeight); + } + + private static Tooltip blockRequirementTooltip(String requirement) { + String summary = requirement != null && !requirement.isBlank() ? requirement : "block requirement"; + return Tooltip.of(Text.translatable("gui.needsofnature.player_request.block_requirement_tooltip", new Object[]{summary})); + } + + private static Tooltip animationCardTooltip(NonPlayerInteractionClient.CandidateGroup group) { + MutableText text = Text.literal(shortAnimationId(group.animationId())); + if (!group.selectable()) { + String summary = group.blockRequirement() != null && !group.blockRequirement().isBlank() ? group.blockRequirement() : "block requirement"; + text.append(Text.literal("\n")).append(Text.translatable("gui.needsofnature.player_request.block_requirement_tooltip", new Object[]{summary})); + } + + return Tooltip.of(text); + } + + private static String shortAnimationId(String raw) { + Identifier id = Identifier.tryParse(raw); + return id == null ? raw : id.getPath(); + } + + private static final class AnimationSelectionScreen extends Screen implements NonPlayerInteractionClient.RequestBoundScreen { + private final UUID requestId; + private final String targetName; + private final List groups; + private final boolean joinRequest; + private boolean resolved; + + private AnimationSelectionScreen(UUID requestId, String targetName, List candidates, boolean joinRequest) { + super(Text.translatable(joinRequest ? "gui.needsofnature.player_request.select_join_title" : "gui.needsofnature.player_request.select_title")); + this.requestId = requestId; + this.targetName = targetName == null ? "Player" : targetName; + this.joinRequest = joinRequest; + Map> grouped = new LinkedHashMap<>(); + + for (NonPlayerInteractionClient.Candidate candidate : candidates) { + grouped.computeIfAbsent(candidate.animation_id(), ignored -> new ArrayList<>()).add(candidate); + } + + this.groups = grouped.entrySet() + .stream() + .map(entry -> new NonPlayerInteractionClient.CandidateGroup(entry.getKey(), List.copyOf(entry.getValue()))) + .toList(); + } + + protected void init() { + int top = 48; + int bottom = 34; + NonPlayerInteractionClient.CandidateGridList list = new NonPlayerInteractionClient.CandidateGridList( + this.client, this.width, Math.max(40, this.height - top - bottom), top, this.textRenderer, this::chooseGroup + ); + list.setGroups(this.groups); + this.addDrawableChild(list); + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.cancel"), ignored -> this.cancel()).dimensions(this.width / 2 - 75, this.height - 28, 150, 20).build() + ); + } + + private void chooseGroup(NonPlayerInteractionClient.CandidateGroup group) { + if (group.candidates().size() == 1) { + if (group.first().selectable()) { + this.choose(group.first()); + } + } else { + MinecraftClient.getInstance().setScreen(new NonPlayerInteractionClient.RoleSelectionScreen(this, this.requestId, group)); + } + } + + private void choose(NonPlayerInteractionClient.Candidate candidate) { + if (ClientPlayNetworking.canSend(PlayerAnimationChoiceC2SPayload.ID)) { + ClientPlayNetworking.send(new PlayerAnimationChoiceC2SPayload(this.requestId, candidate.index())); + } + } + + private void cancel() { + if (!this.resolved && ClientPlayNetworking.canSend(PlayerAnimationCancelC2SPayload.ID)) { + ClientPlayNetworking.send(new PlayerAnimationCancelC2SPayload(this.requestId)); + } + + this.resolved = true; + MinecraftClient.getInstance().setScreen(null); + } + + public void close() { + this.cancel(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.translatable( + this.joinRequest ? "gui.needsofnature.player_request.select_join_for" : "gui.needsofnature.player_request.select_for", + new Object[]{this.targetName} + ), + this.width / 2, + 30, + -5592406 + ); + } + + @Override + public UUID requestId() { + return this.requestId; + } + + @Override + public void markResolved() { + this.resolved = true; + } + } + + private record Candidate( + int index, + String animation_id, + String display_name, + String requester_role, + String requester_activity, + boolean selectable, + String block_requirement, + List actor_keys, + List content_tags, + String pack_name, + String pack_author + ) { + private Candidate { + animation_id = animation_id == null ? "unknown" : animation_id; + display_name = display_name == null ? "" : display_name.trim(); + requester_role = requester_role == null ? "player" : requester_role; + requester_activity = "passive".equalsIgnoreCase(requester_activity) ? "passive" : "active"; + block_requirement = block_requirement == null ? "" : block_requirement; + actor_keys = actor_keys == null ? List.of() : List.copyOf(actor_keys); + content_tags = content_tags == null ? List.of() : List.copyOf(content_tags); + pack_name = pack_name == null ? "" : pack_name; + pack_author = pack_author == null ? "" : pack_author; + } + } + + private static final class CandidateGridList extends ElementListWidget { + private final TextRenderer textRenderer; + private final Consumer onChoose; + + private CandidateGridList( + MinecraftClient client, int width, int height, int top, TextRenderer textRenderer, Consumer onChoose + ) { + super(client, width, height, top, 124); + this.textRenderer = textRenderer; + this.onChoose = onChoose; + this.centerListVertically = false; + } + + private void setGroups(List groups) { + this.clearEntries(); + List available = groups.stream().filter(NonPlayerInteractionClient.CandidateGroup::selectable).toList(); + List unavailable = groups.stream().filter(group -> !group.selectable()).toList(); + this.addSection(Text.translatable("gui.needsofnature.player_request.section.available"), available); + this.addSection(Text.translatable("gui.needsofnature.player_request.section.unavailable"), unavailable); + } + + private void addSection(Text title, List groups) { + if (!groups.isEmpty()) { + this.addEntry(new NonPlayerInteractionClient.CandidateGridList.RowEntry(title, this.textRenderer), 22); + + for (int index = 0; index < groups.size(); index += 3) { + this.addEntry( + new NonPlayerInteractionClient.CandidateGridList.RowEntry( + groups.subList(index, Math.min(groups.size(), index + 3)), this.textRenderer, this.onChoose + ) + ); + } + } + } + + public int getRowWidth() { + return Math.min(510, Math.max(240, this.getWidth() - 28)); + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + protected int getScrollbarX() { + return this.getWidth() - 8; + } + + private static final class RowEntry extends Entry { + private final Text sectionTitle; + private final TextRenderer textRenderer; + private final List cards; + + private RowEntry(Text sectionTitle, TextRenderer textRenderer) { + this.sectionTitle = sectionTitle; + this.textRenderer = textRenderer; + this.cards = List.of(); + } + + private RowEntry( + List groups, TextRenderer textRenderer, Consumer onChoose + ) { + this.sectionTitle = null; + this.textRenderer = textRenderer; + List widgets = new ArrayList<>(); + + for (NonPlayerInteractionClient.CandidateGroup group : groups) { + widgets.add(new NonPlayerInteractionClient.PreviewCardWidget(group, textRenderer, onChoose)); + } + + this.cards = List.copyOf(widgets); + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + if (this.sectionTitle != null) { + int centerX = this.getContentMiddleX(); + int textWidth = this.textRenderer.getWidth(this.sectionTitle); + int lineY = this.getContentMiddleY(); + context.fill(this.getContentX(), lineY, centerX - textWidth / 2 - 8, lineY + 1, -11908534); + context.fill(centerX + textWidth / 2 + 8, lineY, this.getContentRightEnd(), lineY + 1, -11908534); + context.drawCenteredTextWithShadow(this.textRenderer, this.sectionTitle, centerX, lineY - 9 / 2, -5197648); + } else { + int gap = 6; + int cardWidth = Math.max(70, (this.getContentWidth() - gap * 2) / 3); + int x = this.getContentX(); + int y = this.getContentY() + 2; + + for (NonPlayerInteractionClient.PreviewCardWidget card : this.cards) { + card.setX(x); + card.setY(y); + card.setWidth(cardWidth); + card.render(context, mouseX, mouseY, deltaTicks); + x += cardWidth + gap; + } + } + } + + public List selectableChildren() { + return this.cards; + } + + public List children() { + return this.cards; + } + + public void forEachChild(Consumer consumer) { + this.cards.forEach(consumer); + } + } + } + + private record CandidateGroup(String animationId, List candidates) { + private NonPlayerInteractionClient.Candidate first() { + return this.candidates.getFirst(); + } + + private boolean selectable() { + return this.candidates.stream().anyMatch(NonPlayerInteractionClient.Candidate::selectable); + } + + private boolean hasDisplayName() { + return !this.first().display_name().isBlank(); + } + + private String displayName() { + return this.hasDisplayName() ? this.first().display_name() : NonPlayerInteractionClient.shortAnimationId(this.animationId); + } + + private String blockRequirement() { + return this.candidates + .stream() + .map(NonPlayerInteractionClient.Candidate::block_requirement) + .filter(value -> value != null && !value.isBlank()) + .findFirst() + .orElse("block requirement"); + } + } + + private static final class ConsentScreen extends Screen implements NonPlayerInteractionClient.RequestBoundScreen { + private final UUID requestId; + private final UUID requesterUuid; + private final String requesterName; + private final String animationId; + private final String displayName; + private final List contentTags; + private final boolean joinRequest; + private boolean resolved; + + private ConsentScreen( + UUID requestId, UUID requesterUuid, String requesterName, String animationId, String displayName, List contentTags, boolean joinRequest + ) { + super(Text.translatable("gui.needsofnature.player_request.consent_title")); + this.requestId = requestId; + this.requesterUuid = requesterUuid; + this.requesterName = requesterName; + this.animationId = animationId; + this.displayName = displayName != null && !displayName.isBlank() ? displayName.trim() : NonPlayerInteractionClient.shortAnimationId(animationId); + this.contentTags = contentTags; + this.joinRequest = joinRequest; + } + + protected void init() { + int top = Math.max(24, this.height / 2 - 88); + int y = Math.min(this.height - 28, top + 112); + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.needsofnature.player_request.accept"), ignored -> this.respond(true)) + .dimensions(this.width / 2 - 104, y, 100, 20) + .build() + ); + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.needsofnature.player_request.reject"), ignored -> this.respond(false)) + .dimensions(this.width / 2 + 4, y, 100, 20) + .build() + ); + } + + private void respond(boolean accepted) { + if (!this.resolved) { + this.resolved = true; + if (ClientPlayNetworking.canSend(PlayerAnimationConsentC2SPayload.ID)) { + ClientPlayNetworking.send(new PlayerAnimationConsentC2SPayload(this.requestId, accepted)); + } + + MinecraftClient.getInstance().setScreen(null); + } + } + + public void close() { + this.respond(false); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + int centerX = this.width / 2; + int top = Math.max(24, this.height / 2 - 88); + context.fill(centerX - 180, top - 10, centerX + 180, top + 138, -585623528); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, top, -1); + NonPlayerInteractionClient.drawPlayerFace(context, this.requesterUuid, centerX - 96, top + 22, 32); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(this.requesterName), centerX - 80, top + 58, -1); + NonAnimationPreviewRenderer.draw(context, this.textRenderer, this.animationId, centerX + 24, top + 17, 64); + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.translatable( + this.joinRequest ? "gui.needsofnature.player_request.join_prompt" : "gui.needsofnature.player_request.start_prompt", + new Object[]{this.displayName} + ), + centerX, + top + 84, + -3092272 + ); + if (!this.contentTags.isEmpty()) { + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.literal(String.join(" | ", this.contentTags.stream().map(NonTagLocalizer::localize).toList())), + centerX, + top + 97, + -6645094 + ); + } + + super.render(context, mouseX, mouseY, deltaTicks); + } + + @Override + public UUID requestId() { + return this.requestId; + } + + @Override + public void markResolved() { + this.resolved = true; + } + } + + public record KnownPlayerEntry(String uuid, String name) { + } + + private static final class PreviewCardWidget extends ClickableWidget { + private final NonPlayerInteractionClient.CandidateGroup group; + private final TextRenderer textRenderer; + private final Consumer onChoose; + + private PreviewCardWidget( + NonPlayerInteractionClient.CandidateGroup group, TextRenderer textRenderer, Consumer onChoose + ) { + super(0, 0, 100, 116, Text.literal(group.displayName())); + this.group = group; + this.textRenderer = textRenderer; + this.onChoose = onChoose; + this.active = group.selectable(); + if (!this.active || group.hasDisplayName()) { + this.setTooltip(NonPlayerInteractionClient.animationCardTooltip(group)); + } + } + + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + int border = !this.active ? -12632257 : (this.isSelected() ? -1002406 : -11184811); + int background = !this.active ? -300542442 : (this.isSelected() ? -298636510 : -585426149); + context.fill(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), border); + context.fill(this.getX() + 1, this.getY() + 1, this.getX() + this.getWidth() - 1, this.getY() + this.getHeight() - 1, background); + int previewSize = Math.min(72, Math.max(42, this.getWidth() - 18)); + int previewX = this.getX() + (this.getWidth() - previewSize) / 2; + int previewY = this.getY() + 6; + NonAnimationPreviewRenderer.draw(context, this.textRenderer, this.group.animationId(), previewX, previewY, previewSize); + if (!this.active) { + context.fill(previewX, previewY, previewX + previewSize, previewY + previewSize, -1728053248); + } + + NonPlayerInteractionClient.Candidate first = this.group.first(); + String id = this.group.displayName(); + context.drawCenteredTextWithShadow( + this.textRenderer, + this.textRenderer.trimToWidth(id, Math.max(20, this.getWidth() - 8)), + this.getX() + this.getWidth() / 2, + previewY + previewSize + 5, + this.active ? -1 : -8947849 + ); + if (!this.active) { + context.drawCenteredTextWithShadow( + this.textRenderer, + this.textRenderer + .trimToWidth( + Text.translatable("gui.needsofnature.player_request.block_requirement_unavailable").getString(), Math.max(20, this.getWidth() - 8) + ), + this.getX() + this.getWidth() / 2, + previewY + previewSize + 16, + -6710887 + ); + context.drawCenteredTextWithShadow( + this.textRenderer, + this.textRenderer + .trimToWidth( + Text.translatable("gui.needsofnature.player_request.block_requirement_summary", new Object[]{this.group.blockRequirement()}).getString(), + Math.max(20, this.getWidth() - 8) + ), + this.getX() + this.getWidth() / 2, + this.getY() + this.getHeight() - 11, + -8947849 + ); + } else { + String tags = first.content_tags().isEmpty() + ? "" + : String.join(" | ", first.content_tags().stream().map(NonTagLocalizer::localize).toList()); + if (!tags.isBlank()) { + context.drawCenteredTextWithShadow( + this.textRenderer, + this.textRenderer.trimToWidth(tags, Math.max(20, this.getWidth() - 8)), + this.getX() + this.getWidth() / 2, + previewY + previewSize + 16, + -6645094 + ); + } + + if (this.group.candidates().size() > 1) { + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.translatable("gui.needsofnature.player_request.roles", new Object[]{this.group.candidates().size()}), + this.getX() + this.getWidth() / 2, + this.getY() + this.getHeight() - 11, + -1853325 + ); + } + } + } + + public void onClick(Click click, boolean doubled) { + super.onClick(click, doubled); + if (this.active && this.onChoose != null) { + this.onChoose.accept(this.group); + } + } + + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + this.appendDefaultNarrations(builder); + } + } + + public record ProfileEntry(String uuid, String name, String mode) { + } + + public record ProfileSnapshot( + String default_mode, + int allowed_mask, + List overrides, + List known_players + ) { + public ProfileSnapshot { + default_mode = default_mode == null ? "requests_and_attacks" : default_mode; + overrides = overrides == null ? List.of() : List.copyOf(overrides); + known_players = known_players == null ? List.of() : List.copyOf(known_players); + } + } + + private interface RequestBoundScreen { + UUID requestId(); + + void markResolved(); + } + + private static final class RoleSelectionScreen extends Screen implements NonPlayerInteractionClient.RequestBoundScreen { + private final NonPlayerInteractionClient.AnimationSelectionScreen parent; + private final UUID requestId; + private final NonPlayerInteractionClient.CandidateGroup group; + private boolean resolved; + + private RoleSelectionScreen(NonPlayerInteractionClient.AnimationSelectionScreen parent, UUID requestId, NonPlayerInteractionClient.CandidateGroup group) { + super(Text.translatable("gui.needsofnature.player_request.role_title")); + this.parent = parent; + this.requestId = requestId; + this.group = group; + } + + protected void init() { + int buttonWidth = Math.min(360, this.width - 40); + int startY = Math.max(42, (this.height - this.group.candidates().size() * 24) / 2); + + for (int index = 0; index < this.group.candidates().size(); index++) { + NonPlayerInteractionClient.Candidate candidate = this.group.candidates().get(index); + Text label = Text.translatable( + "gui.needsofnature.player_request.role_option", + new Object[]{candidate.requester_role(), Text.translatable("gui.needsofnature.player_request.role_activity." + candidate.requester_activity())} + ); + ButtonWidget button = ButtonWidget.builder(label, ignored -> this.parent.choose(candidate)) + .dimensions(this.width / 2 - buttonWidth / 2, startY + index * 24, buttonWidth, 20) + .build(); + button.active = candidate.selectable(); + if (!candidate.selectable()) { + button.setTooltip(NonPlayerInteractionClient.blockRequirementTooltip(candidate.block_requirement())); + } + + this.addDrawableChild(button); + } + + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.back"), ignored -> this.close()).dimensions(this.width / 2 - 75, this.height - 28, 150, 20).build() + ); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, -1); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(this.group.displayName()), this.width / 2, 30, -5592406); + } + + public void close() { + if (!this.resolved) { + MinecraftClient.getInstance().setScreen(this.parent); + } + } + + @Override + public UUID requestId() { + return this.requestId; + } + + @Override + public void markResolved() { + this.resolved = true; + this.parent.markResolved(); + } + } + + private static final class WaitingScreen extends Screen implements NonPlayerInteractionClient.RequestBoundScreen { + private final UUID requestId; + private boolean resolved; + + private WaitingScreen(UUID requestId) { + super(Text.translatable("gui.needsofnature.player_request.waiting_title")); + this.requestId = requestId; + } + + protected void init() { + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.cancel"), ignored -> this.cancel()) + .dimensions(this.width / 2 - 75, this.height / 2 + 24, 150, 20) + .build() + ); + } + + private void cancel() { + if (!this.resolved && ClientPlayNetworking.canSend(PlayerAnimationCancelC2SPayload.ID)) { + ClientPlayNetworking.send(new PlayerAnimationCancelC2SPayload(this.requestId)); + } + + this.resolved = true; + MinecraftClient.getInstance().setScreen(null); + } + + public void close() { + this.cancel(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, this.height / 2 - 18, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("gui.needsofnature.player_request.waiting_description"), this.width / 2, this.height / 2, -5592406 + ); + } + + @Override + public UUID requestId() { + return this.requestId; + } + + @Override + public void markResolved() { + this.resolved = true; + } + } +} diff --git a/src/main/java/com/nonid/client/NonPregnancyClientState.java b/src/main/java/com/nonid/client/NonPregnancyClientState.java new file mode 100644 index 0000000..6d01b1c --- /dev/null +++ b/src/main/java/com/nonid/client/NonPregnancyClientState.java @@ -0,0 +1,50 @@ +package com.nonid.client; + +import com.nonid.PregnancyCyclePhase; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonPregnancyClientState { + @Nullable + private static Identifier pregnantEntityTypeId; + private static boolean cycleEnabled; + private static int cycleDay; + private static PregnancyCyclePhase cyclePhase = PregnancyCyclePhase.NORMAL; + + private NonPregnancyClientState() { + } + + public static void setPregnantEntityTypeId(@Nullable Identifier entityTypeId) { + pregnantEntityTypeId = entityTypeId; + } + + @Nullable + public static Identifier getPregnantEntityTypeId() { + return pregnantEntityTypeId; + } + + public static void setCycleState(boolean enabled, int day, PregnancyCyclePhase phase) { + cycleEnabled = enabled; + cycleDay = enabled ? Math.max(1, Math.min(30, day)) : 0; + cyclePhase = enabled && phase != null ? phase : PregnancyCyclePhase.NORMAL; + } + + public static boolean isCycleEnabled() { + return cycleEnabled; + } + + public static int getCycleDay() { + return cycleDay; + } + + public static PregnancyCyclePhase getCyclePhase() { + return cyclePhase; + } + + public static void clear() { + pregnantEntityTypeId = null; + cycleEnabled = false; + cycleDay = 0; + cyclePhase = PregnancyCyclePhase.NORMAL; + } +} diff --git a/src/main/java/com/nonid/client/NonPregnantEffectTextures.java b/src/main/java/com/nonid/client/NonPregnantEffectTextures.java new file mode 100644 index 0000000..8f51b3f --- /dev/null +++ b/src/main/java/com/nonid/client/NonPregnantEffectTextures.java @@ -0,0 +1,44 @@ +package com.nonid.client; + +import com.nonid.PregnancyHolder; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.EntityType; +import net.minecraft.item.ItemStack; +import net.minecraft.item.SpawnEggItem; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonPregnantEffectTextures { + private static final Identifier PREGNANT_SPRITE_ID = Identifier.of("needsofnature", "mob_effect/pregnant"); + private static final int ITEM_ICON_SIZE = 16; + + private NonPregnantEffectTextures() { + } + + public static boolean shouldDrawSourceEgg(Identifier sprite) { + return PREGNANT_SPRITE_ID.equals(sprite) && !NonPregnancyClientState.isCycleEnabled(); + } + + public static ItemStack resolveSourceEgg(@Nullable ClientPlayerEntity player) { + Identifier entityTypeId = NonPregnancyClientState.getPregnantEntityTypeId(); + if (entityTypeId == null && player instanceof PregnancyHolder holder) { + entityTypeId = holder.getPregnantEntityTypeId(); + } + + if (entityTypeId == null) { + return ItemStack.EMPTY; + } + + EntityType entityType = (EntityType)Registries.ENTITY_TYPE.getOptionalValue(entityTypeId).orElse(null); + SpawnEggItem egg = entityType == null ? null : SpawnEggItem.forEntity(entityType); + return egg == null ? ItemStack.EMPTY : new ItemStack(egg); + } + + public static void drawSourceEgg(DrawContext context, ItemStack eggStack, int x, int y, int width, int height) { + int drawX = x + Math.max(0, (width - 16) / 2); + int drawY = y + Math.max(0, (height - 16) / 2); + context.drawItemWithoutEntity(eggStack, drawX, drawY); + } +} diff --git a/src/main/java/com/nonid/client/NonQuickSkinCompat.java b/src/main/java/com/nonid/client/NonQuickSkinCompat.java new file mode 100644 index 0000000..0edb23a --- /dev/null +++ b/src/main/java/com/nonid/client/NonQuickSkinCompat.java @@ -0,0 +1,117 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import java.lang.reflect.Method; +import java.util.UUID; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class NonQuickSkinCompat { + private static final String MOD_ID = "quickskin"; + private static boolean initialized; + private static boolean failed; + private static Method appearanceServiceGetInstance; + private static Method getAppearance; + private static Method getSkinId; + private static Method assetManagerGetInstance; + private static Method getMetadata; + private static Method getFriendlyName; + + private NonQuickSkinCompat() { + } + + @Nullable + static NonQuickSkinCompat.SkinSelection getSelectedSkin(UUID playerUuid) { + if (playerUuid != null && initialize()) { + try { + Object appearanceService = appearanceServiceGetInstance.invoke(null); + Object appearance = getAppearance.invoke(appearanceService, playerUuid); + if (appearance == null) { + return null; + } + + String skinId = (String)getSkinId.invoke(appearance); + String assetHash = assetHash(skinId); + if (assetHash == null) { + return null; + } + + Identifier textureId = Identifier.tryParse(skinId); + if (textureId == null) { + return null; + } + + Object assetManager = assetManagerGetInstance.invoke(null); + Object metadata = getMetadata.invoke(assetManager, assetHash); + if (metadata == null) { + return null; + } + + String friendlyName = (String)getFriendlyName.invoke(metadata); + return friendlyName != null && !friendlyName.isBlank() + ? new NonQuickSkinCompat.SkinSelection(textureId, skinId, friendlyName, NonSkinFolderNames.sanitize(friendlyName)) + : null; + } catch (ReflectiveOperationException | LinkageError | RuntimeException e) { + fail(e); + return null; + } + } else { + return null; + } + } + + private static boolean initialize() { + if (initialized) { + return !failed; + } + + initialized = true; + if (!FabricLoader.getInstance().isModLoaded("quickskin")) { + failed = true; + return false; + } + + try { + ClassLoader loader = NonQuickSkinCompat.class.getClassLoader(); + Class appearanceServiceClass = Class.forName("com.quickskin.mod.client.services.PlayerAppearanceService", false, loader); + Class appearanceClass = Class.forName("com.quickskin.mod.common.data.PlayerAppearance", false, loader); + Class assetManagerClass = Class.forName("com.quickskin.mod.client.services.LocalAssetManager", false, loader); + Class metadataClass = Class.forName("com.quickskin.mod.common.data.AssetMetadata", false, loader); + appearanceServiceGetInstance = appearanceServiceClass.getMethod("getInstance"); + getAppearance = appearanceServiceClass.getMethod("getAppearance", UUID.class); + getSkinId = appearanceClass.getMethod("getSkinId"); + assetManagerGetInstance = assetManagerClass.getMethod("getInstance"); + getMetadata = assetManagerClass.getMethod("getMetadata", String.class); + getFriendlyName = metadataClass.getMethod("friendlyName"); + return true; + } catch (ReflectiveOperationException | LinkageError | RuntimeException e) { + fail(e); + return false; + } + } + + @Nullable + private static String assetHash(@Nullable String skinId) { + if (skinId != null && !skinId.isBlank()) { + int separator = skinId.lastIndexOf(58); + String hash = separator >= 0 ? skinId.substring(separator + 1) : skinId; + return hash.isBlank() ? null : hash; + } else { + return null; + } + } + + private static void fail(Throwable error) { + if (!failed) { + failed = true; + NeedsOfNature.LOGGER.warn("[NoN] Quick Skin name integration is unavailable; using the global ripped-skin configuration.", error); + } + } + + record SkinSelection(Identifier textureId, String skinId, String friendlyName, String folderName) { + String cacheKey() { + return this.skinId + "\u0000" + this.folderName; + } + } +} diff --git a/src/main/java/com/nonid/client/NonServerSettingsClient.java b/src/main/java/com/nonid/client/NonServerSettingsClient.java new file mode 100644 index 0000000..d6a0b12 --- /dev/null +++ b/src/main/java/com/nonid/client/NonServerSettingsClient.java @@ -0,0 +1,166 @@ +package com.nonid.client; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.network.AnimationCatalogData; +import com.nonid.network.HostConfigSyncData; +import com.nonid.network.ServerConfigRequestC2SPayload; +import com.nonid.network.ServerConfigResultS2CPayload; +import com.nonid.network.ServerConfigUpdateC2SPayload; +import java.util.Arrays; +import java.util.List; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; + +public final class NonServerSettingsClient { + private static final Gson GSON = new Gson(); + private static volatile boolean canEdit; + private static volatile long configRevision = -1L; + private static volatile NonServerSettingsClient.HostConfigSnapshot editableSnapshot; + private static volatile long catalogRevision = -1L; + private static volatile List animationCatalog = List.of(); + private static NonServerSettingsClient.CatalogTransfer catalogTransfer; + + private NonServerSettingsClient() { + } + + public static void applyConfigSnapshot(String editableJson, long revision, boolean editable) { + if (revision >= configRevision) { + try { + HostConfigSyncData data = (HostConfigSyncData)GSON.fromJson(editableJson, HostConfigSyncData.class); + if (data == null) { + return; + } + + editableSnapshot = new NonServerSettingsClient.HostConfigSnapshot(data); + configRevision = revision; + canEdit = editable; + } catch (JsonParseException | IllegalStateException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to parse editable server config snapshot.", e); + } + } + } + + public static NonConfig createServerConfigDraft() { + NonConfig draft = NeedsOfNature.getLocalConfig().copy(); + NonServerSettingsClient.HostConfigSnapshot snapshot = editableSnapshot; + if (snapshot != null) { + snapshot.data().applyEditableTo(draft); + } + + return draft; + } + + public static boolean canEdit() { + return canEdit; + } + + public static long configRevision() { + return configRevision; + } + + public static boolean hasConfigSnapshot() { + return editableSnapshot != null; + } + + public static void requestRefresh() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.getNetworkHandler() != null) { + ClientPlayNetworking.send(new ServerConfigRequestC2SPayload()); + } + } + + public static void sendConfigUpdate(NonConfig config, long expectedRevision) { + if (config != null && canEdit) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.getNetworkHandler() != null) { + String json = GSON.toJson(HostConfigSyncData.fromEditableConfig(config)); + ClientPlayNetworking.send(new ServerConfigUpdateC2SPayload(expectedRevision, json)); + } + } + } + + public static void acceptCatalogChunk(long revision, int index, int count, String data) { + if (index >= 0 && count > 0 && index < count && count <= 4096 && data != null) { + if (revision >= catalogRevision) { + if (index == 0) { + catalogTransfer = new NonServerSettingsClient.CatalogTransfer(revision, new String[count]); + } else if (catalogTransfer == null || catalogTransfer.revision != revision || catalogTransfer.chunks.length != count) { + return; + } + + NonServerSettingsClient.CatalogTransfer transfer = catalogTransfer; + if (transfer != null && transfer.revision == revision && transfer.chunks.length == count) { + transfer.chunks[index] = data; + + for (String chunk : transfer.chunks) { + if (chunk == null) { + return; + } + } + + try { + StringBuilder joined = new StringBuilder(); + Arrays.stream(transfer.chunks).forEach(joined::append); + AnimationCatalogData catalog = (AnimationCatalogData)GSON.fromJson(joined.toString(), AnimationCatalogData.class); + animationCatalog = catalog != null && catalog.animations() != null ? List.copyOf(catalog.animations()) : List.of(); + catalogRevision = revision; + } catch (JsonParseException | IllegalStateException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to parse server animation catalog.", e); + } finally { + catalogTransfer = null; + } + } + } + } + } + + public static boolean hasAnimationCatalog() { + return catalogRevision >= 0L; + } + + public static long catalogRevision() { + return catalogRevision; + } + + public static List animationCatalogSnapshot() { + return animationCatalog; + } + + public static void handleConfigResult(ServerConfigResultS2CPayload payload) { + if (payload != null) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.inGameHud != null) { + String key = payload.messageKey(); + if (key != null && !key.isBlank()) { + client.inGameHud.setOverlayMessage(Text.translatable(key), false); + } + } + } + } + + public static void clear() { + canEdit = false; + configRevision = -1L; + editableSnapshot = null; + catalogRevision = -1L; + animationCatalog = List.of(); + catalogTransfer = null; + } + + private static final class CatalogTransfer { + private final long revision; + private final String[] chunks; + + private CatalogTransfer(long revision, String[] chunks) { + this.revision = revision; + this.chunks = chunks; + } + } + + private record HostConfigSnapshot(HostConfigSyncData data) { + } +} diff --git a/src/main/java/com/nonid/client/NonSkinFolderNames.java b/src/main/java/com/nonid/client/NonSkinFolderNames.java new file mode 100644 index 0000000..ad30b57 --- /dev/null +++ b/src/main/java/com/nonid/client/NonSkinFolderNames.java @@ -0,0 +1,65 @@ +package com.nonid.client; + +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.Locale; +import java.util.Set; + +final class NonSkinFolderNames { + private static final Set WINDOWS_RESERVED_NAMES = Set.of( + "con", + "prn", + "aux", + "nul", + "com1", + "com2", + "com3", + "com4", + "com5", + "com6", + "com7", + "com8", + "com9", + "lpt1", + "lpt2", + "lpt3", + "lpt4", + "lpt5", + "lpt6", + "lpt7", + "lpt8", + "lpt9" + ); + + private NonSkinFolderNames() { + } + + static String sanitize(String friendlyName) { + String normalized = Normalizer.normalize(friendlyName, Form.NFKD).toLowerCase(Locale.ROOT); + StringBuilder result = new StringBuilder(Math.min(normalized.length(), 64)); + boolean previousSeparator = false; + int offset = 0; + + while (offset < normalized.length() && result.length() < 64) { + int codePoint = normalized.codePointAt(offset); + offset += Character.charCount(codePoint); + if (Character.getType(codePoint) != 6) { + boolean allowed = codePoint >= 97 && codePoint <= 122 || codePoint >= 48 && codePoint <= 57 || codePoint == 45; + if (allowed) { + result.appendCodePoint(codePoint); + previousSeparator = false; + } else if (!previousSeparator && !result.isEmpty()) { + result.append('_'); + previousSeparator = true; + } + } + } + + while (!result.isEmpty() && result.charAt(result.length() - 1) == '_') { + result.setLength(result.length() - 1); + } + + String folderName = result.isEmpty() ? "skin" : result.toString(); + return WINDOWS_RESERVED_NAMES.contains(folderName) ? "skin_" + folderName : folderName; + } +} diff --git a/src/main/java/com/nonid/client/NonSkinOverrideCompat.java b/src/main/java/com/nonid/client/NonSkinOverrideCompat.java new file mode 100644 index 0000000..a9279f2 --- /dev/null +++ b/src/main/java/com/nonid/client/NonSkinOverrideCompat.java @@ -0,0 +1,175 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.UUID; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class NonSkinOverrideCompat { + private static final String MOD_ID = "skin_overrides"; + private static boolean initialized; + private static boolean failed; + private static Object skinOverrideManager; + private static Constructor partialProfileConstructor; + private static Method getOverride; + private static Method getCacheVersion; + private static Method getTexture; + private static Method getTexturePath; + private static Method getPlayerIdent; + private static Class libraryOverrideClass; + private static Method getLibraryEntry; + private static Field libraryEntryFile; + private static Method getEntryName; + private static Method getEntryId; + + private NonSkinOverrideCompat() { + } + + @Nullable + static NonSkinOverrideCompat.SkinSelection getSelectedSkin(String playerName, UUID playerUuid) { + if (playerName != null && !playerName.isBlank() && playerUuid != null && initialize()) { + try { + Object profile = partialProfileConstructor.newInstance(playerName, playerUuid); + if (getOverride.invoke(skinOverrideManager, profile) instanceof Optional optional && !optional.isEmpty()) { + Object override = optional.get(); + if (getTexture.invoke(override) instanceof Identifier textureId) { + Path sourcePath = null; + String friendlyName = null; + String selectionId = String.valueOf(getPlayerIdent.invoke(override)); + if (libraryOverrideClass.isInstance(override)) { + Object entry = getLibraryEntry.invoke(override); + if (libraryEntryFile.get(entry) instanceof File file) { + sourcePath = file.toPath().toAbsolutePath().normalize(); + } + + Object nameValue = getEntryName.invoke(entry); + Object idValue = getEntryId.invoke(entry); + if (nameValue instanceof String name && !name.isBlank()) { + friendlyName = name; + } + + if (idValue instanceof String id && !id.isBlank()) { + selectionId = id; + } + } else if (getTexturePath.invoke(override) instanceof File file) { + sourcePath = file.toPath().toAbsolutePath().normalize(); + } + + String sourceFingerprint = sourceFingerprint(sourcePath); + return new NonSkinOverrideCompat.SkinSelection( + textureId, + selectionId, + friendlyName, + friendlyName != null ? NonSkinFolderNames.sanitize(friendlyName) : null, + sourcePath, + sourceFingerprint + ); + } else { + return null; + } + } else { + return null; + } + } catch (ReflectiveOperationException | LinkageError | RuntimeException e) { + fail(e); + return null; + } + } else { + return null; + } + } + + static int cacheVersion() { + if (!initialize()) { + return -1; + } + + try { + return ((Number)getCacheVersion.invoke(null)).intValue(); + } catch (ReflectiveOperationException | LinkageError | RuntimeException e) { + fail(e); + return -1; + } + } + + private static boolean initialize() { + if (initialized) { + return !failed; + } + + initialized = true; + if (!FabricLoader.getInstance().isModLoaded("skin_overrides")) { + failed = true; + return false; + } + + try { + ClassLoader loader = NonSkinOverrideCompat.class.getClassLoader(); + Class overridesClass = Class.forName("net.orifu.skin_overrides.Overrides", false, loader); + Class managerClass = Class.forName("net.orifu.skin_overrides.OverrideManager", false, loader); + Class overrideClass = Class.forName("net.orifu.skin_overrides.OverrideManager$Override", false, loader); + Class profileClass = Class.forName("net.orifu.skin_overrides.profile.Profile", false, loader); + Class partialProfileClass = Class.forName("net.orifu.skin_overrides.profile.PartialProfile", false, loader); + libraryOverrideClass = Class.forName("net.orifu.skin_overrides.override.LibraryOverrider$LibraryOverride", false, loader); + Class libraryEntryClass = Class.forName("net.orifu.skin_overrides.Library$LibraryEntry", false, loader); + Class abstractLibraryEntryClass = Class.forName("net.orifu.skin_overrides.library.AbstractLibrary$AbstractLibraryEntry", false, loader); + Field skinsField = overridesClass.getField("SKINS"); + skinOverrideManager = skinsField.get(null); + partialProfileConstructor = partialProfileClass.getConstructor(String.class, UUID.class); + getOverride = managerClass.getMethod("get", profileClass); + getCacheVersion = overridesClass.getMethod("cacheVersion"); + getTexture = overrideClass.getMethod("texture"); + getTexturePath = overrideClass.getMethod("texturePath"); + getPlayerIdent = overrideClass.getMethod("playerIdent"); + getLibraryEntry = libraryOverrideClass.getMethod("entry"); + libraryEntryFile = abstractLibraryEntryClass.getDeclaredField("file"); + libraryEntryFile.setAccessible(true); + getEntryName = libraryEntryClass.getMethod("getName"); + getEntryId = libraryEntryClass.getMethod("getId"); + return true; + } catch (ReflectiveOperationException | LinkageError | RuntimeException e) { + fail(e); + return false; + } + } + + private static String sourceFingerprint(@Nullable Path sourcePath) { + if (sourcePath == null) { + return ""; + } + + long size = -1L; + long modified = -1L; + + try { + if (Files.isRegularFile(sourcePath)) { + size = Files.size(sourcePath); + modified = Files.getLastModifiedTime(sourcePath).toMillis(); + } + } catch (RuntimeException | IOException var6) { + } + + return "path=" + sourcePath + "|size=" + size + "|modified=" + modified; + } + + private static void fail(Throwable error) { + if (!failed) { + failed = true; + NeedsOfNature.LOGGER.warn("[NoN] Skin Override integration is unavailable; using the global ripped-skin configuration.", error); + } + } + + record SkinSelection( + Identifier textureId, String selectionId, @Nullable String friendlyName, @Nullable String folderName, @Nullable Path sourcePath, String sourceFingerprint + ) { + } +} diff --git a/src/main/java/com/nonid/client/NonSoundCues.java b/src/main/java/com/nonid/client/NonSoundCues.java new file mode 100644 index 0000000..86719cf --- /dev/null +++ b/src/main/java/com/nonid/client/NonSoundCues.java @@ -0,0 +1,151 @@ +package com.nonid.client; + +import com.nonid.data.NonPeakStages; +import com.nonid.internal.animation.data.AnimationCueEffects; +import java.util.Locale; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.random.Random; + +public final class NonSoundCues { + private static final int DRY_IMPACT_COUNT = 11; + private static final int WET_IMPACT_COUNT = 16; + private static final String[] REACTIVE_DRY = new String[]{"impactdry01", "impactdry02", "impactdry03"}; + private static final String[] REACTIVE_WET_IMPACT = new String[]{"impactwet01", "impactwet02", "impactwet03"}; + private static final String[] REACTIVE_WET = new String[]{"wet04", "wet06", "wet07"}; + private static final String PEAK_REACTIVE_SOUND = "shot_in01"; + private static final String BIRTH_SOUND = "needsofnature:pop"; + + private NonSoundCues() { + } + + public static Identifier resolveSoundId(String effect, LivingEntity anchor, Identifier stageAnimationId, int stageIndex) { + if (effect != null && !effect.isBlank()) { + if (effect.indexOf(58) >= 0) { + Identifier direct = Identifier.tryParse(effect); + if (direct != null) { + if ("minecraft".equals(direct.getNamespace()) && isLocalEffectPath(direct.getPath())) { + return Identifier.of("needsofnature", direct.getPath()); + } + + return direct; + } + } + + String resolved = mapEffect(effect, anchor, stageAnimationId, stageIndex); + if (resolved == null) { + return null; + } + + if (resolved.indexOf(58) >= 0) { + Identifier directResolved = Identifier.tryParse(resolved); + if (directResolved != null) { + if ("minecraft".equals(directResolved.getNamespace()) && isLocalEffectPath(directResolved.getPath())) { + return Identifier.of("needsofnature", directResolved.getPath()); + } + + return directResolved; + } + } + + return Identifier.of("needsofnature", resolved); + } else { + return null; + } + } + + public static boolean isPeakReactiveImpactCue(String effect, Identifier stageAnimationId, int stageIndex) { + return !AnimationCueEffects.isReactiveImpact(effect) ? false : isPeakStage(stageAnimationId, stageIndex); + } + + public static boolean isPeakAnimationStage(Identifier stageAnimationId, int stageIndex) { + return isPeakStage(stageAnimationId, stageIndex); + } + + public static boolean isActionSoundId(Identifier soundId) { + return soundId != null && "needsofnature".equals(soundId.getNamespace()) && isLocalEffectPath(soundId.getPath()); + } + + private static String mapEffect(String effect, LivingEntity anchor, Identifier stageAnimationId, int stageIndex) { + if (effect.equals("impactdry")) { + return pickRandom("impactdry", 11, anchor); + } else if (effect.equals("impactwet")) { + return pickRandom("impactwet", 16, anchor); + } else if ("reactiveimpact".equalsIgnoreCase(effect)) { + return pickReactiveImpact(anchor, stageAnimationId, stageIndex); + } else if (effect.equals("birth")) { + return "needsofnature:pop"; + } else { + return !effect.startsWith("impactdry") + && !effect.startsWith("impactwet") + && !effect.startsWith("wet") + && !effect.startsWith("shot_in") + && !effect.startsWith("shot_out") + && !effect.startsWith("motion") + && !effect.startsWith("retract") + ? null + : effect; + } + } + + private static String pickRandom(String prefix, int count, LivingEntity anchor) { + Random random = anchor != null ? anchor.getEntityWorld().getRandom() : Random.create(); + int index = random.nextInt(count) + 1; + return prefix + String.format(Locale.ROOT, "%02d", index); + } + + private static String pickReactiveImpact(LivingEntity anchor, Identifier stageAnimationId, int stageIndex) { + if (isPeakStage(stageAnimationId, stageIndex)) { + return "shot_in01"; + } else { + float ratio = NonHudOverlay.getLiquidFillRatio(); + if (ratio < 0.1F) { + return pickFromList(REACTIVE_DRY, anchor); + } else { + return ratio > 0.8F ? pickFromList(REACTIVE_WET, anchor) : pickFromList(REACTIVE_WET_IMPACT, anchor); + } + } + } + + private static boolean isPeakStage(Identifier stageAnimationId, int stageIndex) { + if (stageAnimationId == null) { + return false; + } + + if (stageIndex < 0) { + return false; + } + + if (NonPeakStages.stageNumberFromId(stageAnimationId) == null) { + return false; + } + + Identifier baseAnimationId = NonPeakStages.baseAnimationId(stageAnimationId); + Integer peakStage = NonPeakStages.getPeakStage(baseAnimationId); + if (peakStage == null) { + return false; + } + + int logicalStage = stageIndex + 1; + return logicalStage == peakStage; + } + + private static String pickFromList(String[] options, LivingEntity anchor) { + if (options != null && options.length != 0) { + Random random = anchor != null ? anchor.getEntityWorld().getRandom() : Random.create(); + return options[random.nextInt(options.length)]; + } else { + return null; + } + } + + private static boolean isLocalEffectPath(String path) { + return path.startsWith("impactdry") + || path.startsWith("impactwet") + || path.startsWith("wet") + || path.startsWith("shot_in") + || path.startsWith("shot_out") + || path.startsWith("motion") + || path.startsWith("retract"); + } +} diff --git a/src/main/java/com/nonid/client/NonSpinModelLock.java b/src/main/java/com/nonid/client/NonSpinModelLock.java new file mode 100644 index 0000000..c9e6235 --- /dev/null +++ b/src/main/java/com/nonid/client/NonSpinModelLock.java @@ -0,0 +1,114 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; + +public final class NonSpinModelLock { + private static final Set LOCKED = new HashSet<>(); + private static boolean initialized = false; + private static boolean available = false; + private static Method emfEntityOfMethod; + private static Method lockMethod; + private static Method unlockMethod; + + private NonSpinModelLock() { + } + + public static void clientTick(MinecraftClient client) { + if (client != null) { + ClientWorld world = client.world; + if (world == null) { + LOCKED.clear(); + } else if (!NonDebugSpinMode.isEnabled()) { + releaseAll(world); + } else if (ensureApi()) { + Set seen = new HashSet<>(); + + for (Entity entity : world.getEntities()) { + if (entity instanceof LivingEntity living) { + UUID uuid = living.getUuid(); + seen.add(uuid); + if (LOCKED.add(uuid)) { + lockEntity(living); + } + } + } + + LOCKED.removeIf(uuidx -> !seen.contains(uuidx)); + } + } + } + + private static void releaseAll(ClientWorld world) { + if (!LOCKED.isEmpty()) { + if (!ensureApi()) { + LOCKED.clear(); + } else { + for (UUID uuid : Set.copyOf(LOCKED)) { + if (world.getEntity(uuid) instanceof LivingEntity living) { + unlockEntity(living); + } + } + + LOCKED.clear(); + } + } + } + + private static void lockEntity(LivingEntity entity) { + invokeLockMethod(entity, lockMethod); + } + + private static void unlockEntity(LivingEntity entity) { + invokeLockMethod(entity, unlockMethod); + } + + private static void invokeLockMethod(LivingEntity entity, Method method) { + if (entity != null && method != null && emfEntityOfMethod != null) { + try { + Object emfEntity = emfEntityOfMethod.invoke(null, entity); + if (emfEntity != null) { + method.invoke(null, emfEntity); + } + } catch (ReflectiveOperationException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to invoke EMF spin lock integration; disabling it.", e); + disableApi(); + } + } + } + + private static boolean ensureApi() { + if (initialized) { + return available; + } + + initialized = true; + + try { + Class apiClass = Class.forName("traben.entity_model_features.EMFAnimationApi"); + Class emfEntityClass = Class.forName("traben.entity_model_features.utils.EMFEntity"); + emfEntityOfMethod = apiClass.getMethod("emfEntityOf", Entity.class); + lockMethod = apiClass.getMethod("lockEntityToVanillaModel", emfEntityClass); + unlockMethod = apiClass.getMethod("unlockEntityToVanillaModel", emfEntityClass); + available = true; + } catch (ClassNotFoundException | NoSuchMethodException e) { + disableApi(); + } + + return available; + } + + private static void disableApi() { + available = false; + emfEntityOfMethod = null; + lockMethod = null; + unlockMethod = null; + } +} diff --git a/src/main/java/com/nonid/client/NonStatLabelResolver.java b/src/main/java/com/nonid/client/NonStatLabelResolver.java new file mode 100644 index 0000000..5350666 --- /dev/null +++ b/src/main/java/com/nonid/client/NonStatLabelResolver.java @@ -0,0 +1,105 @@ +package com.nonid.client; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.entity.EntityType; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; + +public final class NonStatLabelResolver { + private static final String STAT_PREFIX = "stat.needsofnature."; + private static final String PEAK_ENTITY_PREFIX = "stat.needsofnature.peaked_animations_entity_"; + private static final String LIQUID_ENTITY_PREFIX = "stat.needsofnature.liquid_gained_ml_entity_"; + private static final String OFFSPRING_ENTITY_PREFIX = "stat.needsofnature.offspring_spawned_entity_"; + private static final Map ENTITY_NAME_CACHE = new ConcurrentHashMap<>(); + + private NonStatLabelResolver() { + } + + public static void clearCache() { + ENTITY_NAME_CACHE.clear(); + } + + public static String resolve(String key) { + if (key == null || key.isBlank()) { + return null; + } else if (key.startsWith("stat.needsofnature.peaked_animations_entity_")) { + String suffix = key.substring("stat.needsofnature.peaked_animations_entity_".length()); + String entityName = resolveEntityNameFromSuffix(suffix); + return "NoN: Peaked Animations (" + entityName + ")"; + } else if (key.startsWith("stat.needsofnature.liquid_gained_ml_entity_")) { + String suffix = key.substring("stat.needsofnature.liquid_gained_ml_entity_".length()); + String entityName = resolveEntityNameFromSuffix(suffix); + return "NoN: Liquid Gained (ml) (" + entityName + ")"; + } else if (key.startsWith("stat.needsofnature.offspring_spawned_entity_")) { + String suffix = key.substring("stat.needsofnature.offspring_spawned_entity_".length()); + String entityName = resolveEntityNameFromSuffix(suffix); + return "NoN: Offspring Spawned (" + entityName + ")"; + } else { + return null; + } + } + + private static String resolveEntityNameFromSuffix(String suffix) { + return suffix != null && !suffix.isBlank() ? ENTITY_NAME_CACHE.computeIfAbsent(suffix, NonStatLabelResolver::lookupEntityName) : "Unknown"; + } + + private static String lookupEntityName(String suffix) { + for (Identifier entityId : Registries.ENTITY_TYPE.getIds()) { + if (suffix.equals(sanitizeEntityId(entityId))) { + EntityType type = (EntityType)Registries.ENTITY_TYPE.get(entityId); + return type.getName().getString(); + } + } + + return prettifySuffix(suffix); + } + + private static String sanitizeEntityId(Identifier entityId) { + return sanitizePathPart(entityId.getNamespace()) + "_" + sanitizePathPart(entityId.getPath()); + } + + private static String sanitizePathPart(String value) { + if (value != null && !value.isBlank()) { + StringBuilder out = new StringBuilder(value.length()); + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c >= 'a' && c <= 'z') { + out.append(c); + } else if (c >= '0' && c <= '9') { + out.append(c); + } else { + out.append('_'); + } + } + + return out.isEmpty() ? "unknown" : out.toString(); + } else { + return "unknown"; + } + } + + private static String prettifySuffix(String suffix) { + String normalized = suffix.replace('_', ' ').trim(); + if (normalized.isEmpty()) { + return "Unknown"; + } + + StringBuilder out = new StringBuilder(normalized.length()); + boolean cap = true; + + for (int i = 0; i < normalized.length(); i++) { + char c = normalized.charAt(i); + if (cap && c >= 'a' && c <= 'z') { + out.append((char)(c - ' ')); + } else { + out.append(c); + } + + cap = c == ' '; + } + + return out.toString(); + } +} diff --git a/src/main/java/com/nonid/client/NonTagLocalizer.java b/src/main/java/com/nonid/client/NonTagLocalizer.java new file mode 100644 index 0000000..99491e9 --- /dev/null +++ b/src/main/java/com/nonid/client/NonTagLocalizer.java @@ -0,0 +1,44 @@ +package com.nonid.client; + +import java.util.Map; + +/** + * 将 afw_animdefs 的 content_tags 本地化为中文显示。 + */ +public final class NonTagLocalizer { + private static final Map TAGS = Map.ofEntries( + Map.entry("against_block", "靠方块"), + Map.entry("against_wall", "靠墙"), + Map.entry("blowjob", "口部侍奉"), + Map.entry("cowgirl", "骑乘位"), + Map.entry("doggy", "后入式"), + Map.entry("forcing", "强制"), + Map.entry("from_behind", "背后"), + Map.entry("fullnelson", "背后锁抱"), + Map.entry("handjob", "手部侍奉"), + Map.entry("holding", "抱持"), + Map.entry("mating_press", "交配压制"), + Map.entry("missionary", "传教士式"), + Map.entry("no_pen", "无插入"), + Map.entry("on_a_block", "方块上"), + Map.entry("on_a_wall", "靠墙"), + Map.entry("on_back", "仰躺"), + Map.entry("on_belly", "趴伏"), + Map.entry("oral", "口部"), + Map.entry("side", "侧位"), + Map.entry("spitroast", "双人串烧"), + Map.entry("stand_and_carry", "站立抱姿"), + Map.entry("standing", "站立"), + Map.entry("wall", "靠墙") + ); + + private NonTagLocalizer() { + } + + public static String localize(String tag) { + if (tag == null) { + return ""; + } + return TAGS.getOrDefault(tag, tag); + } +} diff --git a/src/main/java/com/nonid/client/NonWildfireGenderScreenHooks.java b/src/main/java/com/nonid/client/NonWildfireGenderScreenHooks.java new file mode 100644 index 0000000..dd41a65 --- /dev/null +++ b/src/main/java/com/nonid/client/NonWildfireGenderScreenHooks.java @@ -0,0 +1,100 @@ +package com.nonid.client; + +import com.nonid.NeedsOfNature; +import com.nonid.integration.NonModMenuExternalScreens; +import java.time.Duration; +import java.util.List; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents.AfterInit; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Text; + +public final class NonWildfireGenderScreenHooks { + private static final String WARDROBE_SCREEN_CLASS = "com.wildfire.gui.screen.WardrobeBrowserScreen"; + private static final int GENDER_BUTTON_WIDTH = 80; + private static final int GENDER_BUTTON_HEIGHT = 15; + private static final int PERSONALIZATION_BUTTON_WIDTH = 157; + private static final int PERSONALIZATION_BUTTON_HEIGHT = 20; + private static final int REDIRECT_BUTTON_WIDTH = 176; + private static final int REDIRECT_BUTTON_VERTICAL_GAP = 3; + + private NonWildfireGenderScreenHooks() { + } + + public static void register() { + if (NonWildfireGenderSync.isAvailable()) { + ScreenEvents.AFTER_INIT.register((AfterInit)(client, screen, scaledWidth, scaledHeight) -> { + if (isWardrobeScreen(screen)) { + applyLocks(screen, scaledWidth, scaledHeight, Screens.getButtons(screen)); + } + }); + } + } + + private static void applyLocks(Screen screen, int width, int height, List widgets) { + boolean lockGender = NonWildfireGenderSync.shouldLockWildfireGenderControls(NeedsOfNature.getConfig()); + boolean lockPersonalization = NonWildfireGenderSync.shouldLockWildfirePersonalizationControls(NeedsOfNature.getConfig()); + if (lockGender || lockPersonalization) { + ClickableWidget genderButton = lockGender ? findGenderButton(widgets, width, height) : null; + if (genderButton != null) { + disableWithTooltip(genderButton, "config.needsofnature.tooltip.wildfire_gender_controlled_by_non"); + } + + if (lockGender) { + int x = genderButton != null ? genderButton.getX() : width / 2 - 130; + int y = (genderButton != null ? genderButton.getY() : height / 2 + 33) + 15 + 3; + ButtonWidget redirectButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.open_non_player_preferences"), button -> NonModMenuExternalScreens.openPlayerPreferences(screen) + ) + .dimensions(x, y, 176, 15) + .build(); + redirectButton.setTooltip(Tooltip.of(Text.translatable("config.needsofnature.tooltip.open_non_player_preferences"))); + redirectButton.setTooltipDelay(Duration.ofMillis(250L)); + widgets.add(redirectButton); + } + + ClickableWidget personalizationButton = lockPersonalization ? findPersonalizationButton(widgets, width, height) : null; + if (personalizationButton != null) { + disableWithTooltip(personalizationButton, "config.needsofnature.tooltip.wildfire_personalization_controlled_by_non"); + } + } + } + + private static ClickableWidget findGenderButton(List widgets, int width, int height) { + return findButton(widgets, width / 2 - 130, height / 2 + 33, 80, 15); + } + + private static ClickableWidget findPersonalizationButton(List widgets, int width, int height) { + return findButton(widgets, width / 2 - 36, height / 2 - 63, 157, 20); + } + + private static ClickableWidget findButton(List widgets, int x, int y, int width, int height) { + for (ClickableWidget widget : widgets) { + if (widget.getX() == x && widget.getY() == y && widget.getWidth() == width && widget.getHeight() == height) { + return widget; + } + } + + return null; + } + + private static void disableWithTooltip(ClickableWidget widget, String tooltipKey) { + widget.active = false; + widget.setTooltip(Tooltip.of(Text.translatable(tooltipKey))); + widget.setTooltipDelay(Duration.ofMillis(250L)); + } + + private static boolean isWardrobeScreen(Screen screen) { + for (Class type = screen.getClass(); type != null; type = type.getSuperclass()) { + if ("com.wildfire.gui.screen.WardrobeBrowserScreen".equals(type.getName())) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/nonid/client/NonWildfireGenderSync.java b/src/main/java/com/nonid/client/NonWildfireGenderSync.java new file mode 100644 index 0000000..eb86a28 --- /dev/null +++ b/src/main/java/com/nonid/client/NonWildfireGenderSync.java @@ -0,0 +1,630 @@ +package com.nonid.client; + +import com.nonid.GenderHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.integration.NonWildfireGenderConfigCompat; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; + +public final class NonWildfireGenderSync { + private static final String MOD_ID = "wildfire_gender"; + private static final float WILDFIRE_NATIVE_BREAST_HEIGHT_MIN = -1.0F; + private static final int RECONCILIATION_RETRY_INTERVAL_TICKS = 5; + private static final int RECONCILIATION_MAX_ATTEMPTS = 40; + private static final int RECONCILIATION_VERIFY_DELAY_TICKS = 20; + private static boolean initialized; + private static boolean failed; + private static boolean loggedFailure; + private static boolean breastTextureEditorInitialized; + private static boolean loggedBreastTextureEditorFailure; + private static Method getPlayerByIdMethod; + private static Method updateGenderMethod; + private static Method updateBustSizeMethod; + private static Method updateBreastPhysicsMethod; + private static Method updateBounceMultiplierMethod; + private static Method updateFloppinessMethod; + private static Method getBreastsMethod; + private static Method updateXOffsetMethod; + private static Method updateYOffsetMethod; + private static Method updateZOffsetMethod; + private static Method updateCleavageMethod; + private static Method updateUniboobMethod; + private static Method getBustSizeMethod; + private static Method hasBreastPhysicsMethod; + private static Method getBounceMultiplierMethod; + private static Method getFloppinessMethod; + private static Method getXOffsetMethod; + private static Method getYOffsetMethod; + private static Method getZOffsetMethod; + private static Method getCleavageMethod; + private static Method isUniboobMethod; + private static Method getLeftBreastPhysicsMethod; + private static Method getRightBreastPhysicsMethod; + private static Method renderPlayerInFrameMethod; + private static Method saveMethod; + private static Field bustSizeField; + private static Field breastPhysicsField; + private static Field bounceMultiplierField; + private static Field floppyMultiplierField; + private static Field xOffsetField; + private static Field yOffsetField; + private static Field zOffsetField; + private static Field cleavageField; + private static Field uniboobField; + private static Field physicsBreastSizeField; + private static Field physicsPreBreastSizeField; + private static Constructor breastCustomizationScreenConstructor; + private static Constructor breastTextureEditorScreenConstructor; + private static Object wildfireMaleGender; + private static Object wildfireFemaleGender; + private static Object wildfireOtherGender; + private static NonWildfireGenderSync.ReconciliationPhase reconciliationPhase = NonWildfireGenderSync.ReconciliationPhase.IDLE; + private static UUID reconciliationPlayerUuid; + private static int reconciliationDelayTicks; + private static int reconciliationAttemptsRemaining; + + private NonWildfireGenderSync() { + } + + public static boolean isAvailable() { + return FabricLoader.getInstance().isModLoaded("wildfire_gender"); + } + + public static boolean shouldLockWildfireGenderControls(NonConfig config) { + return config != null && config.isSyncFemaleGenderModWithNonGender() && isAvailable(); + } + + public static boolean shouldLockWildfirePersonalizationControls(NonConfig config) { + return config != null && config.isDestroyedSkinSystemEnabled() && config.isDestroyedSkinFemaleGenderOverridesEnabled() && isAvailable(); + } + + public static void syncFromNonConfig(NonConfig config) { + if (config != null && config.isSyncFemaleGenderModWithNonGender() && isAvailable()) { + requestReconciliation(config); + syncFromSelection(config, config.getPlayerGenderSelection()); + } + } + + public static void syncFromMask(NonConfig config, int mask) { + if (config != null && config.isSyncFemaleGenderModWithNonGender() && isAvailable()) { + requestReconciliation(config); + syncFromSelection(config, NonConfig.PlayerGenderSelection.fromMask(mask, config.getPlayerGenderSelection())); + } + } + + public static void syncDestroyedSkinOverrides(NonConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + syncDestroyedSkinOverrides(config, NonDestroyedSkinClient.getStage(client.player.getUuid())); + } + } + + public static void syncDestroyedSkinOverrides(NonConfig config, int destroyedStage) { + if (config != null && config.isDestroyedSkinSystemEnabled() && config.isDestroyedSkinFemaleGenderOverridesEnabled() && isAvailable()) { + requestReconciliation(config); + if (init()) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + NonConfig.FemaleGenderDestroyedProfile profile = destroyedStage >= 2 + ? config.getFemaleGenderDestroyedHigh() + : config.getFemaleGenderDestroyedLow(); + + try { + Object playerConfig = getPlayerByIdMethod.invoke(null, client.player.getUuid()); + if (playerConfig == null) { + return; + } + + applyProfile(playerConfig, profile, true); + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure(e); + } + } + } + } + } + + public static void onWorldJoin() { + requestReconciliation(NeedsOfNature.getLocalConfig()); + } + + public static void onWorldDisconnect() { + clearReconciliation(); + } + + private static void clearReconciliation() { + reconciliationPhase = NonWildfireGenderSync.ReconciliationPhase.IDLE; + reconciliationPlayerUuid = null; + reconciliationDelayTicks = 0; + reconciliationAttemptsRemaining = 0; + } + + public static void clientTick(MinecraftClient client) { + if (reconciliationPhase != NonWildfireGenderSync.ReconciliationPhase.IDLE && client != null && client.world != null && client.player != null) { + UUID playerUuid = client.player.getUuid(); + if (!playerUuid.equals(reconciliationPlayerUuid)) { + reconciliationPlayerUuid = playerUuid; + reconciliationPhase = NonWildfireGenderSync.ReconciliationPhase.WAITING_FOR_PROFILE; + reconciliationDelayTicks = 0; + reconciliationAttemptsRemaining = 40; + } + + if (reconciliationDelayTicks > 0) { + reconciliationDelayTicks--; + } else { + NonWildfireGenderSync.ReconciliationResult result = reconcileLocalState(client, NeedsOfNature.getLocalConfig()); + if (result == NonWildfireGenderSync.ReconciliationResult.FAILED) { + clearReconciliation(); + } else if (result == NonWildfireGenderSync.ReconciliationResult.NOT_READY) { + reconciliationAttemptsRemaining--; + if (reconciliationAttemptsRemaining <= 0) { + clearReconciliation(); + } else { + reconciliationDelayTicks = 5; + } + } else if (reconciliationPhase == NonWildfireGenderSync.ReconciliationPhase.WAITING_FOR_PROFILE) { + reconciliationPhase = NonWildfireGenderSync.ReconciliationPhase.VERIFYING; + reconciliationDelayTicks = 20; + } else { + clearReconciliation(); + } + } + } + } + + private static void requestReconciliation(NonConfig config) { + if (isAvailable() && managesWildfireState(config)) { + MinecraftClient client = MinecraftClient.getInstance(); + reconciliationPlayerUuid = client != null && client.player != null ? client.player.getUuid() : null; + reconciliationPhase = NonWildfireGenderSync.ReconciliationPhase.WAITING_FOR_PROFILE; + reconciliationDelayTicks = 0; + reconciliationAttemptsRemaining = 40; + } + } + + private static boolean managesWildfireState(NonConfig config) { + return config != null + && (config.isSyncFemaleGenderModWithNonGender() || config.isDestroyedSkinSystemEnabled() && config.isDestroyedSkinFemaleGenderOverridesEnabled()); + } + + private static NonWildfireGenderSync.ReconciliationResult reconcileLocalState(MinecraftClient client, NonConfig config) { + if (!managesWildfireState(config)) { + return NonWildfireGenderSync.ReconciliationResult.READY; + } + + if (!init()) { + return NonWildfireGenderSync.ReconciliationResult.FAILED; + } + + try { + Object playerConfig = getPlayerByIdMethod.invoke(null, client.player.getUuid()); + if (playerConfig == null) { + return NonWildfireGenderSync.ReconciliationResult.NOT_READY; + } + + boolean changed = false; + if (config.isSyncFemaleGenderModWithNonGender()) { + int mask = config.getPlayerGenderMask() & 3; + if (client.player instanceof GenderHolder holder) { + int syncedMask = holder.getGenderMask() & 3; + if (syncedMask != 0) { + mask = syncedMask; + } + } + + NonConfig.PlayerGenderSelection selection = NonConfig.PlayerGenderSelection.fromMask(mask, config.getPlayerGenderSelection()); + changed |= Boolean.TRUE.equals(updateGenderMethod.invoke(playerConfig, toWildfireGender(selection))); + } + + if (config.isDestroyedSkinSystemEnabled() && config.isDestroyedSkinFemaleGenderOverridesEnabled()) { + int destroyedStage = NonDestroyedSkinClient.getStage(client.player.getUuid()); + NonConfig.FemaleGenderDestroyedProfile profile = destroyedStage >= 2 ? config.getFemaleGenderDestroyedHigh() : config.getFemaleGenderDestroyedLow(); + changed |= updateProfile(playerConfig, profile); + } + + if (changed) { + saveMethod.invoke(playerConfig); + } + + return NonWildfireGenderSync.ReconciliationResult.READY; + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure(e); + return NonWildfireGenderSync.ReconciliationResult.FAILED; + } + } + + public static boolean openBreastCustomizationScreen(Screen parent) { + if (isAvailable() && init()) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null && breastCustomizationScreenConstructor != null) { + try { + if (breastCustomizationScreenConstructor.newInstance(parent, client.player.getUuid()) instanceof Screen minecraftScreen) { + client.setScreen(minecraftScreen); + return true; + } + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure(e); + } + + return false; + } else { + return false; + } + } else { + return false; + } + } + + public static boolean canOpenBreastTextureEditor() { + return isAvailable() && initBreastTextureEditor(); + } + + public static boolean openBreastTextureEditor(Screen parent) { + if (isAvailable() && initBreastTextureEditor()) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null && breastTextureEditorScreenConstructor != null) { + try { + if (breastTextureEditorScreenConstructor.newInstance(parent, client.player.getUuid()) instanceof Screen minecraftScreen) { + client.setScreen(minecraftScreen); + return true; + } + } catch (ReflectiveOperationException | RuntimeException e) { + logBreastTextureEditorFailure(e); + } + + return false; + } else { + return false; + } + } else { + return false; + } + } + + public static boolean renderBreastPreview( + Screen parent, DrawContext context, NonConfig.FemaleGenderDestroyedProfile profile, int centerX, int frameY, int mouseX, int mouseY + ) { + if (parent != null && context != null && profile != null && isAvailable() && init()) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null && breastCustomizationScreenConstructor != null && renderPlayerInFrameMethod != null) { + try { + Object playerConfig = getPlayerByIdMethod.invoke(null, client.player.getUuid()); + if (playerConfig == null) { + return false; + } + + NonConfig.FemaleGenderDestroyedProfile previous = readProfile(playerConfig); + NonWildfireGenderSync.BreastPhysicsSnapshot physicsSnapshot = snapshotBreastPhysics(playerConfig); + + try { + applyProfile(playerConfig, profile, false); + applyBreastPhysicsSize(playerConfig, profile.breastSize()); + Object screen = breastCustomizationScreenConstructor.newInstance(parent, client.player.getUuid()); + renderPlayerInFrameMethod.invoke(screen, context, centerX, frameY, mouseX, mouseY); + } finally { + applyProfile(playerConfig, previous, false); + if (physicsSnapshot != null) { + physicsSnapshot.restore(); + } + } + + return true; + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure(e); + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + private static boolean applyProfile(Object playerConfig, NonConfig.FemaleGenderDestroyedProfile profile, boolean save) throws ReflectiveOperationException { + if (!save) { + applyProfileFields(playerConfig, profile); + return true; + } + + boolean changed = updateProfile(playerConfig, profile); + if (changed) { + saveMethod.invoke(playerConfig); + } + + return changed; + } + + private static boolean updateProfile(Object playerConfig, NonConfig.FemaleGenderDestroyedProfile profile) throws ReflectiveOperationException { + boolean changed = false; + changed |= applyBreastSize(playerConfig, profile.breastSize()); + changed |= Boolean.TRUE.equals(updateBreastPhysicsMethod.invoke(playerConfig, profile.breastPhysics())); + changed |= Boolean.TRUE.equals(updateBounceMultiplierMethod.invoke(playerConfig, profile.intensity())); + changed |= Boolean.TRUE.equals(updateFloppinessMethod.invoke(playerConfig, profile.momentum())); + Object breasts = getBreastsMethod.invoke(playerConfig); + if (breasts != null) { + changed |= Boolean.TRUE.equals(updateXOffsetMethod.invoke(breasts, profile.separation())); + changed |= applyBreastHeight(breasts, profile.height()); + changed |= Boolean.TRUE.equals(updateZOffsetMethod.invoke(breasts, profile.depth())); + changed |= Boolean.TRUE.equals(updateCleavageMethod.invoke(breasts, profile.rotation())); + changed |= Boolean.TRUE.equals(updateUniboobMethod.invoke(breasts, !profile.dualPhysics())); + } + + return changed; + } + + private static boolean applyBreastSize(Object playerConfig, float breastSize) throws ReflectiveOperationException { + float supportedSize = NonWildfireGenderConfigCompat.clampBreastSize(breastSize); + return Boolean.TRUE.equals(updateBustSizeMethod.invoke(playerConfig, supportedSize)); + } + + private static boolean applyBreastHeight(Object breasts, float height) throws ReflectiveOperationException { + if (height >= -1.0F) { + return Boolean.TRUE.equals(updateYOffsetMethod.invoke(breasts, height)); + } + + if (Float.compare(yOffsetField.getFloat(breasts), height) == 0) { + return false; + } + + yOffsetField.setFloat(breasts, height); + return true; + } + + private static void applyProfileFields(Object playerConfig, NonConfig.FemaleGenderDestroyedProfile profile) throws ReflectiveOperationException { + bustSizeField.setFloat(playerConfig, profile.breastSize()); + breastPhysicsField.setBoolean(playerConfig, profile.breastPhysics()); + bounceMultiplierField.setFloat(playerConfig, profile.intensity()); + floppyMultiplierField.setFloat(playerConfig, profile.momentum()); + Object breasts = getBreastsMethod.invoke(playerConfig); + if (breasts != null) { + xOffsetField.setFloat(breasts, profile.separation()); + yOffsetField.setFloat(breasts, profile.height()); + zOffsetField.setFloat(breasts, profile.depth()); + cleavageField.setFloat(breasts, profile.rotation()); + uniboobField.setBoolean(breasts, !profile.dualPhysics()); + } + } + + private static NonConfig.FemaleGenderDestroyedProfile readProfile(Object playerConfig) throws ReflectiveOperationException { + Object breasts = getBreastsMethod.invoke(playerConfig); + float rotation = 0.0F; + float height = 0.0F; + float depth = 0.0F; + float separation = 0.0F; + boolean dualPhysics = false; + if (breasts != null) { + separation = ((Number)getXOffsetMethod.invoke(breasts)).floatValue(); + height = ((Number)getYOffsetMethod.invoke(breasts)).floatValue(); + depth = ((Number)getZOffsetMethod.invoke(breasts)).floatValue(); + rotation = ((Number)getCleavageMethod.invoke(breasts)).floatValue(); + dualPhysics = !Boolean.TRUE.equals(isUniboobMethod.invoke(breasts)); + } + + return new NonConfig.FemaleGenderDestroyedProfile( + ((Number)getBustSizeMethod.invoke(playerConfig)).floatValue(), + separation, + height, + depth, + rotation, + Boolean.TRUE.equals(hasBreastPhysicsMethod.invoke(playerConfig)), + dualPhysics, + ((Number)getBounceMultiplierMethod.invoke(playerConfig)).floatValue(), + ((Number)getFloppinessMethod.invoke(playerConfig)).floatValue() + ); + } + + private static NonWildfireGenderSync.BreastPhysicsSnapshot snapshotBreastPhysics(Object playerConfig) throws ReflectiveOperationException { + Object left = getLeftBreastPhysicsMethod.invoke(playerConfig); + Object right = getRightBreastPhysicsMethod.invoke(playerConfig); + return left != null && right != null + ? new NonWildfireGenderSync.BreastPhysicsSnapshot( + left, + right, + physicsBreastSizeField.getFloat(left), + physicsPreBreastSizeField.getFloat(left), + physicsBreastSizeField.getFloat(right), + physicsPreBreastSizeField.getFloat(right) + ) + : null; + } + + private static void applyBreastPhysicsSize(Object playerConfig, float breastSize) throws ReflectiveOperationException { + setBreastPhysicsSize(getLeftBreastPhysicsMethod.invoke(playerConfig), breastSize); + setBreastPhysicsSize(getRightBreastPhysicsMethod.invoke(playerConfig), breastSize); + } + + private static void setBreastPhysicsSize(Object breastPhysics, float breastSize) throws IllegalAccessException { + if (breastPhysics != null) { + physicsBreastSizeField.setFloat(breastPhysics, breastSize); + physicsPreBreastSizeField.setFloat(breastPhysics, breastSize); + } + } + + private static void syncFromSelection(NonConfig config, NonConfig.PlayerGenderSelection selection) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + if (init()) { + try { + UUID playerUuid = client.player.getUuid(); + Object playerConfig = getPlayerByIdMethod.invoke(null, playerUuid); + if (playerConfig == null) { + return; + } + + Object targetGender = toWildfireGender(selection); + Object changed = updateGenderMethod.invoke(playerConfig, targetGender); + if (Boolean.TRUE.equals(changed)) { + saveMethod.invoke(playerConfig); + } + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure(e); + } + } + } + } + + private static Object toWildfireGender(NonConfig.PlayerGenderSelection selection) { + if (selection == NonConfig.PlayerGenderSelection.MALE) { + return wildfireMaleGender; + } else { + return selection == NonConfig.PlayerGenderSelection.BOTH ? wildfireOtherGender : wildfireFemaleGender; + } + } + + private static boolean init() { + if (initialized) { + return !failed; + } + + initialized = true; + if (!isAvailable()) { + failed = true; + return false; + } + + try { + Class apiClass = Class.forName("com.wildfire.api.WildfireAPI"); + Class playerConfigClass = Class.forName("com.wildfire.main.entitydata.PlayerConfig"); + Class entityConfigClass = Class.forName("com.wildfire.main.entitydata.EntityConfig"); + Class breastsClass = Class.forName("com.wildfire.main.entitydata.Breasts"); + Class genderClass = Class.forName("com.wildfire.main.config.enums.Gender"); + Class breastPhysicsClass = Class.forName("com.wildfire.physics.BreastPhysics"); + Class breastCustomizationScreenClass = Class.forName("com.wildfire.gui.screen.WildfireBreastCustomizationScreen"); + getPlayerByIdMethod = apiClass.getMethod("getPlayerById", UUID.class); + updateGenderMethod = playerConfigClass.getMethod("updateGender", genderClass); + updateBustSizeMethod = playerConfigClass.getMethod("updateBustSize", float.class); + updateBreastPhysicsMethod = playerConfigClass.getMethod("updateBreastPhysics", boolean.class); + updateBounceMultiplierMethod = playerConfigClass.getMethod("updateBounceMultiplier", float.class); + updateFloppinessMethod = playerConfigClass.getMethod("updateFloppiness", float.class); + getBreastsMethod = playerConfigClass.getMethod("getBreasts"); + getBustSizeMethod = playerConfigClass.getMethod("getBustSize"); + hasBreastPhysicsMethod = playerConfigClass.getMethod("hasBreastPhysics"); + getBounceMultiplierMethod = playerConfigClass.getMethod("getBounceMultiplier"); + getFloppinessMethod = playerConfigClass.getMethod("getFloppiness"); + updateXOffsetMethod = breastsClass.getMethod("updateXOffset", float.class); + updateYOffsetMethod = breastsClass.getMethod("updateYOffset", float.class); + updateZOffsetMethod = breastsClass.getMethod("updateZOffset", float.class); + updateCleavageMethod = breastsClass.getMethod("updateCleavage", float.class); + updateUniboobMethod = breastsClass.getMethod("updateUniboob", boolean.class); + getXOffsetMethod = breastsClass.getMethod("getXOffset"); + getYOffsetMethod = breastsClass.getMethod("getYOffset"); + getZOffsetMethod = breastsClass.getMethod("getZOffset"); + getCleavageMethod = breastsClass.getMethod("getCleavage"); + isUniboobMethod = breastsClass.getMethod("isUniboob"); + getLeftBreastPhysicsMethod = entityConfigClass.getMethod("getLeftBreastPhysics"); + getRightBreastPhysicsMethod = entityConfigClass.getMethod("getRightBreastPhysics"); + saveMethod = playerConfigClass.getMethod("save"); + breastCustomizationScreenConstructor = breastCustomizationScreenClass.getConstructor(Screen.class, UUID.class); + Class baseWildfireScreenClass = Class.forName("com.wildfire.gui.screen.BaseWildfireScreen"); + renderPlayerInFrameMethod = baseWildfireScreenClass.getDeclaredMethod( + "renderPlayerInFrame", DrawContext.class, int.class, int.class, int.class, int.class + ); + renderPlayerInFrameMethod.setAccessible(true); + bustSizeField = accessibleField(entityConfigClass, "pBustSize"); + breastPhysicsField = accessibleField(entityConfigClass, "breastPhysics"); + bounceMultiplierField = accessibleField(entityConfigClass, "bounceMultiplier"); + floppyMultiplierField = accessibleField(entityConfigClass, "floppyMultiplier"); + xOffsetField = accessibleField(breastsClass, "xOffset"); + yOffsetField = accessibleField(breastsClass, "yOffset"); + zOffsetField = accessibleField(breastsClass, "zOffset"); + cleavageField = accessibleField(breastsClass, "cleavage"); + uniboobField = accessibleField(breastsClass, "uniboob"); + physicsBreastSizeField = accessibleField(breastPhysicsClass, "breastSize"); + physicsPreBreastSizeField = accessibleField(breastPhysicsClass, "preBreastSize"); + wildfireMaleGender = enumConstant(genderClass, "MALE"); + wildfireFemaleGender = enumConstant(genderClass, "FEMALE"); + wildfireOtherGender = enumConstant(genderClass, "OTHER"); + return true; + } catch (ReflectiveOperationException | RuntimeException e) { + failed = true; + logFailure(e); + return false; + } + } + + private static synchronized boolean initBreastTextureEditor() { + if (breastTextureEditorInitialized) { + return breastTextureEditorScreenConstructor != null; + } + + breastTextureEditorInitialized = true; + + try { + Class screenClass = Class.forName("com.wildfire.gui.screen.WildfireBreastUVEditorScreen"); + breastTextureEditorScreenConstructor = screenClass.getConstructor(Screen.class, UUID.class); + return true; + } catch (ReflectiveOperationException | RuntimeException e) { + logBreastTextureEditorFailure(e); + return false; + } + } + + private static Object enumConstant(Class enumClass, String name) { + return Enum.valueOf(enumClass.asSubclass(Enum.class), name); + } + + private static Field accessibleField(Class owner, String name) throws NoSuchFieldException { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + private static void logFailure(Exception e) { + if (!loggedFailure) { + loggedFailure = true; + NeedsOfNature.LOGGER.warn("[NoN] Failed to integrate with Female Gender Mod.", e); + } + } + + private static void logBreastTextureEditorFailure(Exception e) { + if (!loggedBreastTextureEditorFailure) { + loggedBreastTextureEditorFailure = true; + NeedsOfNature.LOGGER.warn("[NoN] Female Gender Mod breast texture editor is unavailable.", e); + } + } + + private static final class BreastPhysicsSnapshot { + private final Object left; + private final Object right; + private final float leftSize; + private final float leftPreSize; + private final float rightSize; + private final float rightPreSize; + + private BreastPhysicsSnapshot(Object left, Object right, float leftSize, float leftPreSize, float rightSize, float rightPreSize) { + this.left = left; + this.right = right; + this.leftSize = leftSize; + this.leftPreSize = leftPreSize; + this.rightSize = rightSize; + this.rightPreSize = rightPreSize; + } + + private void restore() throws IllegalAccessException { + NonWildfireGenderSync.physicsBreastSizeField.setFloat(this.left, this.leftSize); + NonWildfireGenderSync.physicsPreBreastSizeField.setFloat(this.left, this.leftPreSize); + NonWildfireGenderSync.physicsBreastSizeField.setFloat(this.right, this.rightSize); + NonWildfireGenderSync.physicsPreBreastSizeField.setFloat(this.right, this.rightPreSize); + } + } + + private enum ReconciliationPhase { + IDLE, + WAITING_FOR_PROFILE, + VERIFYING; + } + + private enum ReconciliationResult { + READY, + NOT_READY, + FAILED; + } +} diff --git a/src/main/java/com/nonid/client/PregnancyEggGeoModel.java b/src/main/java/com/nonid/client/PregnancyEggGeoModel.java new file mode 100644 index 0000000..50855ce --- /dev/null +++ b/src/main/java/com/nonid/client/PregnancyEggGeoModel.java @@ -0,0 +1,85 @@ +package com.nonid.client; + +import com.nonid.entity.PregnancyEggEntity; +import com.nonid.internal.animation.AnimationEngine; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.cache.GeckoLibResources; +import software.bernie.geckolib.constant.dataticket.DataTicket; +import software.bernie.geckolib.model.GeoModel; +import software.bernie.geckolib.renderer.base.GeoRenderState; + +public final class PregnancyEggGeoModel extends GeoModel { + public static final DataTicket<@NotNull Identifier> SOURCE_ENTITY_TYPE_TICKET = DataTicket.create("non_pregnancy_egg_source_entity_type", Identifier.class); + public static final DataTicket<@NotNull Float> GROWTH_SCALE_TICKET = DataTicket.create("non_pregnancy_egg_growth_scale", Float.class); + public static final DataTicket<@NotNull Float> HURT_SHAKE_TICKET = DataTicket.create("non_pregnancy_egg_hurt_shake", Float.class); + public static final DataTicket<@NotNull Identifier> EGG_TEXTURE_TICKET = DataTicket.create("non_pregnancy_egg_texture", Identifier.class); + public static final Identifier DEFAULT_MODEL_ID = Identifier.of("needsofnature", "entity/pregnancy_egg/default"); + public static final Identifier DEFAULT_TEXTURE_ID = Identifier.of("needsofnature", "textures/entity/pregnancy_egg/default.png"); + private static final Identifier PLACEHOLDER_ANIMATION_ID = AnimationEngine.packId("afw/placeholder"); + private static final Map TEXTURE_EXISTS_CACHE = new ConcurrentHashMap<>(); + + @NotNull + public Identifier getModelResource(@NotNull GeoRenderState renderState) { + Identifier specific = resolveSpecificModelId(renderState); + return specific != null && GeckoLibResources.getBakedModels().cache().containsKey(specific) ? specific : DEFAULT_MODEL_ID; + } + + @NotNull + public Identifier getTextureResource(@NotNull GeoRenderState renderState) { + Identifier configured = (Identifier)renderState.getGeckolibData(EGG_TEXTURE_TICKET); + if (configured != null) { + return hasTexture(configured) ? configured : DEFAULT_TEXTURE_ID; + } + + Identifier specific = resolveSpecificTextureId(renderState); + return specific != null && hasTexture(specific) ? specific : DEFAULT_TEXTURE_ID; + } + + @NotNull + public Identifier getAnimationResource(PregnancyEggEntity animatable) { + return PLACEHOLDER_ANIMATION_ID; + } + + private static Identifier resolveSpecificModelId(GeoRenderState renderState) { + Identifier sourceType = (Identifier)renderState.getGeckolibData(SOURCE_ENTITY_TYPE_TICKET); + return sourceType == null ? null : modelIdForSourceEntity(sourceType); + } + + private static Identifier resolveSpecificTextureId(GeoRenderState renderState) { + Identifier sourceType = (Identifier)renderState.getGeckolibData(SOURCE_ENTITY_TYPE_TICKET); + return sourceType == null ? null : Identifier.of("needsofnature", "textures/entity/pregnancy_egg/" + toFileStem(sourceType) + ".png"); + } + + static Identifier modelIdForSourceEntity(Identifier sourceType) { + return Identifier.of("needsofnature", "entity/pregnancy_egg/" + toFileStem(sourceType)); + } + + private static String toFileStem(Identifier typeId) { + String namespace = typeId.getNamespace(); + String path = typeId.getPath().replace('/', '_'); + return namespace + "_" + path; + } + + private static boolean hasTexture(Identifier textureId) { + return textureId == null ? false : TEXTURE_EXISTS_CACHE.computeIfAbsent(textureId, PregnancyEggGeoModel::findTexture); + } + + public static void clearTextureCache() { + TEXTURE_EXISTS_CACHE.clear(); + } + + private static boolean findTexture(Identifier textureId) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return false; + } + + ResourceManager manager = client.getResourceManager(); + return manager == null ? false : manager.getResource(textureId).isPresent(); + } +} diff --git a/src/main/java/com/nonid/client/PregnancyEggRenderer.java b/src/main/java/com/nonid/client/PregnancyEggRenderer.java new file mode 100644 index 0000000..c3a0a84 --- /dev/null +++ b/src/main/java/com/nonid/client/PregnancyEggRenderer.java @@ -0,0 +1,61 @@ +package com.nonid.client; + +import com.nonid.entity.PregnancyEggEntity; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.EntityRendererFactory.Context; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RotationAxis; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.renderer.GeoEntityRenderer; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.RenderPassInfo; + +public final class PregnancyEggRenderer extends GeoEntityRenderer { + public PregnancyEggRenderer(Context context) { + super(context, new PregnancyEggGeoModel()); + } + + public void addRenderData(PregnancyEggEntity animatable, Void relatedObject, R renderState, float partialTick) { + super.addRenderData(animatable, relatedObject, renderState, partialTick); + Identifier sourceEntityTypeId = animatable.getSourceEntityTypeId(); + if (sourceEntityTypeId != null) { + renderState.addGeckolibData(PregnancyEggGeoModel.SOURCE_ENTITY_TYPE_TICKET, sourceEntityTypeId); + } + + renderState.addGeckolibData(PregnancyEggGeoModel.GROWTH_SCALE_TICKET, animatable.getGrowthScale(partialTick)); + renderState.addGeckolibData(PregnancyEggGeoModel.HURT_SHAKE_TICKET, Math.max(0.0F, animatable.hurtTime - partialTick)); + Identifier eggTextureId = animatable.getEggTextureId(); + renderState.addGeckolibData(PregnancyEggGeoModel.EGG_TEXTURE_TICKET, eggTextureId == null ? PregnancyEggGeoModel.DEFAULT_TEXTURE_ID : eggTextureId); + } + + public void scaleModelForRender(RenderPassInfo<@NotNull R> renderPassInfo, float widthScale, float heightScale) { + float growth = (Float)renderPassInfo.getOrDefaultGeckolibData(PregnancyEggGeoModel.GROWTH_SCALE_TICKET, 1.0F); + float clamped = MathHelper.clamp(growth, 0.05F, 4.0F); + super.scaleModelForRender(renderPassInfo, widthScale * clamped, heightScale * clamped); + } + + public void preRenderPass(RenderPassInfo<@NotNull R> renderPassInfo, OrderedRenderCommandQueue renderTasks) { + super.preRenderPass(renderPassInfo, renderTasks); + float hurtShake = (Float)renderPassInfo.getOrDefaultGeckolibData(PregnancyEggGeoModel.HURT_SHAKE_TICKET, 0.0F); + if (!(hurtShake <= 0.0F)) { + float intensity = MathHelper.clamp((hurtShake - 7.0F) / 3.0F, 0.0F, 1.0F); + if (!(intensity <= 0.0F)) { + double wobble = 0.004 * intensity; + float roll = 2.0F * intensity; + renderPassInfo.poseStack().translate(wobble, 0.0, 0.0); + renderPassInfo.poseStack().multiply(RotationAxis.POSITIVE_Z.rotationDegrees(roll)); + } + } + } + + public int getPackedOverlay(PregnancyEggEntity animatable, Void relatedObject, float u, float partialTick) { + return OverlayTexture.DEFAULT_UV; + } + + protected float getDeathMaxRotation(@NotNull GeoRenderState renderState) { + return 0.0F; + } +} diff --git a/src/main/java/com/nonid/client/intiface/NonIntifaceBridge.java b/src/main/java/com/nonid/client/intiface/NonIntifaceBridge.java new file mode 100644 index 0000000..b3e4226 --- /dev/null +++ b/src/main/java/com/nonid/client/intiface/NonIntifaceBridge.java @@ -0,0 +1,191 @@ +package com.nonid.client.intiface; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import net.minecraft.text.Text; + +public final class NonIntifaceBridge { + private static final String CORE_CLASS = "io.github.blackspherefollower.buttplug4j.ButtplugException"; + private static final String CONNECTOR_CLASS = "io.github.blackspherefollower.buttplug4j.connectors.jetty.websocket.client.ButtplugClientWSClient"; + private static final String JACKSON_CORE_CLASS = "com.fasterxml.jackson.core.JsonProcessingException"; + private static final String JACKSON_DATABIND_CLASS = "com.fasterxml.jackson.databind.ObjectMapper"; + private static volatile Boolean available; + private static volatile boolean warnedUnavailable; + + private NonIntifaceBridge() { + } + + public static boolean isRuntimeAvailable() { + return isAvailable(); + } + + public static Text statusText(NonConfig config) { + if (!isUsable()) { + return Text.translatable("config.needsofnature.intiface.status.unavailable"); + } + + try { + return NonIntifaceClient.statusText(config); + } catch (LinkageError e) { + markUnavailable(e); + return Text.translatable("config.needsofnature.intiface.status.unavailable"); + } + } + + public static boolean isConnected() { + if (!isUsable()) { + return false; + } + + try { + return NonIntifaceClient.isConnected(); + } catch (LinkageError e) { + markUnavailable(e); + return false; + } + } + + public static void onWorldJoin(NonConfig config) { + if (isUsable()) { + try { + NonIntifaceClient.onWorldJoin(config); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void onWorldDisconnect(NonConfig config) { + if (isUsable()) { + try { + NonIntifaceClient.onWorldDisconnect(config); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void connect(NonConfig config) { + if (isUsable()) { + try { + NonIntifaceClient.connect(config); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void disconnect() { + if (isUsable()) { + try { + NonIntifaceClient.disconnect(); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void startScanning(NonConfig config) { + if (isUsable()) { + try { + NonIntifaceClient.startScanning(config); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void stopAll() { + if (isUsable()) { + try { + NonIntifaceClient.stopAll(); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void testPulse(NonConfig config, boolean peak) { + if (isUsable()) { + try { + NonIntifaceClient.testPulse(config, peak); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void pulseReactiveImpact(NonConfig config, double nextReactiveImpactDelayMs) { + if (isUsable()) { + try { + NonIntifaceClient.pulseReactiveImpact(config, nextReactiveImpactDelayMs); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void pulsePeakReactiveImpact(NonConfig config, double nextReactiveImpactDelayMs) { + if (isUsable()) { + try { + NonIntifaceClient.pulsePeakReactiveImpact(config, nextReactiveImpactDelayMs); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void clientTick(NonConfig config, boolean localAnimationActive, boolean peakStage, double specialVibratorScalar) { + if (isUsable()) { + try { + NonIntifaceClient.clientTick(config, localAnimationActive, peakStage, specialVibratorScalar); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + public static void clientTick( + NonConfig config, boolean localAnimationActive, boolean peakStage, double specialVibratorScalar, int energizedLevel, double energizedHeartPulse + ) { + if (isUsable()) { + try { + NonIntifaceClient.clientTick(config, localAnimationActive, peakStage, specialVibratorScalar, energizedLevel, energizedHeartPulse); + } catch (LinkageError e) { + markUnavailable(e); + } + } + } + + private static boolean isAvailable() { + Boolean cached = available; + if (cached != null) { + return cached; + } + + try { + ClassLoader loader = NonIntifaceBridge.class.getClassLoader(); + Class.forName("io.github.blackspherefollower.buttplug4j.ButtplugException", false, loader); + Class.forName("io.github.blackspherefollower.buttplug4j.connectors.jetty.websocket.client.ButtplugClientWSClient", false, loader); + Class.forName("com.fasterxml.jackson.core.JsonProcessingException", false, loader); + Class.forName("com.fasterxml.jackson.databind.ObjectMapper", false, loader); + available = true; + return true; + } catch (ClassNotFoundException | LinkageError e) { + markUnavailable(e); + return false; + } + } + + private static boolean isUsable() { + return isAvailable() && !NonIntifaceDependencyManager.generatedBundleOutdated() && !NonIntifaceDependencyManager.restartRequired(); + } + + private static void markUnavailable(Throwable error) { + available = false; + if (!warnedUnavailable) { + warnedUnavailable = true; + NeedsOfNature.LOGGER.warn("[NoN] Buttplug.io integration is unavailable; disabling toy output.", error); + } + } +} diff --git a/src/main/java/com/nonid/client/intiface/NonIntifaceClient.java b/src/main/java/com/nonid/client/intiface/NonIntifaceClient.java new file mode 100644 index 0000000..d6229d4 --- /dev/null +++ b/src/main/java/com/nonid/client/intiface/NonIntifaceClient.java @@ -0,0 +1,843 @@ +package com.nonid.client.intiface; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import io.github.blackspherefollower.buttplug4j.client.ButtplugClientDevice; +import io.github.blackspherefollower.buttplug4j.client.ButtplugOutput; +import io.github.blackspherefollower.buttplug4j.connectors.jetty.websocket.client.ButtplugClientWSClient; +import io.github.blackspherefollower.buttplug4j.protocol.ButtplugMessage; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.URI; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import net.minecraft.text.Text; + +public final class NonIntifaceClient { + private static final int CONNECT_TIMEOUT_MS = 8000; + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r, "NoN Intiface"); + thread.setDaemon(true); + return thread; + }); + private static volatile ButtplugClientWSClient client; + private static volatile NonIntifaceClient.State state = NonIntifaceClient.State.DISCONNECTED; + private static volatile String detail = ""; + private static volatile long lastPulseMs = 0L; + private static volatile long pulseToken = 0L; + private static volatile ScheduledFuture pendingStop; + private static volatile ScheduledFuture pendingStrokerReturn; + private static volatile boolean requestedAnimationActive = false; + private static volatile boolean requestedPeakStage = false; + private static volatile boolean requestedIntifaceEnabled = false; + private static volatile int requestedBaselineStrengthPercent = Integer.MIN_VALUE; + private static volatile int requestedOscillatorRegularSpeedPercent = Integer.MIN_VALUE; + private static volatile int requestedOscillatorPeakSpeedPercent = Integer.MIN_VALUE; + private static volatile double requestedSpecialVibratorScalar = Double.NaN; + private static volatile int requestedEnergizedLevel = 0; + private static volatile int requestedEnergizedBasePercent = 0; + private static volatile int requestedEnergizedPulsePercent = 0; + private static volatile int requestedEnergizedPulsePermille = 0; + private static volatile boolean baselineActive = false; + private static volatile int baselineStrengthPercent = 0; + private static volatile boolean oscillatorActive = false; + private static volatile int oscillatorSpeedPercent = 0; + private static volatile boolean specialVibratorActive = false; + private static volatile double specialVibratorScalar = 0.0; + private static volatile boolean energizedActive = false; + private static volatile int energizedBasePercent = 0; + private static volatile int energizedPulsePercent = 0; + private static volatile double energizedPulse = 0.0; + private static volatile double currentScalar = 0.0; + private static volatile double currentOscillatorScalar = 0.0; + private static volatile boolean suppressBaselineUntilInactive = false; + private static volatile long connectAttemptId = 0L; + + private NonIntifaceClient() { + } + + public static void onWorldJoin(NonConfig config) { + if (config != null && config.isIntifaceEnabled() && config.isIntifaceAutoConnectOnWorldJoin()) { + connect(config); + } else { + updateDisabledState(config); + } + } + + public static void onWorldDisconnect(NonConfig config) { + if (config != null && config.isIntifaceStopOnDisconnect()) { + stopAll(); + } + + disconnect(); + } + + public static void connect(NonConfig config) { + if (config != null && config.isIntifaceEnabled()) { + String url = config.getIntifaceServerUrl(); + NeedsOfNature.LOGGER.info("[NoN] Buttplug.io connect requested: url={}", url); + executeSafely("Connect failed", () -> connectInternal(url)); + } else { + state = NonIntifaceClient.State.DISABLED; + detail = ""; + } + } + + public static void disconnect() { + executeSafely("Disconnect failed", () -> { + cancelPendingStop(); + cancelPendingStrokerReturn(); + clearBaselineState(); + disconnectInternal(); + state = NonIntifaceClient.State.DISCONNECTED; + detail = ""; + }); + } + + public static void startScanning(NonConfig config) { + if (config != null && config.isIntifaceEnabled()) { + executeSafely("Scan failed", () -> { + connectInternal(config.getIntifaceServerUrl()); + ButtplugClientWSClient activeClient = client; + if (activeClient != null && activeClient.isConnected()) { + try { + state = NonIntifaceClient.State.SCANNING; + detail = ""; + activeClient.startScanning(); + } catch (Exception e) { + setError("Scan failed", e); + } + } + }); + } else { + state = NonIntifaceClient.State.DISABLED; + detail = ""; + } + } + + public static void stopAll() { + executeSafely("Stop failed", () -> { + suppressBaselineUntilInactive = true; + baselineActive = false; + oscillatorActive = false; + specialVibratorActive = false; + specialVibratorScalar = 0.0; + energizedActive = false; + energizedBasePercent = 0; + energizedPulsePercent = 0; + energizedPulse = 0.0; + cancelPendingStop(); + cancelPendingStrokerReturn(); + stopAllInternal(true); + }); + } + + public static void testPulse(NonConfig config, boolean peak) { + if (peak) { + pulsePeakReactiveImpact(config); + } else { + pulseReactiveImpact(config); + } + } + + public static void pulseReactiveImpact(NonConfig config) { + pulseReactiveImpact(config, 0.0); + } + + public static void pulseReactiveImpact(NonConfig config, double nextReactiveImpactDelayMs) { + if (config != null && config.isIntifaceEnabled()) { + queueReactiveImpact(config, false, nextReactiveImpactDelayMs); + } + } + + public static void pulsePeakReactiveImpact(NonConfig config) { + pulsePeakReactiveImpact(config, 0.0); + } + + public static void pulsePeakReactiveImpact(NonConfig config, double nextReactiveImpactDelayMs) { + if (config != null && config.isIntifaceEnabled()) { + queueReactiveImpact(config, true, nextReactiveImpactDelayMs); + } + } + + public static void clientTick(NonConfig config, boolean localAnimationActive, boolean peakStage) { + clientTick(config, localAnimationActive, peakStage, Double.NaN); + } + + public static void clientTick(NonConfig config, boolean localAnimationActive, boolean peakStage, double specialVibratorScalar) { + clientTick(config, localAnimationActive, peakStage, specialVibratorScalar, 0, 0.0); + } + + public static void clientTick( + NonConfig config, boolean localAnimationActive, boolean peakStage, double specialVibratorScalar, int energizedLevel, double energizedHeartPulse + ) { + boolean enabled = config != null && config.isIntifaceEnabled(); + int strengthPercent = enabled ? config.getIntifaceAnimationBaselineStrengthPercent() : 0; + int oscillatorRegularSpeedPercent = enabled ? config.getIntifaceOscillatorRegularSpeedPercent() : 0; + int oscillatorPeakSpeedPercent = enabled ? config.getIntifaceOscillatorPeakSpeedPercent() : 0; + double sanitizedSpecialScalar = sanitizeSpecialVibratorScalar(specialVibratorScalar); + int sanitizedEnergizedLevel = enabled ? Math.max(0, Math.min(3, energizedLevel)) : 0; + int energizedBase = sanitizedEnergizedLevel > 0 ? config.getIntifaceEnergizedBasePercent(sanitizedEnergizedLevel) : 0; + int energizedPeak = sanitizedEnergizedLevel > 0 ? config.getIntifaceEnergizedPulsePercent(sanitizedEnergizedLevel) : 0; + double sanitizedEnergizedPulse = sanitizeUnit(energizedHeartPulse); + int pulsePermille = (int)Math.round(sanitizedEnergizedPulse * 1000.0); + if (!localAnimationActive && suppressBaselineUntilInactive) { + suppressBaselineUntilInactive = false; + } + + if (localAnimationActive != requestedAnimationActive + || peakStage != requestedPeakStage + || strengthPercent != requestedBaselineStrengthPercent + || oscillatorRegularSpeedPercent != requestedOscillatorRegularSpeedPercent + || oscillatorPeakSpeedPercent != requestedOscillatorPeakSpeedPercent + || !specialScalarEquals(sanitizedSpecialScalar, requestedSpecialVibratorScalar) + || sanitizedEnergizedLevel != requestedEnergizedLevel + || energizedBase != requestedEnergizedBasePercent + || energizedPeak != requestedEnergizedPulsePercent + || pulsePermille != requestedEnergizedPulsePermille + || enabled != requestedIntifaceEnabled) { + requestedAnimationActive = localAnimationActive; + requestedPeakStage = peakStage; + requestedIntifaceEnabled = enabled; + requestedBaselineStrengthPercent = strengthPercent; + requestedOscillatorRegularSpeedPercent = oscillatorRegularSpeedPercent; + requestedOscillatorPeakSpeedPercent = oscillatorPeakSpeedPercent; + requestedSpecialVibratorScalar = sanitizedSpecialScalar; + requestedEnergizedLevel = sanitizedEnergizedLevel; + requestedEnergizedBasePercent = energizedBase; + requestedEnergizedPulsePercent = energizedPeak; + requestedEnergizedPulsePermille = pulsePermille; + executeSafely( + "Update failed", + () -> updateAnimationOutputs( + enabled, + localAnimationActive, + peakStage, + strengthPercent, + oscillatorRegularSpeedPercent, + oscillatorPeakSpeedPercent, + sanitizedSpecialScalar, + sanitizedEnergizedLevel, + energizedBase, + energizedPeak, + sanitizedEnergizedPulse + ) + ); + } + } + + public static Text statusText(NonConfig config) { + updateDisabledState(config); + int devices = deviceCount(); + String suffix = detail != null && !detail.isBlank() ? " - " + detail : ""; + return Text.translatable("config.needsofnature.intiface.status." + state.name().toLowerCase(Locale.ROOT), new Object[]{devices}) + .append(Text.literal(suffix)); + } + + public static int deviceCount() { + ButtplugClientWSClient activeClient = client; + if (activeClient != null && activeClient.isConnected()) { + try { + return activeClient.getDevices().size(); + } catch (RuntimeException e) { + return 0; + } + } else { + return 0; + } + } + + public static boolean isConnected() { + ButtplugClientWSClient activeClient = client; + return activeClient != null && activeClient.isConnected(); + } + + private static void queueReactiveImpact(NonConfig config, boolean peak, double nextReactiveImpactDelayMs) { + int strengthPercent = peak ? config.getIntifacePeakReactiveImpactStrengthPercent() : config.getIntifaceReactiveImpactStrengthPercent(); + int durationMs = peak ? config.getIntifacePeakReactiveImpactDurationMs() : config.getIntifaceReactiveImpactDurationMs(); + int cooldownMs = config.getIntifaceCooldownMs(); + double scalar = Math.max(sustainedVibratorScalar(), Math.max(0.0, Math.min(1.0, strengthPercent / 100.0))); + int strokerMin = config.getIntifaceStrokerMinDistancePercent(); + int strokerMax = config.getIntifaceStrokerMaxDistancePercent(); + int strokerMoveDurationMs = peak ? config.getIntifaceStrokerPeakMoveDurationMs() : config.getIntifaceStrokerRegularMoveDurationMs(); + executeSafely("Pulse failed", () -> { + ButtplugClientWSClient activeClient = client; + if (activeClient != null && activeClient.isConnected()) { + long now = System.currentTimeMillis(); + if (cooldownMs <= 0 || now - lastPulseMs >= cooldownMs) { + boolean sentAny = false; + if (!specialVibratorActive && scalar > 0.0 && durationMs > 0) { + sentAny = sendVibrate(activeClient, scalar) || sentAny; + long token = ++pulseToken; + cancelPendingStop(); + pendingStop = EXECUTOR.schedule(() -> { + if (token == pulseToken) { + returnToBaselineOrStop(activeClient); + pendingStop = null; + } + }, durationMs, TimeUnit.MILLISECONDS); + } + + sentAny = triggerStrokerImpact(activeClient, strokerMin, strokerMax, strokerMoveDurationMs, nextReactiveImpactDelayMs) || sentAny; + if (sentAny) { + lastPulseMs = now; + } + } + } + }); + } + + private static void updateAnimationOutputs( + boolean enabled, + boolean localAnimationActive, + boolean peakStage, + int strengthPercent, + int oscillatorRegularSpeedPercent, + int oscillatorPeakSpeedPercent, + double specialScalar, + int energizedLevel, + int energizedBaseStrengthPercent, + int energizedPulseStrengthPercent, + double energizedHeartPulse + ) { + if (!localAnimationActive) { + suppressBaselineUntilInactive = false; + } + + boolean shouldUseSpecialVibrator = enabled && localAnimationActive && !Double.isNaN(specialScalar) && !suppressBaselineUntilInactive; + int clampedStrength = Math.max(0, Math.min(100, strengthPercent)); + boolean shouldUseBaseline = enabled && localAnimationActive && clampedStrength > 0 && !suppressBaselineUntilInactive && !shouldUseSpecialVibrator; + baselineActive = shouldUseBaseline; + baselineStrengthPercent = shouldUseBaseline ? clampedStrength : 0; + specialVibratorActive = shouldUseSpecialVibrator; + specialVibratorScalar = shouldUseSpecialVibrator ? Math.max(0.0, Math.min(1.0, specialScalar)) : 0.0; + int clampedEnergizedBase = Math.max(0, Math.min(100, energizedBaseStrengthPercent)); + int clampedEnergizedPeak = Math.max(0, Math.min(100, energizedPulseStrengthPercent)); + boolean shouldUseEnergized = enabled && energizedLevel > 0 && Math.max(clampedEnergizedBase, clampedEnergizedPeak) > 0 && !shouldUseSpecialVibrator; + energizedActive = shouldUseEnergized; + energizedBasePercent = shouldUseEnergized ? clampedEnergizedBase : 0; + energizedPulsePercent = shouldUseEnergized ? clampedEnergizedPeak : 0; + energizedPulse = shouldUseEnergized ? sanitizeUnit(energizedHeartPulse) : 0.0; + int requestedOscillatorSpeed = Math.max(0, Math.min(100, peakStage ? oscillatorPeakSpeedPercent : oscillatorRegularSpeedPercent)); + boolean shouldUseOscillator = enabled && localAnimationActive && requestedOscillatorSpeed > 0 && !suppressBaselineUntilInactive; + oscillatorActive = shouldUseOscillator; + oscillatorSpeedPercent = shouldUseOscillator ? requestedOscillatorSpeed : 0; + if (shouldUseSpecialVibrator) { + cancelPendingStop(); + } + + if (!shouldUseBaseline && !shouldUseOscillator && !shouldUseSpecialVibrator && !shouldUseEnergized) { + cancelPendingStop(); + cancelPendingStrokerReturn(); + if (currentScalar > 0.0) { + sendVibrate(client, 0.0); + } + + if (currentOscillatorScalar > 0.0) { + sendOscillate(client, 0.0); + } + } else { + ButtplugClientWSClient activeClient = client; + if (activeClient != null && activeClient.isConnected()) { + if (pendingStop == null || shouldUseSpecialVibrator) { + double vibrateScalar = shouldUseSpecialVibrator ? specialVibratorScalar : sustainedVibratorScalar(); + if (vibrateScalar > 0.0 && Math.abs(currentScalar - vibrateScalar) > 0.001) { + sendVibrate(activeClient, vibrateScalar); + } else if (vibrateScalar <= 0.0 && currentScalar > 0.0) { + sendVibrate(activeClient, 0.0); + } + + double oscillateScalar = oscillatorScalar(); + if (oscillateScalar > 0.0 && Math.abs(currentOscillatorScalar - oscillateScalar) > 0.001) { + sendOscillate(activeClient, oscillateScalar); + } else if (oscillateScalar <= 0.0 && currentOscillatorScalar > 0.0) { + sendOscillate(activeClient, 0.0); + } + } + } + } + } + + private static double baselineScalar() { + return baselineActive && baselineStrengthPercent > 0 ? Math.max(0.0, Math.min(1.0, baselineStrengthPercent / 100.0)) : 0.0; + } + + private static double energizedScalar() { + if (!energizedActive) { + return 0.0; + } + + double base = Math.max(0.0, Math.min(1.0, energizedBasePercent / 100.0)); + double peak = Math.max(0.0, Math.min(1.0, energizedPulsePercent / 100.0)); + return base + (peak - base) * sanitizeUnit(energizedPulse); + } + + private static double sustainedVibratorScalar() { + return Math.max(baselineScalar(), energizedScalar()); + } + + private static double oscillatorScalar() { + return oscillatorActive && oscillatorSpeedPercent > 0 ? Math.max(0.0, Math.min(1.0, oscillatorSpeedPercent / 100.0)) : 0.0; + } + + private static double sanitizeSpecialVibratorScalar(double scalar) { + if (Double.isNaN(scalar)) { + return Double.NaN; + } else { + return Double.isInfinite(scalar) ? 0.0 : Math.max(0.0, Math.min(1.0, scalar)); + } + } + + private static double sanitizeUnit(double value) { + return !Double.isNaN(value) && !Double.isInfinite(value) ? Math.max(0.0, Math.min(1.0, value)) : 0.0; + } + + private static boolean specialScalarEquals(double first, double second) { + return !Double.isNaN(first) && !Double.isNaN(second) ? Math.abs(first - second) < 0.001 : Double.isNaN(first) && Double.isNaN(second); + } + + private static void returnToBaselineOrStop(ButtplugClientWSClient activeClient) { + double scalar = sustainedVibratorScalar(); + if (!(scalar > 0.0) || activeClient == null || !activeClient.isConnected() || !sendVibrate(activeClient, scalar)) { + if (activeClient != null && activeClient.isConnected()) { + sendVibrate(activeClient, 0.0); + } else { + currentScalar = 0.0; + } + } + } + + private static void connectInternal(String url) { + long attemptId = ++connectAttemptId; + ButtplugClientWSClient activeClient = client; + if (activeClient != null && activeClient.isConnected()) { + state = NonIntifaceClient.State.CONNECTED; + detail = ""; + NeedsOfNature.LOGGER.info("[NoN] Buttplug.io connect skipped: already connected. attempt={}", attemptId); + } else { + disconnectInternal(); + ButtplugClientWSClient nextClient = null; + + try { + state = NonIntifaceClient.State.CONNECTING; + detail = "Connecting to " + url; + NeedsOfNature.LOGGER.info("[NoN] Buttplug.io connect started: attempt={} url={} timeout={}ms", new Object[]{attemptId, url, 8000}); + nextClient = new ButtplugClientWSClient("NeedsOfNature"); + nextClient.setDeviceAddedHandler( + device -> { + if (attemptId == connectAttemptId) { + state = NonIntifaceClient.State.CONNECTED; + detail = Text.translatable("config.needsofnature.intiface.status.device_added", new Object[]{device.getDisplayName()}).getString(); + updateAnimationOutputs( + requestedIntifaceEnabled, + requestedAnimationActive, + requestedPeakStage, + requestedBaselineStrengthPercent, + requestedOscillatorRegularSpeedPercent, + requestedOscillatorPeakSpeedPercent, + requestedSpecialVibratorScalar, + requestedEnergizedLevel, + requestedEnergizedBasePercent, + requestedEnergizedPulsePercent, + requestedEnergizedPulsePermille / 1000.0 + ); + } + } + ); + nextClient.setDeviceRemovedHandler(index -> { + if (attemptId == connectAttemptId) { + state = NonIntifaceClient.State.CONNECTED; + detail = Text.translatable("config.needsofnature.intiface.status.device_removed").getString(); + } + }); + nextClient.setScanningFinishedHandler(() -> { + if (attemptId == connectAttemptId) { + state = NonIntifaceClient.State.CONNECTED; + detail = Text.translatable("config.needsofnature.intiface.status.scan_finished").getString(); + } + }); + nextClient.setErrorHandler(error -> { + if (attemptId == connectAttemptId) { + setError(error.getErrorMessage(), error.getException()); + } + }); + ButtplugClientWSClient connectClient = nextClient; + FutureTask connectTask = new FutureTask<>(() -> { + connectClient.connect(new URI(url)); + return null; + }); + Thread connectThread = new Thread(connectTask, "NoN Intiface Connect"); + connectThread.setDaemon(true); + connectThread.start(); + connectTask.get(8000L, TimeUnit.MILLISECONDS); + if (attemptId != connectAttemptId) { + disconnectClient(nextClient); + return; + } + + client = nextClient; + state = NonIntifaceClient.State.CONNECTED; + detail = ""; + NeedsOfNature.LOGGER.info("[NoN] Buttplug.io connect succeeded: attempt={} url={}", attemptId, url); + updateAnimationOutputs( + requestedIntifaceEnabled, + requestedAnimationActive, + requestedPeakStage, + requestedBaselineStrengthPercent, + requestedOscillatorRegularSpeedPercent, + requestedOscillatorPeakSpeedPercent, + requestedSpecialVibratorScalar, + requestedEnergizedLevel, + requestedEnergizedBasePercent, + requestedEnergizedPulsePercent, + requestedEnergizedPulsePermille / 1000.0 + ); + } catch (TimeoutException e) { + connectAttemptId++; + NeedsOfNature.LOGGER.warn("[NoN] Buttplug.io connect timed out: attempt={} url={} timeout={}ms", new Object[]{attemptId, url, 8000}); + setError("Connect timed out after 8s", e); + disconnectClient(nextClient); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + connectAttemptId++; + NeedsOfNature.LOGGER.warn("[NoN] Buttplug.io connect interrupted: attempt={} url={}", new Object[]{attemptId, url, e}); + setError("Connect interrupted", e); + disconnectClient(nextClient); + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + connectAttemptId++; + handleConnectFailure(attemptId, url, cause); + disconnectClient(nextClient); + } catch (Exception e) { + connectAttemptId++; + handleConnectFailure(attemptId, url, e); + disconnectClient(nextClient); + } catch (Throwable e) { + connectAttemptId++; + handleConnectFailure(attemptId, url, e); + disconnectClient(nextClient); + } + } + } + + private static void handleConnectFailure(long attemptId, String url, Throwable throwable) { + String expectedMessage = expectedConnectFailureMessage(throwable); + if (expectedMessage != null) { + NeedsOfNature.LOGGER.warn("[NoN] Buttplug.io connect failed: attempt={} url={} reason={}", new Object[]{attemptId, url, expectedMessage}); + setError(expectedMessage, null); + } else { + NeedsOfNature.LOGGER.warn("[NoN] Buttplug.io connect failed: attempt={} url={}", new Object[]{attemptId, url, throwable}); + setError("Connect failed: " + simpleThrowableName(throwable), throwable); + } + } + + private static String expectedConnectFailureMessage(Throwable throwable) { + for (Throwable current = throwable; current != null; current = current.getCause()) { + if (current instanceof ConnectException) { + return "Could not reach Buttplug.io?"; + } + + if (current instanceof NoRouteToHostException || current instanceof UnknownHostException || current instanceof UnresolvedAddressException) { + return "Could not reach Buttplug.io server"; + } + + String className = current.getClass().getSimpleName().toLowerCase(Locale.ROOT); + if (className.contains("upgradeexception")) { + return "Could not connect to Buttplug.io. Is Intiface Central running and using the configured WebSocket URL?"; + } + + String message = current.getMessage(); + if (message != null) { + String lower = message.toLowerCase(Locale.ROOT); + if (lower.contains("connection refused") + || lower.contains("failed to connect") + || lower.contains("connection reset") + || lower.contains("connection timed out") + || lower.contains("connection timeout") + || lower.contains("connect timed out")) { + return "Could not reach Buttplug.io. Is Intiface Central running?"; + } + + if (lower.contains("failed to upgrade to websocket") || lower.contains("unexpected http response status code") || lower.contains("400 bad request") + ) + { + return "Could not connect to Buttplug.io. Is Intiface Central running and using the configured WebSocket URL?"; + } + } + } + + return null; + } + + private static String simpleThrowableName(Throwable throwable) { + return throwable == null ? "Unknown error" : throwable.getClass().getSimpleName(); + } + + private static boolean triggerStrokerImpact( + ButtplugClientWSClient activeClient, int minDistancePercent, int maxDistancePercent, int moveDurationMs, double nextReactiveImpactDelayMs + ) { + if (activeClient != null && activeClient.isConnected()) { + int min = Math.max(0, Math.min(100, Math.min(minDistancePercent, maxDistancePercent))); + int max = Math.max(0, Math.min(100, Math.max(minDistancePercent, maxDistancePercent))); + int durationMs = Math.max(0, Math.min(10000, moveDurationMs)); + if (durationMs > 0 && max > min) { + double minPosition = min / 100.0; + double maxPosition = max / 100.0; + boolean sentAny = sendLinear(activeClient, maxPosition, durationMs); + if (!sentAny) { + return false; + } + + cancelPendingStrokerReturn(); + long arrivalDelayMs = nextReactiveImpactDelayMs > 0.0 ? Math.max(0L, Math.round(nextReactiveImpactDelayMs / 2.0)) : 500L; + long scheduleDelayMs = Math.max(durationMs, arrivalDelayMs - durationMs); + pendingStrokerReturn = EXECUTOR.schedule(() -> { + sendLinear(activeClient, minPosition, durationMs); + pendingStrokerReturn = null; + }, scheduleDelayMs, TimeUnit.MILLISECONDS); + return true; + } else { + return false; + } + } else { + return false; + } + } + + private static boolean sendVibrate(ButtplugClientWSClient activeClient, double scalar) { + List devices = activeClient.getDevices(); + if (devices.isEmpty()) { + detail = Text.translatable("config.needsofnature.intiface.status.no_devices").getString(); + return false; + } + + boolean sentAny = false; + + for (ButtplugClientDevice device : devices) { + try { + if (device.hasOutput(ButtplugOutput.VIBRATE)) { + waitForDeviceCommands(device.runVibrateFloat((float)scalar)); + sentAny = true; + } + } catch (Exception e) { + NeedsOfNature.LOGGER.debug("[NoN] Intiface pulse failed for device {}.", device.getDisplayName(), e); + } + } + + if (sentAny) { + currentScalar = scalar; + } + + if (!sentAny) { + detail = Text.translatable("config.needsofnature.intiface.status.no_vibrate_devices").getString(); + } + + return sentAny; + } + + private static boolean sendOscillate(ButtplugClientWSClient activeClient, double scalar) { + if (activeClient != null && activeClient.isConnected()) { + List devices = activeClient.getDevices(); + boolean sentAny = false; + + for (ButtplugClientDevice device : devices) { + try { + if (device.hasOutput(ButtplugOutput.OSCILLATE)) { + waitForDeviceCommands(device.runOscillateFloat((float)scalar)); + sentAny = true; + } + } catch (Exception e) { + NeedsOfNature.LOGGER.debug("[NoN] Intiface oscillate failed for device {}.", device.getDisplayName(), e); + } + } + + if (sentAny) { + currentOscillatorScalar = scalar; + } + + return sentAny; + } else { + currentOscillatorScalar = 0.0; + return false; + } + } + + private static boolean sendLinear(ButtplugClientWSClient activeClient, double position, long durationMs) { + if (activeClient != null && activeClient.isConnected()) { + List devices = activeClient.getDevices(); + boolean sentAny = false; + + for (ButtplugClientDevice device : devices) { + try { + if (device.hasOutput(ButtplugOutput.HW_POSITION_WITH_DURATION)) { + waitForDeviceCommands(device.runHwPositionWithDurationFloat((float)position, (int)Math.max(0L, durationMs))); + } else { + if (!device.hasOutput(ButtplugOutput.POSITION)) { + continue; + } + + waitForDeviceCommands(device.runPositionFloat((float)position)); + } + + sentAny = true; + } catch (Exception e) { + NeedsOfNature.LOGGER.debug("[NoN] Intiface linear failed for device {}.", device.getDisplayName(), e); + } + } + + return sentAny; + } else { + return false; + } + } + + private static void waitForDeviceCommands(List> futures) throws Exception { + if (futures != null && !futures.isEmpty()) { + for (Future future : futures) { + if (future != null) { + future.get(750L, TimeUnit.MILLISECONDS); + } + } + } + } + + private static void stopAllInternal(boolean userVisible) { + ButtplugClientWSClient activeClient = client; + if (activeClient != null && activeClient.isConnected()) { + try { + activeClient.stopAllDevices(); + currentScalar = 0.0; + currentOscillatorScalar = 0.0; + if (userVisible) { + detail = Text.translatable("config.needsofnature.intiface.status.stopped").getString(); + } + } catch (Exception e) { + setError("Stop failed", e); + } + } else { + currentScalar = 0.0; + currentOscillatorScalar = 0.0; + } + } + + private static void disconnectInternal() { + ButtplugClientWSClient activeClient = client; + client = null; + currentScalar = 0.0; + currentOscillatorScalar = 0.0; + if (activeClient != null) { + try { + activeClient.disconnect(); + } catch (RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Intiface disconnect failed.", e); + } + } + } + + private static void disconnectClient(ButtplugClientWSClient targetClient) { + if (targetClient != null) { + if (client == targetClient) { + client = null; + } + + currentScalar = 0.0; + currentOscillatorScalar = 0.0; + + try { + targetClient.disconnect(); + } catch (RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Intiface disconnect failed.", e); + } + } + } + + private static void cancelPendingStop() { + ScheduledFuture stop = pendingStop; + pendingStop = null; + if (stop != null) { + stop.cancel(false); + } + } + + private static void cancelPendingStrokerReturn() { + ScheduledFuture stop = pendingStrokerReturn; + pendingStrokerReturn = null; + if (stop != null) { + stop.cancel(false); + } + } + + private static void setError(String message, Throwable throwable) { + state = NonIntifaceClient.State.ERROR; + detail = message != null && !message.isBlank() ? message : "Unknown error"; + if (throwable != null) { + NeedsOfNature.LOGGER.debug("[NoN] Intiface error: {}", detail, throwable); + } else { + NeedsOfNature.LOGGER.debug("[NoN] Intiface error: {}", detail); + } + } + + private static void executeSafely(String errorMessage, Runnable action) { + EXECUTOR.execute(() -> { + try { + action.run(); + } catch (Throwable throwable) { + NeedsOfNature.LOGGER.warn("[NoN] Buttplug.io worker failure: {}", errorMessage, throwable); + setError(errorMessage + ": " + throwable.getClass().getSimpleName(), throwable); + } + }); + } + + private static void updateDisabledState(NonConfig config) { + if (config != null && !config.isIntifaceEnabled()) { + state = NonIntifaceClient.State.DISABLED; + detail = ""; + } + } + + private static void clearBaselineState() { + requestedAnimationActive = false; + requestedPeakStage = false; + requestedIntifaceEnabled = false; + requestedBaselineStrengthPercent = Integer.MIN_VALUE; + requestedOscillatorRegularSpeedPercent = Integer.MIN_VALUE; + requestedOscillatorPeakSpeedPercent = Integer.MIN_VALUE; + requestedSpecialVibratorScalar = Double.NaN; + requestedEnergizedLevel = 0; + requestedEnergizedBasePercent = 0; + requestedEnergizedPulsePercent = 0; + requestedEnergizedPulsePermille = 0; + baselineActive = false; + baselineStrengthPercent = 0; + oscillatorActive = false; + oscillatorSpeedPercent = 0; + specialVibratorActive = false; + specialVibratorScalar = 0.0; + energizedActive = false; + energizedBasePercent = 0; + energizedPulsePercent = 0; + energizedPulse = 0.0; + currentScalar = 0.0; + currentOscillatorScalar = 0.0; + suppressBaselineUntilInactive = false; + } + + private enum State { + DISABLED, + DISCONNECTED, + CONNECTING, + CONNECTED, + SCANNING, + ERROR; + } +} diff --git a/src/main/java/com/nonid/client/intiface/NonIntifaceDependencyManager.java b/src/main/java/com/nonid/client/intiface/NonIntifaceDependencyManager.java new file mode 100644 index 0000000..417e57c --- /dev/null +++ b/src/main/java/com/nonid/client/intiface/NonIntifaceDependencyManager.java @@ -0,0 +1,458 @@ +package com.nonid.client.intiface; + +import com.nonid.NeedsOfNature; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.stream.Stream; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; + +public final class NonIntifaceDependencyManager { + private static final String GENERATED_MOD_ID = "needsofnature_intiface_libraries"; + private static final String GENERATED_JAR_NAME = "needsofnature-intiface-libs-generated.jar"; + private static final String BUNDLE_MARKER = "META-INF/needsofnature-intiface-libs.sha256"; + private static final String MAVEN_CENTRAL = "https://repo.maven.apache.org/maven2/"; + private static final String CORE_CLASS_ENTRY = "io/github/blackspherefollower/buttplug4j/ButtplugException.class"; + private static final String CONNECTOR_CLASS_ENTRY = "io/github/blackspherefollower/buttplug4j/connectors/jetty/websocket/client/ButtplugClientWSClient.class"; + private static final String JACKSON_CLASS_ENTRY = "com/fasterxml/jackson/databind/ObjectMapper.class"; + private static final List ARTIFACTS = List.of( + new NonIntifaceDependencyManager.Artifact( + "io.github.blackspherefollower", + "buttplug4j.connectors.jetty.websocket.client", + "4.0.278", + "6ffb6507595137ae6cd3cda932d47e3b6ef32e12393c58ef60d7ec231ff2752c", + 6320L + ), + new NonIntifaceDependencyManager.Artifact( + "io.github.blackspherefollower", "buttplug4j", "4.0.278", "094055e1bf64bc416b7a984b80d5216febbc20dcf5b5149ca1e4513056ccc4fa", 82360L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty.websocket", "websocket-client", "9.4.58.v20250814", "fdb8648fb1139ab001c8d5ebc4f3ab5baf007f48bd86ec2969c504ac3011f878", 46116L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty.websocket", "websocket-common", "9.4.58.v20250814", "07c62b7ceaff2d7160aff98b912f28da9f2c66ed7adec972e39af5176afec1ae", 217092L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty.websocket", "websocket-api", "9.4.58.v20250814", "213d146065ebf2cbfa8a9c06b4cdfe79f94c0af3f93c15d9e67c0e3af015067f", 52516L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty", "jetty-client", "9.4.58.v20250814", "629371e5f65f321832930538bd485e5fd637d180412ea75f592881921e2d4b83", 331779L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty", "jetty-http", "9.4.58.v20250814", "8f49b8583fc8dbbfe3a0ba80a04c97df64ebabca2981b56ee403c989aba9d4da", 253188L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty", "jetty-io", "9.4.58.v20250814", "f55b2cc7c05244fd7b1773e99979b740002b0186e45b8a688243d89c7006fe21", 184923L + ), + new NonIntifaceDependencyManager.Artifact( + "org.eclipse.jetty", "jetty-util", "9.4.58.v20250814", "8d591679d318d20c8fb2b1e83900562b3c4571ff7a189696be700293e0204146", 595392L + ), + new NonIntifaceDependencyManager.Artifact( + "com.fasterxml.jackson.core", "jackson-annotations", "2.20", "959a2ffb2d591436f51f183c6a521fc89347912f711bf0cae008cdf045d95319", 79195L + ), + new NonIntifaceDependencyManager.Artifact( + "com.fasterxml.jackson.core", "jackson-core", "2.20.1", "ffab4d957daa2796cf24cb66d0b78a7090f1bcbe17c3a4578f09affaaf137089", 593204L + ), + new NonIntifaceDependencyManager.Artifact( + "com.fasterxml.jackson.core", "jackson-databind", "2.20.1", "34bbeb4526fff4f8565b12106bf85a6afcbae858966d489b54214ac46b2e26e8", 1688464L + ) + ); + private static final ExecutorService INSTALL_EXECUTOR = Executors.newSingleThreadExecutor(r -> { + Thread thread = new Thread(r, "NoN Intiface Library Installer"); + thread.setDaemon(true); + return thread; + }); + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15L)).followRedirects(Redirect.NORMAL).build(); + private static volatile NonIntifaceDependencyManager.InstallState installState = NonIntifaceDependencyManager.InstallState.IDLE; + private static volatile String detail = ""; + private static volatile long cachedBundleModified = Long.MIN_VALUE; + private static volatile boolean cachedBundleValid = false; + + private NonIntifaceDependencyManager() { + } + + public static boolean runtimeLibrariesAvailable() { + return NonIntifaceBridge.isRuntimeAvailable(); + } + + public static boolean librariesUsable() { + return runtimeLibrariesAvailable() && !generatedBundleOutdated() && installState != NonIntifaceDependencyManager.InstallState.INSTALLED_RESTART_REQUIRED; + } + + public static boolean installRequired() { + return !runtimeLibrariesAvailable() || generatedBundleOutdated(); + } + + public static boolean generatedBundleOutdated() { + return generatedBundleExists() && !generatedBundleValid(); + } + + private static boolean generatedBundleExists() { + return Files.exists(generatedBundlePath()); + } + + public static boolean generatedBundleValid() { + Path bundle = generatedBundlePath(); + long modified = -1L; + + try { + if (Files.exists(bundle)) { + modified = Files.getLastModifiedTime(bundle).toMillis(); + } + } catch (IOException ignored) { + modified = -1L; + } + + if (modified == cachedBundleModified) { + return cachedBundleValid; + } + + boolean valid = verifyGeneratedBundle(bundle); + cachedBundleModified = modified; + cachedBundleValid = valid; + return valid; + } + + public static boolean restartRequired() { + return installState == NonIntifaceDependencyManager.InstallState.INSTALLED_RESTART_REQUIRED || !runtimeLibrariesAvailable() && generatedBundleValid(); + } + + public static boolean installing() { + return installState == NonIntifaceDependencyManager.InstallState.DOWNLOADING || installState == NonIntifaceDependencyManager.InstallState.BUILDING; + } + + public static Text statusText() { + if (installing()) { + return Text.translatable("config.needsofnature.intiface.libs.status.installing", new Object[]{detail}); + } else if (restartRequired()) { + return Text.translatable("config.needsofnature.intiface.libs.status.restart_required"); + } else if (installState == NonIntifaceDependencyManager.InstallState.FAILED) { + return Text.translatable("config.needsofnature.intiface.libs.status.failed", new Object[]{detail}); + } else if (generatedBundleOutdated()) { + return Text.translatable("config.needsofnature.intiface.libs.status.outdated"); + } else { + return runtimeLibrariesAvailable() + ? Text.translatable("config.needsofnature.intiface.libs.status.available") + : Text.translatable("config.needsofnature.intiface.libs.status.missing", new Object[]{totalSizeMbText()}); + } + } + + public static String totalSizeMbText() { + double mb = totalBytes() / 1024.0 / 1024.0; + return String.format(Locale.ROOT, "%.1f MB", mb); + } + + public static void installAsync(Runnable callback) { + if (!installing()) { + installState = NonIntifaceDependencyManager.InstallState.DOWNLOADING; + detail = ""; + INSTALL_EXECUTOR.execute(() -> { + try { + List downloads = downloadArtifacts(); + installState = NonIntifaceDependencyManager.InstallState.BUILDING; + detail = "Building library bundle"; + buildGeneratedBundle(downloads); + cachedBundleModified = Long.MIN_VALUE; + cachedBundleValid = false; + installState = NonIntifaceDependencyManager.InstallState.INSTALLED_RESTART_REQUIRED; + detail = ""; + NeedsOfNature.LOGGER.info("[NoN] Installed Buttplug.io libraries to {}", generatedBundlePath()); + } catch (Exception e) { + installState = NonIntifaceDependencyManager.InstallState.FAILED; + detail = e.getMessage() != null && !e.getMessage().isBlank() ? e.getMessage() : e.getClass().getSimpleName(); + NeedsOfNature.LOGGER.warn("[NoN] Failed to install Buttplug.io libraries.", e); + } finally { + if (callback != null) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + client.execute(callback); + } + } + } + }); + } + } + + private static List downloadArtifacts() throws Exception { + List result = new ArrayList<>(ARTIFACTS.size()); + + for (NonIntifaceDependencyManager.Artifact artifact : ARTIFACTS) { + detail = artifact.fileName(); + URI uri = artifact.mavenUri(); + HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(60L)).GET().build(); + HttpResponse response = HTTP_CLIENT.send(request, BodyHandlers.ofByteArray()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IOException("Download failed for " + artifact.fileName() + " (" + response.statusCode() + ")"); + } + + byte[] bytes = response.body(); + String actualHash = sha256(bytes); + if (!artifact.sha256().equalsIgnoreCase(actualHash)) { + throw new IOException("Checksum mismatch for " + artifact.fileName()); + } + + result.add(new NonIntifaceDependencyManager.DownloadedArtifact(artifact, bytes)); + } + + return result; + } + + private static void buildGeneratedBundle(List downloads) throws IOException { + Path modsDir = FabricLoader.getInstance().getGameDir().resolve("mods"); + Files.createDirectories(modsDir); + Path bundle = generatedBundlePath(); + Path tempBundle = modsDir.resolve("needsofnature-intiface-libs-generated.jar.tmp"); + deleteIfExists(tempBundle); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(tempBundle))) { + putEntry(out, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\nMulti-Release: true\n".getBytes(StandardCharsets.UTF_8)); + putEntry(out, "fabric.mod.json", generatedFabricModJson().getBytes(StandardCharsets.UTF_8)); + mergeDownloadedJars(out, downloads); + putEntry(out, "META-INF/needsofnature-intiface-libs.sha256", expectedBundleMarker().getBytes(StandardCharsets.UTF_8)); + } + + Files.move(tempBundle, bundle, StandardCopyOption.REPLACE_EXISTING); + } + + private static String generatedFabricModJson() { + return "{\n \"schemaVersion\": 1,\n \"id\": \"%s\",\n \"version\": \"1.0.0\",\n \"name\": \"NeedsOfNature Buttplug.io Libraries\",\n \"description\": \"Optional client-side libraries for NeedsOfNature Buttplug.io integration.\",\n \"environment\": \"client\"\n}\n" + .formatted("needsofnature_intiface_libraries"); + } + + private static void mergeDownloadedJars(JarOutputStream out, List downloads) throws IOException { + Set written = new HashSet<>(); + written.add("META-INF/MANIFEST.MF"); + written.add("fabric.mod.json"); + Map> serviceEntries = new LinkedHashMap<>(); + + for (NonIntifaceDependencyManager.DownloadedArtifact download : downloads) { + JarEntry entry; + try (JarInputStream in = new JarInputStream(new ByteArrayInputStream(download.bytes()))) { + while ((entry = in.getNextJarEntry()) != null) { + String name = entry.getName(); + if (!shouldSkipShadedEntry(entry, name)) { + byte[] bytes = in.readAllBytes(); + if (name.startsWith("META-INF/services/")) { + serviceEntries.computeIfAbsent(name, ignored -> new LinkedHashSet<>()).add(new String(bytes, StandardCharsets.UTF_8).strip()); + } else if (written.add(name)) { + putEntry(out, name, bytes); + } + } + } + } + } + + for (Entry> service : serviceEntries.entrySet()) { + if (written.add(service.getKey())) { + String text = String.join("\n", service.getValue()).strip(); + if (!text.isBlank()) { + text = text + "\n"; + } + + putEntry(out, service.getKey(), text.getBytes(StandardCharsets.UTF_8)); + } + } + } + + private static boolean shouldSkipShadedEntry(JarEntry entry, String name) { + if (entry.isDirectory()) { + return true; + } + + if (name.equals("fabric.mod.json")) { + return true; + } + + if (name.equals("module-info.class")) { + return true; + } + + if (name.equalsIgnoreCase("META-INF/MANIFEST.MF")) { + return true; + } + + if (name.equalsIgnoreCase("META-INF/INDEX.LIST")) { + return true; + } + + if (name.startsWith("META-INF/maven/")) { + return true; + } + + if (!name.startsWith("META-INF/")) { + return false; + } + + String upper = name.toUpperCase(Locale.ROOT); + return upper.endsWith(".SF") || upper.endsWith(".RSA") || upper.endsWith(".DSA") || upper.endsWith(".EC"); + } + + private static boolean verifyGeneratedBundle(Path bundle) { + if (!Files.exists(bundle)) { + return false; + } + + try (JarFile jar = new JarFile(bundle.toFile())) { + if (jar.getEntry("fabric.mod.json") == null) { + return false; + } + + JarEntry marker = jar.getJarEntry("META-INF/needsofnature-intiface-libs.sha256"); + if (marker == null) { + return false; + } + + try (InputStream in = jar.getInputStream(marker)) { + String actualMarker = new String(in.readAllBytes(), StandardCharsets.UTF_8); + if (!expectedBundleMarker().equals(actualMarker)) { + return false; + } + } + + return jar.getEntry("io/github/blackspherefollower/buttplug4j/ButtplugException.class") != null + && jar.getEntry("io/github/blackspherefollower/buttplug4j/connectors/jetty/websocket/client/ButtplugClientWSClient.class") != null + && jar.getEntry("com/fasterxml/jackson/databind/ObjectMapper.class") != null; + } catch (IOException e) { + return false; + } + } + + private static String expectedBundleMarker() { + StringBuilder marker = new StringBuilder("format=shaded-v1\n"); + + for (NonIntifaceDependencyManager.Artifact artifact : ARTIFACTS) { + marker.append(artifact.group()) + .append(':') + .append(artifact.artifact()) + .append(':') + .append(artifact.version()) + .append(' ') + .append(artifact.sha256()) + .append('\n'); + } + + return marker.toString(); + } + + private static void putEntry(JarOutputStream out, String name, byte[] bytes) throws IOException { + JarEntry entry = new JarEntry(name); + entry.setTime(0L); + out.putNextEntry(entry); + + try (InputStream in = new ByteArrayInputStream(bytes)) { + in.transferTo(out); + } + + out.closeEntry(); + } + + private static Path generatedBundlePath() { + return FabricLoader.getInstance().getGameDir().resolve("mods").resolve("needsofnature-intiface-libs-generated.jar"); + } + + private static long totalBytes() { + long total = 0L; + + for (NonIntifaceDependencyManager.Artifact artifact : ARTIFACTS) { + total += artifact.bytes(); + } + + return total; + } + + private static String sha256(byte[] bytes) throws IOException { + return sha256(new ByteArrayInputStream(bytes)); + } + + private static String sha256(InputStream input) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + + int read; + while ((read = input.read(buffer)) >= 0) { + digest.update(buffer, 0, read); + } + + return HexFormat.of().formatHex(digest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 unavailable", e); + } + } + + private static void deleteIfExists(Path path) { + try { + if (!Files.exists(path)) { + return; + } + + if (Files.isDirectory(path)) { + try (Stream stream = Files.walk(path)) { + stream.sorted(Comparator.reverseOrder()).forEach(child -> { + try { + Files.deleteIfExists(child); + } catch (IOException var2) { + } + }); + } + } else { + Files.deleteIfExists(path); + } + } catch (IOException var6) { + } + } + + private record Artifact(String group, String artifact, String version, String sha256, long bytes) { + String fileName() { + return this.artifact + "-" + this.version + ".jar"; + } + + URI mavenUri() { + String groupPath = this.group.replace('.', '/'); + return URI.create("https://repo.maven.apache.org/maven2/" + groupPath + "/" + this.artifact + "/" + this.version + "/" + this.fileName()); + } + } + + private record DownloadedArtifact(NonIntifaceDependencyManager.Artifact artifact, byte[] bytes) { + } + + private enum InstallState { + IDLE, + DOWNLOADING, + BUILDING, + INSTALLED_RESTART_REQUIRED, + FAILED; + } +} diff --git a/src/main/java/com/nonid/client/particle/LiquidFallingParticle.java b/src/main/java/com/nonid/client/particle/LiquidFallingParticle.java new file mode 100644 index 0000000..b7ec877 --- /dev/null +++ b/src/main/java/com/nonid/client/particle/LiquidFallingParticle.java @@ -0,0 +1,76 @@ +package com.nonid.client.particle; + +import com.nonid.NeedsOfNature; +import com.nonid.particle.NonParticles; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleFactory; +import net.minecraft.client.particle.SpriteProvider; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.Fluids; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.TintedParticleEffect; +import net.minecraft.registry.Registries; +import net.minecraft.registry.tag.FluidTags; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.random.Random; + +public class LiquidFallingParticle extends LiquidLeakParticle { + public LiquidFallingParticle( + ClientWorld world, double x, double y, double z, Fluid fluid, Sprite sprite, int tintRgb, double velocityX, double velocityY, double velocityZ + ) { + super(world, x, y, z, fluid, sprite, tintRgb); + this.maxAge = (int)(64.0 / (Math.random() * 0.8 + 0.2)); + this.velocityX = velocityX; + this.velocityY = velocityY; + this.velocityZ = velocityZ; + } + + @Override + protected void updateVelocity() { + BlockPos pos = BlockPos.ofFloored(this.x, this.y, this.z); + if (this.world.getFluidState(pos).isIn(FluidTags.WATER)) { + this.markDead(); + double offsetX = (this.random.nextDouble() - 0.5) * 0.2; + double offsetZ = (this.random.nextDouble() - 0.5) * 0.2; + double offsetY = MathHelper.nextBetween(this.random, 0.01F, 0.04F); + ParticleEffect waterParticle = NonParticles.liquidWater(this.tintRgb); + this.world + .addParticleClient( + waterParticle, this.x + offsetX, this.y + offsetY, this.z + offsetZ, this.velocityX * 0.02, this.velocityY * 0.02, this.velocityZ * 0.02 + ); + } else { + if (this.onGround) { + SoundEvent sound = (SoundEvent)Registries.SOUND_EVENT.get(NeedsOfNature.id("wet04")); + float pitch = MathHelper.nextBetween(this.random, 0.3F, 1.0F); + this.world.playSoundClient(this.x, this.y, this.z, sound, SoundCategory.BLOCKS, 0.6F, pitch, false); + this.markDead(); + double offsetX = (this.random.nextDouble() - 0.5) * 0.3; + double offsetZ = (this.random.nextDouble() - 0.5) * 0.3; + double offsetY = MathHelper.nextBetween(this.random, 0.005F, 0.02F); + ParticleEffect puddleParticle = NonParticles.liquidPuddle(this.tintRgb); + this.world.addParticleClient(puddleParticle, this.x + offsetX, this.y + offsetY, this.z + offsetZ, 0.0, 0.0, 0.0); + } + } + } + + public static final class Factory implements ParticleFactory { + private final SpriteProvider spriteProvider; + + public Factory(SpriteProvider spriteProvider) { + this.spriteProvider = spriteProvider; + } + + public Particle createParticle( + TintedParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Random random + ) { + return new LiquidFallingParticle( + world, x, y, z, Fluids.LAVA, this.spriteProvider.getSprite(random), LiquidLeakParticle.rgbFrom(effect), velocityX, velocityY, velocityZ + ); + } + } +} diff --git a/src/main/java/com/nonid/client/particle/LiquidLeakParticle.java b/src/main/java/com/nonid/client/particle/LiquidLeakParticle.java new file mode 100644 index 0000000..9751bea --- /dev/null +++ b/src/main/java/com/nonid/client/particle/LiquidLeakParticle.java @@ -0,0 +1,81 @@ +package com.nonid.client.particle; + +import net.minecraft.client.particle.BillboardParticle; +import net.minecraft.client.particle.BillboardParticle.RenderType; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.FluidState; +import net.minecraft.fluid.Fluids; +import net.minecraft.particle.TintedParticleEffect; +import net.minecraft.util.math.BlockPos; + +abstract class LiquidLeakParticle extends BillboardParticle { + protected static final float BASE_GRAVITY = 0.06F; + protected static final double DAMPING = 0.98; + protected final Fluid fluid; + protected final int tintRgb; + + protected LiquidLeakParticle(ClientWorld world, double x, double y, double z, Fluid fluid, Sprite sprite, int tintRgb) { + super(world, x, y, z, sprite); + this.fluid = fluid; + this.tintRgb = tintRgb & 16777215; + this.setBoundingBoxSpacing(0.01F, 0.01F); + this.gravityStrength = 0.06F; + this.applyColor(this.tintRgb); + } + + public RenderType getRenderType() { + return RenderType.PARTICLE_ATLAS_OPAQUE; + } + + public void tick() { + this.lastX = this.x; + this.lastY = this.y; + this.lastZ = this.z; + this.updateAge(); + if (!this.dead) { + this.velocityY = this.velocityY - this.gravityStrength; + this.move(this.velocityX, this.velocityY, this.velocityZ); + this.updateVelocity(); + if (!this.dead) { + this.velocityX *= 0.98; + this.velocityY *= 0.98; + this.velocityZ *= 0.98; + if (this.fluid != Fluids.EMPTY) { + BlockPos pos = BlockPos.ofFloored(this.x, this.y, this.z); + FluidState fluidState = this.world.getFluidState(pos); + if (fluidState.getFluid() == this.fluid) { + double surface = pos.getY() + fluidState.getHeight(this.world, pos); + if (this.y < surface) { + this.markDead(); + } + } + } + } + } + } + + protected void updateAge() { + if (--this.maxAge <= 0) { + this.markDead(); + } + } + + protected void updateVelocity() { + } + + private void applyColor(int rgb) { + float r = (rgb >> 16 & 0xFF) / 255.0F; + float g = (rgb >> 8 & 0xFF) / 255.0F; + float b = (rgb & 0xFF) / 255.0F; + this.setColor(r, g, b); + } + + static int rgbFrom(TintedParticleEffect effect) { + int red = Math.max(0, Math.min(255, Math.round(effect.getRed() * 255.0F))); + int green = Math.max(0, Math.min(255, Math.round(effect.getGreen() * 255.0F))); + int blue = Math.max(0, Math.min(255, Math.round(effect.getBlue() * 255.0F))); + return red << 16 | green << 8 | blue; + } +} diff --git a/src/main/java/com/nonid/client/particle/LiquidParticle.java b/src/main/java/com/nonid/client/particle/LiquidParticle.java new file mode 100644 index 0000000..4eebc91 --- /dev/null +++ b/src/main/java/com/nonid/client/particle/LiquidParticle.java @@ -0,0 +1,57 @@ +package com.nonid.client.particle; + +import com.nonid.particle.NonParticles; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleFactory; +import net.minecraft.client.particle.SpriteProvider; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.Fluids; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.TintedParticleEffect; +import net.minecraft.util.math.random.Random; + +public class LiquidParticle extends LiquidLeakParticle { + private static final float GRAVITY_SCALE = 0.02F; + private static final int MAX_AGE_TICKS = 20; + private static final double VELOCITY_SCALE = 0.02; + private final ParticleEffect nextParticle; + + public LiquidParticle(ClientWorld world, double x, double y, double z, Fluid fluid, ParticleEffect nextParticle, Sprite sprite, int tintRgb) { + super(world, x, y, z, fluid, sprite, tintRgb); + this.nextParticle = nextParticle; + this.gravityStrength *= 0.02F; + this.maxAge = 20; + } + + @Override + protected void updateAge() { + if (--this.maxAge <= 0) { + this.markDead(); + this.world.addParticleClient(this.nextParticle, this.x, this.y, this.z, this.velocityX, this.velocityY, this.velocityZ); + } + } + + @Override + protected void updateVelocity() { + this.velocityX *= 0.02; + this.velocityY *= 0.02; + this.velocityZ *= 0.02; + } + + public static final class Factory implements ParticleFactory { + private final SpriteProvider spriteProvider; + + public Factory(SpriteProvider spriteProvider) { + this.spriteProvider = spriteProvider; + } + + public Particle createParticle( + TintedParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Random random + ) { + int tintRgb = LiquidLeakParticle.rgbFrom(effect); + return new LiquidParticle(world, x, y, z, Fluids.LAVA, NonParticles.liquidFalling(tintRgb), this.spriteProvider.getSprite(random), tintRgb); + } + } +} diff --git a/src/main/java/com/nonid/client/particle/LiquidPuddleParticle.java b/src/main/java/com/nonid/client/particle/LiquidPuddleParticle.java new file mode 100644 index 0000000..df8a5d2 --- /dev/null +++ b/src/main/java/com/nonid/client/particle/LiquidPuddleParticle.java @@ -0,0 +1,96 @@ +package com.nonid.client.particle; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import net.minecraft.client.particle.BillboardParticle; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleFactory; +import net.minecraft.client.particle.SpriteProvider; +import net.minecraft.client.particle.BillboardParticle.RenderType; +import net.minecraft.client.particle.BillboardParticle.Rotator; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.particle.TintedParticleEffect; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.random.Random; + +public class LiquidPuddleParticle extends BillboardParticle { + private static final int DEFAULT_MAX_AGE_TICKS = 800; + private static final int MAX_FADE_TICKS = 200; + private static final Rotator PUDDLE_ROTATOR = (quaternion, camera, tickDelta) -> quaternion.rotationXYZ((float) (-Math.PI / 2), 0.0F, 0.0F); + private final int holdTicks; + private final int fadeTicks; + + public LiquidPuddleParticle(ClientWorld world, double x, double y, double z, Sprite sprite, float scaleFactor, int tintRgb) { + super(world, x, y, z, sprite); + this.maxAge = resolvePuddleMaxAgeTicks(); + this.fadeTicks = Math.min(200, this.maxAge); + this.holdTicks = Math.max(0, this.maxAge - this.fadeTicks); + this.gravityStrength = 0.0F; + this.velocityX = 0.0; + this.velocityY = 0.0; + this.velocityZ = 0.0; + float sizeJitter = MathHelper.nextBetween(this.random, 0.8F, 1.0F); + this.scale = 0.5F * sizeJitter * Math.max(1.0F, scaleFactor); + this.alpha = 1.0F; + this.applyColor(tintRgb); + } + + public Rotator getRotator() { + return PUDDLE_ROTATOR; + } + + protected RenderType getRenderType() { + return RenderType.PARTICLE_ATLAS_TRANSLUCENT; + } + + public void tick() { + this.lastX = this.x; + this.lastY = this.y; + this.lastZ = this.z; + if (!this.hasSupportBlock()) { + this.markDead(); + } else if (++this.age >= this.maxAge) { + this.markDead(); + } else { + if (this.age > this.holdTicks && this.fadeTicks > 0) { + float fade = (float)(this.age - this.holdTicks) / this.fadeTicks; + this.alpha = MathHelper.clamp(1.0F - fade, 0.0F, 1.0F); + } else { + this.alpha = 1.0F; + } + } + } + + private static int resolvePuddleMaxAgeTicks() { + NonConfig config = NeedsOfNature.getConfig(); + return config == null ? 800 : Math.max(1, config.getLiquidPuddleDespawnSeconds() * 20); + } + + private boolean hasSupportBlock() { + BlockPos supportPos = BlockPos.ofFloored(this.x, this.y - 0.05, this.z); + return !this.world.getBlockState(supportPos).getCollisionShape(this.world, supportPos).isEmpty(); + } + + private void applyColor(int rgb) { + float r = (rgb >> 16 & 0xFF) / 255.0F; + float g = (rgb >> 8 & 0xFF) / 255.0F; + float b = (rgb & 0xFF) / 255.0F; + this.setColor(r, g, b); + } + + public static final class Factory implements ParticleFactory { + private final SpriteProvider spriteProvider; + + public Factory(SpriteProvider spriteProvider) { + this.spriteProvider = spriteProvider; + } + + public Particle createParticle( + TintedParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Random random + ) { + return new LiquidPuddleParticle(world, x, y, z, this.spriteProvider.getSprite(random), 1.0F, LiquidLeakParticle.rgbFrom(effect)); + } + } +} diff --git a/src/main/java/com/nonid/client/particle/LiquidWaterParticle.java b/src/main/java/com/nonid/client/particle/LiquidWaterParticle.java new file mode 100644 index 0000000..8a8a0d0 --- /dev/null +++ b/src/main/java/com/nonid/client/particle/LiquidWaterParticle.java @@ -0,0 +1,84 @@ +package com.nonid.client.particle; + +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleFactory; +import net.minecraft.client.particle.SpriteProvider; +import net.minecraft.client.particle.BillboardParticle.RenderType; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.fluid.Fluids; +import net.minecraft.particle.TintedParticleEffect; +import net.minecraft.util.math.random.Random; + +public class LiquidWaterParticle extends LiquidLeakParticle { + private static final int WATER_LIFE_TICKS = 100; + private static final float WATER_GRAVITY = 0.0018F; + private static final double WATER_DAMPING = 0.6; + private static final int WATER_FRAMES = 5; + + public LiquidWaterParticle( + ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Sprite sprite, int maxAge, int tintRgb + ) { + super(world, x, y, z, Fluids.EMPTY, sprite, tintRgb); + this.maxAge = Math.max(1, maxAge); + this.gravityStrength = 0.0018F; + this.velocityX = velocityX; + this.velocityY = velocityY; + this.velocityZ = velocityZ; + } + + @Override + public RenderType getRenderType() { + return RenderType.PARTICLE_ATLAS_OPAQUE; + } + + @Override + public void tick() { + this.lastX = this.x; + this.lastY = this.y; + this.lastZ = this.z; + if (this.age++ >= this.maxAge) { + this.markDead(); + } else { + this.velocityY = this.velocityY - this.gravityStrength; + this.move(this.velocityX, this.velocityY, this.velocityZ); + this.velocityX *= 0.6; + this.velocityY *= 0.6; + this.velocityZ *= 0.6; + } + } + + protected float getMinV() { + float minV = this.sprite.getMinV(); + float maxV = this.sprite.getMaxV(); + float frameSize = (maxV - minV) / 5.0F; + int frame = Math.min(4, (int)((float)this.age / this.maxAge * 5.0F)); + return minV + frame * frameSize; + } + + protected float getMaxV() { + float minV = this.sprite.getMinV(); + float maxV = this.sprite.getMaxV(); + float frameSize = (maxV - minV) / 5.0F; + int frame = Math.min(4, (int)((float)this.age / this.maxAge * 5.0F)); + return minV + (frame + 1) * frameSize; + } + + public static final class Factory implements ParticleFactory { + private final SpriteProvider spriteProvider; + + public Factory(SpriteProvider spriteProvider) { + this.spriteProvider = spriteProvider; + } + + public Particle createParticle( + TintedParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Random random + ) { + int variance = random.nextInt(21) - 10; + int age = 100 + variance; + return new LiquidWaterParticle( + world, x, y, z, velocityX, velocityY, velocityZ, this.spriteProvider.getSprite(random), age, LiquidLeakParticle.rgbFrom(effect) + ); + } + } +} diff --git a/src/main/java/com/nonid/client/particle/RippedFabricParticle.java b/src/main/java/com/nonid/client/particle/RippedFabricParticle.java new file mode 100644 index 0000000..12097e6 --- /dev/null +++ b/src/main/java/com/nonid/client/particle/RippedFabricParticle.java @@ -0,0 +1,92 @@ +package com.nonid.client.particle; + +import net.minecraft.client.particle.BillboardParticle; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleFactory; +import net.minecraft.client.particle.SpriteProvider; +import net.minecraft.client.particle.BillboardParticle.RenderType; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.particle.SimpleParticleType; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.random.Random; + +public class RippedFabricParticle extends BillboardParticle { + private static final int FRAMES = 5; + private static final int TICKS_PER_FRAME = 4; + private static SpriteProvider sprites; + + public RippedFabricParticle( + ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Sprite sprite, int rgb, int maxAge, float scale + ) { + super(world, x, y, z, velocityX, velocityY, velocityZ, sprite); + this.velocityMultiplier = 0.82F; + this.gravityStrength = 0.1F; + this.maxAge = Math.max(1, maxAge); + this.scale = scale; + this.collidesWithWorld = true; + this.setBoundingBoxSpacing(0.01F, 0.01F); + this.setColor((rgb >> 16 & 0xFF) / 255.0F, (rgb >> 8 & 0xFF) / 255.0F, (rgb & 0xFF) / 255.0F); + } + + public static Particle create(ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, int rgb, Random random) { + SpriteProvider provider = sprites; + Sprite sprite = provider != null ? provider.getFirst() : null; + if (sprite == null) { + return null; + } + + int maxAge = 100 + random.nextInt(50); + float scale = 0.075F + random.nextFloat() * 0.045F; + return new RippedFabricParticle(world, x, y, z, velocityX, velocityY, velocityZ, sprite, rgb, maxAge, scale); + } + + public RenderType getRenderType() { + return RenderType.PARTICLE_ATLAS_OPAQUE; + } + + public void tick() { + super.tick(); + if (this.onGround) { + this.markDead(); + } + } + + public float getSize(float tickProgress) { + float life = (this.age + tickProgress) / this.maxAge; + float size = this.scale * MathHelper.clamp(life * 10.0F, 0.0F, 1.0F); + if (life > 0.75F) { + size *= MathHelper.lerp(MathHelper.clamp((life - 0.75F) / 0.25F, 0.0F, 1.0F), 1.0F, 0.15F); + } + + return size; + } + + protected float getMinV() { + float minV = this.sprite.getMinV(); + float maxV = this.sprite.getMaxV(); + float frameSize = (maxV - minV) / 5.0F; + int frame = this.age / 4 % 5; + return minV + frame * frameSize; + } + + protected float getMaxV() { + float minV = this.sprite.getMinV(); + float maxV = this.sprite.getMaxV(); + float frameSize = (maxV - minV) / 5.0F; + int frame = this.age / 4 % 5; + return minV + (frame + 1) * frameSize; + } + + public static final class Factory implements ParticleFactory { + public Factory(SpriteProvider spriteProvider) { + RippedFabricParticle.sprites = spriteProvider; + } + + public Particle createParticle( + SimpleParticleType type, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Random random + ) { + return RippedFabricParticle.create(world, x, y, z, velocityX, velocityY, velocityZ, 15920872, random); + } + } +} diff --git a/src/main/java/com/nonid/client/particle/SmallHeartParticle.java b/src/main/java/com/nonid/client/particle/SmallHeartParticle.java new file mode 100644 index 0000000..7d5d437 --- /dev/null +++ b/src/main/java/com/nonid/client/particle/SmallHeartParticle.java @@ -0,0 +1,56 @@ +package com.nonid.client.particle; + +import net.minecraft.client.particle.BillboardParticle; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleFactory; +import net.minecraft.client.particle.SpriteProvider; +import net.minecraft.client.particle.BillboardParticle.RenderType; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.particle.SimpleParticleType; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.random.Random; + +public class SmallHeartParticle extends BillboardParticle { + protected SmallHeartParticle(ClientWorld world, double x, double y, double z, Sprite sprite) { + super(world, x, y, z, 0.0, 0.0, 0.0, sprite); + this.ascending = true; + this.velocityMultiplier = 0.86F; + this.velocityX *= 0.01F; + this.velocityY *= 0.01F; + this.velocityZ *= 0.01F; + this.velocityY += 0.1; + this.scale = 0.145F + this.random.nextFloat() * 0.08F; + this.maxAge = 16; + this.collidesWithWorld = false; + } + + public RenderType getRenderType() { + return RenderType.PARTICLE_ATLAS_OPAQUE; + } + + public float getSize(float tickProgress) { + float life = (this.age + tickProgress) / this.maxAge; + float base = this.scale * MathHelper.clamp(life * 32.0F, 0.0F, 1.0F); + if (life > 0.8F) { + float fade = MathHelper.clamp((life - 0.8F) / 0.2F, 0.0F, 1.0F); + base *= MathHelper.lerp(fade, 1.0F, 0.8F); + } + + return base; + } + + public static final class Factory implements ParticleFactory { + private final SpriteProvider spriteProvider; + + public Factory(SpriteProvider spriteProvider) { + this.spriteProvider = spriteProvider; + } + + public Particle createParticle( + SimpleParticleType type, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, Random random + ) { + return new SmallHeartParticle(world, x, y, z, this.spriteProvider.getSprite(random)); + } + } +} diff --git a/src/main/java/com/nonid/data/NonAccessoryBehavior.java b/src/main/java/com/nonid/data/NonAccessoryBehavior.java new file mode 100644 index 0000000..d93ee33 --- /dev/null +++ b/src/main/java/com/nonid/data/NonAccessoryBehavior.java @@ -0,0 +1,76 @@ +package com.nonid.data; + +import java.util.Locale; +import java.util.Set; +import org.jetbrains.annotations.Nullable; + +public record NonAccessoryBehavior( + Set occupiesSlots, + Set blocksInjectorTypes, + @Nullable String exclusiveGroup, + int maxDurability, + int protectionDurabilityCost, + boolean ignoreInjectorSlotVisualShedding, + boolean ignoreInjectorSlotEffectSuppression, + int vInjectionDurabilityCost, + int aInjectionDurabilityCost, + boolean blocksPregnancy +) { + public static final NonAccessoryBehavior NONE = new NonAccessoryBehavior(Set.of(), Set.of(), null, 0, 1, false, false, 0, 0, false); + + public boolean isEmpty() { + return this.occupiesSlots.isEmpty() + && this.blocksInjectorTypes.isEmpty() + && (this.exclusiveGroup == null || this.exclusiveGroup.isBlank()) + && !this.ignoreInjectorSlotVisualShedding + && !this.ignoreInjectorSlotEffectSuppression + && this.vInjectionDurabilityCost <= 0 + && this.aInjectionDurabilityCost <= 0 + && !this.blocksPregnancy; + } + + public boolean occupiesSlot(String slotId) { + if (slotId == null) { + return false; + } + + String normalized = normalizeSlot(slotId); + + for (String occupied : this.occupiesSlots) { + if (isSlot(normalized, occupied)) { + return true; + } + } + + return false; + } + + public boolean blocks(NonLiquidRoles.InjectorRole role) { + return role != null && this.blocksInjectorTypes.contains(role); + } + + public static String normalizeSlot(String slotId) { + if (slotId == null) { + return ""; + } else { + String slot = slotId.trim().toLowerCase(Locale.ROOT); + if ("v".equals(slot)) { + return "legs/v"; + } else if ("a".equals(slot)) { + return "legs/a"; + } else { + return "d".equals(slot) ? "legs/d" : slot; + } + } + } + + public static boolean isSlot(String actual, String expected) { + if (actual != null && expected != null) { + String normalizedActual = normalizeSlot(actual); + String normalizedExpected = normalizeSlot(expected); + return normalizedActual.equals(normalizedExpected) || normalizedActual.startsWith(normalizedExpected + "/"); + } else { + return false; + } + } +} diff --git a/src/main/java/com/nonid/data/NonAccessoryDefinitions.java b/src/main/java/com/nonid/data/NonAccessoryDefinitions.java new file mode 100644 index 0000000..32eb6d5 --- /dev/null +++ b/src/main/java/com/nonid/data/NonAccessoryDefinitions.java @@ -0,0 +1,316 @@ +package com.nonid.data; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.nonid.NeedsOfNature; +import com.nonid.NonAccessoryEffects; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonAccessoryDefinitions { + private static volatile Map DEFINITIONS_BY_ITEM = Map.of(); + + private NonAccessoryDefinitions() { + } + + public static void loadStartupDefinitions() { + DEFINITIONS_BY_ITEM = NonAccessoryItemRegistry.getDefinitionsByItem(); + } + + public static NonAccessoryEffects getEffects(Identifier itemId) { + if (itemId == null) { + return NonAccessoryEffects.NEUTRAL; + } + + NonAccessoryItemRegistry.AccessoryItemDefinition definition = DEFINITIONS_BY_ITEM.get(itemId); + return definition == null ? NonAccessoryEffects.NEUTRAL : definition.effects(); + } + + public static boolean hasDefinition(Identifier itemId) { + return itemId != null && DEFINITIONS_BY_ITEM.containsKey(itemId); + } + + public static NonAccessoryBehavior getBehavior(Identifier itemId) { + if (itemId == null) { + return NonAccessoryBehavior.NONE; + } + + NonAccessoryItemRegistry.AccessoryItemDefinition definition = DEFINITIONS_BY_ITEM.get(itemId); + return definition == null ? NonAccessoryBehavior.NONE : definition.behavior(); + } + + @Nullable + public static NonAccessoryDefinitions.TooltipColor getTooltipColor(Identifier itemId, String effectKey) { + if (itemId != null && effectKey != null) { + NonAccessoryItemRegistry.AccessoryItemDefinition definition = DEFINITIONS_BY_ITEM.get(itemId); + Map colors = definition == null ? null : definition.tooltipColors(); + return colors == null ? null : colors.get(effectKey); + } else { + return null; + } + } + + public static NonAccessoryEffects parseEffectsForStartup(Identifier itemId, JsonObject root) { + JsonObject effects = objectOrSelf(root.get("effects"), root); + return parseEffectsObject(itemId, effects); + } + + public static Map parseTooltipColorsForStartup(Identifier itemId, JsonObject root) { + JsonElement effectsElement = root.get("effects"); + if (effectsElement != null && effectsElement.isJsonObject()) { + JsonObject effects = effectsElement.getAsJsonObject(); + return parseInlineTooltipColors(itemId, effects); + } else { + return Map.of(); + } + } + + public static NonAccessoryBehavior parseBehaviorForStartup(Identifier itemId, JsonObject root) { + return parseBehavior(itemId, root); + } + + private static NonAccessoryEffects parseEffectsObject(Identifier fileId, JsonObject effects) { + return new NonAccessoryEffects( + parseMultiplier(fileId, effects, "liquid_decay_multiplier", 1.0, 0.0, 20.0), + parseBoolean(effects, "equalize_liquid_decay_context", false), + parseMultiplier(fileId, effects, "player_energy_gain_multiplier", 1.0, 0.0, 20.0), + parseInt(fileId, effects, "liquid_capacity_add", 0, -10000, 10000), + parseMultiplier(fileId, effects, "liquid_gain_multiplier", 1.0, 0.0, 20.0), + parseMultiplier(fileId, effects, "filled_effect_multiplier", 1.0, 0.0, 20.0), + parseMultiplier(fileId, effects, "pregnancy_chance_multiplier", 1.0, 0.0, 20.0), + parseMultiplier(fileId, effects, "pregnancy_duration_multiplier", 1.0, 0.01, 20.0), + parseMultiplier(fileId, effects, "mess_gain_multiplier", 1.0, 0.0, 20.0), + parseMultiplier(fileId, effects, "destroyed_skin_damage_multiplier", 1.0, 0.0, 20.0), + parseInt(fileId, effects, "attack_escape_hits_add", 0, -49, 49), + parseMultiplier(fileId, effects, "attack_escape_damage_multiplier", 1.0, 0.0, 20.0), + parseMultiplier(fileId, effects, "player_energy_aura_multiplier", 1.0, 0.0, 20.0), + parseMultiplier(fileId, effects, "near_animation_mob_energy_multiplier", 1.0, 0.0, 20.0) + ); + } + + private static JsonObject objectOrSelf(@Nullable JsonElement element, JsonObject fallback) { + return element != null && element.isJsonObject() ? element.getAsJsonObject() : fallback; + } + + private static NonAccessoryBehavior parseBehavior(Identifier fileId, JsonObject root) { + Set occupiesSlots = parseSlotSet(root.get("occupies_slots")); + Set blockedRoles = parseInjectorRoleSet(fileId, root.get("blocks_injector_types")); + String exclusiveGroup = parseOptionalString(root.get("exclusive_group")); + int maxDurability = parseInt(fileId, root, "max_durability", 0, 0, 100000); + int durabilityCost = parseInt(fileId, root, "protection_durability_cost", 1, 1, 100); + boolean ignoreVisualShedding = parseBoolean(root, "ignore_injector_slot_visual_shedding", false); + boolean ignoreEffectSuppression = parseBoolean(root, "ignore_injector_slot_effect_suppression", false); + int vInjectionDurabilityCost = parseInt(fileId, root, "v_injection_durability_cost", 0, 0, 100); + int aInjectionDurabilityCost = parseInt(fileId, root, "a_injection_durability_cost", 0, 0, 100); + boolean blocksPregnancy = parseBoolean(root, "blocks_pregnancy", false); + return new NonAccessoryBehavior( + occupiesSlots, + blockedRoles, + exclusiveGroup, + maxDurability, + durabilityCost, + ignoreVisualShedding, + ignoreEffectSuppression, + vInjectionDurabilityCost, + aInjectionDurabilityCost, + blocksPregnancy + ); + } + + private static Set parseSlotSet(@Nullable JsonElement element) { + if (element == null) { + return Set.of(); + } + + LinkedHashSet slots = new LinkedHashSet<>(); + if (element.isJsonArray()) { + for (JsonElement child : element.getAsJsonArray()) { + addSlot(slots, parseOptionalString(child)); + } + } else { + addSlot(slots, parseOptionalString(element)); + } + + return Set.copyOf(slots); + } + + private static void addSlot(Set slots, @Nullable String raw) { + if (raw != null && !raw.isBlank()) { + String slot = NonAccessoryBehavior.normalizeSlot(raw); + if (!"legs/v".equals(slot) && !"legs/a".equals(slot) && !"legs/d".equals(slot)) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring unknown accessory occupied slot '{}'.", raw); + } else { + slots.add(slot); + } + } + } + + private static Set parseInjectorRoleSet(Identifier fileId, @Nullable JsonElement element) { + if (element == null) { + return Set.of(); + } + + LinkedHashSet roles = new LinkedHashSet<>(); + if (element.isJsonArray()) { + for (JsonElement child : element.getAsJsonArray()) { + addInjectorRole(fileId, roles, parseOptionalString(child)); + } + } else { + addInjectorRole(fileId, roles, parseOptionalString(element)); + } + + return Set.copyOf(roles); + } + + private static void addInjectorRole(Identifier fileId, Set roles, @Nullable String raw) { + if (raw != null && !raw.isBlank()) { + String normalized = raw.trim().toUpperCase(Locale.ROOT); + + try { + NonLiquidRoles.InjectorRole role = NonLiquidRoles.InjectorRole.valueOf(normalized); + if (role != NonLiquidRoles.InjectorRole.NONE) { + roles.add(role); + } + } catch (IllegalArgumentException ignored) { + warn(fileId, "Ignoring unknown blocked injector type '" + raw + "'."); + } + } + } + + @Nullable + private static String parseOptionalString(@Nullable JsonElement element) { + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String raw = element.getAsString(); + return raw != null && !raw.isBlank() ? raw.trim() : null; + } else { + return null; + } + } + + private static double parseMultiplier(Identifier fileId, JsonObject object, String key, double fallback, double min, double max) { + Double value = parseDouble(object.get(key)); + if (value == null) { + return fallback; + } else if (!Double.isFinite(value)) { + warn(fileId, "Ignoring non-finite '" + key + "'."); + return fallback; + } else { + return Math.max(min, Math.min(max, value)); + } + } + + private static int parseInt(Identifier fileId, JsonObject object, String key, int fallback, int min, int max) { + Double value = parseDouble(object.get(key)); + if (value == null) { + return fallback; + } + + if (Double.isFinite(value) && value == Math.rint(value)) { + return Math.max(min, Math.min(max, value.intValue())); + } + + warn(fileId, "Ignoring non-integer '" + key + "'."); + return fallback; + } + + @Nullable + private static Double parseDouble(@Nullable JsonElement element) { + element = unwrapEffectValue(element); + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + return primitive.isNumber() ? primitive.getAsDouble() : null; + } else { + return null; + } + } + + private static boolean parseBoolean(JsonObject object, String key, boolean fallback) { + JsonElement element = unwrapEffectValue(object.get(key)); + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + return primitive.isBoolean() ? primitive.getAsBoolean() : fallback; + } else { + return fallback; + } + } + + @Nullable + private static JsonElement unwrapEffectValue(@Nullable JsonElement element) { + return element != null && element.isJsonObject() ? element.getAsJsonObject().get("value") : element; + } + + private static Map parseInlineTooltipColors(Identifier fileId, JsonObject effects) { + LinkedHashMap colors = new LinkedHashMap<>(); + + for (Entry entry : effects.entrySet()) { + JsonElement value = entry.getValue(); + if (value != null && value.isJsonObject()) { + JsonElement tooltip = value.getAsJsonObject().get("tooltip"); + NonAccessoryDefinitions.TooltipColor color = parseTooltipColor(fileId, entry.getKey(), tooltip); + if (color != null) { + colors.put(entry.getKey(), color); + } + } + } + + return Map.copyOf(colors); + } + + @Nullable + private static NonAccessoryDefinitions.TooltipColor parseTooltipColor(Identifier fileId, String key, @Nullable JsonElement element) { + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String raw = element.getAsString().trim(); + if (raw.isEmpty()) { + return null; + } + + if ("positive".equalsIgnoreCase(raw)) { + return NonAccessoryDefinitions.TooltipColor.formatting(Formatting.GREEN); + } + + if ("negative".equalsIgnoreCase(raw)) { + return NonAccessoryDefinitions.TooltipColor.formatting(Formatting.RED); + } + + if ("neutral".equalsIgnoreCase(raw)) { + return NonAccessoryDefinitions.TooltipColor.formatting(Formatting.WHITE); + } + + String hex = raw.startsWith("#") ? raw.substring(1) : raw; + if (hex.length() == 6) { + try { + return NonAccessoryDefinitions.TooltipColor.rgb(Integer.parseInt(hex, 16)); + } catch (NumberFormatException var6) { + } + } + + warn(fileId, "Ignoring unknown tooltip color '" + raw + "' for '" + key + "'."); + return null; + } else { + warn(fileId, "Ignoring invalid tooltip color for '" + key + "'."); + return null; + } + } + + private static void warn(Identifier fileId, String reason) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring accessory definition {}: {}", fileId, reason); + } + + public record TooltipColor(@Nullable Formatting formatting, @Nullable Integer rgb) { + public static NonAccessoryDefinitions.TooltipColor formatting(Formatting formatting) { + return new NonAccessoryDefinitions.TooltipColor(formatting, null); + } + + public static NonAccessoryDefinitions.TooltipColor rgb(int rgb) { + return new NonAccessoryDefinitions.TooltipColor(null, rgb & 16777215); + } + } +} diff --git a/src/main/java/com/nonid/data/NonAccessoryItemRegistry.java b/src/main/java/com/nonid/data/NonAccessoryItemRegistry.java new file mode 100644 index 0000000..478c025 --- /dev/null +++ b/src/main/java/com/nonid/data/NonAccessoryItemRegistry.java @@ -0,0 +1,634 @@ +package com.nonid.data; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.nonid.NeedsOfNature; +import com.nonid.NonAccessoryEffects; +import com.nonid.NonTrinketsIntegration; +import com.nonid.item.NonDataAccessoryItem; +import com.nonid.pack.NonExternalPackCatalog; +import com.nonid.pack.NonExternalPackProvider; +import com.nonid.pack.NonPackRootResolver; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.DyedColorComponent; +import net.minecraft.item.Item; +import net.minecraft.item.Item.Settings; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonAccessoryItemRegistry { + private static final String ITEM_ROOT = "assets/needsofnature/non_accessory_items"; + private static final int DURABLE_ACCESSORY_ENCHANTABILITY = 10; + private static final List REGISTERED_ITEMS = new ArrayList<>(); + private static final Map DEFINITIONS_BY_ITEM = new LinkedHashMap<>(); + private static final Map> ITEM_IDS_BY_TRINKETS_SLOT = new LinkedHashMap<>(); + + private NonAccessoryItemRegistry() { + } + + public static void registerStartupItems() { + if (REGISTERED_ITEMS.isEmpty()) { + if (!NonTrinketsIntegration.isTrinketsLoaded()) { + NeedsOfNature.LOGGER.info("[NoN] Trinkets is not installed; skipping data-driven accessory item registration."); + } else { + LinkedHashMap definitions = new LinkedHashMap<>(); + loadBundledDefinitions(definitions); + loadExternalDefinitions(definitions); + definitions.values().forEach(NonAccessoryItemRegistry::register); + NeedsOfNature.LOGGER.info("[NoN] Registered {} startup data-driven accessory items.", REGISTERED_ITEMS.size()); + } + } + } + + public static List getRegisteredItems() { + return List.copyOf(REGISTERED_ITEMS); + } + + public static Map getDefinitionsByItem() { + return Map.copyOf(DEFINITIONS_BY_ITEM); + } + + public static List getItemIdsForTrinketsSlot(String slotId) { + return slotId == null ? List.of() : ITEM_IDS_BY_TRINKETS_SLOT.getOrDefault(slotId, List.of()); + } + + public static NonAccessoryItemRegistry.SkinOverlays getSkinOverlays(Identifier itemId) { + if (itemId == null) { + return NonAccessoryItemRegistry.SkinOverlays.EMPTY; + } + + NonAccessoryItemRegistry.AccessoryItemDefinition definition = DEFINITIONS_BY_ITEM.get(itemId); + return definition == null ? NonAccessoryItemRegistry.SkinOverlays.EMPTY : definition.skinOverlays(); + } + + public static Map getClientModelsByItem() { + LinkedHashMap models = new LinkedHashMap<>(); + + for (NonAccessoryItemRegistry.AccessoryItemDefinition definition : DEFINITIONS_BY_ITEM.values()) { + if (!definition.clientModel().isEmpty()) { + models.put(definition.id(), definition.clientModel()); + } + } + + return Map.copyOf(models); + } + + private static void loadBundledDefinitions(Map definitions) { + FabricLoader.getInstance().getModContainer("needsofnature").ifPresent(container -> { + for (Path root : container.getRootPaths()) { + readDefinitionsFromDirectory(root.resolve("assets/needsofnature/non_accessory_items"), definitions); + } + }); + } + + private static void loadExternalDefinitions(Map definitions) { + Path packsDir = NonExternalPackProvider.resolveDefaultPacksDir(); + List configuredOrder = NeedsOfNature.getConfig() == null ? List.of() : NeedsOfNature.getConfig().getExternalNoNPackLoadOrder(); + + for (NonExternalPackCatalog.Entry entry : NonExternalPackCatalog.resolveLowToHigh(packsDir, configuredOrder)) { + readDefinitionsFromPack(entry.root(), definitions); + } + } + + @Nullable + public static NonAccessoryItemRegistry.AccessoryItemDefinition getDefinition(Identifier itemId) { + return itemId == null ? null : DEFINITIONS_BY_ITEM.get(itemId); + } + + public static List getDyeableItemIds() { + return DEFINITIONS_BY_ITEM.values() + .stream() + .filter(NonAccessoryItemRegistry.AccessoryItemDefinition::dyeable) + .map(NonAccessoryItemRegistry.AccessoryItemDefinition::id) + .toList(); + } + + public static List getRegisteredItemIds() { + return DEFINITIONS_BY_ITEM.values().stream().map(NonAccessoryItemRegistry.AccessoryItemDefinition::id).toList(); + } + + public static List getDurabilityEnchantableItemIds() { + return DEFINITIONS_BY_ITEM.values() + .stream() + .filter(definition -> supportsDurabilityEnchantments(definition.maxDurability())) + .map(NonAccessoryItemRegistry.AccessoryItemDefinition::id) + .toList(); + } + + static boolean supportsDurabilityEnchantments(int maxDurability) { + return maxDurability > 1; + } + + private static void readDefinitionsFromPack( + NonPackRootResolver.ResolvedRoot root, Map definitions + ) { + if (!root.isZip()) { + readDefinitionsFromDirectory(root.rootPath().resolve("assets/needsofnature/non_accessory_items"), definitions); + } else { + Path path = root.sourcePath(); + + try (ZipFile zip = new ZipFile(path.toFile())) { + zip.stream() + .filter(entry -> !entry.isDirectory()) + .filter(entry -> { + String name = root.stripZipPrefix(entry.getName()); + return name != null && name.startsWith("assets/needsofnature/non_accessory_items/") && name.endsWith(".json"); + }) + .sorted(Comparator.comparing(ZipEntry::getName, String.CASE_INSENSITIVE_ORDER)) + .forEach(entry -> readDefinitionFromZip(path, root, zip, entry, definitions)); + } catch (IOException e) { + NeedsOfNature.logSetupWarning("[NoN] Failed to scan accessory item pack {}", path, e); + } + } + } + + private static void readDefinitionsFromDirectory(Path dir, Map definitions) { + if (Files.isDirectory(dir)) { + try (Stream paths = Files.list(dir)) { + paths.filter(path -> path.getFileName().toString().endsWith(".json")) + .sorted(Comparator.comparing(path -> path.getFileName().toString(), String.CASE_INSENSITIVE_ORDER)) + .forEach(path -> readDefinitionFromPath(path, definitions)); + } catch (IOException e) { + NeedsOfNature.logSetupWarning("[NoN] Failed to scan accessory item definitions in {}", dir, e); + } + } + } + + private static void readDefinitionFromPath(Path path, Map definitions) { + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + readDefinition(path.toString(), JsonParser.parseReader(reader).getAsJsonObject(), definitions); + } catch (IOException | RuntimeException e) { + NeedsOfNature.logSetupWarning("[NoN] Failed to read accessory item definition {}", path, e); + } + } + + private static void readDefinitionFromZip( + Path zipPath, + NonPackRootResolver.ResolvedRoot root, + ZipFile zip, + ZipEntry entry, + Map definitions + ) { + String logicalPath = root.stripZipPrefix(entry.getName()); + if (logicalPath != null) { + try (InputStreamReader reader = new InputStreamReader(zip.getInputStream(entry), StandardCharsets.UTF_8)) { + readDefinition(zipPath + "!" + logicalPath, JsonParser.parseReader(reader).getAsJsonObject(), definitions); + } catch (IOException | RuntimeException e) { + NeedsOfNature.logSetupWarning("[NoN] Failed to read accessory item definition {}!{}", zipPath, logicalPath, e); + } + } + } + + private static void readDefinition(String source, JsonObject root, Map definitions) { + Identifier id = parseItemId(root); + if (id == null) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring accessory item definition {}: missing or invalid id.", source); + } else if (!"needsofnature".equals(id.getNamespace())) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring accessory item definition {}: namespace must be '{}'.", source, "needsofnature"); + } else if (definitions.containsKey(id)) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring duplicate accessory item definition {} for {}.", source, id); + } else { + int maxCount = parseInt(root.get("max_count"), 1, 1, 64); + int maxDurability = parseInt(root.get("max_durability"), 0, 0, 100000); + if (maxDurability > 0) { + maxCount = 1; + } + + boolean showInItemGroup = parseBoolean(root.get("show_in_item_group"), true); + Set trinketsSlots = parseTrinketsSlots(root.get("trinkets_slots")); + if (trinketsSlots.isEmpty()) { + trinketsSlots = Set.of("legs/v", "legs/a"); + } + + NonAccessoryEffects effects = root.has("effects") ? NonAccessoryDefinitions.parseEffectsForStartup(id, root) : NonAccessoryEffects.NEUTRAL; + Map tooltipColors = NonAccessoryDefinitions.parseTooltipColorsForStartup(id, root); + NonAccessoryBehavior behavior = NonAccessoryDefinitions.parseBehaviorForStartup(id, root); + NonAccessoryItemRegistry.SkinOverlays skinOverlays = parseSkinOverlays(id, root.get("skin_overlays")); + NonAccessoryItemRegistry.ItemClientModel clientModel = parseItemClientModel(id, root); + boolean dyeable = parseBoolean(root.get("dyeable"), false); + int defaultDyeColor = parseDyeColor(root.get("default_dye_color")); + NonAccessoryItemRegistry.ActorVisualBehavior actorVisualBehavior = parseActorVisualBehavior(id, root); + definitions.put( + id, + new NonAccessoryItemRegistry.AccessoryItemDefinition( + id, + maxCount, + maxDurability, + showInItemGroup, + trinketsSlots, + effects, + tooltipColors, + behavior, + skinOverlays, + clientModel, + dyeable, + defaultDyeColor, + actorVisualBehavior + ) + ); + } + } + + private static void register(NonAccessoryItemRegistry.AccessoryItemDefinition definition) { + RegistryKey key = RegistryKey.of(Registries.ITEM.getKey(), definition.id()); + Settings settings = new Settings().registryKey(key).maxCount(definition.maxCount()); + if (definition.maxDurability() > 0) { + settings.maxDamage(definition.maxDurability()); + settings.component(DataComponentTypes.MAX_DAMAGE, definition.maxDurability()); + settings.component(DataComponentTypes.DAMAGE, 0); + } + + if (supportsDurabilityEnchantments(definition.maxDurability())) { + settings.enchantable(10); + } + + if (definition.dyeable()) { + settings.component(DataComponentTypes.DYED_COLOR, new DyedColorComponent(definition.defaultDyeColor())); + } + + Item item = new NonDataAccessoryItem(settings); + Registry.register(Registries.ITEM, key, item); + NonTrinketsIntegration.registerTrinketItem(item); + if (definition.showInItemGroup()) { + REGISTERED_ITEMS.add(item); + } + + DEFINITIONS_BY_ITEM.put(definition.id(), definition); + + for (String slot : definition.trinketsSlots()) { + List ids = new ArrayList<>(ITEM_IDS_BY_TRINKETS_SLOT.getOrDefault(slot, List.of())); + ids.add(definition.id()); + ITEM_IDS_BY_TRINKETS_SLOT.put(slot, List.copyOf(ids)); + } + } + + private static NonAccessoryItemRegistry.SkinOverlays parseSkinOverlays(Identifier itemId, @Nullable JsonElement element) { + if (element != null && element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + List v = parseVisualLayers(itemId, "v", object.get("v"), true); + List a = parseVisualLayers(itemId, "a", object.get("a"), true); + List d = parseVisualLayers(itemId, "d", object.get("d"), true); + return new NonAccessoryItemRegistry.SkinOverlays(v, a, d); + } else { + return NonAccessoryItemRegistry.SkinOverlays.EMPTY; + } + } + + private static NonAccessoryItemRegistry.ItemClientModel parseItemClientModel(Identifier itemId, JsonObject root) { + Identifier itemTexture = parseAssetIdentifier(itemId, root.get("item_texture"), "item/"); + Identifier itemModel = parseAssetIdentifier(itemId, root.get("item_model"), "item/"); + List layers = parseVisualLayers(itemId, "item_layers", root.get("item_layers"), false); + if (layers.isEmpty() && itemTexture != null) { + layers = List.of(new NonAccessoryItemRegistry.VisualLayer(itemTexture, NonAccessoryItemRegistry.TintMode.NONE)); + } + + if (itemModel == null && !layers.isEmpty()) { + itemModel = Identifier.of(itemId.getNamespace(), "item/" + itemId.getPath()); + } + + return new NonAccessoryItemRegistry.ItemClientModel(itemModel, layers); + } + + private static NonAccessoryItemRegistry.ActorVisualBehavior parseActorVisualBehavior(Identifier itemId, JsonObject root) { + Set virtualTags = parseStringSet(root.get("virtual_actor_tags")); + String modelGender = parseString(root.get("animation_model_gender")); + if (modelGender != null) { + modelGender = modelGender.trim().toLowerCase(Locale.ROOT); + if (!"male".equals(modelGender) && !"female".equals(modelGender)) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring unknown animation model gender '{}' for {}.", modelGender, itemId); + modelGender = null; + } + } + + Map boneTextures = new LinkedHashMap<>(); + JsonElement boneElement = root.get("animation_bone_textures"); + if (boneElement != null && boneElement.isJsonObject()) { + for (Entry entry : boneElement.getAsJsonObject().entrySet()) { + List layers = parseVisualLayers(itemId, entry.getKey(), entry.getValue(), true); + if (!layers.isEmpty()) { + boneTextures.put(entry.getKey(), layers.getFirst()); + } + } + } + + int storedLiquidCapacityMl = parseInt(root.get("stored_liquid_capacity_ml"), 0, 0, 1000); + return new NonAccessoryItemRegistry.ActorVisualBehavior(virtualTags, modelGender, Map.copyOf(boneTextures), storedLiquidCapacityMl); + } + + private static List parseVisualLayers( + Identifier itemId, String key, @Nullable JsonElement element, boolean texturePath + ) { + if (element == null) { + return List.of(); + } + + List layers = new ArrayList<>(); + if (element.isJsonArray()) { + for (JsonElement child : element.getAsJsonArray()) { + NonAccessoryItemRegistry.VisualLayer layer = parseVisualLayer(itemId, key, child, texturePath); + if (layer != null) { + layers.add(layer); + } + } + } else { + NonAccessoryItemRegistry.VisualLayer layer = parseVisualLayer(itemId, key, element, texturePath); + if (layer != null) { + layers.add(layer); + } + } + + return layers.isEmpty() ? List.of() : List.copyOf(layers); + } + + @Nullable + private static NonAccessoryItemRegistry.VisualLayer parseVisualLayer(Identifier itemId, String key, JsonElement element, boolean texturePath) { + JsonElement textureElement = element; + NonAccessoryItemRegistry.TintMode tintMode = NonAccessoryItemRegistry.TintMode.NONE; + if (element != null && element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + textureElement = object.get("texture"); + tintMode = NonAccessoryItemRegistry.TintMode.parse(parseString(object.get("tint"))); + } + + Identifier texture = texturePath ? parseTextureIdentifier(itemId, key, textureElement) : parseAssetIdentifier(itemId, textureElement, "item/"); + return texture == null ? null : new NonAccessoryItemRegistry.VisualLayer(texture, tintMode); + } + + private static Set parseStringSet(@Nullable JsonElement element) { + if (element == null) { + return Set.of(); + } + + LinkedHashSet values = new LinkedHashSet<>(); + if (element.isJsonArray()) { + for (JsonElement child : element.getAsJsonArray()) { + String value = parseString(child); + if (value != null && !value.isBlank()) { + values.add(value.trim()); + } + } + } else { + String value = parseString(element); + if (value != null && !value.isBlank()) { + values.add(value.trim()); + } + } + + return Set.copyOf(values); + } + + private static int parseDyeColor(@Nullable JsonElement element) { + String raw = parseString(element); + DyeColor color = DyeColor.byId(raw == null ? "purple" : raw.trim().toLowerCase(Locale.ROOT), DyeColor.PURPLE); + return color.getEntityColor(); + } + + @Nullable + private static Identifier parseAssetIdentifier(Identifier itemId, @Nullable JsonElement element, String defaultPathPrefix) { + String raw = parseString(element); + if (raw != null && !raw.isBlank()) { + raw = raw.trim(); + if (!raw.contains(":")) { + raw = itemId.getNamespace() + ":" + defaultPathPrefix + raw; + } + + Identifier id = Identifier.tryParse(raw); + if (id == null) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring invalid accessory asset identifier '{}' for {}.", raw, itemId); + } + + return id; + } else { + return null; + } + } + + @Nullable + private static Identifier parseTextureIdentifier(Identifier itemId, String key, @Nullable JsonElement element) { + String raw = parseString(element); + if (raw != null && !raw.isBlank()) { + raw = raw.trim(); + if (!raw.contains(":")) { + raw = itemId.getNamespace() + ":" + raw; + } + + Identifier id = Identifier.tryParse(raw); + if (id == null) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring invalid accessory skin overlay '{}' for {}.", key, itemId); + return null; + } else { + return toTexturePath(id); + } + } else { + return null; + } + } + + private static Identifier toTexturePath(Identifier id) { + String path = id.getPath(); + if (!path.startsWith("textures/")) { + path = "textures/" + path; + } + + if (!path.endsWith(".png")) { + path = path + ".png"; + } + + return Identifier.of(id.getNamespace(), path); + } + + private static Set parseTrinketsSlots(@Nullable JsonElement element) { + if (element == null) { + return Set.of(); + } + + LinkedHashSet slots = new LinkedHashSet<>(); + if (element.isJsonArray()) { + for (JsonElement child : element.getAsJsonArray()) { + addTrinketsSlot(slots, parseString(child)); + } + } else { + addTrinketsSlot(slots, parseString(element)); + } + + return Set.copyOf(slots); + } + + private static void addTrinketsSlot(Set slots, @Nullable String raw) { + if (raw != null && !raw.isBlank()) { + String slot = raw.trim().toLowerCase(Locale.ROOT); + if ("v".equals(slot)) { + slot = "legs/v"; + } + + if ("a".equals(slot)) { + slot = "legs/a"; + } + + if ("d".equals(slot)) { + slot = "legs/d"; + } + + if (!"legs/v".equals(slot) && !"legs/a".equals(slot) && !"legs/d".equals(slot)) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring unknown accessory Trinkets slot '{}'.", raw); + } else { + slots.add(slot); + } + } + } + + @Nullable + private static Identifier parseItemId(JsonObject root) { + String raw = parseString(root.get("id")); + if (raw != null && !raw.isBlank()) { + raw = raw.trim(); + if (!raw.contains(":")) { + raw = "needsofnature:" + raw; + } + + return Identifier.tryParse(raw); + } else { + return null; + } + } + + @Nullable + private static String parseString(@Nullable JsonElement element) { + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString() ? element.getAsString() : null; + } + + private static int parseInt(@Nullable JsonElement element, int fallback, int min, int max) { + if (element == null || !element.isJsonPrimitive()) { + return fallback; + } + + if (!element.getAsJsonPrimitive().isNumber()) { + return fallback; + } + + try { + int value = element.getAsInt(); + return Math.max(min, Math.min(max, value)); + } catch (RuntimeException ignored) { + return fallback; + } + } + + private static boolean parseBoolean(@Nullable JsonElement element, boolean fallback) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + return primitive.isBoolean() ? primitive.getAsBoolean() : fallback; + } else { + return fallback; + } + } + + public record AccessoryItemDefinition( + Identifier id, + int maxCount, + int maxDurability, + boolean showInItemGroup, + Set trinketsSlots, + NonAccessoryEffects effects, + Map tooltipColors, + NonAccessoryBehavior behavior, + NonAccessoryItemRegistry.SkinOverlays skinOverlays, + NonAccessoryItemRegistry.ItemClientModel clientModel, + boolean dyeable, + int defaultDyeColor, + NonAccessoryItemRegistry.ActorVisualBehavior actorVisualBehavior + ) { + } + + public record ActorVisualBehavior( + Set virtualActorTags, + @Nullable String animationModelGender, + Map animationBoneTextures, + int storedLiquidCapacityMl + ) { + public static final NonAccessoryItemRegistry.ActorVisualBehavior NONE = new NonAccessoryItemRegistry.ActorVisualBehavior(Set.of(), null, Map.of(), 0); + + public boolean substitutesMaleRole() { + return this.virtualActorTags.contains("gender.male") && "male".equals(this.animationModelGender); + } + } + + public record ItemClientModel(@Nullable Identifier itemModel, List layers) { + public static final NonAccessoryItemRegistry.ItemClientModel EMPTY = new NonAccessoryItemRegistry.ItemClientModel(null, List.of()); + + public boolean isEmpty() { + return this.itemModel == null && this.layers.isEmpty(); + } + } + + public record SkinOverlays( + List v, List a, List d + ) { + public static final NonAccessoryItemRegistry.SkinOverlays EMPTY = new NonAccessoryItemRegistry.SkinOverlays(List.of(), List.of(), List.of()); + + public boolean isEmpty() { + return this.v.isEmpty() && this.a.isEmpty() && this.d.isEmpty(); + } + + public List forSlot(String slotId) { + if (slotId == null) { + return List.of(); + } else { + String slot = slotId.toLowerCase(Locale.ROOT); + if (slot.equals("legs/v") || slot.startsWith("legs/v/")) { + return this.v; + } else if (slot.equals("legs/a") || slot.startsWith("legs/a/")) { + return this.a; + } else { + return !slot.equals("legs/d") && !slot.startsWith("legs/d/") ? List.of() : this.d; + } + } + } + } + + public enum TintMode { + NONE, + DYE, + STORED_LIQUID; + + static NonAccessoryItemRegistry.TintMode parse(@Nullable String raw) { + if (raw != null && !raw.isBlank()) { + return switch (raw.trim().toLowerCase(Locale.ROOT)) { + case "dye" -> DYE; + case "stored_liquid" -> STORED_LIQUID; + default -> NONE; + }; + } else { + return NONE; + } + } + } + + public record VisualLayer(Identifier texture, NonAccessoryItemRegistry.TintMode tintMode) { + } +} diff --git a/src/main/java/com/nonid/data/NonAccessoryLiquidFilling.java b/src/main/java/com/nonid/data/NonAccessoryLiquidFilling.java new file mode 100644 index 0000000..ef955de --- /dev/null +++ b/src/main/java/com/nonid/data/NonAccessoryLiquidFilling.java @@ -0,0 +1,65 @@ +package com.nonid.data; + +import com.nonid.potion.NonPotions; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonAccessoryLiquidFilling { + private NonAccessoryLiquidFilling() { + } + + public static boolean canFill(ItemStack accessory, ItemStack liquidBottle) { + return resolveFill(accessory, liquidBottle) != null; + } + + public static boolean fillInPlace(ItemStack accessory, ItemStack liquidBottle) { + NonAccessoryLiquidFilling.FillData fill = resolveFill(accessory, liquidBottle); + if (fill == null) { + return false; + } + + NonAccessoryStackData.setStoredLiquid(accessory, fill.entityTypeId(), fill.amountMl()); + return true; + } + + public static ItemStack createFilledCopy(ItemStack accessory, ItemStack liquidBottle) { + NonAccessoryLiquidFilling.FillData fill = resolveFill(accessory, liquidBottle); + if (fill == null) { + return ItemStack.EMPTY; + } + + ItemStack result = accessory.copyWithCount(1); + NonAccessoryStackData.setStoredLiquid(result, fill.entityTypeId(), fill.amountMl()); + return result; + } + + @Nullable + private static NonAccessoryLiquidFilling.FillData resolveFill(ItemStack accessory, ItemStack liquidBottle) { + if (accessory == null || accessory.isEmpty() || liquidBottle == null || liquidBottle.isEmpty()) { + return null; + } + + if (NonAccessoryStackData.getStoredLiquid(accessory) != null) { + return null; + } + + Identifier accessoryId = Registries.ITEM.getId(accessory.getItem()); + NonAccessoryItemRegistry.AccessoryItemDefinition definition = NonAccessoryItemRegistry.getDefinition(accessoryId); + if (definition == null) { + return null; + } + + int capacityMl = definition.actorVisualBehavior().storedLiquidCapacityMl(); + if (capacityMl <= 0) { + return null; + } + + Identifier entityTypeId = NonPotions.getLiquidBottleEntityTypeId(liquidBottle); + return entityTypeId == null ? null : new NonAccessoryLiquidFilling.FillData(entityTypeId, capacityMl); + } + + private record FillData(Identifier entityTypeId, int amountMl) { + } +} diff --git a/src/main/java/com/nonid/data/NonAccessoryStackData.java b/src/main/java/com/nonid/data/NonAccessoryStackData.java new file mode 100644 index 0000000..ca73d41 --- /dev/null +++ b/src/main/java/com/nonid/data/NonAccessoryStackData.java @@ -0,0 +1,69 @@ +package com.nonid.data; + +import com.nonid.NeedsOfNature; +import java.util.List; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.CustomModelDataComponent; +import net.minecraft.component.type.DyedColorComponent; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonAccessoryStackData { + private static final String LIQUID_ENTITY_KEY = "NonAccessoryLiquidEntity"; + private static final String LIQUID_AMOUNT_KEY = "NonAccessoryLiquidAmount"; + + private NonAccessoryStackData() { + } + + public static int resolveDyeRgb(ItemStack stack, int fallbackRgb) { + if (stack != null && !stack.isEmpty()) { + DyedColorComponent dyed = (DyedColorComponent)stack.get(DataComponentTypes.DYED_COLOR); + return (dyed == null ? fallbackRgb : dyed.rgb()) & 16777215; + } else { + return fallbackRgb & 16777215; + } + } + + @Nullable + public static NonAccessoryStackData.StoredLiquid getStoredLiquid(ItemStack stack) { + if (stack != null && !stack.isEmpty()) { + NbtComponent custom = (NbtComponent)stack.get(DataComponentTypes.CUSTOM_DATA); + if (custom != null && !custom.isEmpty()) { + NbtCompound nbt = custom.copyNbt(); + Identifier entityTypeId = Identifier.tryParse(nbt.getString("NonAccessoryLiquidEntity", "")); + int amount = nbt.getInt("NonAccessoryLiquidAmount", 0); + return entityTypeId != null && amount > 0 ? new NonAccessoryStackData.StoredLiquid(entityTypeId, amount) : null; + } else { + return null; + } + } else { + return null; + } + } + + public static void setStoredLiquid(ItemStack stack, @Nullable Identifier entityTypeId, int amountMl) { + if (stack != null && !stack.isEmpty()) { + int amount = Math.max(0, amountMl); + if (entityTypeId != null && amount != 0) { + NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> { + nbt.putString("NonAccessoryLiquidEntity", entityTypeId.toString()); + nbt.putInt("NonAccessoryLiquidAmount", amount); + }); + int argb = 0xFF000000 | NeedsOfNature.resolveBottleLiquidTintRgb(entityTypeId); + stack.set(DataComponentTypes.CUSTOM_MODEL_DATA, new CustomModelDataComponent(List.of(), List.of(true), List.of(), List.of(argb))); + } else { + NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> { + nbt.remove("NonAccessoryLiquidEntity"); + nbt.remove("NonAccessoryLiquidAmount"); + }); + stack.remove(DataComponentTypes.CUSTOM_MODEL_DATA); + } + } + } + + public record StoredLiquid(Identifier entityTypeId, int amountMl) { + } +} diff --git a/src/main/java/com/nonid/data/NonEntityProfiles.java b/src/main/java/com/nonid/data/NonEntityProfiles.java new file mode 100644 index 0000000..96a022c --- /dev/null +++ b/src/main/java/com/nonid/data/NonEntityProfiles.java @@ -0,0 +1,605 @@ +package com.nonid.data; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.entity.Entity; +import net.minecraft.registry.Registries; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonEntityProfiles { + private static final String ROOT_PATH = "non_entity_profiles"; + private static final String ENTRIES_KEY = "entries"; + private static final String ENTITY_KEY = "entity"; + private static final String PREGNANCY_CHANCE_KEY = "pregnancy_chance_percent"; + private static final String OFFSPRING_MIN_KEY = "offspring_min"; + private static final String OFFSPRING_MAX_KEY = "offspring_max"; + private static final String BIRTH_ENTITY_KEY = "birth_entity"; + private static final String BIRTH_MODE_KEY = "birth_mode"; + private static final String EGG_KEY = "egg"; + private static final String EGG_START_SIZE_KEY = "start_size"; + private static final String EGG_END_SIZE_KEY = "end_size"; + private static final String EGG_TEXTURE_KEY = "texture"; + private static final String EGG_HEALTH_KEY = "health"; + private static final String GENDER_SPAWN_KEY = "gender_spawn"; + private static final String MALE_CHANCE_KEY = "male_chance"; + private static final String FEMALE_CHANCE_KEY = "female_chance"; + private static final String BOTH_CHANCE_KEY = "both_chance"; + private static final String LOOK_AT_NEARBY_ANIMATIONS_KEY = "look_at_nearby_animations"; + private static final String MULTI_ACTOR_JOIN_CHANCE_KEY = "multi_actor_join_chance_percent"; + private static final String ENERGY_GAIN_MULTIPLIER_KEY = "energy_gain_multiplier"; + private static volatile Map PACK_PROFILES_BY_ENTITY = Map.of(); + private static volatile ConcurrentMap ENERGY_GAIN_MULTIPLIER_CACHE = new ConcurrentHashMap<>(); + + private NonEntityProfiles() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.SERVER_DATA).registerReloader(NonEntityProfiles.Reloader.RELOADER_ID, new NonEntityProfiles.Reloader()); + } + + public static NonEntityProfiles.ResolvedProfile resolve(@Nullable Entity entity) { + return entity == null ? fallbackProfile(null) : resolve(Registries.ENTITY_TYPE.getId(entity.getType())); + } + + public static NonEntityProfiles.ResolvedProfile resolve(@Nullable Identifier entityTypeId) { + NonEntityProfiles.ResolvedProfile profile = fallbackProfile(entityTypeId); + if (entityTypeId == null) { + return profile; + } + + NonConfig.EntityProfile hardcodedDefault = hardcodedDefaultProfile(entityTypeId); + if (hardcodedDefault != null) { + profile = apply(profile, hardcodedDefault, entityTypeId); + } + + NonConfig.EntityProfile packProfile = PACK_PROFILES_BY_ENTITY.get(entityTypeId); + if (packProfile != null) { + profile = apply(profile, packProfile, entityTypeId); + } + + NonConfig.EntityProfile configProfile = NeedsOfNature.getConfig().getEntityProfilesByEntity().get(entityTypeId.toString()); + if (configProfile != null) { + profile = apply(profile, configProfile, entityTypeId); + } + + return profile; + } + + public static float resolveEnergyGainMultiplier(@Nullable Entity entity) { + if (entity == null) { + return 1.0F; + } + + Identifier entityTypeId = Registries.ENTITY_TYPE.getId(entity.getType()); + return ENERGY_GAIN_MULTIPLIER_CACHE.computeIfAbsent(entityTypeId, id -> resolve(id).energyGainMultiplier()); + } + + public static void invalidateEnergyGainMultiplierCache() { + ENERGY_GAIN_MULTIPLIER_CACHE = new ConcurrentHashMap<>(); + } + + public static Map getPackProfilesByEntity() { + if (PACK_PROFILES_BY_ENTITY.isEmpty()) { + return Map.of(); + } + + Map out = new LinkedHashMap<>(); + + for (Entry entry : PACK_PROFILES_BY_ENTITY.entrySet()) { + out.put(entry.getKey().toString(), entry.getValue().sanitized()); + } + + return Map.copyOf(out); + } + + public static Map resolveEffectiveProfilesMap() { + Set ids = new LinkedHashSet<>(); + ids.addAll(hardcodedDefaultProfileIds()); + ids.addAll(PACK_PROFILES_BY_ENTITY.keySet()); + + for (String raw : NeedsOfNature.getConfig().getEntityProfilesByEntity().keySet()) { + Identifier id = Identifier.tryParse(raw); + if (id != null) { + ids.add(id); + } + } + + Map out = new LinkedHashMap<>(); + + for (Identifier id : ids) { + out.put(id.toString(), resolve(id).toConfigProfile()); + } + + return Map.copyOf(out); + } + + public static Map resolveDefaultProfilesMap() { + Set ids = new LinkedHashSet<>(); + ids.addAll(hardcodedDefaultProfileIds()); + ids.addAll(PACK_PROFILES_BY_ENTITY.keySet()); + Map out = new LinkedHashMap<>(); + + for (Identifier id : ids) { + NonEntityProfiles.ResolvedProfile profile = fallbackProfile(id); + NonConfig.EntityProfile hardcodedDefault = hardcodedDefaultProfile(id); + if (hardcodedDefault != null) { + profile = apply(profile, hardcodedDefault, id); + } + + NonConfig.EntityProfile packProfile = PACK_PROFILES_BY_ENTITY.get(id); + if (packProfile != null) { + profile = apply(profile, packProfile, id); + } + + out.put(id.toString(), profile.toConfigProfile()); + } + + return Map.copyOf(out); + } + + public static NonEntityProfiles.ResolvedProfile resolveDefaultProfile(@Nullable Identifier entityTypeId) { + NonEntityProfiles.ResolvedProfile profile = fallbackProfile(entityTypeId); + if (entityTypeId == null) { + return profile; + } + + NonConfig.EntityProfile hardcodedDefault = hardcodedDefaultProfile(entityTypeId); + if (hardcodedDefault != null) { + profile = apply(profile, hardcodedDefault, entityTypeId); + } + + NonConfig.EntityProfile packProfile = PACK_PROFILES_BY_ENTITY.get(entityTypeId); + if (packProfile != null) { + profile = apply(profile, packProfile, entityTypeId); + } + + return profile; + } + + public static Map resolveEffectiveGenderSpawnMap() { + Map out = new LinkedHashMap<>(); + + for (Entry entry : resolveEffectiveProfilesMap().entrySet()) { + NonConfig.GenderSpawnChances chances = entry.getValue().genderSpawn(); + if (chances != null) { + out.put(entry.getKey(), chances); + } + } + + return Map.copyOf(out); + } + + private static NonEntityProfiles.ResolvedProfile fallbackProfile(@Nullable Identifier entityTypeId) { + String entity = entityTypeId == null ? "minecraft:pig" : entityTypeId.toString(); + return new NonEntityProfiles.ResolvedProfile( + NeedsOfNature.getConfig().getPregnancyChancePercent(), + 1, + 1, + Identifier.tryParse(entity) == null ? Identifier.of("minecraft", "pig") : Identifier.tryParse(entity), + "direct", + NeedsOfNature.getConfig().getGlobalGenderSpawnChances(), + NonConfig.EggProfile.defaults(), + true, + null, + 1.0F + ); + } + + private static NonEntityProfiles.ResolvedProfile apply(NonEntityProfiles.ResolvedProfile base, NonConfig.EntityProfile patch, Identifier sourceEntityId) { + if (patch == null) { + return base; + } + + NonConfig.EntityProfile safe = patch.sanitized(); + int chance = safe.pregnancyChancePercent() == null ? base.pregnancyChancePercent() : safe.pregnancyChancePercent(); + int min = safe.offspringMin() == null ? base.offspringMin() : safe.offspringMin(); + int max = safe.offspringMax() == null ? base.offspringMax() : safe.offspringMax(); + if (max < min) { + max = min; + } + + Identifier birthEntity = base.birthEntityTypeId(); + String rawBirthEntity = safe.birthEntity(); + if (rawBirthEntity != null) { + Identifier parsed = Identifier.tryParse(rawBirthEntity); + if (parsed != null && Registries.ENTITY_TYPE.containsId(parsed)) { + birthEntity = parsed; + } else { + NeedsOfNature.logSetupWarning("[NoN setup] Ignoring invalid birth_entity '{}' for entity profile {}.", rawBirthEntity, sourceEntityId); + } + } + + String birthMode = safe.birthMode() == null ? base.birthMode() : safe.birthMode(); + NonConfig.GenderSpawnChances genderSpawn = safe.genderSpawn() == null ? base.genderSpawn() : safe.genderSpawn(); + NonConfig.EggProfile eggProfile = safe.egg() == null ? base.eggProfile() : safe.egg().resolvedOver(base.eggProfile()); + boolean lookAtNearbyAnimations = safe.lookAtNearbyAnimations() == null ? base.lookAtNearbyAnimations() : safe.lookAtNearbyAnimations(); + Integer multiActorJoinChancePercent = safe.multiActorJoinChancePercent() == null + ? base.multiActorJoinChancePercent() + : safe.multiActorJoinChancePercent(); + float energyGainMultiplier = safe.energyGainMultiplier() == null ? base.energyGainMultiplier() : safe.energyGainMultiplier(); + return new NonEntityProfiles.ResolvedProfile( + chance, min, max, birthEntity, birthMode, genderSpawn, eggProfile, lookAtNearbyAnimations, multiActorJoinChancePercent, energyGainMultiplier + ); + } + + @Nullable + private static NonConfig.EntityProfile hardcodedDefaultProfile(Identifier entityTypeId) { + return null; + } + + private static Set hardcodedDefaultProfileIds() { + return Set.of(); + } + + private static void reload(ResourceManager resourceManager) { + Map resources = resourceManager.findResources("non_entity_profiles", id -> id.getPath().endsWith(".json")); + List> ordered = new ArrayList<>(resources.entrySet()); + ordered.sort(Comparator.comparing(entryx -> ((Identifier)entryx.getKey()).toString())); + Map profiles = new LinkedHashMap<>(); + + for (Entry entry : ordered) { + Identifier fileId = entry.getKey(); + + try (InputStreamReader reader = new InputStreamReader(entry.getValue().getInputStream(), StandardCharsets.UTF_8)) { + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + JsonArray entries = asArray(root.get("entries")); + if (entries != null && !entries.isEmpty()) { + parseEntries(fileId, entries, profiles); + } + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to read entity profile file {}", fileId, e); + } + } + + PACK_PROFILES_BY_ENTITY = Map.copyOf(profiles); + invalidateEnergyGainMultiplierCache(); + } + + private static void parseEntries(Identifier fileId, JsonArray entries, Map profiles) { + for (int i = 0; i < entries.size(); i++) { + JsonObject obj = asObject(entries.get(i)); + if (obj == null) { + warnMalformed(fileId, i, "Entry is not an object."); + } else { + Identifier entityId = parseEntityId(obj.get("entity")); + if (entityId == null) { + warnMalformed(fileId, i, "Missing or invalid 'entity'."); + } else { + NonConfig.EntityProfile profile = parseProfile(fileId, i, obj, entityId); + if (profile != null && !profile.isEmpty()) { + profiles.put(entityId, profile); + } else { + warnMalformed(fileId, i, "Entry has no valid profile fields."); + } + } + } + } + } + + @Nullable + private static NonConfig.EntityProfile parseProfile(Identifier fileId, int index, JsonObject obj, Identifier entityId) { + Integer chance = parseOptionalInt(obj.get("pregnancy_chance_percent")); + Integer offspringMin = parseOptionalInt(obj.get("offspring_min")); + Integer offspringMax = parseOptionalInt(obj.get("offspring_max")); + String birthEntity = null; + if (obj.has("birth_entity")) { + Identifier parsed = parseEntityId(obj.get("birth_entity")); + if (parsed != null && Registries.ENTITY_TYPE.containsId(parsed)) { + birthEntity = parsed.toString(); + } else { + warnMalformed(fileId, index, "Invalid 'birth_entity'."); + } + } + + String birthMode = null; + if (obj.has("birth_mode")) { + birthMode = parseOptionalString(obj.get("birth_mode")); + birthMode = NonConfig.normalizeBirthMode(birthMode); + if (birthMode == null) { + warnMalformed(fileId, index, "Invalid 'birth_mode'. Use 'egg' or 'direct'."); + } + } + + NonConfig.GenderSpawnChances genderSpawn = null; + JsonObject genderObj = asObject(obj.get("gender_spawn")); + if (genderObj != null) { + genderSpawn = parseGenderSpawn(fileId, index, genderObj); + } + + NonConfig.EggProfile eggProfile = null; + JsonObject eggObj = asObject(obj.get("egg")); + if (eggObj != null) { + eggProfile = parseEggProfile(fileId, index, eggObj); + } + + Boolean lookAtNearbyAnimations = parseOptionalBoolean(obj.get("look_at_nearby_animations")); + if (obj.has("look_at_nearby_animations") && lookAtNearbyAnimations == null) { + warnMalformed(fileId, index, "'look_at_nearby_animations' must be a boolean."); + } + + Integer multiActorJoinChancePercent = parseOptionalInt(obj.get("multi_actor_join_chance_percent")); + if (obj.has("multi_actor_join_chance_percent") && multiActorJoinChancePercent == null) { + warnMalformed(fileId, index, "'multi_actor_join_chance_percent' must be an integer from 0 to 100."); + } else if (multiActorJoinChancePercent != null && (multiActorJoinChancePercent < 0 || multiActorJoinChancePercent > 100)) { + warnMalformed(fileId, index, "'multi_actor_join_chance_percent' must be 0-100."); + multiActorJoinChancePercent = null; + } + + Float energyGainMultiplier = parseOptionalFloat(obj.get("energy_gain_multiplier")); + if (obj.has("energy_gain_multiplier") && energyGainMultiplier == null) { + warnMalformed(fileId, index, "'energy_gain_multiplier' must be a number from 0.1 to 10.0."); + } else if (energyGainMultiplier != null && (energyGainMultiplier < 0.1F || energyGainMultiplier > 10.0F)) { + warnMalformed(fileId, index, "'energy_gain_multiplier' must be 0.1-10.0."); + energyGainMultiplier = null; + } + + if (chance != null && (chance < 0 || chance > 100)) { + warnMalformed(fileId, index, "'pregnancy_chance_percent' must be 0-100."); + chance = null; + } + + if (offspringMin != null && (offspringMin < 1 || offspringMin > 16)) { + warnMalformed(fileId, index, "'offspring_min' is out of range."); + offspringMin = null; + } + + if (offspringMax != null && (offspringMax < 1 || offspringMax > 16)) { + warnMalformed(fileId, index, "'offspring_max' is out of range."); + offspringMax = null; + } + + if (offspringMin != null && offspringMax != null && offspringMax < offspringMin) { + warnMalformed(fileId, index, "'offspring_max' must be at least 'offspring_min'."); + offspringMax = offspringMin; + } + + return new NonConfig.EntityProfile( + chance, + offspringMin, + offspringMax, + birthEntity, + birthMode, + genderSpawn, + eggProfile, + lookAtNearbyAnimations, + multiActorJoinChancePercent, + energyGainMultiplier + ) + .sanitized(); + } + + @Nullable + private static NonConfig.EggProfile parseEggProfile(Identifier fileId, int index, JsonObject obj) { + Float startSize = parseOptionalFloat(obj.get("start_size")); + Float endSize = parseOptionalFloat(obj.get("end_size")); + Float health = parseOptionalFloat(obj.get("health")); + String texture = null; + if (obj.has("texture")) { + texture = parseOptionalString(obj.get("texture")); + if (texture != null && NonConfig.normalizeTextureIdString(texture) == null) { + warnMalformed(fileId, index, "Invalid egg 'texture'."); + texture = null; + } + } + + if (obj.has("start_size") && startSize == null) { + warnMalformed(fileId, index, "Egg 'start_size' must be a number."); + } + + if (obj.has("end_size") && endSize == null) { + warnMalformed(fileId, index, "Egg 'end_size' must be a number."); + } + + if (obj.has("health") && health == null) { + warnMalformed(fileId, index, "Egg 'health' must be a number."); + } + + if (startSize != null && (startSize < 0.05F || startSize > 4.0F)) { + warnMalformed(fileId, index, "Egg 'start_size' is out of range."); + startSize = null; + } + + if (endSize != null && (endSize < 0.05F || endSize > 4.0F)) { + warnMalformed(fileId, index, "Egg 'end_size' is out of range."); + endSize = null; + } + + if (health != null && (health < 0.5F || health > 100.0F)) { + warnMalformed(fileId, index, "Egg 'health' is out of range."); + health = null; + } + + NonConfig.EggProfile profile = new NonConfig.EggProfile(startSize, endSize, texture, health).sanitized(); + return profile.isEmpty() ? null : profile; + } + + @Nullable + private static NonConfig.GenderSpawnChances parseGenderSpawn(Identifier fileId, int index, JsonObject obj) { + Integer male = parseOptionalInt(obj.get("male_chance")); + Integer female = parseOptionalInt(obj.get("female_chance")); + Integer both = parseOptionalInt(obj.get("both_chance")); + if (male != null && female != null && both != null) { + NonConfig.GenderSpawnChances chances = NonConfig.sanitizeGenderSpawnChances(new NonConfig.GenderSpawnChances(male, female, both)); + if (chances == null) { + warnMalformed(fileId, index, "Gender spawn percentages must be 0-100 and total exactly 100."); + } + + return chances; + } else { + warnMalformed(fileId, index, "Gender spawn needs male_chance, female_chance, and both_chance."); + return null; + } + } + + @Nullable + private static Identifier parseEntityId(@Nullable JsonElement element) { + String raw = parseOptionalString(element); + return raw == null ? null : Identifier.tryParse(raw); + } + + @Nullable + private static String parseOptionalString(@Nullable JsonElement element) { + if (element == null || !element.isJsonPrimitive()) { + return null; + } + + if (!element.getAsJsonPrimitive().isString()) { + return null; + } + + String raw = element.getAsString(); + return raw != null && !raw.isBlank() ? raw.trim() : null; + } + + @Nullable + private static Integer parseOptionalInt(@Nullable JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isNumber()) { + double value = primitive.getAsDouble(); + if (!Double.isFinite(value) || value != Math.rint(value)) { + return null; + } else { + return !(value < -2.1474836E9F) && !(value > 2.147483647E9) ? (int)value : null; + } + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static Float parseOptionalFloat(@Nullable JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (!primitive.isNumber()) { + return null; + } else { + double value = primitive.getAsDouble(); + if (!Double.isFinite(value)) { + return null; + } else { + return !(value < -Float.MAX_VALUE) && !(value > Float.MAX_VALUE) ? (float)value : null; + } + } + } else { + return null; + } + } + + @Nullable + private static Boolean parseOptionalBoolean(@Nullable JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + return primitive.isBoolean() ? primitive.getAsBoolean() : null; + } else { + return null; + } + } + + @Nullable + private static JsonObject asObject(@Nullable JsonElement element) { + return element != null && element.isJsonObject() ? element.getAsJsonObject() : null; + } + + @Nullable + private static JsonArray asArray(@Nullable JsonElement element) { + return element != null && element.isJsonArray() ? element.getAsJsonArray() : null; + } + + private static void warnMalformed(Identifier fileId, int index, String reason) { + NeedsOfNature.logSetupWarning("[NoN setup] Ignoring malformed entity profile entry in {} at index {}: {}", fileId, index, reason); + } + + public static final class Reloader implements SynchronousResourceReloader { + static final Identifier RELOADER_ID = Identifier.of("needsofnature", "entity_profiles"); + + public void reload(ResourceManager manager) { + NonEntityProfiles.reload(manager); + } + } + + public record ResolvedProfile( + int pregnancyChancePercent, + int offspringMin, + int offspringMax, + Identifier birthEntityTypeId, + String birthMode, + NonConfig.GenderSpawnChances genderSpawn, + NonConfig.EggProfile eggProfile, + boolean lookAtNearbyAnimations, + @Nullable Integer multiActorJoinChancePercent, + float energyGainMultiplier + ) { + public ResolvedProfile { + pregnancyChancePercent = Math.max(0, Math.min(100, pregnancyChancePercent)); + offspringMin = Math.max(1, Math.min(16, offspringMin)); + offspringMax = Math.max(offspringMin, Math.min(16, offspringMax)); + if (birthEntityTypeId == null) { + birthEntityTypeId = Identifier.of("minecraft", "pig"); + } + + birthMode = NonConfig.normalizeBirthMode(birthMode); + if (birthMode == null) { + birthMode = "direct"; + } + + genderSpawn = NonConfig.validOrDefault(genderSpawn); + eggProfile = eggProfile == null ? NonConfig.EggProfile.defaults() : eggProfile.resolvedOver(NonConfig.EggProfile.defaults()); + if (multiActorJoinChancePercent != null) { + multiActorJoinChancePercent = Math.max(0, Math.min(100, multiActorJoinChancePercent)); + } + + if (!Float.isFinite(energyGainMultiplier)) { + energyGainMultiplier = 1.0F; + } + + energyGainMultiplier = Math.max(0.1F, Math.min(10.0F, energyGainMultiplier)); + } + + public NonConfig.OffspringCountRange offspringRange() { + return new NonConfig.OffspringCountRange(this.offspringMin, this.offspringMax); + } + + public NonConfig.EntityProfile toConfigProfile() { + return NonConfig.EntityProfile.full( + this.pregnancyChancePercent, + this.offspringMin, + this.offspringMax, + this.birthEntityTypeId.toString(), + this.birthMode, + this.genderSpawn, + this.eggProfile, + this.lookAtNearbyAnimations, + this.multiActorJoinChancePercent, + this.energyGainMultiplier + ); + } + } +} diff --git a/src/main/java/com/nonid/data/NonLiquidGainOverrides.java b/src/main/java/com/nonid/data/NonLiquidGainOverrides.java new file mode 100644 index 0000000..da5c477 --- /dev/null +++ b/src/main/java/com/nonid/data/NonLiquidGainOverrides.java @@ -0,0 +1,218 @@ +package com.nonid.data; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.nonid.NeedsOfNature; +import com.nonid.client.NonLiquidColors; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonLiquidGainOverrides { + private static final String ROOT_PATH = "non_liquid_gains"; + private static final String ENTRIES_KEY = "entries"; + private static final String ENTITY_KEY = "entity"; + private static final String GAIN_ML_KEY = "gain_ml"; + private static final String COLOR_KEY = "color"; + private static final String MIXED_COLOR_KEY = "mixed_color"; + private static volatile Map GAIN_BY_ENTITY = Map.of(); + private static volatile Map COLOR_HEX_BY_ENTITY = Map.of(); + @Nullable + private static volatile String MIXED_COLOR_HEX = null; + + private NonLiquidGainOverrides() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.SERVER_DATA).registerReloader(NonLiquidGainOverrides.Reloader.RELOADER_ID, new NonLiquidGainOverrides.Reloader()); + } + + @Nullable + public static Integer getGain(@Nullable Identifier entityTypeId) { + return entityTypeId == null ? null : GAIN_BY_ENTITY.get(entityTypeId); + } + + @Nullable + public static Integer getColorRgb(@Nullable Identifier entityTypeId) { + if (entityTypeId == null) { + return null; + } + + String hex = COLOR_HEX_BY_ENTITY.get(entityTypeId); + return NonLiquidColors.parseHexColor(hex); + } + + @Nullable + public static Integer getMixedColorRgb() { + return NonLiquidColors.parseHexColor(MIXED_COLOR_HEX); + } + + @Nullable + public static String getMixedColorHex() { + return MIXED_COLOR_HEX; + } + + public static Map getGainByEntity() { + if (GAIN_BY_ENTITY.isEmpty()) { + return Map.of(); + } + + Map out = new LinkedHashMap<>(); + + for (Entry entry : GAIN_BY_ENTITY.entrySet()) { + out.put(entry.getKey().toString(), entry.getValue()); + } + + return Map.copyOf(out); + } + + public static Map getColorByEntity() { + if (COLOR_HEX_BY_ENTITY.isEmpty()) { + return Map.of(); + } + + Map out = new LinkedHashMap<>(); + + for (Entry entry : COLOR_HEX_BY_ENTITY.entrySet()) { + out.put(entry.getKey().toString(), entry.getValue()); + } + + return Map.copyOf(out); + } + + private static void reload(ResourceManager resourceManager) { + Map resources = resourceManager.findResources("non_liquid_gains", id -> id.getPath().endsWith(".json")); + Map gains = new LinkedHashMap<>(); + Map colors = new LinkedHashMap<>(); + String mixedColorHex = null; + List> ordered = new ArrayList<>(resources.entrySet()); + ordered.sort(Comparator.comparing(entryx -> ((Identifier)entryx.getKey()).toString())); + + for (Entry entry : ordered) { + Identifier fileId = entry.getKey(); + + try (InputStreamReader reader = new InputStreamReader(entry.getValue().getInputStream(), StandardCharsets.UTF_8)) { + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + String mixedColor = parseOptionalColor(root.get("mixed_color")); + if (mixedColor != null) { + mixedColorHex = mixedColor; + } + + JsonArray entries = asArray(root.get("entries")); + if (entries != null && !entries.isEmpty()) { + parseEntries(fileId, entries, gains, colors); + } + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to read liquid gain override file {}", fileId, e); + } + } + + GAIN_BY_ENTITY = Map.copyOf(gains); + COLOR_HEX_BY_ENTITY = Map.copyOf(colors); + MIXED_COLOR_HEX = NonLiquidColors.normalizeHexColor(mixedColorHex); + } + + private static void parseEntries(Identifier fileId, JsonArray entries, Map gains, Map colors) { + for (int i = 0; i < entries.size(); i++) { + JsonObject obj = asObject(entries.get(i)); + if (obj == null) { + warnMalformed(fileId, i, "Entry is not an object."); + } else { + Identifier entityId = parseEntityId(obj.get("entity")); + if (entityId == null) { + warnMalformed(fileId, i, "Missing or invalid 'entity'."); + } else { + Integer gain = parseOptionalInt(obj.get("gain_ml")); + if (gain != null) { + gains.put(entityId, Math.max(0, Math.min(1000, gain))); + } + + String normalizedHex = parseOptionalColor(obj.get("color")); + if (normalizedHex != null) { + colors.put(entityId, normalizedHex); + } + } + } + } + } + + @Nullable + private static Identifier parseEntityId(@Nullable JsonElement element) { + if (element == null || !element.isJsonPrimitive()) { + return null; + } + + if (!element.getAsJsonPrimitive().isString()) { + return null; + } + + String raw = element.getAsString(); + return raw != null && !raw.isBlank() ? Identifier.tryParse(raw.trim()) : null; + } + + @Nullable + private static Integer parseOptionalInt(@Nullable JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isNumber()) { + double value = primitive.getAsDouble(); + if (!Double.isFinite(value) || value != Math.rint(value)) { + return null; + } else { + return !(value < -2.1474836E9F) && !(value > 2.147483647E9) ? (int)value : null; + } + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static String parseOptionalColor(@Nullable JsonElement element) { + if (element == null || !element.isJsonPrimitive()) { + return null; + } else { + return !element.getAsJsonPrimitive().isString() ? null : NonLiquidColors.normalizeHexColor(element.getAsString()); + } + } + + @Nullable + private static JsonObject asObject(@Nullable JsonElement element) { + return element != null && element.isJsonObject() ? element.getAsJsonObject() : null; + } + + @Nullable + private static JsonArray asArray(@Nullable JsonElement element) { + return element != null && element.isJsonArray() ? element.getAsJsonArray() : null; + } + + private static void warnMalformed(Identifier fileId, int index, String reason) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring malformed liquid gain entry in {} at index {}: {}", fileId, index, reason); + } + + public static final class Reloader implements SynchronousResourceReloader { + static final Identifier RELOADER_ID = Identifier.of("needsofnature", "liquid_gain_overrides"); + + public void reload(ResourceManager manager) { + NonLiquidGainOverrides.reload(manager); + } + } +} diff --git a/src/main/java/com/nonid/data/NonLiquidRoles.java b/src/main/java/com/nonid/data/NonLiquidRoles.java new file mode 100644 index 0000000..8a04ef3 --- /dev/null +++ b/src/main/java/com/nonid/data/NonLiquidRoles.java @@ -0,0 +1,19 @@ +package com.nonid.data; + +import java.util.Map; +import java.util.Set; + +public final class NonLiquidRoles { + private NonLiquidRoles() { + } + + public enum InjectorRole { + NONE, + V, + A, + M; + } + + public record LiquidRoles(Set injectorKeys, Set receiverKeys, Map injectorRoles) { + } +} diff --git a/src/main/java/com/nonid/data/NonManualPeakDefinitions.java b/src/main/java/com/nonid/data/NonManualPeakDefinitions.java new file mode 100644 index 0000000..95ce72d --- /dev/null +++ b/src/main/java/com/nonid/data/NonManualPeakDefinitions.java @@ -0,0 +1,158 @@ +package com.nonid.data; + +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.util.WeightedPicker; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonManualPeakDefinitions { + private static final String MANUAL_PEAK_TAG = "manualpeak"; + + private NonManualPeakDefinitions() { + } + + @Nullable + public static NonManualPeakDefinitions.Selection select(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + List actors = List.of(player); + List matching = new ArrayList<>(); + + for (AnimationDefinitions.Definition definition : AnimationDefinitions.getLoadedDefinitionsSnapshot()) { + if (definition != null && definition.matches(actors, Set.of("manualpeak"))) { + matching.add(definition); + } + } + + if (matching.isEmpty()) { + return null; + } + + NonManualPeakDefinitions.HeldItemMatch heldItemMatch = findHeldItemMatch(world, matching, player); + if (heldItemMatch != null) { + boolean useHeldItemProp = heldItemMatch.rule().propFromHeldItem(); + return buildSelection( + world, + actors, + heldItemMatch.definition(), + useHeldItemProp ? heldItemMatch.itemId() : null, + useHeldItemProp ? heldItemMatch.rule().heldItemPropSlot() : null, + heldItemMatch.itemId() + ); + } + + List fallback = new ArrayList<>(); + + for (AnimationDefinitions.Definition definition : matching) { + AnimationGameplayMetadata.ManualPeakRule rule = AnimationGameplayMetadata.manualPeakRule(definition.id()); + if (rule == null || rule.heldItems().isEmpty()) { + fallback.add(definition); + } + } + + AnimationDefinitions.Definition picked = pickWeighted(world, fallback); + return picked == null ? null : buildSelection(world, actors, picked, null, null, null); + } else { + return null; + } + } + + @Nullable + private static NonManualPeakDefinitions.HeldItemMatch findHeldItemMatch( + ServerWorld world, List definitions, ServerPlayerEntity player + ) { + List matches = new ArrayList<>(); + + for (AnimationDefinitions.Definition definition : definitions) { + AnimationGameplayMetadata.ManualPeakRule rule = AnimationGameplayMetadata.manualPeakRule(definition.id()); + if (rule != null && !rule.heldItems().isEmpty()) { + Identifier itemId = firstMatchingHeldItem(player, rule.heldItems()); + if (itemId != null) { + matches.add(new NonManualPeakDefinitions.HeldItemMatch(definition, rule, itemId)); + } + } + } + + if (matches.isEmpty()) { + return null; + } + + int highestPriority = matches.stream().mapToInt(match -> match.rule().priority()).max().orElse(Integer.MIN_VALUE); + List topPriority = matches.stream() + .filter(match -> match.rule().priority() == highestPriority) + .sorted(Comparator.comparing(match -> match.definition().id().toString())) + .toList(); + return WeightedPicker.pick(topPriority, match -> match.definition().weight(), world.getRandom().nextDouble()); + } + + @Nullable + private static Identifier firstMatchingHeldItem(ServerPlayerEntity player, Set accepted) { + Identifier main = itemId(player.getMainHandStack()); + if (main != null && accepted.contains(main)) { + return main; + } + + Identifier offhand = itemId(player.getOffHandStack()); + return offhand != null && accepted.contains(offhand) ? offhand : null; + } + + @Nullable + private static Identifier itemId(ItemStack stack) { + return stack != null && !stack.isEmpty() ? Registries.ITEM.getId(stack.getItem()) : null; + } + + @Nullable + private static NonManualPeakDefinitions.Selection buildSelection( + ServerWorld world, + List actors, + AnimationDefinitions.Definition definition, + @Nullable Identifier heldItemPropItemId, + @Nullable AnimationStageInfo.PropSlot heldItemPropSlot, + @Nullable Identifier matchedHeldItemId + ) { + List actorKeys = AnimationDefinitions.resolveActorKeys(definition, actors, world.getRandom()); + return actorKeys != null && actorKeys.size() == actors.size() + ? new NonManualPeakDefinitions.Selection(definition, List.copyOf(actorKeys), heldItemPropItemId, heldItemPropSlot, matchedHeldItemId) + : null; + } + + @Nullable + private static AnimationDefinitions.Definition pickWeighted(ServerWorld world, List definitions) { + if (world != null && definitions != null && !definitions.isEmpty()) { + definitions.sort(Comparator.comparing(definition -> definition.id().toString())); + return WeightedPicker.pick(definitions, AnimationDefinitions.Definition::weight, world.getRandom().nextDouble()); + } else { + return null; + } + } + + private record HeldItemMatch(AnimationDefinitions.Definition definition, AnimationGameplayMetadata.ManualPeakRule rule, Identifier itemId) { + } + + public record Selection( + AnimationDefinitions.Definition definition, + List actorKeys, + @Nullable Identifier heldItemPropItemId, + @Nullable AnimationStageInfo.PropSlot heldItemPropSlot, + @Nullable Identifier matchedHeldItemId + ) { + public List stages() { + if (this.heldItemPropItemId != null && this.heldItemPropSlot != null && !this.actorKeys.isEmpty()) { + String actorKey = this.actorKeys.getFirst(); + return this.definition.stages().stream().map(stage -> stage.withActorProp(actorKey, this.heldItemPropSlot, this.heldItemPropItemId)).toList(); + } else { + return this.definition.stages(); + } + } + } +} diff --git a/src/main/java/com/nonid/data/NonPeakStages.java b/src/main/java/com/nonid/data/NonPeakStages.java new file mode 100644 index 0000000..9e0a03a --- /dev/null +++ b/src/main/java/com/nonid/data/NonPeakStages.java @@ -0,0 +1,64 @@ +package com.nonid.data; + +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.internal.animation.util.AnimationIds; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import net.minecraft.util.Identifier; + +public final class NonPeakStages { + private static volatile Map syncedPeakStages = Map.of(); + + private NonPeakStages() { + } + + public static Integer getPeakStage(Identifier animationId) { + if (animationId == null) { + return null; + } + + Identifier base = baseAnimationId(animationId); + Integer synced = syncedPeakStages.get(base); + return synced != null ? synced : AnimationGameplayMetadata.peakStage(base); + } + + public static Map snapshotPeakStagesForSync() { + Map out = new LinkedHashMap<>(); + AnimationGameplayMetadata.peakStages().forEach((id, stage) -> { + if (id != null && stage != null && stage > 0) { + out.put(id.toString(), stage); + } + }); + return out; + } + + public static void applySyncedPeakStages(Map synced) { + if (synced != null && !synced.isEmpty()) { + Map parsed = new HashMap<>(); + synced.forEach((rawId, stage) -> { + if (rawId != null && stage != null && stage > 0) { + Identifier id = Identifier.tryParse(rawId); + if (id != null) { + parsed.put(baseAnimationId(id), stage); + } + } + }); + syncedPeakStages = Map.copyOf(parsed); + } else { + syncedPeakStages = Map.of(); + } + } + + public static void clearSyncedPeakStages() { + syncedPeakStages = Map.of(); + } + + public static Identifier baseAnimationId(Identifier id) { + return AnimationIds.baseId(id); + } + + public static Integer stageNumberFromId(Identifier stageId) { + return stageId == null ? null : AnimationIds.stageNumberOrDefault(stageId, 1); + } +} diff --git a/src/main/java/com/nonid/effect/EnergizedStatusEffect.java b/src/main/java/com/nonid/effect/EnergizedStatusEffect.java new file mode 100644 index 0000000..9c390c0 --- /dev/null +++ b/src/main/java/com/nonid/effect/EnergizedStatusEffect.java @@ -0,0 +1,10 @@ +package com.nonid.effect; + +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectCategory; + +public class EnergizedStatusEffect extends StatusEffect { + public EnergizedStatusEffect() { + super(StatusEffectCategory.HARMFUL, 15906891); + } +} diff --git a/src/main/java/com/nonid/effect/EnergyReliefStatusEffect.java b/src/main/java/com/nonid/effect/EnergyReliefStatusEffect.java new file mode 100644 index 0000000..13fdbd6 --- /dev/null +++ b/src/main/java/com/nonid/effect/EnergyReliefStatusEffect.java @@ -0,0 +1,86 @@ +package com.nonid.effect; + +import com.nonid.EnergyHolder; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectCategory; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.server.world.ServerWorld; + +public class EnergyReliefStatusEffect extends StatusEffect { + private static final double TOTAL_RATIO = 0.5; + private final Map applications = new HashMap<>(); + + public EnergyReliefStatusEffect() { + super(StatusEffectCategory.BENEFICIAL, 5219327); + } + + public boolean canApplyUpdateEffect(int duration, int amplifier) { + return true; + } + + public boolean applyUpdateEffect(ServerWorld world, LivingEntity entity, int amplifier) { + if (!(entity instanceof EnergyHolder holder)) { + return true; + } else { + StatusEffectInstance instance = null; + + for (StatusEffectInstance effect : entity.getStatusEffects()) { + if (effect.getEffectType().value() == this) { + instance = effect; + break; + } + } + + if (instance == null) { + return true; + } + + UUID id = entity.getUuid(); + int duration = instance.getDuration(); + EnergyReliefStatusEffect.ApplicationState state = this.applications.get(id); + if (state == null || state.instance != instance) { + state = new EnergyReliefStatusEffect.ApplicationState(instance, duration); + this.applications.put(id, state); + } else if (duration > state.initialDuration) { + state.initialDuration = duration; + } + + int max = holder.getMaxEnergy(); + if (max > 0) { + double total = max * 0.5 * (amplifier + 1); + double perTick = total / Math.max(1, state.initialDuration); + double accum = state.remainder + perTick; + int sub = (int)Math.floor(accum); + state.remainder = accum - sub; + if (sub > 0) { + holder.setEnergy(Math.max(0, holder.getEnergy() - sub)); + } + } + + if (duration <= 1) { + this.applications.remove(id); + } + + return true; + } + } + + public void clearRuntimeState() { + this.applications.clear(); + } + + private static final class ApplicationState { + private final StatusEffectInstance instance; + private int initialDuration; + private double remainder; + + private ApplicationState(StatusEffectInstance instance, int initialDuration) { + this.instance = instance; + this.initialDuration = initialDuration; + } + } +} diff --git a/src/main/java/com/nonid/effect/EnergyStatusEffect.java b/src/main/java/com/nonid/effect/EnergyStatusEffect.java new file mode 100644 index 0000000..43ae417 --- /dev/null +++ b/src/main/java/com/nonid/effect/EnergyStatusEffect.java @@ -0,0 +1,86 @@ +package com.nonid.effect; + +import com.nonid.EnergyHolder; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectCategory; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.server.world.ServerWorld; + +public class EnergyStatusEffect extends StatusEffect { + private static final double TOTAL_RATIO = 0.5; + private final Map applications = new HashMap<>(); + + public EnergyStatusEffect() { + super(StatusEffectCategory.HARMFUL, 10494192); + } + + public boolean canApplyUpdateEffect(int duration, int amplifier) { + return true; + } + + public boolean applyUpdateEffect(ServerWorld world, LivingEntity entity, int amplifier) { + if (!(entity instanceof EnergyHolder holder)) { + return true; + } else { + StatusEffectInstance instance = null; + + for (StatusEffectInstance effect : entity.getStatusEffects()) { + if (effect.getEffectType().value() == this) { + instance = effect; + break; + } + } + + if (instance == null) { + return true; + } + + UUID id = entity.getUuid(); + int duration = instance.getDuration(); + EnergyStatusEffect.ApplicationState state = this.applications.get(id); + if (state == null || state.instance != instance) { + state = new EnergyStatusEffect.ApplicationState(instance, duration); + this.applications.put(id, state); + } else if (duration > state.initialDuration) { + state.initialDuration = duration; + } + + int max = holder.getMaxEnergy(); + if (max > 0) { + double total = max * 0.5 * (amplifier + 1); + double perTick = total / Math.max(1, state.initialDuration); + double accum = state.remainder + perTick; + int add = (int)Math.floor(accum); + state.remainder = accum - add; + if (add > 0) { + holder.setEnergy(Math.min(max, holder.getEnergy() + add)); + } + } + + if (duration <= 1) { + this.applications.remove(id); + } + + return true; + } + } + + public void clearRuntimeState() { + this.applications.clear(); + } + + private static final class ApplicationState { + private final StatusEffectInstance instance; + private int initialDuration; + private double remainder; + + private ApplicationState(StatusEffectInstance instance, int initialDuration) { + this.instance = instance; + this.initialDuration = initialDuration; + } + } +} diff --git a/src/main/java/com/nonid/effect/FertilityIncreaserStatusEffect.java b/src/main/java/com/nonid/effect/FertilityIncreaserStatusEffect.java new file mode 100644 index 0000000..6662c1c --- /dev/null +++ b/src/main/java/com/nonid/effect/FertilityIncreaserStatusEffect.java @@ -0,0 +1,10 @@ +package com.nonid.effect; + +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectCategory; + +public final class FertilityIncreaserStatusEffect extends StatusEffect { + public FertilityIncreaserStatusEffect() { + super(StatusEffectCategory.BENEFICIAL, 16044143); + } +} diff --git a/src/main/java/com/nonid/effect/FilledStatusEffect.java b/src/main/java/com/nonid/effect/FilledStatusEffect.java new file mode 100644 index 0000000..3a82b1d --- /dev/null +++ b/src/main/java/com/nonid/effect/FilledStatusEffect.java @@ -0,0 +1,10 @@ +package com.nonid.effect; + +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectCategory; + +public class FilledStatusEffect extends StatusEffect { + public FilledStatusEffect() { + super(StatusEffectCategory.HARMFUL, 5913124); + } +} diff --git a/src/main/java/com/nonid/effect/NonStatusEffects.java b/src/main/java/com/nonid/effect/NonStatusEffects.java new file mode 100644 index 0000000..e8c7740 --- /dev/null +++ b/src/main/java/com/nonid/effect/NonStatusEffects.java @@ -0,0 +1,30 @@ +package com.nonid.effect; + +import com.nonid.NeedsOfNature; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; + +public final class NonStatusEffects { + public static final EnergyStatusEffect ENERGY = register("energy", new EnergyStatusEffect()); + public static final EnergyReliefStatusEffect ENERGY_RELIEF = register("energy_relief", new EnergyReliefStatusEffect()); + public static final StatusEffect FERTILITY_INCREASER = register("fertility_increaser", new FertilityIncreaserStatusEffect()); + public static final StatusEffect FILLED = register("filled", new FilledStatusEffect()); + public static final StatusEffect ENERGIZED = register("energized", new EnergizedStatusEffect()); + public static final StatusEffect PREGNANT = register("pregnant", new PregnantStatusEffect()); + + private NonStatusEffects() { + } + + public static void registerAll() { + } + + public static void clearRuntimeState() { + ENERGY.clearRuntimeState(); + ENERGY_RELIEF.clearRuntimeState(); + } + + private static T register(String path, T effect) { + return (T)Registry.register(Registries.STATUS_EFFECT, NeedsOfNature.id(path), effect); + } +} diff --git a/src/main/java/com/nonid/effect/PregnantStatusEffect.java b/src/main/java/com/nonid/effect/PregnantStatusEffect.java new file mode 100644 index 0000000..6e9e9dc --- /dev/null +++ b/src/main/java/com/nonid/effect/PregnantStatusEffect.java @@ -0,0 +1,10 @@ +package com.nonid.effect; + +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectCategory; + +public class PregnantStatusEffect extends StatusEffect { + public PregnantStatusEffect() { + super(StatusEffectCategory.HARMFUL, 13611389); + } +} diff --git a/src/main/java/com/nonid/entity/HorseLiquidCollectorEntity.java b/src/main/java/com/nonid/entity/HorseLiquidCollectorEntity.java new file mode 100644 index 0000000..a9f9874 --- /dev/null +++ b/src/main/java/com/nonid/entity/HorseLiquidCollectorEntity.java @@ -0,0 +1,335 @@ +package com.nonid.entity; + +import com.nonid.EnergyHolder; +import com.nonid.NeedsOfNature; +import com.nonid.particle.NonParticles; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.attribute.DefaultAttributeContainer.Builder; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.mob.PathAwareEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.storage.ReadView; +import net.minecraft.storage.WriteView; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.manager.AnimatableManager.ControllerRegistrar; +import software.bernie.geckolib.util.GeckoLibUtil; + +public class HorseLiquidCollectorEntity extends PathAwareEntity implements GeoEntity { + private static final TrackedData COLLECTOR_FULL = DataTracker.registerData(HorseLiquidCollectorEntity.class, TrackedDataHandlerRegistry.BOOLEAN); + private static final String LIQUID_TYPE_NBT = "NeedsOfNatureCollectorLiquidType"; + private static final int FIXED_ENERGY_VALUE = 70; + private static final int FULL_DRIP_INTERVAL_TICKS = 60; + private static final int NATURAL_REGEN_INTERVAL_TICKS = 80; + private static final float NATURAL_REGEN_AMOUNT = 1.0F; + private static final Vec3d FULL_DRIP_LOCAL_OFFSET = new Vec3d(0.0, 0.6, -0.55); + private AnimatableInstanceCache animatableCache; + @Nullable + private Identifier storedLiquidEntityTypeId; + private boolean storedMixedLiquid; + private boolean orientationLocked; + private float lockedYaw; + private float lockedPitch; + private long nextFullDripTick = Long.MIN_VALUE; + + public HorseLiquidCollectorEntity(EntityType entityType, World world) { + super(entityType, world); + } + + public static Builder createAttributes() { + return MobEntity.createMobAttributes().add(EntityAttributes.MAX_HEALTH, 2.0).add(EntityAttributes.MOVEMENT_SPEED, 0.0); + } + + protected void initGoals() { + } + + protected void initDataTracker(net.minecraft.entity.data.DataTracker.Builder builder) { + super.initDataTracker(builder); + builder.add(COLLECTOR_FULL, false); + } + + public void tickMovement() { + super.tickMovement(); + this.freezeStationaryState(); + } + + public void tick() { + super.tick(); + this.enforceFixedEnergy(); + if (!this.getEntityWorld().isClient() && this.deathTime > 0) { + this.discard(); + } else { + this.tickNaturalRegen(); + this.freezeStationaryState(); + this.tickFullDripParticles(); + } + } + + public boolean damage(ServerWorld world, DamageSource source, float amount) { + if (!this.isAlive() || this.isRemoved()) { + return false; + } + + if (!this.isInvulnerableTo(world, source) && !(amount <= 0.0F)) { + if (!(this.getHealth() - amount <= 0.0F)) { + return super.damage(world, source, amount); + } + + if (!(source.getAttacker() instanceof PlayerEntity player && player.isCreative())) { + this.dropStack(world, new ItemStack(NeedsOfNature.HORSE_LIQUID_COLLECTOR)); + } + + this.discard(); + return true; + } else { + return false; + } + } + + public boolean isPushable() { + return false; + } + + public void pushAwayFrom(Entity entity) { + } + + public void addVelocity(double deltaX, double deltaY, double deltaZ) { + } + + public void takeKnockback(double strength, double x, double z) { + } + + public boolean isPushedByFluids() { + return false; + } + + public boolean canImmediatelyDespawn(double distanceSquared) { + return false; + } + + public void writeData(WriteView view) { + super.writeData(view); + if (this.isCollectorFull()) { + if (this.storedMixedLiquid) { + view.putString("NeedsOfNatureCollectorLiquidType", "mixed"); + } else { + if (this.storedLiquidEntityTypeId != null) { + view.putString("NeedsOfNatureCollectorLiquidType", this.storedLiquidEntityTypeId.toString()); + } + } + } + } + + public void readData(ReadView view) { + super.readData(view); + String raw = view.getOptionalString("NeedsOfNatureCollectorLiquidType").orElse(""); + if (raw.isBlank()) { + this.setStoredEmpty(); + } else if ("mixed".equalsIgnoreCase(raw)) { + this.setStoredMixedLiquid(); + } else { + Identifier parsed = Identifier.tryParse(raw); + if (parsed == null) { + this.setStoredEmpty(); + } else { + this.setStoredEntityLiquid(parsed); + this.ensureOrientationLock(); + } + } + } + + public boolean isCollectorFull() { + return (Boolean)this.dataTracker.get(COLLECTOR_FULL); + } + + @Nullable + public Identifier getBottleLiquidEntityTypeId() { + return this.isCollectorFull() && !this.storedMixedLiquid ? this.storedLiquidEntityTypeId : null; + } + + public void clearStoredLiquid() { + this.setStoredEmpty(); + } + + public void setLockedOrientation(float yaw, float pitch) { + this.orientationLocked = true; + this.lockedYaw = yaw; + this.lockedPitch = pitch; + this.applyLockedOrientation(); + } + + public boolean absorbMixedLiquid() { + if (this.isCollectorFull() && this.storedMixedLiquid) { + return false; + } + + this.setStoredMixedLiquid(); + return true; + } + + public boolean absorbEntityLiquid(@Nullable Identifier donorTypeId) { + if (donorTypeId == null) { + return this.absorbMixedLiquid(); + } + + if (!this.isCollectorFull()) { + this.setStoredEntityLiquid(donorTypeId); + return true; + } + + if (this.storedMixedLiquid) { + return false; + } + + if (donorTypeId.equals(this.storedLiquidEntityTypeId)) { + return false; + } + + this.setStoredMixedLiquid(); + return true; + } + + private void setStoredEntityLiquid(Identifier entityTypeId) { + this.storedLiquidEntityTypeId = entityTypeId; + this.storedMixedLiquid = false; + this.dataTracker.set(COLLECTOR_FULL, true); + this.resetFullDripTimer(); + } + + private void setStoredMixedLiquid() { + this.storedLiquidEntityTypeId = null; + this.storedMixedLiquid = true; + this.dataTracker.set(COLLECTOR_FULL, true); + this.resetFullDripTimer(); + } + + private void setStoredEmpty() { + this.storedLiquidEntityTypeId = null; + this.storedMixedLiquid = false; + this.dataTracker.set(COLLECTOR_FULL, false); + this.nextFullDripTick = Long.MIN_VALUE; + } + + private void freezeStationaryState() { + this.setSilent(true); + this.setNoGravity(true); + this.setAiDisabled(true); + this.getNavigation().stop(); + this.setVelocity(Vec3d.ZERO); + this.fallDistance = 0.0; + this.applyLockedOrientation(); + } + + private void ensureOrientationLock() { + if (!this.orientationLocked) { + this.orientationLocked = true; + this.lockedYaw = this.getYaw(); + this.lockedPitch = this.getPitch(); + } + } + + private void applyLockedOrientation() { + this.ensureOrientationLock(); + this.setYaw(this.lockedYaw); + this.setPitch(this.lockedPitch); + this.setHeadYaw(this.lockedYaw); + this.setBodyYaw(this.lockedYaw); + } + + private void tickFullDripParticles() { + if (this.getEntityWorld() instanceof ServerWorld world) { + if (this.isCollectorFull() && this.isAlive() && !this.isRemoved()) { + long now = world.getTime(); + if (this.nextFullDripTick == Long.MIN_VALUE) { + this.nextFullDripTick = now + 60L; + } + + if (now >= this.nextFullDripTick) { + Vec3d spawnPos = this.resolveFullDripSpawnPos(); + world.spawnParticles( + NonParticles.liquidFalling(NeedsOfNature.resolveBottleLiquidTintRgb(this.getBottleLiquidEntityTypeId())), + spawnPos.x, + spawnPos.y, + spawnPos.z, + 1, + 0.0, + 0.0, + 0.0, + 0.0 + ); + this.nextFullDripTick = now + 60L; + } + } + } + } + + private void resetFullDripTimer() { + if (this.getEntityWorld() instanceof ServerWorld world) { + this.nextFullDripTick = world.getTime() + 60L; + } else { + this.nextFullDripTick = Long.MIN_VALUE; + } + } + + private void tickNaturalRegen() { + if (this.getEntityWorld() instanceof ServerWorld world) { + if (this.isAlive() && !this.isRemoved()) { + if (this.deathTime <= 0 && this.hurtTime <= 0) { + float maxHealth = this.getMaxHealth(); + if (!(this.getHealth() >= maxHealth)) { + if (world.getTime() % 80L == 0L) { + this.heal(1.0F); + } + } + } + } + } + } + + private void enforceFixedEnergy() { + if (this.getEntityWorld() instanceof ServerWorld) { + if (this instanceof EnergyHolder holder) { + if (holder.getEnergy() != 70) { + holder.setEnergy(70); + } + } + } + } + + private Vec3d resolveFullDripSpawnPos() { + double yawRad = Math.toRadians(this.getYaw()); + double sin = Math.sin(yawRad); + double cos = Math.cos(yawRad); + Vec3d forward = new Vec3d(-sin, 0.0, cos); + Vec3d right = new Vec3d(forward.z, 0.0, -forward.x); + return new Vec3d(this.getX(), this.getY(), this.getZ()) + .add(right.multiply(FULL_DRIP_LOCAL_OFFSET.x)) + .add(0.0, FULL_DRIP_LOCAL_OFFSET.y, 0.0) + .add(forward.multiply(FULL_DRIP_LOCAL_OFFSET.z)); + } + + public void registerControllers(@NotNull ControllerRegistrar controllers) { + } + + @NotNull + public AnimatableInstanceCache getAnimatableInstanceCache() { + if (this.animatableCache == null) { + this.animatableCache = GeckoLibUtil.createInstanceCache(this); + } + + return this.animatableCache; + } +} diff --git a/src/main/java/com/nonid/entity/PregnancyEggEntity.java b/src/main/java/com/nonid/entity/PregnancyEggEntity.java new file mode 100644 index 0000000..1ca9ccf --- /dev/null +++ b/src/main/java/com/nonid/entity/PregnancyEggEntity.java @@ -0,0 +1,539 @@ +package com.nonid.entity; + +import com.mojang.serialization.Codec; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import java.util.List; +import java.util.UUID; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityDimensions; +import net.minecraft.entity.EntityPose; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.SpawnReason; +import net.minecraft.entity.attribute.EntityAttributeInstance; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.attribute.DefaultAttributeContainer.Builder; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.mob.PathAwareEntity; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.storage.ReadView; +import net.minecraft.storage.WriteView; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.manager.AnimatableManager.ControllerRegistrar; +import software.bernie.geckolib.util.GeckoLibUtil; + +public class PregnancyEggEntity extends PathAwareEntity implements GeoEntity { + private static final TrackedData SOURCE_ENTITY_TYPE_ID = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.STRING); + private static final TrackedData TARGET_ENTITY_TYPE_ID = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.STRING); + private static final TrackedData HATCH_START_GLOBAL_TICK = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.LONG); + private static final TrackedData HATCH_AT_GLOBAL_TICK = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.LONG); + private static final TrackedData EGG_START_SIZE = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.FLOAT); + private static final TrackedData EGG_END_SIZE = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.FLOAT); + private static final TrackedData EGG_TEXTURE_ID = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.STRING); + private static final TrackedData EGG_HEALTH = DataTracker.registerData(PregnancyEggEntity.class, TrackedDataHandlerRegistry.FLOAT); + private static final String NBT_SOURCE_ENTITY_TYPE = "NeedsOfNatureEggSourceType"; + private static final String NBT_TARGET_ENTITY_TYPE = "NeedsOfNatureEggTargetType"; + private static final String NBT_HATCH_GLOBAL_TICK = "NeedsOfNatureEggHatchGlobalTick"; + private static final String NBT_HATCH_START_GLOBAL_TICK = "NeedsOfNatureEggHatchStartGlobalTick"; + private static final String NBT_OWNER_UUID = "NeedsOfNatureEggOwnerUuid"; + private static final String NBT_VARIANT_DATA = "NeedsOfNatureEggVariantData"; + private static final String NBT_EGG_START_SIZE = "NeedsOfNatureEggStartSize"; + private static final String NBT_EGG_END_SIZE = "NeedsOfNatureEggEndSize"; + private static final String NBT_EGG_TEXTURE_ID = "NeedsOfNatureEggTexture"; + private static final String NBT_EGG_HEALTH = "NeedsOfNatureEggHealth"; + private static final float BASE_DIMENSION = 0.3F; + private AnimatableInstanceCache animatableCache; + private float lastDimensionsScale = -1.0F; + @Nullable + private UUID ownerUuid; + @Nullable + private NeedsOfNature.PregnancyVariantData variantData; + + public PregnancyEggEntity(EntityType entityType, World world) { + super(entityType, world); + } + + public static Builder createAttributes() { + return MobEntity.createMobAttributes().add(EntityAttributes.MAX_HEALTH, 2.0).add(EntityAttributes.MOVEMENT_SPEED, 0.0); + } + + protected void initGoals() { + } + + protected void initDataTracker(net.minecraft.entity.data.DataTracker.Builder builder) { + super.initDataTracker(builder); + builder.add(SOURCE_ENTITY_TYPE_ID, ""); + builder.add(TARGET_ENTITY_TYPE_ID, ""); + builder.add(HATCH_START_GLOBAL_TICK, 0L); + builder.add(HATCH_AT_GLOBAL_TICK, 0L); + builder.add(EGG_START_SIZE, 0.5F); + builder.add(EGG_END_SIZE, 1.0F); + builder.add(EGG_TEXTURE_ID, ""); + builder.add(EGG_HEALTH, 2.0F); + } + + public void configure( + @Nullable UUID ownerPlayerUuid, + @Nullable Identifier sourceEntityTypeId, + @Nullable Identifier targetEntityTypeId, + long hatchAtTick, + @Nullable NeedsOfNature.PregnancyVariantData variantData, + @Nullable NonConfig.EggProfile eggProfile + ) { + this.ownerUuid = ownerPlayerUuid; + this.variantData = NeedsOfNature.normalizePregnancyVariantData(variantData); + this.setSourceEntityTypeId(sourceEntityTypeId); + this.setTargetEntityTypeId(targetEntityTypeId); + this.applyEggProfile(eggProfile); + long startTick = resolveCurrentTick(this.getEntityWorld()); + long endTick = Math.max(0L, hatchAtTick); + if (endTick <= startTick) { + endTick = startTick + 1L; + } + + this.setHatchStartGlobalTick(startTick); + this.setHatchAtGlobalTick(endTick); + this.refreshEggDimensionsIfNeeded(true); + } + + @Nullable + public Identifier getSourceEntityTypeId() { + String raw = (String)this.dataTracker.get(SOURCE_ENTITY_TYPE_ID); + return raw != null && !raw.isBlank() ? Identifier.tryParse(raw) : null; + } + + public void setSourceEntityTypeId(@Nullable Identifier entityTypeId) { + this.dataTracker.set(SOURCE_ENTITY_TYPE_ID, entityTypeId == null ? "" : entityTypeId.toString()); + } + + @Nullable + public Identifier getTargetEntityTypeId() { + String raw = (String)this.dataTracker.get(TARGET_ENTITY_TYPE_ID); + return raw != null && !raw.isBlank() ? Identifier.tryParse(raw) : null; + } + + public void setTargetEntityTypeId(@Nullable Identifier entityTypeId) { + this.dataTracker.set(TARGET_ENTITY_TYPE_ID, entityTypeId == null ? "" : entityTypeId.toString()); + } + + public long getHatchStartGlobalTick() { + return Math.max(0L, (Long)this.dataTracker.get(HATCH_START_GLOBAL_TICK)); + } + + public void setHatchStartGlobalTick(long tick) { + this.dataTracker.set(HATCH_START_GLOBAL_TICK, Math.max(0L, tick)); + } + + public long getHatchAtGlobalTick() { + return Math.max(0L, (Long)this.dataTracker.get(HATCH_AT_GLOBAL_TICK)); + } + + public void setHatchAtGlobalTick(long tick) { + this.dataTracker.set(HATCH_AT_GLOBAL_TICK, Math.max(0L, tick)); + } + + public float getEggStartSize() { + return clampEggSize((Float)this.dataTracker.get(EGG_START_SIZE)); + } + + public void setEggStartSize(float size) { + this.dataTracker.set(EGG_START_SIZE, clampEggSize(size)); + this.refreshEggDimensionsIfNeeded(true); + } + + public float getEggEndSize() { + return clampEggSize((Float)this.dataTracker.get(EGG_END_SIZE)); + } + + public void setEggEndSize(float size) { + this.dataTracker.set(EGG_END_SIZE, clampEggSize(size)); + this.refreshEggDimensionsIfNeeded(true); + } + + @Nullable + public Identifier getEggTextureId() { + String raw = (String)this.dataTracker.get(EGG_TEXTURE_ID); + if (raw != null && !raw.isBlank()) { + Identifier parsed = Identifier.tryParse(raw); + return parsed != null && !parsed.getPath().isBlank() ? parsed : null; + } else { + return null; + } + } + + public void setEggTextureId(@Nullable Identifier textureId) { + this.dataTracker.set(EGG_TEXTURE_ID, textureId != null && !textureId.getPath().isBlank() ? textureId.toString() : ""); + } + + public float getEggHealth() { + return clampEggHealth((Float)this.dataTracker.get(EGG_HEALTH)); + } + + public void setEggHealth(float health) { + this.setEggHealth(health, false); + } + + private void setEggHealth(float health, boolean healToMax) { + float clamped = clampEggHealth(health); + this.dataTracker.set(EGG_HEALTH, clamped); + this.applyEggHealth(clamped, healToMax); + } + + public void tickMovement() { + super.tickMovement(); + this.freezeStationaryState(); + this.refreshEggDimensionsIfNeeded(false); + } + + public void tick() { + super.tick(); + this.freezeStationaryState(); + this.refreshEggDimensionsIfNeeded(false); + this.pushAwayFromNearbyEggs(); + if (this.getEntityWorld() instanceof ServerWorld serverWorld) { + Identifier targetTypeId = this.getTargetEntityTypeId(); + if (targetTypeId == null) { + this.discard(); + } else if (resolveGlobalTick(serverWorld) >= this.getHatchAtGlobalTick()) { + this.hatch(serverWorld, targetTypeId); + } + } + } + + public boolean isPushable() { + return true; + } + + public void pushAwayFrom(Entity entity) { + if (entity instanceof PregnancyEggEntity) { + super.pushAwayFrom(entity); + } + } + + public void addVelocity(double deltaX, double deltaY, double deltaZ) { + super.addVelocity(deltaX, deltaY, deltaZ); + } + + public void takeKnockback(double strength, double x, double z) { + } + + public boolean isPushedByFluids() { + return false; + } + + public boolean canImmediatelyDespawn(double distanceSquared) { + return false; + } + + public void writeData(WriteView view) { + super.writeData(view); + Identifier source = this.getSourceEntityTypeId(); + if (source != null) { + view.putString("NeedsOfNatureEggSourceType", source.toString()); + } + + Identifier target = this.getTargetEntityTypeId(); + if (target != null) { + view.putString("NeedsOfNatureEggTargetType", target.toString()); + } + + view.put("NeedsOfNatureEggHatchStartGlobalTick", Codec.LONG, this.getHatchStartGlobalTick()); + view.put("NeedsOfNatureEggHatchGlobalTick", Codec.LONG, this.getHatchAtGlobalTick()); + if (this.ownerUuid != null) { + view.putString("NeedsOfNatureEggOwnerUuid", this.ownerUuid.toString()); + } + + String variantJson = NeedsOfNature.encodePregnancyVariantData(this.variantData); + if (variantJson != null) { + view.putString("NeedsOfNatureEggVariantData", variantJson); + } + + view.putFloat("NeedsOfNatureEggStartSize", this.getEggStartSize()); + view.putFloat("NeedsOfNatureEggEndSize", this.getEggEndSize()); + Identifier textureId = this.getEggTextureId(); + if (textureId != null) { + view.putString("NeedsOfNatureEggTexture", textureId.toString()); + } + + view.putFloat("NeedsOfNatureEggHealth", this.getEggHealth()); + } + + public void readData(ReadView view) { + super.readData(view); + String sourceRaw = view.getOptionalString("NeedsOfNatureEggSourceType").orElse(""); + this.setSourceEntityTypeId(Identifier.tryParse(sourceRaw)); + String targetRaw = view.getOptionalString("NeedsOfNatureEggTargetType").orElse(""); + this.setTargetEntityTypeId(Identifier.tryParse(targetRaw)); + long start = Math.max(0L, view.read("NeedsOfNatureEggHatchStartGlobalTick", Codec.LONG).orElse(0L)); + long end = Math.max(0L, view.read("NeedsOfNatureEggHatchGlobalTick", Codec.LONG).orElse(0L)); + if (end > 0L && start >= end) { + start = Math.max(0L, end - 1L); + } + + this.setHatchStartGlobalTick(start); + this.setHatchAtGlobalTick(end); + String ownerRaw = view.getOptionalString("NeedsOfNatureEggOwnerUuid").orElse(""); + this.ownerUuid = parseUuid(ownerRaw); + this.variantData = NeedsOfNature.decodePregnancyVariantData(view.getOptionalString("NeedsOfNatureEggVariantData").orElse("")); + this.setEggStartSize(view.read("NeedsOfNatureEggStartSize", Codec.FLOAT).orElse(0.5F)); + this.setEggEndSize(view.read("NeedsOfNatureEggEndSize", Codec.FLOAT).orElse(1.0F)); + this.setEggTextureId(Identifier.tryParse(view.getOptionalString("NeedsOfNatureEggTexture").orElse(""))); + this.setEggHealth(view.read("NeedsOfNatureEggHealth", Codec.FLOAT).orElse(2.0F)); + this.refreshEggDimensionsIfNeeded(true); + } + + @Nullable + private static UUID parseUuid(@Nullable String raw) { + if (raw != null && !raw.isBlank()) { + try { + return UUID.fromString(raw); + } catch (IllegalArgumentException e) { + return null; + } + } else { + return null; + } + } + + private void hatch(ServerWorld world, Identifier targetTypeId) { + if (!Registries.ENTITY_TYPE.containsId(targetTypeId)) { + this.discard(); + } else { + EntityType entityType = (EntityType)Registries.ENTITY_TYPE.get(targetTypeId); + Entity entity = entityType.create(world, SpawnReason.BREEDING); + if (entity == null) { + this.discard(); + } else { + entity.refreshPositionAndAngles(this.getX(), this.getY(), this.getZ(), this.getYaw(), this.getPitch()); + if (entity instanceof MobEntity mob) { + mob.initialize(world, world.getLocalDifficulty(mob.getBlockPos()), SpawnReason.BREEDING, null); + NeedsOfNature.applyPregnancyVariantDataForEgg(world, mob, this.variantData); + NeedsOfNature.applyPregnancyMobFlagsForEgg(mob, this.resolveOwnerPlayer(world)); + } + + if (world.spawnEntity(entity)) { + this.playHatchEffects(world); + } + + this.discard(); + } + } + } + + public float getGrowthScale(float partialTick) { + long startTick = this.getHatchStartGlobalTick(); + long endTick = this.getHatchAtGlobalTick(); + float startSize = this.getEggStartSize(); + float endSize = this.getEggEndSize(); + if (endTick <= startTick) { + return endSize; + } + + double now = (float)resolveCurrentTick(this.getEntityWorld()) + Math.max(0.0F, partialTick); + double progress = (now - startTick) / (endTick - startTick); + progress = MathHelper.clamp(progress, 0.0, 1.0); + return (float)(startSize + (endSize - startSize) * progress); + } + + protected EntityDimensions getBaseDimensions(EntityPose pose) { + float scale = this.getGrowthScale(0.0F); + return EntityDimensions.fixed(0.3F * scale, 0.3F * scale); + } + + public void onTrackedDataSet(TrackedData data) { + super.onTrackedDataSet(data); + if (EGG_START_SIZE.equals(data) || EGG_END_SIZE.equals(data)) { + this.refreshEggDimensionsIfNeeded(true); + } + + if (EGG_HEALTH.equals(data)) { + this.applyEggHealth(this.getEggHealth(), false); + } + } + + private void applyEggProfile(@Nullable NonConfig.EggProfile eggProfile) { + NonConfig.EggProfile safe = eggProfile == null ? NonConfig.EggProfile.defaults() : eggProfile.resolvedOver(NonConfig.EggProfile.defaults()); + Float startSize = safe.startSize(); + Float endSize = safe.endSize(); + Float health = safe.health(); + this.setEggStartSize(startSize == null ? 0.5F : startSize); + this.setEggEndSize(endSize == null ? 1.0F : endSize); + String texture = safe.texture(); + this.setEggTextureId(texture == null ? null : Identifier.tryParse(texture)); + this.setEggHealth(health == null ? 2.0F : health, true); + } + + private void applyEggHealth(float health, boolean healToMax) { + EntityAttributeInstance attribute = this.getAttributeInstance(EntityAttributes.MAX_HEALTH); + if (attribute != null && Math.abs(attribute.getBaseValue() - health) > 0.001) { + attribute.setBaseValue(health); + } + + if (healToMax || this.getHealth() > health || this.getHealth() <= 0.0F) { + this.setHealth(health); + } + } + + private void refreshEggDimensionsIfNeeded(boolean force) { + float scale = this.getGrowthScale(0.0F); + if (force || !(Math.abs(scale - this.lastDimensionsScale) < 0.01F)) { + this.lastDimensionsScale = scale; + this.calculateDimensions(); + } + } + + private static float clampEggSize(float value) { + return !Float.isFinite(value) ? 1.0F : MathHelper.clamp(value, 0.05F, 4.0F); + } + + private static float clampEggHealth(float value) { + return !Float.isFinite(value) ? 2.0F : MathHelper.clamp(value, 0.5F, 100.0F); + } + + private void playHatchEffects(ServerWorld world) { + if (world != null) { + world.playSound( + null, + this.getX(), + this.getY(), + this.getZ(), + SoundEvents.ENTITY_TURTLE_EGG_HATCH, + SoundCategory.NEUTRAL, + 0.9F, + 0.95F + world.getRandom().nextFloat() * 0.1F + ); + world.spawnParticles(ParticleTypes.POOF, this.getX(), this.getY() + 0.25, this.getZ(), 12, 0.25, 0.2, 0.25, 0.02); + world.spawnParticles(ParticleTypes.CLOUD, this.getX(), this.getY() + 0.2, this.getZ(), 8, 0.18, 0.12, 0.18, 0.01); + } + } + + @Nullable + private ServerPlayerEntity resolveOwnerPlayer(ServerWorld world) { + return this.ownerUuid != null && world.getServer() != null ? world.getServer().getPlayerManager().getPlayer(this.ownerUuid) : null; + } + + private static long resolveGlobalTick(ServerWorld world) { + if (world != null && world.getServer() != null) { + ServerWorld overworld = world.getServer().getOverworld(); + return overworld == null ? world.getTime() : overworld.getTime(); + } else { + return 0L; + } + } + + private static long resolveCurrentTick(@Nullable World world) { + if (world == null) { + return 0L; + } else { + return world instanceof ServerWorld serverWorld ? resolveGlobalTick(serverWorld) : world.getTime(); + } + } + + private void freezeStationaryState() { + this.setSilent(true); + this.setNoGravity(true); + this.setAiDisabled(true); + this.getNavigation().stop(); + Vec3d velocity = this.getVelocity(); + double x = MathHelper.clamp(velocity.x * 0.65, -0.08, 0.08); + double z = MathHelper.clamp(velocity.z * 0.65, -0.08, 0.08); + if (Math.abs(x) < 0.001) { + x = 0.0; + } + + if (Math.abs(z) < 0.001) { + z = 0.0; + } + + this.setVelocity(x, 0.0, z); + this.fallDistance = 0.0; + } + + private void pushAwayFromNearbyEggs() { + if (!this.isRemoved()) { + Box searchBox = this.getBoundingBox().expand(0.05, 0.0, 0.05); + List nearbyEggs = this.getEntityWorld() + .getEntitiesByClass(PregnancyEggEntity.class, searchBox, egg -> egg != this && !egg.isRemoved()); + if (!nearbyEggs.isEmpty()) { + double pushX = 0.0; + double pushZ = 0.0; + double ownRadius = Math.max(0.1, this.getBoundingBox().getLengthX() * 0.5); + + for (PregnancyEggEntity other : nearbyEggs) { + double otherRadius = Math.max(0.1, other.getBoundingBox().getLengthX() * 0.5); + double minDistance = ownRadius + otherRadius + 0.04; + double dx = this.getX() - other.getX(); + double dz = this.getZ() - other.getZ(); + double distanceSq = dx * dx + dz * dz; + if (!(distanceSq >= minDistance * minDistance)) { + if (distanceSq < 1.0E-6) { + double angle = this.deterministicSeparationAngle(); + dx = Math.cos(angle); + dz = Math.sin(angle); + distanceSq = 1.0; + } + + double distance = Math.sqrt(distanceSq); + if (!(distance <= 0.0)) { + double strength = Math.min(0.08, (minDistance - distance) * 0.15); + pushX += dx / distance * strength; + pushZ += dz / distance * strength; + } + } + } + + if (!(Math.abs(pushX) < 1.0E-5) || !(Math.abs(pushZ) < 1.0E-5)) { + pushX = MathHelper.clamp(pushX, -0.08, 0.08); + pushZ = MathHelper.clamp(pushZ, -0.08, 0.08); + this.updatePosition(this.getX() + pushX, this.getY(), this.getZ() + pushZ); + this.setVelocity(pushX * 0.25, 0.0, pushZ * 0.25); + this.applySettledPushRotation(pushX, pushZ); + } + } + } + } + + private double deterministicSeparationAngle() { + long bits = this.getUuid().getMostSignificantBits() ^ this.getUuid().getLeastSignificantBits(); + double unit = (bits & 65535L) / 65536.0; + return unit * Math.PI * 2.0; + } + + private void applySettledPushRotation(double pushX, double pushZ) { + if (!(Math.abs(pushX) < 1.0E-5) || !(Math.abs(pushZ) < 1.0E-5)) { + float yaw = (float)(Math.atan2(pushZ, pushX) * (180.0 / Math.PI)) - 90.0F; + this.setYaw(yaw); + this.setHeadYaw(yaw); + this.setBodyYaw(yaw); + this.lastYaw = yaw; + } + } + + public void registerControllers(@NotNull ControllerRegistrar controllers) { + } + + @NotNull + public AnimatableInstanceCache getAnimatableInstanceCache() { + if (this.animatableCache == null) { + this.animatableCache = GeckoLibUtil.createInstanceCache(this); + } + + return this.animatableCache; + } +} diff --git a/src/main/java/com/nonid/integration/FloatValueSliderWidget.java b/src/main/java/com/nonid/integration/FloatValueSliderWidget.java new file mode 100644 index 0000000..65367e2 --- /dev/null +++ b/src/main/java/com/nonid/integration/FloatValueSliderWidget.java @@ -0,0 +1,87 @@ +package com.nonid.integration; + +import java.util.Locale; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleFunction; +import net.minecraft.client.gui.widget.SliderWidget; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +final class FloatValueSliderWidget extends SliderWidget { + private final double min; + private final double max; + private final double step; + private final DoubleConsumer listener; + private final DoubleFunction formatter; + private float floatValue; + private double warningAbove = Double.POSITIVE_INFINITY; + + FloatValueSliderWidget(int x, int y, int width, int height, float initialValue, float min, float max, float step, DoubleConsumer listener) { + this(x, y, width, height, initialValue, min, max, step, listener, FloatValueSliderWidget::format); + } + + FloatValueSliderWidget( + int x, int y, int width, int height, float initialValue, float min, float max, float step, DoubleConsumer listener, DoubleFunction formatter + ) { + super(x, y, width, height, Text.literal(""), toSliderValue(initialValue, min, max)); + this.min = min; + this.max = max; + this.step = Math.max(1.0E-4, step); + this.listener = listener; + this.formatter = formatter == null ? FloatValueSliderWidget::format : formatter; + this.floatValue = this.clampAndRound(initialValue); + this.value = toSliderValue(this.floatValue, min, max); + this.updateMessage(); + } + + float getFloatValue() { + return this.floatValue; + } + + void setFloatValue(float value) { + this.floatValue = this.clampAndRound(value); + this.value = toSliderValue(this.floatValue, this.min, this.max); + this.updateMessage(); + if (this.listener != null) { + this.listener.accept(this.floatValue); + } + } + + void setWarningAbove(float value) { + this.warningAbove = value; + this.updateMessage(); + } + + protected void updateMessage() { + this.floatValue = this.clampAndRound((float)(this.min + this.value * (this.max - this.min))); + MutableText message = Text.literal(this.formatter.apply(this.floatValue)); + if (this.floatValue > this.warningAbove) { + message.formatted(Formatting.YELLOW); + } + + this.setMessage(message); + } + + protected void applyValue() { + this.floatValue = this.clampAndRound((float)(this.min + this.value * (this.max - this.min))); + this.value = toSliderValue(this.floatValue, this.min, this.max); + if (this.listener != null) { + this.listener.accept(this.floatValue); + } + } + + private float clampAndRound(float input) { + double clamped = Math.max(this.min, Math.min(this.max, input)); + double rounded = Math.round(clamped / this.step) * this.step; + return (float)Math.max(this.min, Math.min(this.max, rounded)); + } + + private static double toSliderValue(double value, double min, double max) { + return max <= min ? 0.0 : Math.max(0.0, Math.min(1.0, (value - min) / (max - min))); + } + + private static String format(double value) { + return String.format(Locale.ROOT, "%.3f", value).replaceAll("0+$", "").replaceAll("\\.$", ""); + } +} diff --git a/src/main/java/com/nonid/integration/NonCreditsButtonHooks.java b/src/main/java/com/nonid/integration/NonCreditsButtonHooks.java new file mode 100644 index 0000000..8f57aab --- /dev/null +++ b/src/main/java/com/nonid/integration/NonCreditsButtonHooks.java @@ -0,0 +1,41 @@ +package com.nonid.integration; + +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents.AfterInit; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public final class NonCreditsButtonHooks { + private static final int BUTTON_WIDTH = 52; + private static final int EDGE_MARGIN = 8; + private static boolean registered; + + private NonCreditsButtonHooks() { + } + + public static void register() { + if (!registered) { + registered = true; + ScreenEvents.AFTER_INIT + .register( + (AfterInit)(client, screen, scaledWidth, scaledHeight) -> { + if (isNoNSettingsScreen(screen)) { + NonModMenuScreens.FlatTextButton creditsButton = new NonModMenuScreens.FlatTextButton( + 52, Text.translatable("config.needsofnature.category.credits"), button -> client.setScreen(new NonCreditsScreen(screen)) + ); + creditsButton.setX(scaledWidth - 52 - 8); + creditsButton.setY(scaledHeight - creditsButton.getHeight() - 8); + creditsButton.setAlpha(0.7F); + NonModMenuScreens.setTooltip(creditsButton, "config.needsofnature.tooltip.category.credits"); + Screens.getButtons(screen).add(creditsButton); + } + } + ); + } + } + + private static boolean isNoNSettingsScreen(Screen screen) { + return screen != null && screen.getClass().getPackageName().equals(NonCreditsButtonHooks.class.getPackageName()) && !(screen instanceof NonCreditsScreen); + } +} diff --git a/src/main/java/com/nonid/integration/NonCreditsData.java b/src/main/java/com/nonid/integration/NonCreditsData.java new file mode 100644 index 0000000..ef6368a --- /dev/null +++ b/src/main/java/com/nonid/integration/NonCreditsData.java @@ -0,0 +1,197 @@ +package com.nonid.integration; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.nonid.NeedsOfNature; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +final class NonCreditsData { + private static final Gson GSON = new Gson(); + private static final String CREDITS_RESOURCE = "/assets/needsofnature/credits.json"; + + private NonCreditsData() { + } + + static NonCreditsData.Credits load() { + try (InputStream stream = NonCreditsData.class.getResourceAsStream("/assets/needsofnature/credits.json")) { + if (stream == null) { + NeedsOfNature.LOGGER.warn("[NoN] Bundled credits file is missing: {}", "/assets/needsofnature/credits.json"); + return fallback(); + } else { + NonCreditsData.RawCredits raw = (NonCreditsData.RawCredits)GSON.fromJson( + new InputStreamReader(stream, StandardCharsets.UTF_8), NonCreditsData.RawCredits.class + ); + NonCreditsData.Credits credits = normalize(raw); + if (credits.notice() == null && credits.sections().isEmpty() && credits.resources().isEmpty() && credits.projectLinks().isEmpty()) { + NeedsOfNature.LOGGER.warn("[NoN] Bundled credits file contains no valid entries."); + return fallback(); + } else { + return credits; + } + } + } catch (Exception e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to read bundled credits; using fallback credits.", e); + return fallback(); + } + } + + private static NonCreditsData.Credits normalize(NonCreditsData.RawCredits raw) { + if (raw == null) { + return new NonCreditsData.Credits(null, List.of(), List.of(), List.of()); + } + + List sections = new ArrayList<>(); + if (raw.sections != null) { + for (NonCreditsData.RawSection rawSection : raw.sections) { + if (rawSection != null && !blank(rawSection.title)) { + List contributors = new ArrayList<>(); + if (rawSection.contributors != null) { + for (NonCreditsData.RawContributor rawContributor : rawSection.contributors) { + if (rawContributor != null && !blank(rawContributor.name)) { + contributors.add( + new NonCreditsData.Contributor(rawContributor.name.trim(), cleanStrings(rawContributor.roles), clean(rawContributor.contribution)) + ); + } + } + } + + if (!contributors.isEmpty()) { + sections.add(new NonCreditsData.Section(rawSection.title.trim(), List.copyOf(contributors))); + } + } + } + } + + List resources = new ArrayList<>(); + if (raw.resources != null) { + for (NonCreditsData.RawResource rawResource : raw.resources) { + if (rawResource != null && !blank(rawResource.title)) { + resources.add( + new NonCreditsData.ResourceCredit( + clean(rawResource.category), + rawResource.title.trim(), + cleanStrings(rawResource.assets), + clean(rawResource.creator), + clean(rawResource.source), + validWebUrl(rawResource.sourceUrl), + clean(rawResource.license), + validWebUrl(rawResource.licenseUrl), + rawResource.modified + ) + ); + } + } + } + + List projectLinks = new ArrayList<>(); + if (raw.projectLinks != null) { + for (NonCreditsData.RawLink rawLink : raw.projectLinks) { + if (rawLink != null && !blank(rawLink.label)) { + String url = validWebUrl(rawLink.url); + if (url != null) { + projectLinks.add(new NonCreditsData.ProjectLink(rawLink.label.trim(), url)); + } + } + } + } + + return new NonCreditsData.Credits(clean(raw.notice), List.copyOf(sections), List.copyOf(resources), List.copyOf(projectLinks)); + } + + private static String validWebUrl(String value) { + if (blank(value)) { + return null; + } + + try { + URI uri = URI.create(value.trim()); + String scheme = uri.getScheme(); + if (uri.getHost() != null && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) { + return uri.toString(); + } + } catch (IllegalArgumentException var3) { + } + + NeedsOfNature.LOGGER.warn("[NoN] Ignoring invalid credits URL: {}", value); + return null; + } + + private static List cleanStrings(List values) { + return values != null && !values.isEmpty() ? values.stream().filter(value -> !blank(value)).map(String::trim).toList() : List.of(); + } + + private static String clean(String value) { + return blank(value) ? null : value.trim(); + } + + private static boolean blank(String value) { + return value == null || value.isBlank(); + } + + private static NonCreditsData.Credits fallback() { + NonCreditsData.Contributor contributor = new NonCreditsData.Contributor("L1Z0", List.of("Project Lead", "Programming"), null); + return new NonCreditsData.Credits(null, List.of(new NonCreditsData.Section("Development", List.of(contributor))), List.of(), List.of()); + } + + record Contributor(String name, List roles, String contribution) { + } + + record Credits( + String notice, List sections, List resources, List projectLinks + ) { + } + + record ProjectLink(String label, String url) { + } + + private static final class RawContributor { + private String name; + private List roles; + private String contribution; + } + + private static final class RawCredits { + private String notice; + private List sections; + private List resources; + @SerializedName("project_links") + private List projectLinks; + } + + private static final class RawLink { + private String label; + private String url; + } + + private static final class RawResource { + private String category; + private String title; + private List assets; + private String creator; + private String source; + @SerializedName("source_url") + private String sourceUrl; + private String license; + @SerializedName("license_url") + private String licenseUrl; + private boolean modified; + } + + private static final class RawSection { + private String title; + private List contributors; + } + + record ResourceCredit( + String category, String title, List assets, String creator, String source, String sourceUrl, String license, String licenseUrl, boolean modified + ) { + } + + record Section(String title, List contributors) { + } +} diff --git a/src/main/java/com/nonid/integration/NonCreditsScreen.java b/src/main/java/com/nonid/integration/NonCreditsScreen.java new file mode 100644 index 0000000..b85a6db --- /dev/null +++ b/src/main/java/com/nonid/integration/NonCreditsScreen.java @@ -0,0 +1,402 @@ +package com.nonid.integration; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.ConfirmLinkScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.gui.widget.ElementListWidget.Entry; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; + +final class NonCreditsScreen extends Screen { + private final Screen parent; + private final NonCreditsData.Credits credits; + + NonCreditsScreen(Screen parent) { + super(Text.translatable("config.needsofnature.credits_title")); + this.parent = parent; + this.credits = NonCreditsData.load(); + } + + protected void init() { + int listTop = 42; + int listHeight = Math.max(0, this.height - listTop - 40); + NonCreditsScreen.CreditsList creditsList = new NonCreditsScreen.CreditsList( + this.client, this.width, listHeight, listTop, this.textRenderer, url -> ConfirmLinkScreen.open(this, url) + ); + creditsList.setCredits(this.credits); + this.addDrawableChild(creditsList); + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(this.width / 2 - 100, this.height - 28, 200, 20) + .build() + ); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 10, -1); + context.drawCenteredTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.credits_intro"), this.width / 2, 24, -4671304); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private record ContributorRow(NonCreditsData.Contributor contributor) implements NonCreditsScreen.CreditsRow { + } + + private static final class CreditsList extends ElementListWidget { + private final TextRenderer textRenderer; + private final Consumer linkOpener; + + private CreditsList(MinecraftClient client, int width, int height, int top, TextRenderer textRenderer, Consumer linkOpener) { + super(client, width, height, top, 28); + this.textRenderer = textRenderer; + this.linkOpener = linkOpener; + this.centerListVertically = false; + } + + private void setCredits(NonCreditsData.Credits credits) { + this.clearEntries(); + int contentWidth = Math.max(100, this.getRowWidth() - 16); + if (credits.notice() != null) { + this.addRow(new NonCreditsScreen.NoticeRow(credits.notice()), contentWidth); + } + + for (NonCreditsData.Section section : credits.sections()) { + this.addRow(new NonCreditsScreen.SectionRow(Text.literal(section.title())), contentWidth); + + for (NonCreditsData.Contributor contributor : section.contributors()) { + this.addRow(new NonCreditsScreen.ContributorRow(contributor), contentWidth); + } + } + + if (!credits.resources().isEmpty()) { + this.addRow(new NonCreditsScreen.SectionRow(Text.translatable("config.needsofnature.credits.resource_credits")), contentWidth); + + for (NonCreditsData.ResourceCredit resource : credits.resources()) { + this.addRow(new NonCreditsScreen.ResourceRow(resource), contentWidth); + } + } + + if (!credits.projectLinks().isEmpty()) { + this.addRow(new NonCreditsScreen.SectionRow(Text.translatable("config.needsofnature.credits.project_links")), contentWidth); + this.addRow(new NonCreditsScreen.LinksRow(credits.projectLinks()), contentWidth); + } + } + + private void addRow(NonCreditsScreen.CreditsRow row, int contentWidth) { + NonCreditsScreen.CreditsList.RowEntry entry = new NonCreditsScreen.CreditsList.RowEntry(row, this.textRenderer, contentWidth, this.linkOpener); + this.addEntry(entry, entry.rowHeight()); + } + + public int getRowWidth() { + return Math.min(520, Math.max(120, this.getWidth() - 32)); + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + protected int getScrollbarX() { + return this.getWidth() - 8; + } + + private static final class RowEntry extends Entry { + private final NonCreditsScreen.CreditsRow row; + private final TextRenderer textRenderer; + private final int rowHeight; + private final List widgets = new ArrayList<>(); + + private RowEntry(NonCreditsScreen.CreditsRow row, TextRenderer textRenderer, int contentWidth, Consumer linkOpener) { + this.row = row; + this.textRenderer = textRenderer; + this.rowHeight = calculateHeight(row, textRenderer, contentWidth); + if (row instanceof NonCreditsScreen.ResourceRow resourceRow) { + NonCreditsData.ResourceCredit resource = resourceRow.resource(); + this.addLinkButton("config.needsofnature.credits.source", resource.sourceUrl(), linkOpener); + this.addLinkButton("config.needsofnature.credits.license", resource.licenseUrl(), linkOpener); + } else if (row instanceof NonCreditsScreen.LinksRow linksRow) { + for (NonCreditsData.ProjectLink link : linksRow.links()) { + ButtonWidget button = ButtonWidget.builder(Text.literal(link.label()), ignored -> linkOpener.accept(link.url())) + .dimensions(0, 0, 180, 20) + .build(); + NonModMenuScreens.setTooltip(button, NonModMenuScreens.tooltip(Text.literal(link.url()))); + this.widgets.add(button); + } + } + } + + private void addLinkButton(String translationKey, String url, Consumer linkOpener) { + if (url != null) { + ButtonWidget button = ButtonWidget.builder(Text.translatable(translationKey), ignored -> linkOpener.accept(url)) + .dimensions(0, 0, 90, 18) + .build(); + NonModMenuScreens.setTooltip(button, NonModMenuScreens.tooltip(Text.literal(url))); + this.widgets.add(button); + } + } + + private static int calculateHeight(NonCreditsScreen.CreditsRow row, TextRenderer renderer, int contentWidth) { + if (row instanceof NonCreditsScreen.NoticeRow noticeRow) { + int lines = wrappedLineCount(renderer, noticeRow.text(), contentWidth - 16); + return Math.max(34, 16 + lines * 10); + } else if (row instanceof NonCreditsScreen.SectionRow) { + return 30; + } else if (row instanceof NonCreditsScreen.ContributorRow) { + return 18; + } else if (row instanceof NonCreditsScreen.ResourceRow resourceRow) { + NonCreditsData.ResourceCredit resource = resourceRow.resource(); + int detailLines = resourceDetailLines(resource); + String assets = joinedAssets(resource.assets()); + int assetLines = assets == null + ? 0 + : renderer.wrapLines(Text.translatable("config.needsofnature.credits.assets_value", new Object[]{assets}), Math.max(20, contentWidth - 8)) + .size(); + int buttonHeight = resource.sourceUrl() == null && resource.licenseUrl() == null ? 0 : 22; + return 18 + detailLines * 10 + assetLines * 10 + buttonHeight + 6; + } else { + return row instanceof NonCreditsScreen.LinksRow linksRow ? Math.max(26, 6 + linksRow.links().size() * 24) : 28; + } + } + + private static int wrappedLineCount(TextRenderer renderer, String value, int width) { + return value != null && !value.isBlank() ? renderer.wrapLines(Text.literal(value), Math.max(20, width)).size() : 0; + } + + private static int resourceDetailLines(NonCreditsData.ResourceCredit resource) { + int count = 0; + if (resource.category() != null) { + count++; + } + + if (resource.creator() != null) { + count++; + } + + if (resource.source() != null) { + count++; + } + + if (resource.license() != null || resource.modified()) { + count++; + } + + return count; + } + + private static String joinedAssets(List assets) { + return assets != null && !assets.isEmpty() ? String.join(", ", assets) : null; + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int x = this.getContentX() + 8; + int y = this.getContentY() + 3; + int width = this.getContentWidth() - 16; + if (this.row instanceof NonCreditsScreen.NoticeRow noticeRow) { + this.renderNotice(context, x, y, width, noticeRow.text()); + } else if (this.row instanceof NonCreditsScreen.SectionRow sectionRow) { + this.renderSection(context, x, y, width, sectionRow.title()); + } else if (this.row instanceof NonCreditsScreen.ContributorRow contributorRow) { + this.renderContributor(context, x, y, width, contributorRow.contributor()); + } else if (this.row instanceof NonCreditsScreen.ResourceRow resourceRow) { + this.renderResource(context, mouseX, mouseY, deltaTicks, x, y, width, hovered, resourceRow.resource()); + } else if (this.row instanceof NonCreditsScreen.LinksRow) { + this.renderProjectLinks(context, mouseX, mouseY, deltaTicks, x, y, width); + } + } + + private void renderNotice(DrawContext context, int x, int y, int width, String notice) { + context.fill(x, y - 1, x + width, y + this.rowHeight - 5, 1144000528); + int textY = y + 6; + + for (OrderedText line : this.textRenderer.wrapLines(Text.literal(notice), Math.max(20, width - 16))) { + context.drawCenteredTextWithShadow(this.textRenderer, line, x + width / 2, textY, -20304); + textY += 10; + } + } + + private void renderSection(DrawContext context, int x, int y, int width, Text title) { + Text displayedTitle = Text.literal(this.textRenderer.trimToWidth(title.getString(), Math.max(20, width - 40))); + int centerX = x + width / 2; + int textWidth = this.textRenderer.getWidth(displayedTitle); + int lineY = y + 10; + context.fill(x, y + 2, x + width, y + 22, 671088640); + context.fill(x + 6, lineY, Math.max(x + 6, centerX - textWidth / 2 - 10), lineY + 1, 1717197402); + context.fill(Math.min(x + width - 6, centerX + textWidth / 2 + 10), lineY, x + width - 6, lineY + 1, 1717197402); + context.drawCenteredTextWithShadow(this.textRenderer, displayedTitle, centerX, y + 6, -2042444); + } + + private void renderContributor(DrawContext context, int x, int y, int width, NonCreditsData.Contributor contributor) { + int textX = x + 5; + int textY = y + 2; + String prefix = contributor.name(); + String details = contributorDetails(contributor); + if (details != null) { + prefix = prefix + ": "; + } + + prefix = this.textRenderer.trimToWidth(prefix, width - 10); + context.drawTextWithShadow(this.textRenderer, prefix, textX, textY, -796531); + if (details != null) { + int detailX = textX + this.textRenderer.getWidth(prefix); + int detailWidth = Math.max(0, x + width - 5 - detailX); + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(details, detailWidth), detailX, textY, -4671304); + } + } + + private static String contributorDetails(NonCreditsData.Contributor contributor) { + String roles = contributor.roles().isEmpty() ? null : String.join(", ", contributor.roles()); + if (contributor.contribution() == null) { + return roles; + } else { + return roles == null ? contributor.contribution() : roles + " - " + contributor.contribution(); + } + } + + private void renderResource( + DrawContext context, int mouseX, int mouseY, float delta, int x, int y, int width, boolean hovered, NonCreditsData.ResourceCredit resource + ) { + context.fill(x, y - 1, x + width, y + this.rowHeight - 5, hovered ? 939524096 : 570425344); + int textX = x + 5; + int currentY = y + 3; + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(resource.title(), width - 10), textX, currentY, -796531); + currentY += 10; + if (resource.category() != null) { + this.drawTrimmed( + context, + Text.translatable("config.needsofnature.credits.category_value", new Object[]{resource.category()}), + textX, + currentY, + width - 10, + -5592406 + ); + currentY += 10; + } + + if (resource.creator() != null) { + this.drawTrimmed( + context, + Text.translatable("config.needsofnature.credits.creator_value", new Object[]{resource.creator()}), + textX, + currentY, + width - 10, + -3092272 + ); + currentY += 10; + } + + if (resource.source() != null) { + this.drawTrimmed( + context, + Text.translatable("config.needsofnature.credits.source_value", new Object[]{resource.source()}), + textX, + currentY, + width - 10, + -4671304 + ); + currentY += 10; + } + + if (resource.license() != null || resource.modified()) { + Text license = resource.license() == null + ? Text.translatable("config.needsofnature.credits.modified") + : ( + resource.modified() + ? Text.translatable("config.needsofnature.credits.license_modified_value", new Object[]{resource.license()}) + : Text.translatable("config.needsofnature.credits.license_value", new Object[]{resource.license()}) + ); + this.drawTrimmed(context, license, textX, currentY, width - 10, -4671304); + currentY += 10; + } + + String assets = joinedAssets(resource.assets()); + if (assets != null) { + Text assetText = Text.translatable("config.needsofnature.credits.assets_value", new Object[]{assets}); + + for (OrderedText line : this.textRenderer.wrapLines(assetText, Math.max(20, width - 10))) { + context.drawTextWithShadow(this.textRenderer, line, textX, currentY, -7303024); + currentY += 10; + } + } + + if (!this.widgets.isEmpty()) { + int totalWidth = this.widgets.size() * 90 + Math.max(0, this.widgets.size() - 1) * 4; + int buttonX = x + (width - totalWidth) / 2; + int buttonY = y + this.rowHeight - 27; + + for (ClickableWidget widget : this.widgets) { + widget.setX(buttonX); + widget.setY(buttonY); + widget.render(context, mouseX, mouseY, delta); + buttonX += 94; + } + } + } + + private void renderProjectLinks(DrawContext context, int mouseX, int mouseY, float delta, int x, int y, int width) { + int buttonWidth = Math.min(180, width - 8); + int buttonX = x + (width - buttonWidth) / 2; + int buttonY = y + 2; + + for (ClickableWidget widget : this.widgets) { + widget.setWidth(buttonWidth); + widget.setX(buttonX); + widget.setY(buttonY); + widget.render(context, mouseX, mouseY, delta); + buttonY += 24; + } + } + + private void drawTrimmed(DrawContext context, Text value, int x, int y, int width, int color) { + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(value.getString(), Math.max(20, width)), x, y, color); + } + + private int rowHeight() { + return this.rowHeight; + } + + public List selectableChildren() { + return List.copyOf(this.widgets); + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + + public List children() { + return List.copyOf(this.widgets); + } + } + } + + private sealed interface CreditsRow + permits NonCreditsScreen.NoticeRow, + NonCreditsScreen.SectionRow, + NonCreditsScreen.ContributorRow, + NonCreditsScreen.ResourceRow, + NonCreditsScreen.LinksRow { + } + + private record LinksRow(List links) implements NonCreditsScreen.CreditsRow { + } + + private record NoticeRow(String text) implements NonCreditsScreen.CreditsRow { + } + + private record ResourceRow(NonCreditsData.ResourceCredit resource) implements NonCreditsScreen.CreditsRow { + } + + private record SectionRow(Text title) implements NonCreditsScreen.CreditsRow { + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuDebugScreens.java b/src/main/java/com/nonid/integration/NonModMenuDebugScreens.java new file mode 100644 index 0000000..c35a28a --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuDebugScreens.java @@ -0,0 +1,4450 @@ +package com.nonid.integration; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.NonDebugChatMode; +import com.nonid.client.NonAnimationPreviewRenderer; +import com.nonid.client.NonLiquidColors; +import com.nonid.client.NonServerSettingsClient; +import com.nonid.client.NonTagLocalizer; +import com.nonid.data.NonEntityProfiles; +import com.nonid.data.NonLiquidGainOverrides; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.AnimationEngineClient; +import com.nonid.internal.animation.client.render.gecko.GeckoResourceResolver; +import com.nonid.internal.animation.client.render.gecko.GenderedModelResolver; +import com.nonid.internal.animation.client.render.gecko.VanillaTextureResolver; +import com.nonid.network.AnimationCatalogData; +import com.nonid.pack.NonExternalPackProvider; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.Map.Entry; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.gui.widget.ButtonWidget.PressAction; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.registry.Registries; +import net.minecraft.resource.InputSupplier; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.text.MutableText; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class NonModMenuDebugScreens { + private NonModMenuDebugScreens() { + } + + static class DebugAdvancedScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig localConfig; + private final NonConfig defaults = new NonConfig(); + private boolean serverConfigEditable; + private int scanBudget; + private NonDebugChatMode debugChatMode; + private boolean debugSpinMode; + private boolean convertMaleOnlyVInjectionsToA; + private final boolean initialForceVanillaEntityTextures; + private boolean forceVanillaEntityTextures; + private int animationBlockSearchRadius; + private TextFieldWidget budgetField; + private ButtonWidget debugModeButton; + private ButtonWidget debugSpinModeButton; + private ButtonWidget convertMaleOnlyVInjectionsToAButton; + private ButtonWidget resetBudgetButton; + private ButtonWidget resetDebugModeButton; + private ButtonWidget resetDebugSpinModeButton; + private ButtonWidget resetConvertMaleOnlyVInjectionsToAButton; + private ButtonWidget forceVanillaEntityTexturesButton; + private ButtonWidget animationBlockSearchRadiusButton; + private ButtonWidget resetForceVanillaEntityTexturesButton; + private ButtonWidget resetAnimationBlockSearchRadiusButton; + + protected DebugAdvancedScreen(Screen parent, NonConfig config, NonConfig localConfig) { + super(Text.translatable("config.needsofnature.debug_advanced_title")); + this.parent = parent; + this.config = config; + this.localConfig = localConfig; + this.scanBudget = config.getScanBudgetPerTick(); + this.debugChatMode = localConfig.debugChatMode(); + this.debugSpinMode = localConfig.isDebugSpinMode(); + this.convertMaleOnlyVInjectionsToA = config.convertMaleOnlyVInjectionsToA(); + this.initialForceVanillaEntityTextures = localConfig.forceVanillaEntityTextures(); + this.forceVanillaEntityTextures = this.initialForceVanillaEntityTextures; + this.animationBlockSearchRadius = config.blockSearchRadius(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.budgetField != null) { + this.scanBudget = this.parseField(this.budgetField, this.scanBudget); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.runtime_budget"))); + this.budgetField = this.newNumberField(fieldWidth, this.scanBudget); + this.resetBudgetButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), button -> this.budgetField.setText(String.valueOf(this.defaults.getScanBudgetPerTick())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetBudgetButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.scan_budget"), + this.budgetField, + this.resetBudgetButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.scan_budget") + ) + ); + this.budgetField.setEditable(this.serverConfigEditable); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.debug_feedback"))); + this.debugModeButton = ButtonWidget.builder(this.debugChatLabel(this.debugChatMode), button -> { + this.debugChatMode = this.nextDebugChatMode(this.debugChatMode); + button.setMessage(this.debugChatLabel(this.debugChatMode)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetDebugModeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.debugChatMode = this.defaults.debugChatMode(); + this.debugModeButton.setMessage(this.debugChatLabel(this.debugChatMode)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetDebugModeButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.debug_chat_mode"), + this.debugModeButton, + this.resetDebugModeButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.debug_chat_mode") + ) + ); + this.debugSpinModeButton = ButtonWidget.builder(this.debugSpinModeLabel(this.debugSpinMode), button -> { + this.debugSpinMode = !this.debugSpinMode; + button.setMessage(this.debugSpinModeLabel(this.debugSpinMode)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetDebugSpinModeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.debugSpinMode = this.defaults.isDebugSpinMode(); + this.debugSpinModeButton.setMessage(this.debugSpinModeLabel(this.debugSpinMode)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetDebugSpinModeButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.debug_spin_mode"), + this.debugSpinModeButton, + this.resetDebugSpinModeButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.debug_spin_mode") + ) + ); + this.convertMaleOnlyVInjectionsToAButton = ButtonWidget.builder(this.debugBooleanLabel(this.convertMaleOnlyVInjectionsToA), button -> { + this.convertMaleOnlyVInjectionsToA = !this.convertMaleOnlyVInjectionsToA; + button.setMessage(this.debugBooleanLabel(this.convertMaleOnlyVInjectionsToA)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetConvertMaleOnlyVInjectionsToAButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.convertMaleOnlyVInjectionsToA = this.defaults.convertMaleOnlyVInjectionsToA(); + this.convertMaleOnlyVInjectionsToAButton.setMessage(this.debugBooleanLabel(this.convertMaleOnlyVInjectionsToA)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetConvertMaleOnlyVInjectionsToAButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.convert_male_v_to_a"), + this.convertMaleOnlyVInjectionsToAButton, + this.resetConvertMaleOnlyVInjectionsToAButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.convert_male_v_to_a") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.animation_runtime"))); + this.forceVanillaEntityTexturesButton = ButtonWidget.builder(this.onOffLabel(this.forceVanillaEntityTextures), button -> { + this.forceVanillaEntityTextures = !this.forceVanillaEntityTextures; + button.setMessage(this.onOffLabel(this.forceVanillaEntityTextures)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetForceVanillaEntityTexturesButton = this.resetButton(button -> { + this.forceVanillaEntityTextures = this.defaults.forceVanillaEntityTextures(); + this.forceVanillaEntityTexturesButton.setMessage(this.onOffLabel(this.forceVanillaEntityTextures)); + this.updateResetButtons(); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.animation.force_vanilla_entity_textures"), + this.forceVanillaEntityTexturesButton, + this.resetForceVanillaEntityTexturesButton, + null + ) + ); + this.animationBlockSearchRadiusButton = ButtonWidget.builder(Text.literal(String.valueOf(this.animationBlockSearchRadius)), button -> { + this.animationBlockSearchRadius = this.animationBlockSearchRadius >= 16 ? 1 : this.animationBlockSearchRadius + 1; + button.setMessage(Text.literal(String.valueOf(this.animationBlockSearchRadius))); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetAnimationBlockSearchRadiusButton = this.resetButton(button -> { + this.animationBlockSearchRadius = this.defaults.blockSearchRadius(); + this.animationBlockSearchRadiusButton.setMessage(Text.literal(String.valueOf(this.animationBlockSearchRadius))); + this.updateResetButtons(); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.animation.block_search_radius"), + this.animationBlockSearchRadiusButton, + this.resetAnimationBlockSearchRadiusButton, + null + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.animation_data"))); + ButtonWidget loadedAnimationsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.loaded_animations"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.LoadedAnimationsScreen(this, this.config)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(loadedAnimationsButton, "config.needsofnature.tooltip.loaded_animations"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, loadedAnimationsButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.loaded_animations") + ) + ); + ButtonWidget loadOrderButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.loaded_animations.load_order"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.LoadOrderScreen(this, this.localConfig)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(loadOrderButton, "config.needsofnature.tooltip.loaded_animations.load_order"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, loadOrderButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.loaded_animations.load_order") + ) + ); + this.budgetField.setChangedListener(ignored -> this.updateResetButtons()); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.scanBudget = this.clampBudget(this.parseField(this.budgetField, this.scanBudget)); + this.config.setScanBudgetPerTick(this.scanBudget); + this.config.setConvertMaleOnlyVInjectionsToA(this.convertMaleOnlyVInjectionsToA); + this.config.setBlockSearchRadius(this.animationBlockSearchRadius); + NonModMenuScreens.saveServerConfig(this.config); + } + + this.localConfig.setDebugChatMode(this.debugChatMode); + this.localConfig.setDebugSpinMode(this.debugSpinMode); + this.localConfig.setForceVanillaEntityTextures(this.forceVanillaEntityTextures); + this.localConfig.save(); + if (this.forceVanillaEntityTextures != this.initialForceVanillaEntityTextures) { + VanillaTextureResolver.clearCache(); + } + + AnimationEngineClient.sendDebugChatPreference(); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private int clampBudget(int v) { + return Math.max(1, Math.min(512, v)); + } + + private Text debugChatLabel(NonDebugChatMode mode) { + NonDebugChatMode resolved = mode == null ? NonDebugChatMode.SETUP_ERRORS : mode; + return Text.translatable("config.needsofnature.debug_chat_mode." + resolved.id()); + } + + private NonDebugChatMode nextDebugChatMode(NonDebugChatMode current) { + if (current == null) { + return NonDebugChatMode.SETUP_ERRORS; + } + + return switch (current) { + case ALL -> NonDebugChatMode.SETUP_WARNINGS_ERRORS; + case SETUP_WARNINGS_ERRORS -> NonDebugChatMode.SETUP_ERRORS; + case SETUP_ERRORS -> NonDebugChatMode.ERRORS_ONLY; + case ERRORS_ONLY -> NonDebugChatMode.ALL; + }; + } + + private Text debugSpinModeLabel(boolean enabled) { + return Text.translatable("config.needsofnature.debug_spin_mode.value", new Object[]{Text.translatable(enabled ? "options.on" : "options.off")}); + } + + private Text debugBooleanLabel(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private Text onOffLabel(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private ButtonWidget resetButton(PressAction action) { + ButtonWidget button = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), action).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(button, "config.needsofnature.tooltip.reset"); + return button; + } + + private void updateResetButtons() { + if (this.resetBudgetButton != null) { + this.resetBudgetButton.active = this.serverConfigEditable + && this.parseField(this.budgetField, this.defaults.getScanBudgetPerTick()) != this.defaults.getScanBudgetPerTick(); + } + + if (this.resetDebugModeButton != null) { + this.resetDebugModeButton.active = this.debugChatMode != this.defaults.debugChatMode(); + } + + if (this.resetDebugSpinModeButton != null) { + this.resetDebugSpinModeButton.active = this.debugSpinMode != this.defaults.isDebugSpinMode(); + } + + if (this.resetConvertMaleOnlyVInjectionsToAButton != null) { + this.resetConvertMaleOnlyVInjectionsToAButton.active = this.serverConfigEditable + && this.convertMaleOnlyVInjectionsToA != this.defaults.convertMaleOnlyVInjectionsToA(); + } + + if (this.resetForceVanillaEntityTexturesButton != null) { + this.resetForceVanillaEntityTexturesButton.active = this.forceVanillaEntityTextures != this.defaults.forceVanillaEntityTextures(); + } + + if (this.resetAnimationBlockSearchRadiusButton != null) { + this.resetAnimationBlockSearchRadiusButton.active = this.serverConfigEditable + && this.animationBlockSearchRadius != this.defaults.blockSearchRadius(); + } + + if (this.animationBlockSearchRadiusButton != null) { + this.animationBlockSearchRadiusButton.active = this.serverConfigEditable; + } + + if (this.convertMaleOnlyVInjectionsToAButton != null) { + this.convertMaleOnlyVInjectionsToAButton.active = this.serverConfigEditable; + } + } + } + + static class GenderSpawnConfigScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private boolean serverConfigEditable; + private Map appliedMap; + private NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList genderList; + private final List rows = new ArrayList<>(); + private ButtonWidget applyButton; + private ButtonWidget doneButton; + + protected GenderSpawnConfigScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.gender_spawn_title")); + this.parent = parent; + this.config = config; + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + this.appliedMap = new LinkedHashMap<>(NonEntityProfiles.resolveEffectiveGenderSpawnMap()); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + List preserved = this.snapshotRows(); + if (preserved == null) { + preserved = new ArrayList<>(); + + for (Entry entry : this.appliedMap.entrySet()) { + NonConfig.GenderSpawnChances chances = entry.getValue(); + if (chances != null) { + preserved.add( + new NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderRowData( + entry.getKey(), String.valueOf(chances.male()), String.valueOf(chances.female()), String.valueOf(chances.both()) + ) + ); + } + } + } + + int listTop = 44; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + this.genderList = new NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(this.genderList); + this.rows.clear(); + + for (NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderRowData data : preserved) { + this.addRow(data.id(), data.male(), data.female(), data.both()); + } + + if (this.rows.isEmpty()) { + this.addRow("", "48", "47", "5"); + } + + int centerX = this.width / 2; + ButtonWidget addButton = ButtonWidget.builder(Text.translatable("config.needsofnature.gender_spawn_add"), button -> { + this.addRow("", "48", "47", "5"); + this.updateApplyButton(); + }).dimensions(centerX - 100, this.height - 52, 96, 20).build(); + addButton.active = this.serverConfigEditable; + NonModMenuScreens.setTooltip(addButton, "config.needsofnature.tooltip.gender_spawn_add"); + this.addDrawableChild(addButton); + ButtonWidget resetDefaultsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.gender_spawn_reset"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.GenderSpawnConfigScreen.ResetGenderSpawnConfirmScreen(this, () -> { + this.applyDefaultsToRows(); + this.updateApplyButton(); + MinecraftClient.getInstance().setScreen(this); + })) + ) + .dimensions(centerX + 4, this.height - 52, 96, 20) + .build(); + resetDefaultsButton.active = this.serverConfigEditable; + NonModMenuScreens.setTooltip(resetDefaultsButton, "config.needsofnature.tooltip.gender_spawn_reset"); + this.addDrawableChild(resetDefaultsButton); + this.applyButton = ButtonWidget.builder(Text.translatable("config.needsofnature.apply"), button -> { + if (this.serverConfigEditable) { + if (this.hasInvalidGenderChanceRows()) { + this.updateApplyButton(); + } else { + Map next = this.collectGenderMapFromFields(); + this.config.setEntityProfilesByEntity(this.mergeGenderSpawnIntoEntityProfiles(next)); + NonModMenuScreens.saveServerConfig(this.config); + this.appliedMap = new LinkedHashMap<>(next); + this.updateApplyButton(); + } + } + }).dimensions(centerX - 100, this.height - 28, 96, 20).build(); + NonModMenuScreens.setTooltip(this.applyButton, "config.needsofnature.tooltip.apply"); + this.addDrawableChild(this.applyButton); + this.doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 4, this.height - 28, 96, 20) + .build(); + NonModMenuScreens.setTooltip(this.doneButton, "config.needsofnature.tooltip.done_unsaved"); + this.addDrawableChild(this.doneButton); + this.updateApplyButton(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateApplyButton(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + this.drawGenderSpawnHeader(context); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void drawGenderSpawnHeader(DrawContext context) { + if (this.genderList != null) { + int rowX = this.genderList.getRowLeft(); + int rowWidth = this.genderList.getRowWidth(); + int fieldW = 42; + int removeW = 20; + int idW = Math.max(130, rowWidth - removeW - fieldW * 3 - 30); + int maleX = rowX + idW + 6; + int femaleX = maleX + fieldW + 6; + int bothX = femaleX + fieldW + 6; + int y = 34; + int color = -4342339; + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.gender_spawn_column.entity"), rowX + 4, y, color); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.gender_spawn_column.male"), maleX + fieldW / 2, y, color + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.gender_spawn_column.female"), femaleX + fieldW / 2, y, color + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.gender_spawn_column.both"), bothX + fieldW / 2, y, color + ); + } + } + + private void addRow(String idText, String maleText, String femaleText, String bothText) { + NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry[] holder = new NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry[1]; + NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry entry = new NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry( + this.textRenderer, idText, maleText, femaleText, bothText, () -> { + NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry current = holder[0]; + if (current != null) { + this.rows.removeIf(row -> row == current); + this.genderList.removeEntryRow(current); + this.updateApplyButton(); + } + } + ); + holder[0] = entry; + entry.setOnChange(this::updateApplyButton); + entry.setEditable(this.serverConfigEditable); + this.rows.add(entry); + this.genderList.addEntryRow(entry); + } + + private void updateApplyButton() { + if (this.applyButton != null) { + if (!this.serverConfigEditable) { + this.applyButton.active = false; + if (this.doneButton != null) { + this.doneButton.setMessage(Text.translatable("config.needsofnature.done")); + } + } else { + Map current = this.collectGenderMapFromFields(); + boolean hasUnsavedChanges = !current.equals(this.appliedMap); + boolean hasInvalidRows = this.hasInvalidGenderChanceRows(); + this.applyButton.active = hasUnsavedChanges && !hasInvalidRows; + this.applyButton + .setMessage(Text.translatable(hasInvalidRows ? "config.needsofnature.gender_spawn_invalid_total" : "config.needsofnature.apply")); + if (this.doneButton != null) { + this.doneButton.setMessage(Text.translatable(hasUnsavedChanges ? "config.needsofnature.done_unsaved" : "config.needsofnature.done")); + } + } + } + } + + private boolean hasInvalidGenderChanceRows() { + for (NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry row : this.rows) { + if (row != null) { + String id = row.getIdText(); + if (id != null && !id.trim().isEmpty()) { + int total = this.parseInt(row.getMaleText()) + this.parseInt(row.getFemaleText()) + this.parseInt(row.getBothText()); + if (total != 100) { + return true; + } + } + } + } + + return false; + } + + private void applyDefaultsToRows() { + Map defaultsMap = new LinkedHashMap<>(); + + for (Entry entry : NonEntityProfiles.resolveDefaultProfilesMap().entrySet()) { + NonConfig.GenderSpawnChances chances = entry.getValue().genderSpawn(); + if (chances != null) { + defaultsMap.put(entry.getKey(), chances); + } + } + + if (this.genderList != null) { + for (NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry entry : new ArrayList<>(this.rows)) { + this.genderList.removeEntryRow(entry); + } + } + + this.rows.clear(); + + for (Entry entry : defaultsMap.entrySet()) { + NonConfig.GenderSpawnChances chances = entry.getValue(); + if (chances != null) { + this.addRow(entry.getKey(), String.valueOf(chances.male()), String.valueOf(chances.female()), String.valueOf(chances.both())); + } + } + + if (this.rows.isEmpty()) { + this.addRow("", "48", "47", "5"); + } + } + + private Map mergeGenderSpawnIntoEntityProfiles(Map genderMap) { + Map profiles = new LinkedHashMap<>(); + if (genderMap != null) { + for (Entry entry : genderMap.entrySet()) { + Identifier id = Identifier.tryParse(entry.getKey()); + if (id != null && entry.getValue() != null) { + NonConfig.EntityProfile base = this.config.getEntityProfilesByEntity().get(entry.getKey()); + NonConfig.GenderSpawnChances defaultGender = NonEntityProfiles.resolveDefaultProfile(id).genderSpawn(); + NonConfig.GenderSpawnChances savedGender = entry.getValue().equals(defaultGender) ? null : entry.getValue(); + NonConfig.EntityProfile next = new NonConfig.EntityProfile( + base == null ? null : base.pregnancyChancePercent(), + base == null ? null : base.offspringMin(), + base == null ? null : base.offspringMax(), + base == null ? null : base.birthEntity(), + base == null ? null : base.birthMode(), + savedGender, + base == null ? null : base.egg(), + base == null ? null : base.lookAtNearbyAnimations(), + base == null ? null : base.multiActorJoinChancePercent(), + base == null ? null : base.energyGainMultiplier() + ) + .sanitized(); + if (!next.isEmpty()) { + profiles.put(entry.getKey(), next); + } + } + } + } + + return profiles; + } + + private Map collectGenderMapFromFields() { + Map map = new LinkedHashMap<>(); + + for (NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry row : this.rows) { + if (row != null) { + String id = row.getIdText(); + if (id != null) { + id = id.trim(); + if (!id.isEmpty() && Identifier.tryParse(id) != null) { + NonConfig.GenderSpawnChances chances = NonConfig.sanitizeGenderSpawnChances( + new NonConfig.GenderSpawnChances(this.parseInt(row.getMaleText()), this.parseInt(row.getFemaleText()), this.parseInt(row.getBothText())) + ); + if (chances != null) { + map.put(id, chances); + } + } + } + } + } + + return map; + } + + private List snapshotRows() { + if (this.rows.isEmpty()) { + return null; + } + + List data = new ArrayList<>(); + + for (NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry row : this.rows) { + data.add( + new NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderRowData(row.getIdText(), row.getMaleText(), row.getFemaleText(), row.getBothText()) + ); + } + + return data; + } + + private int parseInt(String value) { + if (value == null) { + return 0; + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private record GenderRowData(String id, String male, String female, String both) { + } + + private static class GenderSpawnList extends ElementListWidget { + private GenderSpawnList(MinecraftClient client, int width, int height, int top) { + super(client, width, height, top, 24); + this.centerListVertically = false; + } + + private void addEntryRow(NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry entry) { + super.addEntry(entry); + } + + private void removeEntryRow(NonModMenuDebugScreens.GenderSpawnConfigScreen.GenderSpawnList.GenderEntry entry) { + super.removeEntry(entry); + } + + public int getRowWidth() { + return 420; + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + private static final class GenderEntry + extends net.minecraft.client.gui.widget.ElementListWidget.Entry { + private final TextFieldWidget idField; + private final TextFieldWidget maleField; + private final TextFieldWidget femaleField; + private final TextFieldWidget bothField; + private final ButtonWidget removeButton; + private final List widgets; + private Runnable onChange; + + private GenderEntry(TextRenderer textRenderer, String idText, String maleText, String femaleText, String bothText, Runnable onRemove) { + this.idField = new TextFieldWidget(textRenderer, 0, 0, 170, 20, Text.literal("")); + this.idField.setText(idText == null ? "" : idText); + this.idField.setMaxLength(80); + NonModMenuScreens.setTooltip(this.idField, "config.needsofnature.tooltip.gender_spawn_entry_entity"); + this.maleField = percentField(textRenderer, maleText); + NonModMenuScreens.setTooltip(this.maleField, "config.needsofnature.tooltip.gender_spawn_entry_male"); + this.femaleField = percentField(textRenderer, femaleText); + NonModMenuScreens.setTooltip(this.femaleField, "config.needsofnature.tooltip.gender_spawn_entry_female"); + this.bothField = percentField(textRenderer, bothText); + NonModMenuScreens.setTooltip(this.bothField, "config.needsofnature.tooltip.gender_spawn_entry_both"); + this.removeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.remove_icon"), button -> { + if (onRemove != null) { + onRemove.run(); + } + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(this.removeButton, "config.needsofnature.tooltip.remove"); + this.widgets = new ArrayList<>(5); + this.widgets.add(this.idField); + this.widgets.add(this.maleField); + this.widgets.add(this.femaleField); + this.widgets.add(this.bothField); + this.widgets.add(this.removeButton); + this.idField.setChangedListener(ignored -> this.notifyChanged()); + this.maleField.setChangedListener(ignored -> this.notifyChanged()); + this.femaleField.setChangedListener(ignored -> this.notifyChanged()); + this.bothField.setChangedListener(ignored -> this.notifyChanged()); + } + + private static TextFieldWidget percentField(TextRenderer textRenderer, String text) { + TextFieldWidget field = new TextFieldWidget(textRenderer, 0, 0, 42, 20, Text.literal("")); + field.setText(text == null ? "0" : text); + field.setMaxLength(3); + return field; + } + + private void setOnChange(Runnable onChange) { + this.onChange = onChange; + } + + private void notifyChanged() { + if (this.onChange != null) { + this.onChange.run(); + } + } + + private void setEditable(boolean editable) { + this.idField.setEditable(editable); + this.maleField.setEditable(editable); + this.femaleField.setEditable(editable); + this.bothField.setEditable(editable); + this.removeButton.active = editable; + } + + private String getIdText() { + return this.idField.getText(); + } + + private String getMaleText() { + return this.maleField.getText(); + } + + private String getFemaleText() { + return this.femaleField.getText(); + } + + private String getBothText() { + return this.bothField.getText(); + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int rowX = this.getContentX(); + int rowY = this.getContentY(); + int rowWidth = this.getContentWidth(); + int widgetY = rowY + 2; + int removeW = this.removeButton.getWidth(); + int fieldW = this.maleField.getWidth(); + int idW = Math.max(130, rowWidth - removeW - fieldW * 3 - 30); + int maleX = rowX + idW + 6; + int femaleX = maleX + fieldW + 6; + int bothX = femaleX + fieldW + 6; + int removeX = bothX + fieldW + 6; + this.idField.setX(rowX); + this.idField.setY(widgetY); + this.idField.setWidth(idW); + this.maleField.setX(maleX); + this.maleField.setY(widgetY); + this.femaleField.setX(femaleX); + this.femaleField.setY(widgetY); + this.bothField.setX(bothX); + this.bothField.setY(widgetY); + this.removeButton.setX(removeX); + this.removeButton.setY(widgetY); + + for (ClickableWidget widget : this.widgets) { + widget.render(context, mouseX, mouseY, deltaTicks); + } + } + + public List selectableChildren() { + return this.widgets; + } + + public List children() { + return this.widgets; + } + } + } + + private static final class ResetGenderSpawnConfirmScreen extends Screen { + private final Screen parent; + private final Runnable onConfirm; + + private ResetGenderSpawnConfirmScreen(Screen parent, Runnable onConfirm) { + super(Text.translatable("config.needsofnature.gender_spawn_reset_title")); + this.parent = parent; + this.onConfirm = onConfirm; + } + + protected void init() { + int centerX = this.width / 2; + int buttonY = this.height / 2 + 20; + ButtonWidget yesButton = ButtonWidget.builder(Text.translatable("gui.yes"), button -> { + if (this.onConfirm != null) { + this.onConfirm.run(); + } + + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(centerX - 100, buttonY, 96, 20).build(); + this.addDrawableChild(yesButton); + ButtonWidget noButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 4, buttonY, 96, 20) + .build(); + this.addDrawableChild(noButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, this.height / 2 - 20, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.gender_spawn_reset_body"), this.width / 2, this.height / 2 - 4, -3355444 + ); + } + } + } + + static class LiquidGainConfigScreen extends Screen { + private static final String MIXED_COLOR_ROW_ID = "needsofnature:mixed"; + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean serverConfigEditable; + private Map appliedMap; + private Map appliedColorMap; + private NonModMenuDebugScreens.LiquidGainConfigScreen.GainList gainList; + private final List rows = new ArrayList<>(); + private ButtonWidget applyButton; + private ButtonWidget doneButton; + + protected LiquidGainConfigScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.liquid_gain_title")); + this.parent = parent; + this.config = config; + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + this.appliedMap = new LinkedHashMap<>(NonLiquidGainOverrides.getGainByEntity()); + this.appliedMap.putAll(config.getLiquidGainByEntity()); + this.appliedColorMap = new LinkedHashMap<>(NonLiquidGainOverrides.getColorByEntity()); + this.appliedColorMap.putAll(config.getLiquidColorByEntity()); + this.appliedMap.remove("needsofnature:mixed"); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + List preserved = this.snapshotRows(); + if (preserved == null) { + preserved = new ArrayList<>(); + preserved.add( + new NonModMenuDebugScreens.LiquidGainConfigScreen.RowData( + "needsofnature:mixed", "0", this.appliedColorMap.getOrDefault("needsofnature:mixed", resolveDefaultMixedColorHex()) + ) + ); + LinkedHashSet ids = new LinkedHashSet<>(); + ids.addAll(this.appliedMap.keySet()); + ids.addAll(this.appliedColorMap.keySet()); + ids.remove("needsofnature:mixed"); + + for (String id : ids) { + int gain = this.appliedMap.getOrDefault(id, 0); + String color = this.appliedColorMap.getOrDefault(id, ""); + preserved.add(new NonModMenuDebugScreens.LiquidGainConfigScreen.RowData(id, String.valueOf(gain), color)); + } + } + + preserved = this.ensureMixedRowPresent(preserved); + int listTop = 44; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + this.gainList = new NonModMenuDebugScreens.LiquidGainConfigScreen.GainList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(this.gainList); + this.rows.clear(); + + for (NonModMenuDebugScreens.LiquidGainConfigScreen.RowData data : preserved) { + this.addRow(data.id(), data.value(), data.colorHex()); + } + + if (this.rows.isEmpty()) { + this.addRow("", "0", ""); + } + + int centerX = this.width / 2; + ButtonWidget addButton = ButtonWidget.builder(Text.translatable("config.needsofnature.liquid_gain_add"), button -> { + this.addRow("", "0", ""); + this.updateApplyButton(); + }).dimensions(centerX - 100, this.height - 52, 96, 20).build(); + addButton.active = this.serverConfigEditable; + NonModMenuScreens.setTooltip(addButton, "config.needsofnature.tooltip.liquid_gain_add"); + this.addDrawableChild(addButton); + ButtonWidget resetDefaultsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.liquid_gain_reset"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.LiquidGainConfigScreen.ResetLiquidGainConfirmScreen(this, () -> { + this.applyDefaultsToRows(); + this.updateApplyButton(); + MinecraftClient.getInstance().setScreen(this); + })) + ) + .dimensions(centerX + 4, this.height - 52, 96, 20) + .build(); + resetDefaultsButton.active = this.serverConfigEditable; + NonModMenuScreens.setTooltip(resetDefaultsButton, "config.needsofnature.tooltip.liquid_gain_reset"); + this.addDrawableChild(resetDefaultsButton); + this.applyButton = ButtonWidget.builder(Text.translatable("config.needsofnature.apply"), button -> { + if (this.serverConfigEditable) { + Map nextGain = this.collectGainMapFromFields(); + Map nextColor = this.collectColorMapFromFields(); + this.config.setLiquidGainByEntity(nextGain); + this.config.setLiquidColorByEntity(nextColor); + NonModMenuScreens.saveServerConfig(this.config); + this.appliedMap = new LinkedHashMap<>(nextGain); + this.appliedColorMap = new LinkedHashMap<>(nextColor); + this.updateApplyButton(); + } + }).dimensions(centerX - 100, this.height - 28, 96, 20).build(); + NonModMenuScreens.setTooltip(this.applyButton, "config.needsofnature.tooltip.apply"); + this.addDrawableChild(this.applyButton); + this.doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 4, this.height - 28, 96, 20) + .build(); + NonModMenuScreens.setTooltip(this.doneButton, "config.needsofnature.tooltip.done_unsaved"); + this.addDrawableChild(this.doneButton); + this.updateApplyButton(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateApplyButton(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + this.drawLiquidGainHeader(context); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void drawLiquidGainHeader(DrawContext context) { + if (this.gainList != null) { + int rowX = this.gainList.getRowLeft(); + int rowWidth = this.gainList.getRowWidth(); + int removeW = 20; + int valueW = 50; + int colorW = 80; + int idW = Math.max(120, rowWidth - removeW - valueW - colorW - 18); + int valueX = rowX + idW + 6; + int colorX = valueX + valueW + 6; + int y = 34; + int color = -4342339; + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.liquid_gain_column.entity"), rowX + 4, y, color); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.liquid_gain_column.ml"), valueX + valueW / 2, y, color + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.liquid_gain_column.color"), colorX + colorW / 2, y, color + ); + } + } + + private void addRow(String idText, String valueText, String colorHexText) { + NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry[] holder = new NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry[1]; + NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry entry = new NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry( + this.textRenderer, idText, valueText, colorHexText, () -> { + NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry current = holder[0]; + if (current != null) { + this.rows.removeIf(row -> row == current); + this.gainList.removeEntryRow(current); + this.updateApplyButton(); + } + } + ); + holder[0] = entry; + entry.setOnChange(this::updateApplyButton); + entry.setEditable(this.serverConfigEditable); + if (isMixedRowId(idText)) { + entry.setMixedRowMode(this.serverConfigEditable, "needsofnature:mixed"); + } + + this.rows.add(entry); + this.gainList.addEntryRow(entry); + } + + private void updateApplyButton() { + if (this.applyButton != null) { + if (!this.serverConfigEditable) { + this.applyButton.active = false; + if (this.doneButton != null) { + this.doneButton.setMessage(Text.translatable("config.needsofnature.done")); + } + } else { + Map currentGain = this.collectGainMapFromFields(); + Map currentColor = this.collectColorMapFromFields(); + boolean hasUnsavedChanges = !currentGain.equals(this.appliedMap) || !currentColor.equals(this.appliedColorMap); + this.applyButton.active = hasUnsavedChanges; + if (this.doneButton != null) { + this.doneButton.setMessage(Text.translatable(hasUnsavedChanges ? "config.needsofnature.done_unsaved" : "config.needsofnature.done")); + } + } + } + } + + private void applyDefaultsToRows() { + Map defaultsMap = new LinkedHashMap<>(this.defaults.getLiquidGainByEntity()); + defaultsMap.putAll(NonLiquidGainOverrides.getGainByEntity()); + defaultsMap.remove("needsofnature:mixed"); + Map defaultColors = new LinkedHashMap<>(this.defaults.getLiquidColorByEntity()); + defaultColors.putAll(NonLiquidGainOverrides.getColorByEntity()); + defaultColors.remove("needsofnature:mixed"); + if (this.gainList != null) { + for (NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry entry : new ArrayList<>(this.rows)) { + this.gainList.removeEntryRow(entry); + } + } + + this.rows.clear(); + this.addRow("needsofnature:mixed", "0", resolveDefaultMixedColorHex()); + LinkedHashSet ids = new LinkedHashSet<>(); + ids.addAll(defaultsMap.keySet()); + ids.addAll(defaultColors.keySet()); + + for (String id : ids) { + int gain = defaultsMap.getOrDefault(id, 0); + String color = defaultColors.getOrDefault(id, ""); + this.addRow(id, String.valueOf(gain), color); + } + + if (this.rows.isEmpty()) { + this.addRow("", "0", ""); + } + } + + private Map collectGainMapFromFields() { + Map map = new LinkedHashMap<>(); + + for (NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry row : this.rows) { + if (row != null) { + String id = row.getIdText(); + if (id != null) { + id = id.trim(); + if (!id.isEmpty() && !isMixedRowId(id)) { + int value = this.parseInt(row.getValueText()); + value = Math.max(0, Math.min(1000, value)); + map.put(id, value); + } + } + } + } + + return map; + } + + private Map collectColorMapFromFields() { + Map map = new LinkedHashMap<>(); + + for (NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry row : this.rows) { + if (row != null) { + String id = row.getIdText(); + if (id != null) { + id = id.trim(); + if (!id.isEmpty()) { + String normalized = NonLiquidColors.normalizeHexColor(row.getColorHexText()); + if (normalized != null) { + map.put(id, normalized); + } + } + } + } + } + + return map; + } + + private List snapshotRows() { + if (this.rows.isEmpty()) { + return null; + } + + List data = new ArrayList<>(); + + for (NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry row : this.rows) { + data.add(new NonModMenuDebugScreens.LiquidGainConfigScreen.RowData(row.getIdText(), row.getValueText(), row.getColorHexText())); + } + + return data; + } + + private List ensureMixedRowPresent( + List source + ) { + List out = new ArrayList<>(); + String mixedColor = resolveDefaultMixedColorHex(); + if (source != null) { + for (NonModMenuDebugScreens.LiquidGainConfigScreen.RowData row : source) { + if (row != null) { + if (isMixedRowId(row.id())) { + String candidate = NonLiquidColors.normalizeHexColor(row.colorHex()); + if (candidate != null) { + mixedColor = candidate; + } + } else { + out.add(row); + } + } + } + } + + out.add(0, new NonModMenuDebugScreens.LiquidGainConfigScreen.RowData("needsofnature:mixed", "0", mixedColor)); + return out; + } + + private static boolean isMixedRowId(String id) { + return id == null ? false : "needsofnature:mixed".equals(id.trim()); + } + + private static String resolveDefaultMixedColorHex() { + String packHex = NonLiquidGainOverrides.getMixedColorHex(); + return packHex != null ? packHex : String.format(Locale.ROOT, "%06X", 15920063); + } + + private int parseInt(String value) { + if (value == null) { + return 0; + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static final class GainList extends ElementListWidget { + private GainList(MinecraftClient client, int width, int height, int top) { + super(client, width, height, top, 24); + this.centerListVertically = false; + } + + private void addEntryRow(NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry entry) { + super.addEntry(entry); + } + + private void removeEntryRow(NonModMenuDebugScreens.LiquidGainConfigScreen.GainList.GainEntry entry) { + super.removeEntry(entry); + } + + public int getRowWidth() { + return 360; + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + private static final class GainEntry + extends net.minecraft.client.gui.widget.ElementListWidget.Entry { + private final TextFieldWidget idField; + private final TextFieldWidget valueField; + private final TextFieldWidget colorHexField; + private final ButtonWidget removeButton; + private final List widgets; + private Runnable onChange; + + private GainEntry(TextRenderer textRenderer, String idText, String valueText, String colorHexText, Runnable onRemove) { + this.idField = new TextFieldWidget(textRenderer, 0, 0, 160, 20, Text.literal("")); + this.idField.setText(idText == null ? "" : idText); + this.idField.setMaxLength(80); + NonModMenuScreens.setTooltip(this.idField, "config.needsofnature.tooltip.liquid_gain_entry_entity"); + this.valueField = new TextFieldWidget(textRenderer, 0, 0, 50, 20, Text.literal("")); + this.valueField.setText(valueText == null ? "0" : valueText); + this.valueField.setMaxLength(6); + NonModMenuScreens.setTooltip(this.valueField, "config.needsofnature.tooltip.liquid_gain_entry_ml"); + this.colorHexField = new TextFieldWidget(textRenderer, 0, 0, 80, 20, Text.literal("")); + this.colorHexField.setText(colorHexText == null ? "" : colorHexText); + this.colorHexField.setPlaceholder(Text.literal("#RRGGBB")); + this.colorHexField.setMaxLength(7); + NonModMenuScreens.setTooltip(this.colorHexField, "config.needsofnature.tooltip.liquid_gain_entry_color"); + this.removeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.remove_icon"), button -> { + if (onRemove != null) { + onRemove.run(); + } + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(this.removeButton, "config.needsofnature.tooltip.remove"); + this.widgets = new ArrayList<>(4); + this.widgets.add(this.idField); + this.widgets.add(this.valueField); + this.widgets.add(this.colorHexField); + this.widgets.add(this.removeButton); + this.idField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.valueField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.colorHexField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + } + + private void setOnChange(Runnable onChange) { + this.onChange = onChange; + } + + private void setEditable(boolean editable) { + this.idField.setEditable(editable); + this.valueField.setEditable(editable); + this.colorHexField.setEditable(editable); + this.removeButton.active = editable; + } + + private void setMixedRowMode(boolean editable, String mixedRowId) { + this.idField.setText(mixedRowId); + this.idField.setEditable(false); + this.valueField.setText("0"); + this.valueField.setEditable(false); + this.colorHexField.setEditable(editable); + this.removeButton.active = false; + } + + private String getIdText() { + return this.idField.getText(); + } + + private String getValueText() { + return this.valueField.getText(); + } + + private String getColorHexText() { + return this.colorHexField.getText(); + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int rowX = this.getContentX(); + int rowY = this.getContentY(); + int rowWidth = this.getContentWidth(); + int widgetY = rowY + 2; + int removeW = this.removeButton.getWidth(); + int valueW = this.valueField.getWidth(); + int colorW = this.colorHexField.getWidth(); + int idW = Math.max(120, rowWidth - removeW - valueW - colorW - 18); + int valueX = rowX + idW + 6; + int colorX = valueX + valueW + 6; + int removeX = colorX + colorW + 6; + this.idField.setX(rowX); + this.idField.setY(widgetY); + this.idField.setWidth(idW); + this.valueField.setX(valueX); + this.valueField.setY(widgetY); + this.colorHexField.setX(colorX); + this.colorHexField.setY(widgetY); + this.removeButton.setX(removeX); + this.removeButton.setY(widgetY); + + for (ClickableWidget widget : this.widgets) { + widget.render(context, mouseX, mouseY, deltaTicks); + } + } + + public List selectableChildren() { + return List.copyOf(this.widgets); + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + + public List children() { + return List.copyOf(this.widgets); + } + } + } + + private static final class ResetLiquidGainConfirmScreen extends Screen { + private final Screen parent; + private final Runnable onConfirm; + + private ResetLiquidGainConfirmScreen(Screen parent, Runnable onConfirm) { + super(Text.translatable("config.needsofnature.liquid_gain_reset_title")); + this.parent = parent; + this.onConfirm = onConfirm; + } + + protected void init() { + int centerX = this.width / 2; + int buttonY = this.height / 2 + 20; + ButtonWidget yesButton = ButtonWidget.builder(Text.translatable("gui.yes"), button -> { + if (this.onConfirm != null) { + this.onConfirm.run(); + } + + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(centerX - 100, buttonY, 96, 20).build(); + this.addDrawableChild(yesButton); + ButtonWidget noButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 4, buttonY, 96, 20) + .build(); + this.addDrawableChild(noButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, this.height / 2 - 20, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.liquid_gain_reset_body"), this.width / 2, this.height / 2 - 4, -4144960 + ); + } + } + + private record RowData(String id, String value, String colorHex) { + } + } + + static class LoadOrderScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private boolean editable; + private final List externalPackOrder = new ArrayList<>(); + private List appliedExternalOrder = List.of(); + private List defaultExternalOrder = List.of(); + private NonModMenuDebugScreens.LoadOrderScreen.PackOrderList packOrderList; + private ButtonWidget resetButton; + + protected LoadOrderScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.loaded_animations.load_order_title")); + this.parent = parent; + this.config = config == null ? new NonConfig() : config; + } + + protected void init() { + this.editable = NonModMenuScreens.canEditLocalPackLoadOrder(); + this.reloadExternalOrder(); + int centerX = this.width / 2; + int listTop = 40; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + this.packOrderList = new NonModMenuDebugScreens.LoadOrderScreen.PackOrderList( + this.client, this.width, listHeight, listTop, this.textRenderer, this::moveUp, this::moveDown, this.editable + ); + this.addDrawableChild(this.packOrderList); + this.rebuildRows(); + this.resetButton = ButtonWidget.builder(Text.translatable("config.needsofnature.liquid_gain_reset"), button -> { + this.externalPackOrder.clear(); + this.externalPackOrder.addAll(this.defaultExternalOrder); + this.rebuildRows(); + this.updateButtons(); + }).dimensions(centerX - 100, this.height - 50, 98, 20).build(); + NonModMenuScreens.setTooltip(this.resetButton, "config.needsofnature.tooltip.loaded_animations.load_order.reset"); + this.addDrawableChild(this.resetButton); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> { + boolean changed = !this.externalPackOrder.equals(this.appliedExternalOrder); + if (this.editable) { + this.config.setExternalNoNPackLoadOrder(this.externalPackOrder); + this.config.save(); + this.appliedExternalOrder = List.copyOf(this.externalPackOrder); + if (changed) { + this.triggerInWorldReload(); + } + } + + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(centerX + 2, this.height - 50, 98, 20).build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + this.updateButtons(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 12, -1); + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.loaded_animations.load_order.summary"), 8, 24, -5197648); + if (!this.editable) { + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.loaded_animations.load_order.read_only"), 8, 33, -14739); + } + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void reloadExternalOrder() { + List discovered = NonExternalPackProvider.listExternalPackFileNames(NonExternalPackProvider.resolveDefaultPacksDir()); + this.defaultExternalOrder = List.copyOf(discovered); + this.externalPackOrder.clear(); + this.externalPackOrder.addAll(NonExternalPackProvider.normalizeConfiguredOrder(this.config.getExternalNoNPackLoadOrder(), discovered)); + this.appliedExternalOrder = List.copyOf(this.externalPackOrder); + } + + private void triggerInWorldReload() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null) { + IntegratedServer server = client.getServer(); + if (server != null) { + server.execute(() -> { + try { + server.getCommandManager().getDispatcher().execute("reload", server.getCommandSource()); + } catch (Exception e) { + server.reloadResources(server.getDataPackManager().getEnabledIds()); + } + }); + } + + client.reloadResources(); + } + } + + private void moveUp(String key) { + this.moveBy(key, -1); + } + + private void moveDown(String key) { + this.moveBy(key, 1); + } + + private void moveBy(String key, int delta) { + if (this.editable && key != null && !key.isBlank()) { + int index = this.externalPackOrder.indexOf(key); + if (index >= 0) { + int target = index + delta; + if (target >= 0 && target < this.externalPackOrder.size()) { + String tmp = this.externalPackOrder.get(index); + this.externalPackOrder.set(index, this.externalPackOrder.get(target)); + this.externalPackOrder.set(target, tmp); + this.rebuildRows(); + this.updateButtons(); + } + } + } + } + + private void rebuildRows() { + if (this.packOrderList != null) { + List rows = new ArrayList<>(); + + for (int i = 0; i < this.externalPackOrder.size(); i++) { + String fileName = this.externalPackOrder.get(i); + rows.add(NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow.external(fileName, i, this.externalPackOrder.size())); + } + + rows.add( + NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow.locked( + "builtin:needsofnature", Text.translatable("config.needsofnature.loaded_animations.load_order.base_non") + ) + ); + this.packOrderList.setRows(rows); + } + } + + private void updateButtons() { + if (this.resetButton != null) { + this.resetButton.active = this.editable && !this.externalPackOrder.equals(this.defaultExternalOrder); + } + } + + private static final class PackOrderList extends ElementListWidget { + private static final int ROW_HEIGHT = 30; + private static final int MIN_ROW_WIDTH = 260; + private static final int MAX_ROW_WIDTH = 420; + private final TextRenderer textRenderer; + private final Consumer onMoveUp; + private final Consumer onMoveDown; + private final boolean editable; + private int preferredRowWidth = 260; + + private PackOrderList( + MinecraftClient client, + int width, + int height, + int top, + TextRenderer textRenderer, + Consumer onMoveUp, + Consumer onMoveDown, + boolean editable + ) { + super(client, width, height, top, 30); + this.textRenderer = textRenderer; + this.onMoveUp = onMoveUp; + this.onMoveDown = onMoveDown; + this.editable = editable; + this.centerListVertically = false; + } + + private void setRows(List rows) { + this.clearEntries(); + int widestContent = 260; + + for (NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow row : rows) { + int requiredWidth = this.textRenderer.getWidth(row.label()) + 28; + if (row.movable()) { + requiredWidth += 42; + } + + widestContent = Math.max(widestContent, requiredWidth); + this.addEntry( + new NonModMenuDebugScreens.LoadOrderScreen.PackOrderList.RowEntry(row, this.textRenderer, this.onMoveUp, this.onMoveDown, this.editable), 30 + ); + } + + this.preferredRowWidth = Math.min(420, widestContent); + } + + public int getRowWidth() { + return Math.min(this.preferredRowWidth, Math.max(120, this.getWidth() - 32)); + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + protected int getScrollbarX() { + return Math.min(this.getWidth() - 8, this.getRowLeft() + this.getRowWidth() + 6); + } + + private static final class OrderArrowButton extends ClickableWidget { + private static final int SIZE = 15; + private final boolean pointsUp; + private final Runnable onPress; + + private OrderArrowButton(boolean pointsUp, Runnable onPress) { + super( + 0, + 0, + 15, + 15, + Text.translatable( + pointsUp ? "config.needsofnature.loaded_animations.load_order.move_up" : "config.needsofnature.loaded_animations.load_order.move_down" + ) + ); + this.pointsUp = pointsUp; + this.onPress = onPress; + } + + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + boolean highlighted = this.active && this.isSelected(); + int background = !this.active ? -2009055168 : (highlighted ? -12035736 : -583779260); + int border = !this.active ? -11513776 : (highlighted ? -7227697 : -10062464); + int arrow = !this.active ? -8947849 : (highlighted ? -1 : -2563609); + int left = this.getX(); + int top = this.getY(); + int right = left + this.getWidth(); + int bottom = top + this.getHeight(); + context.fill(left, top, right, bottom, background); + context.fill(left, top, right, top + 1, border); + context.fill(left, bottom - 1, right, bottom, border); + context.fill(left, top, left + 1, bottom, border); + context.fill(right - 1, top, right, bottom, border); + int centerX = left + this.getWidth() / 2; + int centerY = top + this.getHeight() / 2; + if (this.pointsUp) { + context.fill(centerX, centerY - 3, centerX + 1, centerY - 2, arrow); + context.fill(centerX - 1, centerY - 2, centerX + 2, centerY - 1, arrow); + context.fill(centerX - 2, centerY - 1, centerX + 3, centerY, arrow); + } else { + context.fill(centerX - 2, centerY, centerX + 3, centerY + 1, arrow); + context.fill(centerX - 1, centerY + 1, centerX + 2, centerY + 2, arrow); + context.fill(centerX, centerY + 2, centerX + 1, centerY + 3, arrow); + } + } + + public void onClick(Click click, boolean doubled) { + super.onClick(click, doubled); + if (this.active && this.onPress != null) { + this.onPress.run(); + } + } + + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + this.appendDefaultNarrations(builder); + } + } + + private static final class RowEntry + extends net.minecraft.client.gui.widget.ElementListWidget.Entry { + private final NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow row; + private final TextRenderer textRenderer; + private final Consumer onMoveUp; + private final Consumer onMoveDown; + private final boolean editable; + private final NonModMenuDebugScreens.LoadOrderScreen.PackOrderList.OrderArrowButton upButton; + private final NonModMenuDebugScreens.LoadOrderScreen.PackOrderList.OrderArrowButton downButton; + private final List widgets; + + private RowEntry( + NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow row, + TextRenderer textRenderer, + Consumer onMoveUp, + Consumer onMoveDown, + boolean editable + ) { + this.row = row; + this.textRenderer = textRenderer; + this.onMoveUp = onMoveUp; + this.onMoveDown = onMoveDown; + this.editable = editable; + if (row.movable()) { + this.upButton = new NonModMenuDebugScreens.LoadOrderScreen.PackOrderList.OrderArrowButton(true, () -> { + if (this.onMoveUp != null) { + this.onMoveUp.accept(this.row.key()); + } + }); + this.downButton = new NonModMenuDebugScreens.LoadOrderScreen.PackOrderList.OrderArrowButton(false, () -> { + if (this.onMoveDown != null) { + this.onMoveDown.accept(this.row.key()); + } + }); + NonModMenuScreens.setTooltip(this.upButton, "config.needsofnature.tooltip.loaded_animations.load_order.move_up"); + NonModMenuScreens.setTooltip(this.downButton, "config.needsofnature.tooltip.loaded_animations.load_order.move_down"); + this.widgets = List.of(this.upButton, this.downButton); + } else { + this.upButton = null; + this.downButton = null; + this.widgets = List.of(); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int cardLeft = this.getContentX() + 2; + int cardTop = this.getContentY() + 2; + int cardRight = this.getContentX() + this.getContentWidth() - 2; + int cardBottom = cardTop + 24; + int background = this.row.movable() ? (hovered ? -801490374 : -1339545560) : (hovered ? -802343867 : -1339939021); + int border = this.row.movable() ? -11513776 : -12365718; + context.fill(cardLeft, cardTop, cardRight, cardBottom, background); + context.fill(cardLeft, cardTop, cardRight, cardTop + 1, border); + context.fill(cardLeft, cardBottom - 1, cardRight, cardBottom, border); + context.fill(cardLeft, cardTop, cardLeft + 1, cardBottom, border); + context.fill(cardRight - 1, cardTop, cardRight, cardBottom, border); + int x = cardLeft + 8; + int y = cardTop + 8; + int right = cardRight - 6; + int textRight = this.row.movable() ? right - 40 : right; + int textWidth = Math.max(20, textRight - x); + int color = this.row.movable() ? -2565928 : -7362305; + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(this.row.label().getString(), textWidth), x, y, color); + if (this.upButton != null && this.downButton != null) { + this.upButton.active = this.editable && this.row.canMoveUp(); + this.downButton.active = this.editable && this.row.canMoveDown(); + this.upButton.setX(right - 32); + this.upButton.setY(cardTop + 4); + this.downButton.setX(right - 15); + this.downButton.setY(cardTop + 4); + this.upButton.render(context, mouseX, mouseY, deltaTicks); + this.downButton.render(context, mouseX, mouseY, deltaTicks); + } + } + + public List selectableChildren() { + return List.copyOf(this.widgets); + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + + public List children() { + return List.copyOf(this.widgets); + } + } + } + + private record PackOrderRow(String key, Text label, boolean movable, boolean canMoveUp, boolean canMoveDown) { + private static NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow external(String fileName, int index, int total) { + String safeName = fileName != null && !fileName.isBlank() ? fileName : ""; + return new NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow( + safeName, Text.literal(index + 1 + ". " + safeName), true, index > 0, index < total - 1 + ); + } + + private static NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow locked(String key, Text label) { + String safeKey = key != null && !key.isBlank() ? key : ""; + Text safeLabel = (Text)(label == null ? Text.literal(safeKey) : label); + return new NonModMenuDebugScreens.LoadOrderScreen.PackOrderRow(safeKey, safeLabel, false, false, false); + } + } + } + + static class LoadedAnimationsScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final Set expandedRowKeys = new HashSet<>(); + private final Set expandedPackIds = new HashSet<>(); + private final Map sourceHintByDefinitionId = new LinkedHashMap<>(); + private TextFieldWidget filterField; + private ButtonWidget filterAllButton; + private ButtonWidget filterMissingButton; + private ButtonWidget filterPartialButton; + private ButtonWidget filterOkButton; + private ButtonWidget sortModeButton; + private ButtonWidget sortOrderButton; + private NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationsList loadedAnimationsList; + private List allRows = List.of(); + private final Map packIconByResourcePackId = new LinkedHashMap<>(); + private final Set disabledAnimationPacks = new LinkedHashSet<>(); + private final Set disabledAnimations = new LinkedHashSet<>(); + private int filteredCount = 0; + private boolean editable = true; + private NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter statusFilter = NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.ALL; + private NonModMenuDebugScreens.LoadedAnimationsScreen.SortMode sortMode = NonModMenuDebugScreens.LoadedAnimationsScreen.SortMode.ID; + private boolean sortAscending = true; + private long observedCatalogRevision = Long.MIN_VALUE; + private Text copyFeedbackText = Text.literal(""); + private long copyFeedbackUntilMs = 0L; + private static final long COPY_FEEDBACK_DURATION_MS = 1600L; + private static final int MISSING_ANIM_PREVIEW_COUNT = 3; + private static final int MISSING_MODEL_PREVIEW_COUNT = 3; + private static final int MAX_CONTENT_WIDTH = 500; + + protected LoadedAnimationsScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.loaded_animations_title")); + this.parent = parent; + this.config = config; + } + + protected void init() { + String previousFilter = this.filterField == null ? "" : this.filterField.getText(); + NonServerSettingsClient.requestRefresh(); + this.editable = NonModMenuScreens.canEditServerGameplaySettings(); + this.reloadDisabledAnimationState(); + int centerX = this.width / 2; + int filterY = 34; + int contentWidth = this.contentWidth(); + int contentLeft = (this.width - contentWidth) / 2; + int contentRight = contentLeft + contentWidth; + this.filterField = new TextFieldWidget( + this.textRenderer, contentLeft, filterY, contentWidth, 20, Text.translatable("config.needsofnature.loaded_animations_search") + ); + this.filterField.setMaxLength(128); + this.filterField.setText(previousFilter); + this.filterField.setChangedListener(this::applyFilter); + this.addDrawableChild(this.filterField); + int controlsY = filterY + 24; + int buttonH = 20; + int smallButtonW = 62; + int filterGap = 4; + int startX = contentLeft; + this.filterAllButton = ButtonWidget.builder(Text.literal("全部"), button -> { + this.statusFilter = NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.ALL; + this.updateControlButtons(); + this.applyFilter(this.filterField.getText()); + }).dimensions(startX, controlsY, smallButtonW, buttonH).build(); + this.addDrawableChild(this.filterAllButton); + this.filterMissingButton = ButtonWidget.builder(Text.literal("缺失"), button -> { + this.statusFilter = NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.MISSING; + this.updateControlButtons(); + this.applyFilter(this.filterField.getText()); + }).dimensions(startX + smallButtonW + filterGap, controlsY, smallButtonW, buttonH).build(); + this.addDrawableChild(this.filterMissingButton); + this.filterPartialButton = ButtonWidget.builder(Text.literal("部分"), button -> { + this.statusFilter = NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.PARTIAL; + this.updateControlButtons(); + this.applyFilter(this.filterField.getText()); + }).dimensions(startX + (smallButtonW + filterGap) * 2, controlsY, smallButtonW, buttonH).build(); + this.addDrawableChild(this.filterPartialButton); + this.filterOkButton = ButtonWidget.builder(Text.literal("确定"), button -> { + this.statusFilter = NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.OK; + this.updateControlButtons(); + this.applyFilter(this.filterField.getText()); + }).dimensions(startX + (smallButtonW + filterGap) * 3, controlsY, smallButtonW, buttonH).build(); + this.addDrawableChild(this.filterOkButton); + int orderButtonW = 56; + int sortButtonW = 116; + int orderX = contentRight - orderButtonW; + int sortX = orderX - 6 - sortButtonW; + this.sortModeButton = ButtonWidget.builder(this.sortModeLabel(), button -> { + this.sortMode = switch (this.sortMode) { + case ID -> NonModMenuDebugScreens.LoadedAnimationsScreen.SortMode.STATUS; + case STATUS -> NonModMenuDebugScreens.LoadedAnimationsScreen.SortMode.MISSING; + case MISSING -> NonModMenuDebugScreens.LoadedAnimationsScreen.SortMode.ENTITIES; + case ENTITIES -> NonModMenuDebugScreens.LoadedAnimationsScreen.SortMode.ID; + }; + this.updateControlButtons(); + this.applyFilter(this.filterField.getText(), false); + }).dimensions(sortX, controlsY, sortButtonW, buttonH).build(); + this.addDrawableChild(this.sortModeButton); + this.sortOrderButton = ButtonWidget.builder(this.sortOrderLabel(), button -> { + this.sortAscending = !this.sortAscending; + this.updateControlButtons(); + this.applyFilter(this.filterField.getText(), false); + }).dimensions(orderX, controlsY, orderButtonW, buttonH).build(); + this.addDrawableChild(this.sortOrderButton); + int listTop = controlsY + buttonH + 6; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + this.loadedAnimationsList = new NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationsList( + this.client, + this.width, + listHeight, + listTop, + this.textRenderer, + this::togglePackExpanded, + this::toggleRowExpanded, + this::copyRowMissingDetails, + this::togglePackEnabled, + this::toggleAnimationEnabled, + () -> this.editable + ); + this.addDrawableChild(this.loadedAnimationsList); + this.rebuildSourceHints(); + this.rebuildRows(); + this.updateControlButtons(); + this.applyFilter(previousFilter); + ButtonWidget doneButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent) + ) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 12, -1); + int contentWidth = this.contentWidth(); + int contentLeft = (this.width - contentWidth) / 2; + context.drawTextWithShadow( + this.textRenderer, + Text.translatable("config.needsofnature.loaded_animations_summary", new Object[]{this.allRows.size(), this.filteredCount}), + contentLeft, + 22, + -5197648 + ); + if (this.copyFeedbackText != null && !this.copyFeedbackText.getString().isBlank() && System.currentTimeMillis() < this.copyFeedbackUntilMs) { + int feedbackWidth = this.textRenderer.getWidth(this.copyFeedbackText); + int feedbackX = Math.max(contentLeft, contentLeft + contentWidth - feedbackWidth); + context.drawTextWithShadow(this.textRenderer, this.copyFeedbackText, feedbackX, 22, -6560902); + } + } + + private int contentWidth() { + return Math.min(500, Math.max(120, this.width - 16)); + } + + public void tick() { + super.tick(); + long revision = NonServerSettingsClient.catalogRevision(); + if (revision != this.observedCatalogRevision) { + this.observedCatalogRevision = revision; + this.reloadDisabledAnimationState(); + this.rebuildRows(); + this.applyFilter(this.filterField == null ? "" : this.filterField.getText(), false); + } + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void updateControlButtons() { + if (this.filterAllButton != null) { + this.filterAllButton.setMessage(this.statusFilterLabel(NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.ALL)); + this.filterAllButton.active = this.statusFilter != NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.ALL; + } + + if (this.filterMissingButton != null) { + this.filterMissingButton.setMessage(this.statusFilterLabel(NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.MISSING)); + this.filterMissingButton.active = this.statusFilter != NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.MISSING; + } + + if (this.filterPartialButton != null) { + this.filterPartialButton.setMessage(this.statusFilterLabel(NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.PARTIAL)); + this.filterPartialButton.active = this.statusFilter != NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.PARTIAL; + } + + if (this.filterOkButton != null) { + this.filterOkButton.setMessage(this.statusFilterLabel(NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.OK)); + this.filterOkButton.active = this.statusFilter != NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter.OK; + } + + if (this.sortModeButton != null) { + this.sortModeButton.setMessage(this.sortModeLabel()); + } + + if (this.sortOrderButton != null) { + this.sortOrderButton.setMessage(this.sortOrderLabel()); + } + } + + private Text statusFilterLabel(NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter mode) { + boolean selected = this.statusFilter == mode; + + String base = switch (mode) { + case ALL -> "All"; + case MISSING -> "Missing"; + case PARTIAL -> "Partial"; + case OK -> "OK"; + }; + return Text.literal(selected ? "[" + base + "]" : base); + } + + private Text sortModeLabel() { + String value = switch (this.sortMode) { + case ID -> "ID"; + case STATUS -> "Status"; + case MISSING -> "Missing"; + case ENTITIES -> "Entities"; + }; + return Text.literal("Sort: " + value); + } + + private Text sortOrderLabel() { + return Text.literal(this.sortAscending ? "Asc" : "Desc"); + } + + private void rebuildSourceHints() { + this.sourceHintByDefinitionId.clear(); + ResourceManager manager = this.resolveResourceManagerForAnimdefs(); + if (manager != null) { + try { + Map> resources = manager.findAllResources("afw_animdefs", id -> id.getPath().endsWith(".json")); + + for (Entry> entry : resources.entrySet()) { + Identifier definitionId = definitionIdFromFile(entry.getKey()); + String sourceHint = summarizeSources(entry.getValue()); + this.sourceHintByDefinitionId.put(definitionId.toString(), sourceHint); + } + } catch (RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to resolve animation definition source hints.", e); + } + } + } + + private ResourceManager resolveResourceManagerForAnimdefs() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return null; + } + + try { + if (client.getServer() != null) { + return client.getServer().getResourceManager(); + } + } catch (RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to resolve server resource manager for loaded animations.", e); + } + + try { + return client.getResourceManager(); + } catch (RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to resolve client resource manager for loaded animations.", e); + return null; + } + } + + private static Identifier definitionIdFromFile(Identifier fileId) { + if (fileId == null) { + return AnimationEngine.packId("unknown"); + } + + String path = fileId.getPath(); + String prefix = "afw_animdefs/"; + if (path.startsWith(prefix)) { + path = path.substring(prefix.length()); + } + + if (path.endsWith(".json")) { + path = path.substring(0, path.length() - 5); + } + + return Identifier.of(fileId.getNamespace(), path); + } + + private static String summarizeSources(List resources) { + if (resources != null && !resources.isEmpty()) { + LinkedHashSet sources = new LinkedHashSet<>(); + + for (Resource resource : resources) { + if (resource != null) { + sources.add(normalizeSourceId(resource.getPackId())); + } + } + + if (sources.isEmpty()) { + return "unknown"; + } + + List list = new ArrayList<>(sources); + int take = Math.min(2, list.size()); + String summary = String.join(", ", list.subList(0, take)); + int more = list.size() - take; + if (more > 0) { + summary = summary + ", +" + more + " more"; + } + + return summary; + } else { + return "unknown"; + } + } + + private static String normalizeSourceId(String rawPackId) { + if (rawPackId == null || rawPackId.isBlank()) { + return "unknown"; + } + + if ("vanilla".equals(rawPackId)) { + return "builtin:vanilla"; + } + + if (!rawPackId.startsWith("needsofnature/")) { + return rawPackId; + } + + String value = rawPackId.substring("needsofnature/".length()); + int slash = value.lastIndexOf(47); + String tail = slash >= 0 && slash < value.length() - 1 ? value.substring(slash + 1) : value; + int underscore = tail.indexOf(95); + if (underscore > 0) { + boolean numericPrefix = true; + + for (int i = 0; i < underscore; i++) { + char c = tail.charAt(i); + if (c < '0' || c > '9') { + numericPrefix = false; + break; + } + } + + if (numericPrefix && underscore + 1 < tail.length()) { + tail = tail.substring(underscore + 1); + } + } + + return "nonpack:" + tail; + } + + private void reloadDisabledAnimationState() { + this.disabledAnimationPacks.clear(); + this.disabledAnimations.clear(); + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null && NonServerSettingsClient.hasAnimationCatalog()) { + for (AnimationCatalogData.Entry entry : NonServerSettingsClient.animationCatalogSnapshot()) { + if (entry != null) { + if (entry.packDisabled() && entry.pack() != null) { + this.disabledAnimationPacks.add(entry.pack().id()); + } + + if (entry.animationDisabled()) { + this.disabledAnimations.add(entry.id()); + } + } + } + } else if (this.config != null) { + this.disabledAnimationPacks.addAll(this.config.getDisabledAnimationPacks()); + this.disabledAnimations.addAll(this.config.getDisabledAnimations()); + } + } + + private void togglePackEnabled(String packId) { + if (this.editable && packId != null && !packId.isBlank()) { + if (this.disabledAnimationPacks.contains(packId)) { + this.disabledAnimationPacks.remove(packId); + } else { + this.disabledAnimationPacks.add(packId); + } + + this.saveDisabledAnimationState(); + } + } + + private void toggleAnimationEnabled(String animationId) { + if (this.editable && animationId != null && !animationId.isBlank()) { + if (this.disabledAnimations.contains(animationId)) { + this.disabledAnimations.remove(animationId); + } else { + this.disabledAnimations.add(animationId); + } + + this.saveDisabledAnimationState(); + } + } + + private void saveDisabledAnimationState() { + if (this.config != null) { + this.config.setDisabledAnimationPacks(new ArrayList<>(this.disabledAnimationPacks)); + this.config.setDisabledAnimations(new ArrayList<>(this.disabledAnimations)); + NonModMenuScreens.saveServerConfig(this.config); + this.rebuildRows(); + this.applyFilter(this.filterField == null ? "" : this.filterField.getText(), false); + } + } + + @Nullable + private Identifier resolvePackIcon(String resourcePackId) { + if (resourcePackId == null || resourcePackId.isBlank()) { + return null; + } + + if (this.packIconByResourcePackId.containsKey(resourcePackId)) { + return this.packIconByResourcePackId.get(resourcePackId); + } + + Identifier textureId = this.loadPackIcon(resourcePackId); + this.packIconByResourcePackId.put(resourcePackId, textureId); + return textureId; + } + + @Nullable + private Identifier loadPackIcon(String resourcePackId) { + MinecraftClient client = MinecraftClient.getInstance(); + ResourceManager manager = this.resolveResourceManagerForAnimdefs(); + if (client != null && manager != null) { + try (Stream stream = manager.streamResourcePacks()) { + ResourcePack pack = stream.filter(candidate -> resourcePackId.equals(candidate.getId())).findFirst().orElse(null); + if (pack == null) { + return null; + } + + try (InputStream input = openRootResource(pack, "pack.png")) { + if (input != null) { + NativeImage image = NativeImage.read(input); + Identifier id = Identifier.of("needsofnature", "dynamic/animation_pack_icons/" + sanitizeTexturePath(resourcePackId)); + client.getTextureManager().registerTexture(id, new NativeImageBackedTexture(id::toString, image)); + return id; + } else { + return null; + } + } + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to load animation pack icon for {}", resourcePackId, e); + return null; + } + } else { + return null; + } + } + + @Nullable + private static InputStream openRootResource(ResourcePack pack, String fileName) { + if (pack != null && fileName != null) { + InputSupplier supplier = pack.openRoot(new String[]{fileName}); + if (supplier == null) { + return null; + } + + try { + return (InputStream)supplier.get(); + } catch (IOException | RuntimeException e) { + return null; + } + } else { + return null; + } + } + + private static String sanitizeTexturePath(String raw) { + if (raw != null && !raw.isBlank()) { + String lower = raw.toLowerCase(Locale.ROOT); + StringBuilder sb = new StringBuilder(lower.length()); + + for (int i = 0; i < lower.length(); i++) { + char c = lower.charAt(i); + if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' && c != '-' && c != '/') { + sb.append('_'); + } else { + sb.append(c); + } + } + + return sb.isEmpty() ? "unknown" : sb.toString(); + } else { + return "unknown"; + } + } + + private void copyRowMissingDetails(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow row) { + if (row != null && row.hasMissingDetails()) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + String text = String.join("\n", row.missingLines()); + client.keyboard.setClipboard(text); + this.showCopyFeedback(Text.translatable("config.needsofnature.loaded_animations.copied_missing")); + } + } + } + + private void showCopyFeedback(Text message) { + this.copyFeedbackText = (Text)(message == null ? Text.literal("") : message); + this.copyFeedbackUntilMs = System.currentTimeMillis() + 1600L; + } + + private void rebuildRows() { + MinecraftClient client = MinecraftClient.getInstance(); + List entries; + if (client != null && client.world != null && NonServerSettingsClient.hasAnimationCatalog()) { + entries = NonServerSettingsClient.animationCatalogSnapshot(); + this.observedCatalogRevision = NonServerSettingsClient.catalogRevision(); + } else { + entries = AnimationCatalogData.capture(this.config).animations(); + } + + List rows = new ArrayList<>(entries.size()); + + for (AnimationCatalogData.Entry entry : entries) { + if (entry != null) { + rows.add(this.buildRow(entry)); + } + } + + this.allRows = List.copyOf(rows); + } + + private NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow buildRow(AnimationCatalogData.Entry definition) { + List actors = buildActorDisplays(definition.actors()); + Integer peakStage = definition.peakStage(); + String peakLabel = peakStage == null ? "-" : "p" + peakStage; + NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceCheck check = evaluateResourceCheck(definition); + NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus status = check.status(); + String key = definition.id(); + Identifier definitionId = Identifier.tryParse(key); + AnimationCatalogData.Pack packInfo = definition.pack(); + String packId = packInfo == null ? "unknown" : packInfo.id(); + String resourcePackId = packInfo == null ? "unknown" : packInfo.resourcePackId(); + String packName = packInfo == null ? "Unknown Pack" : packInfo.name(); + String packAuthor = packInfo == null ? "Unknown" : packInfo.author(); + boolean packDisabled = this.disabledAnimationPacks.contains(packId); + boolean animationDisabled = this.disabledAnimations.contains(key); + String source = definitionId == null ? "unknown:" : definitionId.getNamespace() + ":"; + String title = definitionId == null ? key : definitionId.getPath(); + title = title + (packDisabled ? " [pack off]" : (animationDisabled ? " [off]" : "")); + List detailLines = new ArrayList<>(); + detailLines.add("stages: " + definition.stages().size() + " | peak: " + peakLabel); + detailLines.add(summarizeCatalogStages(definition.stages())); + detailLines.addAll(summarizeCatalogActorRoles(definition.actors())); + String resourcesLine = "resources: anim " + + check.animationFound + + "/" + + check.animationTotal + + " | model " + + check.modelFound + + "/" + + check.modelTotal; + if (check.modelUnchecked > 0) { + resourcesLine = resourcesLine + " | model n/a " + check.modelUnchecked; + } + + List contentTags = definition.contentTags() == null + ? List.of() + : definition.contentTags().stream().filter(Objects::nonNull).map(String::trim).filter(tag -> !tag.isEmpty()).distinct().toList(); + String blockRequirementLine = definition.blockRequirement(); + List infoLines = new ArrayList<>(); + if (!definition.attackEligible()) { + infoLines.add("attacks: disabled"); + } + + if (blockRequirementLine != null) { + infoLines.add(blockRequirementLine); + } + + if (definition.waterRequirement() != null && !"none".equals(definition.waterRequirement())) { + infoLines.add("water: " + definition.waterRequirement()); + } + + if (status != NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.OK) { + infoLines.add(resourcesLine); + } + + if (!check.localModelDetails().isEmpty()) { + int take = Math.min(3, check.localModelDetails().size()); + String localSummary = String.join(", ", check.localModelDetails().subList(0, take)); + int remaining = check.localModelDetails().size() - take; + infoLines.add("local models: " + localSummary + (remaining > 0 ? ", +" + remaining : "")); + } + + boolean hasMissingDetails = check.hasMissingDetails(); + List missingLines = check.missingLinesStructured(); + int entityTypeCount = definition.actors().stream().mapToInt(actor -> actor.entityTypes() == null ? 0 : actor.entityTypes().size()).sum(); + int missingCount = check.totalMissingCount(); + String actorSearch = actors.stream().map(NonModMenuDebugScreens.LoadedAnimationsScreen.ActorDisplay::searchText).collect(Collectors.joining(" ")); + String searchBlob = (definition.id() + + " " + + actorSearch + + " " + + String.join(" ", infoLines) + + " " + + String.join(" ", detailLines) + + " " + + String.join(" ", contentTags) + + " " + + resourcesLine + + " " + + packName + + " " + + packAuthor + + " " + + this.sourceHintByDefinitionId.getOrDefault(key, "unknown") + + (hasMissingDetails ? " " + String.join(" ", missingLines) : "")) + .toLowerCase(Locale.ROOT); + return new NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow( + key, + packId, + resourcePackId, + packName, + packAuthor, + source, + title, + actors, + List.copyOf(detailLines), + List.copyOf(infoLines), + List.copyOf(contentTags), + missingLines, + searchBlob, + status, + missingCount, + entityTypeCount, + packDisabled, + animationDisabled, + hasMissingDetails, + this.expandedRowKeys.contains(key) + ); + } + + private void applyFilter(String rawFilter) { + this.applyFilter(rawFilter, true); + } + + private void applyFilter(String rawFilter, boolean resetScroll) { + String filter = rawFilter == null ? "" : rawFilter.trim().toLowerCase(Locale.ROOT); + List filtered = new ArrayList<>(); + + for (NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow row : this.allRows) { + if (row.matches(filter) && row.matchesStatus(this.statusFilter)) { + filtered.add(row); + } + } + + filtered.sort(this.groupedRowComparator()); + this.filteredCount = filtered.size(); + if (this.loadedAnimationsList != null) { + this.loadedAnimationsList.setRows(this.buildGroupedDisplayRows(filtered)); + if (resetScroll) { + this.loadedAnimationsList.setScrollY(0.0); + } + } + } + + private List buildGroupedDisplayRows( + List filtered + ) { + if (filtered != null && !filtered.isEmpty()) { + Map totalsByPack = new LinkedHashMap<>(); + Map disabledByPack = new LinkedHashMap<>(); + + for (NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow row : this.allRows) { + totalsByPack.merge(row.packId(), 1, Integer::sum); + if (row.animationDisabled()) { + disabledByPack.merge(row.packId(), 1, Integer::sum); + } + } + + List out = new ArrayList<>(); + String currentPackId = null; + List currentRows = new ArrayList<>(); + + for (NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow row : filtered) { + if (!Objects.equals(currentPackId, row.packId())) { + this.appendPackGroup(out, currentPackId, currentRows, totalsByPack, disabledByPack); + currentRows.clear(); + currentPackId = row.packId(); + } + + currentRows.add(row); + } + + this.appendPackGroup(out, currentPackId, currentRows, totalsByPack, disabledByPack); + return List.copyOf(out); + } else { + return List.of(); + } + } + + private void appendPackGroup( + List out, + @Nullable String packId, + List rows, + Map totalsByPack, + Map disabledByPack + ) { + if (out != null && packId != null && rows != null && !rows.isEmpty()) { + NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow first = rows.getFirst(); + out.add( + new NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow( + "pack:" + packId, + packId, + first.resourcePackId(), + first.packName(), + first.packAuthor(), + rows.size(), + totalsByPack.getOrDefault(packId, rows.size()), + disabledByPack.getOrDefault(packId, 0), + this.disabledAnimationPacks.contains(packId), + this.expandedPackIds.contains(packId), + this.resolvePackIcon(first.resourcePackId()) + ) + ); + if (this.expandedPackIds.contains(packId)) { + out.addAll(rows); + } + } + } + + private void togglePackExpanded(String packId) { + if (packId != null && !packId.isBlank()) { + if (this.expandedPackIds.contains(packId)) { + this.expandedPackIds.remove(packId); + } else { + this.expandedPackIds.add(packId); + } + + this.applyFilter(this.filterField == null ? "" : this.filterField.getText(), false); + } + } + + private void toggleRowExpanded(String key) { + if (key != null && !key.isBlank()) { + if (this.expandedRowKeys.contains(key)) { + this.expandedRowKeys.remove(key); + } else { + this.expandedRowKeys.add(key); + } + + this.rebuildRows(); + this.applyFilter(this.filterField == null ? "" : this.filterField.getText(), false); + } + } + + private Comparator rowComparator() { + Comparator comparator = switch (this.sortMode) { + case ID -> Comparator.comparing(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow::key); + case STATUS -> Comparator.comparingInt(row -> row.status().sortRank()) + .thenComparing(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow::key); + case MISSING -> Comparator.comparingInt(row -> row.missingCount()) + .thenComparing(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow::key); + case ENTITIES -> Comparator.comparingInt(row -> row.entityTypeCount()) + .thenComparing(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow::key); + }; + return this.sortAscending ? comparator : comparator.reversed(); + } + + private Comparator groupedRowComparator() { + return Comparator.comparing(row -> row.packName().toLowerCase(Locale.ROOT)) + .thenComparing(row -> row.packId().toLowerCase(Locale.ROOT)) + .thenComparing(this.rowComparator()); + } + + private static NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceCheck evaluateResourceCheck(AnimationCatalogData.Entry definition) { + LinkedHashSet stageIds = new LinkedHashSet<>(); + if (definition.stages() != null) { + for (AnimationCatalogData.Stage stage : definition.stages()) { + if (stage != null) { + Identifier id = Identifier.tryParse(stage.animationId()); + if (id != null) { + stageIds.add(id); + } + } + } + } + + if (stageIds.isEmpty()) { + Identifier id = Identifier.tryParse(definition.id()); + if (id != null) { + stageIds.add(id); + } + } + + int animationTotal = 0; + int animationFound = 0; + int modelTotal = 0; + int modelFound = 0; + int modelUnchecked = 0; + LinkedHashSet missingAnimations = new LinkedHashSet<>(); + LinkedHashSet missingModels = new LinkedHashSet<>(); + LinkedHashSet localModelDetails = new LinkedHashSet<>(); + + for (AnimationCatalogData.Actor actor : definition.actors() == null ? List.of() : definition.actors()) { + if (actor != null) { + String actorKey = actor.key(); + List runtimeTypes = normalizeEntityTypesForRuntime(actor.entityTypes()); + if (runtimeTypes.isEmpty()) { + modelUnchecked++; + + for (Identifier stageId : stageIds) { + Identifier animationResource = GeckoResourceResolver.resolveAnimationResource(stageId, actorKey, null, false); + String animationKey = GeckoResourceResolver.resolveAnimationKey(animationResource, stageId, actorKey, null, false); + animationTotal++; + if (GeckoResourceResolver.hasBakedAnimationKey(animationResource, animationKey)) { + animationFound++; + } else { + missingAnimations.add("actor=" + actorKey + " -> " + missingAnimationDetail(animationResource, animationKey)); + } + } + } else { + for (Identifier entityTypeId : runtimeTypes) { + List modelProviders = modelProvidersFor(definition.pack(), entityTypeId); + NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage coverage = evaluateModelCoverage( + actor.requiredTags(), entityTypeId, modelProviders + ); + modelTotal++; + if (coverage.found()) { + modelFound++; + } else { + missingModels.add(coverage.missingDetail()); + } + + localModelDetails.addAll(coverage.localModelDetails()); + + for (Identifier stageId : stageIds) { + Identifier animationResource = GeckoResourceResolver.resolveAnimationResource(stageId, actorKey, entityTypeId, false); + String animationKey = GeckoResourceResolver.resolveAnimationKey(animationResource, stageId, actorKey, entityTypeId, false); + animationTotal++; + if (GeckoResourceResolver.hasBakedAnimationKey(animationResource, animationKey)) { + animationFound++; + } else { + missingAnimations.add( + "actor=" + actorKey + " type=" + entityTypeId + " -> " + missingAnimationDetail(animationResource, animationKey) + ); + } + } + } + } + } + } + + return new NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceCheck( + animationTotal, + animationFound, + modelTotal, + modelFound, + modelUnchecked, + List.copyOf(missingAnimations), + List.copyOf(missingModels), + List.copyOf(localModelDetails) + ); + } + + private static String missingAnimationDetail(Identifier resource, String animationKey) { + return animationKey != null && !animationKey.isBlank() ? resource + "#" + animationKey : resource + " (actor clip missing)"; + } + + private static List normalizeEntityTypesForRuntime(List entityTypes) { + if (entityTypes != null && !entityTypes.isEmpty()) { + LinkedHashSet normalized = new LinkedHashSet<>(); + + for (String raw : entityTypes) { + if (raw != null && !raw.isBlank()) { + Identifier id = Identifier.tryParse(raw); + if (id != null) { + normalized.add(id); + } + } + } + + List sorted = new ArrayList<>(normalized); + sorted.sort(Comparator.comparing(Identifier::toString)); + return List.copyOf(sorted); + } else { + return List.of(); + } + } + + private static NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage evaluateModelCoverage( + List requiredTags, Identifier entityTypeId, List modelProviders + ) { + Set tags = requiredTags == null ? Set.of() : new HashSet<>(requiredTags); + return evaluateModelCoverage(tags, entityTypeId, modelProviders); + } + + private static NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage evaluateModelCoverage( + Set requiredTags, Identifier entityTypeId, List modelProviders + ) { + List modelRoots = modelProviders.stream().map(NonModMenuDebugScreens.LoadedAnimationsScreen.ModelProvider::root).toList(); + GeckoResourceResolver.ModelAndTexture maleModel = GeckoResourceResolver.resolveModelAndTexture( + entityTypeId, modelRoots, List.of(entityTypeId.getPath()), GenderedModelResolver.ModelGender.MALE + ); + GeckoResourceResolver.ModelAndTexture femaleModel = GeckoResourceResolver.resolveModelAndTexture( + entityTypeId, modelRoots, List.of(entityTypeId.getPath()), GenderedModelResolver.ModelGender.FEMALE + ); + GeckoResourceResolver.ModelAndTexture bothModel = GeckoResourceResolver.resolveModelAndTexture( + entityTypeId, modelRoots, List.of(entityTypeId.getPath()), GenderedModelResolver.ModelGender.BOTH + ); + NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement requirement = genderRequirement(requiredTags); + + boolean found = switch (requirement) { + case ANY -> !maleModel.missingModel() && !femaleModel.missingModel(); + case MALE_ONLY -> !maleModel.missingModel(); + case FEMALE_ONLY -> !femaleModel.missingModel(); + case BOTH_ONLY -> !bothModel.missingModel(); + }; + LinkedHashSet localDetails = new LinkedHashSet<>(); + switch (requirement) { + case ANY: + addLocalModelDetail(localDetails, entityTypeId, "♂", maleModel.modelRoot(), modelProviders); + addLocalModelDetail(localDetails, entityTypeId, "♀", femaleModel.modelRoot(), modelProviders); + break; + case MALE_ONLY: + addLocalModelDetail(localDetails, entityTypeId, "♂", maleModel.modelRoot(), modelProviders); + break; + case FEMALE_ONLY: + addLocalModelDetail(localDetails, entityTypeId, "♀", femaleModel.modelRoot(), modelProviders); + break; + case BOTH_ONLY: + addLocalModelDetail(localDetails, entityTypeId, "♂♀", bothModel.modelRoot(), modelProviders); + } + + if (found) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage.present(List.copyOf(localDetails)); + } + + String detail = switch (requirement) { + case ANY -> entityTypeId + " (need base or both .m/.f)"; + case MALE_ONLY -> entityTypeId + " (need base or .m)"; + case FEMALE_ONLY -> entityTypeId + " (need base or .f)"; + case BOTH_ONLY -> entityTypeId + " (need base or .mf; .m/.f can fall back)"; + }; + return NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage.missingCoverage(detail, List.copyOf(localDetails)); + } + + private static List modelProvidersFor( + AnimationCatalogData.Pack pack, Identifier entityTypeId + ) { + if (pack != null && entityTypeId != null) { + LinkedHashMap providers = new LinkedHashMap<>(); + Identifier localRoot = pack.localModelRoot() == null ? null : Identifier.tryParse(pack.localModelRoot()); + if (localRoot != null) { + providers.put(localRoot, ""); + } + + if (pack.modelImports() != null) { + for (AnimationCatalogData.ModelImport modelImport : pack.modelImports()) { + if (modelImport != null && modelImport.entityTypes() != null && modelImport.entityTypes().contains(entityTypeId.toString())) { + Identifier root = modelImport.modelRoot() == null ? null : Identifier.tryParse(modelImport.modelRoot()); + if (root != null) { + providers.putIfAbsent(root, modelImport.packId()); + } + } + } + } + + return providers.entrySet() + .stream() + .map(entry -> new NonModMenuDebugScreens.LoadedAnimationsScreen.ModelProvider(entry.getKey(), entry.getValue())) + .toList(); + } else { + return List.of(); + } + } + + private static void addLocalModelDetail( + LinkedHashSet details, + Identifier entityTypeId, + String gender, + @Nullable Identifier selectedRoot, + List providers + ) { + if (selectedRoot != null) { + String provider = providers.stream() + .filter(candidate -> candidate.root().equals(selectedRoot)) + .map(NonModMenuDebugScreens.LoadedAnimationsScreen.ModelProvider::label) + .findFirst() + .orElse(selectedRoot.toString()); + String detail = entityTypeId.getPath() + " " + gender; + if (!provider.isBlank()) { + detail = detail + " (" + provider + ")"; + } + + details.add(detail); + } + } + + private static NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement genderRequirement(Set requiredTags) { + if (requiredTags != null && !requiredTags.isEmpty()) { + boolean needsMale = requiredTags.contains("gender.male"); + boolean needsFemale = requiredTags.contains("gender.female"); + if (needsMale && needsFemale) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement.BOTH_ONLY; + } else if (needsMale && !needsFemale) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement.MALE_ONLY; + } else { + return needsFemale && !needsMale + ? NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement.FEMALE_ONLY + : NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement.ANY; + } + } else { + return NonModMenuDebugScreens.LoadedAnimationsScreen.GenderRequirement.ANY; + } + } + + private static String summarizeCatalogStages(List stages) { + if (stages != null && !stages.isEmpty()) { + List parts = new ArrayList<>(); + int previewCount = Math.min(3, stages.size()); + + for (int i = 0; i < previewCount; i++) { + AnimationCatalogData.Stage stage = stages.get(i); + String stageLabel = "p" + (stage.stageNumber() == null ? "?" : stage.stageNumber()); + String loopLabel = stage.loop() ? "loop" : "once"; + String cycle = stage.cycleTicks() > 0.0 ? String.format(Locale.ROOT, "%.2fs", stage.cycleTicks() / 20.0) : "-"; + String speed = String.format(Locale.ROOT, "%.2fx", stage.speed()); + parts.add(stageLabel + " " + loopLabel + " " + cycle + " @" + speed); + } + + if (stages.size() > previewCount) { + parts.add("+" + (stages.size() - previewCount) + " more"); + } + + return "loop/cycle: " + String.join(", ", parts); + } else { + return "loop/cycle: -"; + } + } + + private static List summarizeCatalogActorRoles(List actors) { + List injectors = new ArrayList<>(); + List receivers = new ArrayList<>(); + if (actors != null) { + for (AnimationCatalogData.Actor actor : actors) { + if (actor != null) { + String actorKey = actor.key() != null && !actor.key().isBlank() ? actor.key() : "actor"; + String injectorType = actor.injectorType(); + if (injectorType != null && !injectorType.isBlank() && !"NONE".equalsIgnoreCase(injectorType)) { + injectors.add(actorKey + " (" + injectorType.toUpperCase(Locale.ROOT) + ")"); + } + + if (actor.receiver()) { + receivers.add(actorKey); + } + } + } + } + + return List.of( + "injectors: " + (injectors.isEmpty() ? "none" : String.join(", ", injectors)), + "receivers: " + (receivers.isEmpty() ? "none" : String.join(", ", receivers)) + ); + } + + private static List buildActorDisplays(List actors) { + if (actors != null && !actors.isEmpty()) { + List displays = new ArrayList<>(actors.size()); + + for (int actorIndex = 0; actorIndex < actors.size(); actorIndex++) { + AnimationCatalogData.Actor actor = actors.get(actorIndex); + if (actor != null) { + List entityTypes = new ArrayList<>(); + + for (Identifier id : normalizeEntityTypesForRuntime(actor.entityTypes())) { + String displayName = Registries.ENTITY_TYPE.getOptionalValue(id).map(type -> type.getName().getString()).orElse(id.getPath()); + entityTypes.add( + new NonModMenuDebugScreens.LoadedAnimationsScreen.EntityTypeDisplay( + id.getNamespace(), displayName, id.toString(), "minecraft".equals(id.getNamespace()) + ) + ); + } + + String key = actor.key() != null && !actor.key().isBlank() ? actor.key() : "actor"; + NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge gender = resolveActorGenderBadge(actor.requiredTags(), actor.requiredTagsAny()); + if (gender == NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.NONE + && actor.receiver() + && hasOtherVInjector(actors, actorIndex)) { + gender = NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.FEMALE_IMPLIED; + } + + displays.add(new NonModMenuDebugScreens.LoadedAnimationsScreen.ActorDisplay(key, List.copyOf(entityTypes), gender)); + } + } + + return List.copyOf(displays); + } else { + return List.of(); + } + } + + private static boolean hasOtherVInjector(List actors, int receiverIndex) { + for (int actorIndex = 0; actorIndex < actors.size(); actorIndex++) { + if (actorIndex != receiverIndex) { + AnimationCatalogData.Actor actor = actors.get(actorIndex); + if (actor != null && "V".equalsIgnoreCase(actor.injectorType())) { + return true; + } + } + } + + return false; + } + + private static NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge resolveActorGenderBadge( + List requiredTags, List requiredTagsAny + ) { + Set required = requiredTags == null ? Set.of() : new HashSet<>(requiredTags); + Set any = requiredTagsAny == null ? Set.of() : new HashSet<>(requiredTagsAny); + boolean requiredMale = required.contains("gender.male"); + boolean requiredFemale = required.contains("gender.female"); + boolean anyMale = any.contains("gender.male"); + boolean anyFemale = any.contains("gender.female"); + boolean anyOther = any.stream().anyMatch(tag -> !"gender.male".equals(tag) && !"gender.female".equals(tag)); + if (requiredMale && requiredFemale) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.BOTH; + } + + if (requiredMale) { + return !any.isEmpty() && anyFemale && !anyMale && !anyOther + ? NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.BOTH + : NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.MALE; + } + + if (!requiredFemale) { + if (anyOther) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.NONE; + } else if (anyMale && anyFemale) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.EITHER; + } else if (anyMale) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.MALE; + } else { + return anyFemale + ? NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.FEMALE + : NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.NONE; + } + } else { + return !any.isEmpty() && anyMale && !anyFemale && !anyOther + ? NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.BOTH + : NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.FEMALE; + } + } + + private record ActorDisplay( + String key, + List entityTypes, + NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge gender + ) { + private String searchText() { + String types = this.entityTypes == null + ? "" + : this.entityTypes.stream().map(NonModMenuDebugScreens.LoadedAnimationsScreen.EntityTypeDisplay::searchText).collect(Collectors.joining(" ")); + return this.key + " " + types + " " + (this.gender == null ? "" : this.gender.name()); + } + } + + private enum ActorGenderBadge { + NONE, + MALE, + FEMALE, + FEMALE_IMPLIED, + BOTH, + EITHER; + } + + private record EntityTypeDisplay(String namespace, String displayName, String rawId, boolean vanilla) { + private String searchText() { + return this.rawId + " " + this.namespace + " " + this.displayName; + } + } + + private enum GenderRequirement { + ANY, + MALE_ONLY, + FEMALE_ONLY, + BOTH_ONLY; + } + + private record LoadedAnimationRow( + String key, + String packId, + String resourcePackId, + String packName, + String packAuthor, + String source, + String title, + List actors, + List detailLines, + List infoLines, + List contentTags, + List missingLines, + String searchBlob, + NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus status, + int missingCount, + int entityTypeCount, + boolean packDisabled, + boolean animationDisabled, + boolean hasMissingDetails, + boolean expanded + ) implements NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedListRow { + boolean effectivelyDisabled() { + return this.packDisabled || this.animationDisabled; + } + + private boolean matches(String filter) { + return filter != null && !filter.isBlank() ? this.searchBlob.contains(filter) : true; + } + + private boolean matchesStatus(NonModMenuDebugScreens.LoadedAnimationsScreen.StatusFilter filter) { + return switch (filter) { + case ALL -> true; + case MISSING -> this.status == NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.MISSING; + case PARTIAL -> this.status == NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.PARTIAL; + case OK -> this.status == NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.OK; + }; + } + } + + private static final class LoadedAnimationsList extends ElementListWidget { + private static final int TAG_CARD_HEIGHT = 13; + private static final int TAG_CARD_PADDING = 4; + private static final int TAG_HORIZONTAL_GAP = 4; + private static final int TAG_VERTICAL_GAP = 3; + private static final int CARD_BOTTOM_PADDING = 8; + private static final int ANIMATION_PREVIEW_SIZE = 46; + private static final int ANIMATION_PREVIEW_COLUMN_WIDTH = 54; + private static final int MIN_ANIMATION_CARD_HEIGHT = 64; + private final TextRenderer textRenderer; + private final Consumer onTogglePackExpanded; + private final Consumer onToggleRow; + private final Consumer onCopyMissing; + private final Consumer onTogglePackEnabled; + private final Consumer onToggleAnimationEnabled; + private final BooleanSupplier editableSupplier; + + private LoadedAnimationsList( + MinecraftClient client, + int width, + int height, + int top, + TextRenderer textRenderer, + Consumer onTogglePackExpanded, + Consumer onToggleRow, + Consumer onCopyMissing, + Consumer onTogglePackEnabled, + Consumer onToggleAnimationEnabled, + BooleanSupplier editableSupplier + ) { + super(client, width, height, top, 60); + this.textRenderer = textRenderer; + this.onTogglePackExpanded = onTogglePackExpanded; + this.onToggleRow = onToggleRow; + this.onCopyMissing = onCopyMissing; + this.onTogglePackEnabled = onTogglePackEnabled; + this.onToggleAnimationEnabled = onToggleAnimationEnabled; + this.editableSupplier = editableSupplier; + this.centerListVertically = false; + } + + private void setRows(List rows) { + this.clearEntries(); + + for (NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedListRow row : rows) { + this.addEntry( + new NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationsList.RowEntry( + row, + this.textRenderer, + this.onToggleRow, + this.onCopyMissing, + this.onTogglePackExpanded, + this.onTogglePackEnabled, + this.onToggleAnimationEnabled, + this.editableSupplier + ), + this.rowHeight(row) + ); + } + } + + private int rowHeight(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedListRow row) { + if (row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow) { + return 34; + } + + if (row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRow) { + int infoLines = Math.max(0, animationRow.infoLines() == null ? 0 : animationRow.infoLines().size()); + int contentEnd = 23 + infoLines * 9; + int tagRows = countTagRows(this.textRenderer, animationRow.contentTags(), Math.max(20, this.getRowWidth() - 12 - 54)); + if (tagRows > 0) { + contentEnd += 4 + tagRows * 13 + (tagRows - 1) * 3; + } + + int collapsedHeight = contentEnd + 18 + 8; + if (!animationRow.expanded()) { + return Math.max(64, collapsedHeight); + } + + int detailLines = animationRow.detailLines() == null ? 0 : animationRow.detailLines().size(); + int missingLines = animationRow.hasMissingDetails() && animationRow.missingLines() != null + ? Math.min(10, animationRow.missingLines().size()) + : 0; + return Math.max(64, collapsedHeight + detailLines * 9 + (missingLines > 0 ? 2 + missingLines * 9 : 0)); + } else { + return 48; + } + } + + private static int countTagRows(TextRenderer textRenderer, List tags, int maxWidth) { + if (tags != null && !tags.isEmpty() && maxWidth > 0) { + int rows = 1; + int usedWidth = 0; + + for (String tag : tags) { + if (tag != null && !tag.isBlank()) { + int width = Math.min(maxWidth, textRenderer.getWidth(tag) + 8); + if (usedWidth > 0 && usedWidth + 4 + width > maxWidth) { + rows++; + usedWidth = width; + } else { + usedWidth += (usedWidth == 0 ? 0 : 4) + width; + } + } + } + + return usedWidth == 0 ? 0 : rows; + } else { + return 0; + } + } + + public int getRowWidth() { + return Math.min(500, Math.max(120, this.getWidth() - 24)); + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + protected int getScrollbarX() { + return Math.min(this.getWidth() - 8, this.getRowLeft() + this.getRowWidth() + 6); + } + + private static final class RowEntry + extends net.minecraft.client.gui.widget.ElementListWidget.Entry { + private final NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedListRow row; + private final TextRenderer textRenderer; + private final Consumer onToggleRow; + private final Consumer onCopyMissing; + private final Consumer onTogglePackExpanded; + private final Consumer onTogglePackEnabled; + private final Consumer onToggleAnimationEnabled; + private final BooleanSupplier editableSupplier; + private final NonModMenuScreens.FlatTextButton toggleButton; + private final NonModMenuScreens.FlatTextButton copyMissingButton; + private final ButtonWidget enabledButton; + private final List widgets; + + private RowEntry( + NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedListRow row, + TextRenderer textRenderer, + Consumer onToggleRow, + Consumer onCopyMissing, + Consumer onTogglePackExpanded, + Consumer onTogglePackEnabled, + Consumer onToggleAnimationEnabled, + BooleanSupplier editableSupplier + ) { + this.row = row; + this.textRenderer = textRenderer; + this.onToggleRow = onToggleRow; + this.onCopyMissing = onCopyMissing; + this.onTogglePackExpanded = onTogglePackExpanded; + this.onTogglePackEnabled = onTogglePackEnabled; + this.onToggleAnimationEnabled = onToggleAnimationEnabled; + this.editableSupplier = editableSupplier == null ? () -> true : editableSupplier; + this.toggleButton = new NonModMenuScreens.FlatTextButton( + 18, + Text.literal( + row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRow && animationRow.expanded() ? "-" : "+" + ), + button -> { + if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow headerRow) { + if (this.onTogglePackExpanded != null) { + this.onTogglePackExpanded.accept(headerRow.packId()); + } + } else if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRowx && this.onToggleRow != null) { + this.onToggleRow.accept(animationRowx.key()); + } + } + ); + this.copyMissingButton = new NonModMenuScreens.FlatTextButton( + 24, + Text.literal("MS"), + button -> { + if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRowx + && animationRowx.hasMissingDetails() + && this.onCopyMissing != null) { + this.onCopyMissing.accept(animationRowx); + } + } + ); + this.enabledButton = ButtonWidget.builder( + Text.literal("ON"), + button -> { + if (this.editableSupplier.getAsBoolean()) { + if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow headerRow) { + if (this.onTogglePackEnabled != null) { + this.onTogglePackEnabled.accept(headerRow.packId()); + } + } else if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRowx + && this.onToggleAnimationEnabled != null) { + this.onToggleAnimationEnabled.accept(animationRowx.key()); + } + } + } + ) + .dimensions(0, 0, 42, 18) + .build(); + List ws = new ArrayList<>(); + ws.add(this.enabledButton); + if (row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRow) { + ws.add(this.toggleButton); + if (animationRow.hasMissingDetails()) { + ws.add(this.copyMissingButton); + } + } + + this.widgets = List.copyOf(ws); + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int x = this.getContentX() + 4; + int y = this.getContentY() + 3; + int right = this.getContentX() + this.getContentWidth() - 2; + if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow headerRow) { + this.renderPackHeader(context, mouseX, mouseY, deltaTicks, x, y, right, headerRow); + } else if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRow) { + int var47 = this.getContentX() + 2; + int cardTop = this.getContentY() + 1; + int cardRight = this.getContentX() + this.getContentWidth() - 2; + int cardBottom = this.getContentY() + this.getContentHeight() - 2; + int cardBorder = animationRow.effectivelyDisabled() ? -14079703 : (hovered ? -9281155 : -11450790); + int cardBackground = animationRow.effectivelyDisabled() ? 1108809495 : (hovered ? 1747199012 : 1427971357); + context.fill(var47 + 1, cardTop + 1, cardRight - 1, cardBottom - 1, cardBackground); + context.fill(var47, cardTop, cardRight, cardTop + 1, cardBorder); + context.fill(var47, cardBottom - 1, cardRight, cardBottom, cardBorder); + context.fill(var47, cardTop + 1, var47 + 1, cardBottom - 1, cardBorder); + context.fill(cardRight - 1, cardTop + 1, cardRight, cardBottom - 1, cardBorder); + x = this.getContentX() + 6; + y = this.getContentY() + 5; + right = this.getContentX() + this.getContentWidth() - 4; + this.enabledButton.setMessage(Text.literal(animationRow.animationDisabled() ? "OFF" : "ON")); + this.enabledButton.active = this.editableSupplier.getAsBoolean(); + this.enabledButton.setX(right - 44); + this.enabledButton.setY(y - 3); + this.enabledButton.render(context, mouseX, mouseY, deltaTicks); + if (animationRow.hasMissingDetails()) { + this.copyMissingButton.setX(right - 86); + this.copyMissingButton.setY(y - 1); + this.copyMissingButton.render(context, mouseX, mouseY, deltaTicks); + } + + NonAnimationPreviewRenderer.draw(context, this.textRenderer, animationRow.key(), x, y, 46); + if (animationRow.effectivelyDisabled()) { + context.fill(x, y, x + 46, y + 46, 1711276032); + } + + int textX = x + 54; + int statusButtonLeft = animationRow.hasMissingDetails() ? right - 86 : right - 44; + String statusText = "[" + Text.translatable(animationRow.status().translationKey).getString() + "]"; + int statusWidth = this.textRenderer.getWidth(statusText); + int statusX = Math.max(textX, statusButtonLeft - statusWidth - 6); + int textWidth = Math.max(20, statusX - textX - 6); + int titleColor = animationRow.effectivelyDisabled() ? -8947849 : animationRow.status().color; + int statusColor = animationRow.effectivelyDisabled() ? -8947849 : animationRow.status().color; + int sourceColor = animationRow.effectivelyDisabled() ? -10526881 : -8947849; + String visibleSource = this.textRenderer.trimToWidth(animationRow.source(), textWidth); + context.drawTextWithShadow(this.textRenderer, visibleSource, textX, y, sourceColor); + int sourceWidth = this.textRenderer.getWidth(visibleSource); + int remainingTitleWidth = Math.max(0, textWidth - sourceWidth); + if (remainingTitleWidth > 0) { + context.drawTextWithShadow( + this.textRenderer, this.textRenderer.trimToWidth(animationRow.title(), remainingTitleWidth), textX + sourceWidth, y, titleColor + ); + } + + context.drawTextWithShadow(this.textRenderer, statusText, statusX, y, statusColor); + Text actorText = this.buildActorText(animationRow); + List actorLines = this.textRenderer.wrapLines(actorText, textWidth); + if (!actorLines.isEmpty()) { + context.drawTextWithShadow(this.textRenderer, actorLines.getFirst(), textX, y + 9, -1); + } + + List infoLines = animationRow.infoLines() == null ? List.of() : animationRow.infoLines(); + + for (int i = 0; i < infoLines.size(); i++) { + int color = animationRow.effectivelyDisabled() + ? -8947849 + : ( + infoLines.get(i).startsWith("attacks:") + ? -2710919 + : (infoLines.get(i).startsWith("block:") ? -4795766 : (infoLines.get(i).startsWith("local models:") ? -4086064 : -7303024)) + ); + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(infoLines.get(i), textWidth), textX, y + 18 + i * 9, color); + } + + int detailsY = y + 18 + infoLines.size() * 9; + List contentTags = animationRow.contentTags() == null ? List.of() : animationRow.contentTags(); + if (!contentTags.isEmpty()) { + detailsY = this.renderTagCards(context, contentTags, textX, detailsY + 2, right - 2, mouseX, mouseY, animationRow.effectivelyDisabled()) + + 2; + } + + int dividerColor = animationRow.effectivelyDisabled() ? -13816531 : -12306101; + context.fill(textX, detailsY + 1, right - 2, detailsY + 2, dividerColor); + int footerY = detailsY + 3; + int detailsTextX = textX + 20; + int detailsWidth = Math.max(20, right - detailsTextX - 2); + this.toggleButton.setMessage(Text.literal(animationRow.expanded() ? "-" : "+")); + this.toggleButton.setX(textX); + this.toggleButton.setY(footerY - 1); + this.toggleButton.render(context, mouseX, mouseY, deltaTicks); + context.drawTextWithShadow( + this.textRenderer, + this.textRenderer.trimToWidth(Text.translatable("config.needsofnature.loaded_animations.details").getString(), detailsWidth), + detailsTextX, + footerY, + animationRow.effectivelyDisabled() ? -9474193 : -6642524 + ); + if (animationRow.expanded()) { + int lineY = footerY + 13; + List detailLines = animationRow.detailLines() == null ? List.of() : animationRow.detailLines(); + + for (int i = 0; i < detailLines.size(); i++) { + context.drawTextWithShadow( + this.textRenderer, + this.textRenderer.trimToWidth(detailLines.get(i), detailsWidth), + detailsTextX, + lineY, + animationRow.effectivelyDisabled() ? -9474193 : (i == 0 ? -4671304 : -7303024) + ); + lineY += 9; + } + + if (animationRow.hasMissingDetails()) { + lineY += 2; + List missingLines = animationRow.missingLines(); + int maxLines = Math.min(10, missingLines == null ? 0 : missingLines.size()); + + for (int i = 0; i < maxLines; i++) { + context.drawTextWithShadow( + this.textRenderer, + this.textRenderer.trimToWidth(missingLines.get(i), detailsWidth), + detailsTextX, + lineY, + animationRow.effectivelyDisabled() ? -9474193 : -7303024 + ); + lineY += 9; + } + } + } + } + } + + private Text buildActorText(NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedAnimationRow animationRow) { + boolean disabled = animationRow.effectivelyDisabled(); + int labelColor = disabled ? 7829367 : 9079434; + int entityColor = disabled ? 7829367 : 13684944; + int namespaceColor = disabled ? 6710886 : 7570829; + int separatorColor = disabled ? 6710886 : 7829367; + List actors = animationRow.actors() == null ? List.of() : animationRow.actors(); + if (actors.isEmpty()) { + return coloredText("No actors", labelColor); + } + + MutableText line = Text.literal(""); + + for (int actorIndex = 0; actorIndex < actors.size(); actorIndex++) { + if (actorIndex > 0) { + line.append(coloredText(", ", separatorColor)); + } + + NonModMenuDebugScreens.LoadedAnimationsScreen.ActorDisplay actor = actors.get(actorIndex); + line.append(coloredText(actor.key() + ": ", labelColor)); + List entityTypes = actor.entityTypes() == null + ? List.of() + : actor.entityTypes(); + if (entityTypes.isEmpty()) { + line.append(coloredText("Any entity", entityColor)); + } else { + for (int typeIndex = 0; typeIndex < entityTypes.size(); typeIndex++) { + if (typeIndex > 0) { + line.append(coloredText(" | ", separatorColor)); + } + + NonModMenuDebugScreens.LoadedAnimationsScreen.EntityTypeDisplay entityType = entityTypes.get(typeIndex); + if (!entityType.vanilla()) { + line.append(coloredText(entityType.namespace() + ": ", namespaceColor)); + } + + line.append(coloredText(entityType.displayName(), entityColor)); + } + } + + appendGenderBadge(line, actor.gender(), disabled); + } + + return line; + } + + private static MutableText coloredText(String value, int color) { + return Text.literal(value).styled(style -> style.withColor(color)); + } + + private static void appendGenderBadge(MutableText line, NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge badge, boolean disabled) { + if (badge != null && badge != NonModMenuDebugScreens.LoadedAnimationsScreen.ActorGenderBadge.NONE) { + int neutral = disabled ? 7829367 : 10066329; + int male = disabled ? 7829367 : 5880040; + int female = disabled ? 7829367 : 15239605; + line.append(coloredText(" ", neutral)); + switch (badge) { + case NONE: + default: + break; + case MALE: + line.append(coloredText("♂", male)); + break; + case FEMALE: + line.append(coloredText("♀", female)); + break; + case FEMALE_IMPLIED: + line.append(coloredText("♀", neutral)); + break; + case BOTH: + line.append(coloredText("♂", male)).append(coloredText("♀", female)); + break; + case EITHER: + line.append(coloredText("♂", male)).append(coloredText("/", neutral)).append(coloredText("♀", female)); + } + } + } + + private int renderTagCards(DrawContext context, List tags, int startX, int startY, int endX, int mouseX, int mouseY, boolean disabled) { + int maxWidth = Math.max(1, endX - startX); + int x = startX; + int y = startY; + String truncatedHoveredTag = null; + + for (String tag : tags) { + if (tag != null && !tag.isBlank()) { + String display = NonTagLocalizer.localize(tag); + int naturalWidth = this.textRenderer.getWidth(display) + 8; + int cardWidth = Math.min(maxWidth, naturalWidth); + if (x > startX && x + cardWidth > endX) { + x = startX; + y += 16; + } + + boolean hovered = mouseX >= x && mouseX < x + cardWidth && mouseY >= y && mouseY < y + 13; + int border = disabled ? -11974327 : (hovered ? -7703658 : -10135953); + int background = disabled ? -1339348181 : (hovered ? -532925628 : -802281165); + int textColor = disabled ? -8947849 : -1712918; + context.fill(x, y, x + cardWidth, y + 13, border); + context.fill(x + 1, y + 1, x + cardWidth - 1, y + 13 - 1, background); + context.drawText(this.textRenderer, this.textRenderer.trimToWidth(display, Math.max(1, cardWidth - 8)), x + 4, y + 2, textColor, false); + if (hovered && naturalWidth > cardWidth) { + truncatedHoveredTag = display; + } + + x += cardWidth + 4; + } + } + + if (truncatedHoveredTag != null) { + context.drawTooltip(this.textRenderer, Text.literal(truncatedHoveredTag), mouseX, mouseY); + } + + return y + 13; + } + + public boolean mouseClicked(Click click, boolean doubled) { + if (this.row instanceof NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow headerRow + && click.button() == 0 + && this.onTogglePackExpanded != null + && !this.enabledButton.isMouseOver(click.x(), click.y())) { + this.onTogglePackExpanded.accept(headerRow.packId()); + return true; + } else { + return super.mouseClicked(click, doubled); + } + } + + private void renderPackHeader( + DrawContext context, + int mouseX, + int mouseY, + float delta, + int x, + int y, + int right, + NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedPackHeaderRow headerRow + ) { + this.enabledButton.setMessage(Text.literal(headerRow.disabled() ? "OFF" : "ON")); + this.enabledButton.active = this.editableSupplier.getAsBoolean(); + this.enabledButton.setX(right - 44); + this.enabledButton.setY(y + 4); + this.enabledButton.render(context, mouseX, mouseY, delta); + int iconSize = 24; + int iconX = x; + int iconY = y + 1; + context.fill(iconX - 1, iconY - 1, iconX + iconSize + 1, iconY + iconSize + 1, -14671840); + if (headerRow.iconTexture() != null) { + context.drawTexture(RenderPipelines.GUI_TEXTURED, headerRow.iconTexture(), iconX, iconY, 0.0F, 0.0F, iconSize, iconSize, iconSize, iconSize); + } else { + context.fill(iconX, iconY, iconX + iconSize, iconY + iconSize, -12829636); + context.drawTextWithShadow(this.textRenderer, "NoN", iconX + 2, iconY + 8, -4671304); + } + + int textX = iconX + iconSize + 7; + int textRight = right - 48; + String disabledBadge = headerRow.disabledAnimations() > 0 + ? "[" + + Text.translatable("config.needsofnature.loaded_animations.disabled_count", new Object[]{headerRow.disabledAnimations()}).getString() + + "]" + : ""; + int disabledBadgeWidth = disabledBadge.isEmpty() ? 0 : this.textRenderer.getWidth(disabledBadge); + int badgeRight = textRight; + int disabledBadgeX = badgeRight - disabledBadgeWidth; + if (!disabledBadge.isEmpty()) { + badgeRight = disabledBadgeX - 6; + } + + int titleRight = badgeRight; + int titleWidth = Math.max(20, titleRight - textX); + int subtitleWidth = Math.max(20, textRight - textX); + int titleColor = headerRow.disabled() ? -7829368 : -1; + String title = headerRow.packName() + + " (" + + headerRow.visibleAnimations() + + "/" + + headerRow.totalAnimations() + + ")" + + (headerRow.expanded() ? "" : " [collapsed]"); + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(title, titleWidth), textX, y + 2, titleColor); + if (!disabledBadge.isEmpty()) { + context.drawTextWithShadow(this.textRenderer, disabledBadge, disabledBadgeX, y + 2, headerRow.disabled() ? -7702680 : -2778520); + } + + String subtitle = "author: " + headerRow.packAuthor() + " | id: " + headerRow.packId(); + context.drawTextWithShadow(this.textRenderer, this.textRenderer.trimToWidth(subtitle, subtitleWidth), textX, y + 13, -6645094); + } + + public List selectableChildren() { + return List.copyOf(this.widgets); + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + + public List children() { + return List.copyOf(this.widgets); + } + } + } + + private interface LoadedListRow { + String key(); + } + + private record LoadedPackHeaderRow( + String key, + String packId, + String resourcePackId, + String packName, + String packAuthor, + int visibleAnimations, + int totalAnimations, + int disabledAnimations, + boolean disabled, + boolean expanded, + @Nullable Identifier iconTexture + ) implements NonModMenuDebugScreens.LoadedAnimationsScreen.LoadedListRow { + } + + private record ModelCoverage(boolean found, String missingDetail, List localModelDetails) { + private static NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage present(List localModelDetails) { + return new NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage(true, "", localModelDetails); + } + + private static NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage missingCoverage(String detail, List localModelDetails) { + return new NonModMenuDebugScreens.LoadedAnimationsScreen.ModelCoverage(false, detail, localModelDetails); + } + } + + private record ModelProvider(Identifier root, String label) { + } + + private record ResourceCheck( + int animationTotal, + int animationFound, + int modelTotal, + int modelFound, + int modelUnchecked, + List missingAnimationDetails, + List missingModelDetails, + List localModelDetails + ) { + private NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus status() { + boolean animationsOk = this.animationTotal == 0 || this.animationFound == this.animationTotal; + boolean modelsOk = this.modelTotal == 0 || this.modelFound == this.modelTotal; + if (animationsOk && modelsOk) { + return NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.OK; + } + + boolean anyFound = this.animationFound > 0 || this.modelFound > 0; + boolean anyChecked = this.animationTotal > 0 || this.modelTotal > 0; + return anyChecked && !anyFound + ? NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.MISSING + : NonModMenuDebugScreens.LoadedAnimationsScreen.ResourceStatus.PARTIAL; + } + + private boolean hasMissingDetails() { + return this.missingAnimationDetails != null && !this.missingAnimationDetails.isEmpty() + || this.missingModelDetails != null && !this.missingModelDetails.isEmpty(); + } + + private int totalMissingCount() { + int anim = this.missingAnimationDetails == null ? 0 : this.missingAnimationDetails.size(); + int model = this.missingModelDetails == null ? 0 : this.missingModelDetails.size(); + return anim + model; + } + + private List missingLinesStructured() { + if (!this.hasMissingDetails()) { + return List.of(); + } + + List lines = new ArrayList<>(); + lines.add("missing:"); + if (this.missingAnimationDetails != null && !this.missingAnimationDetails.isEmpty()) { + lines.add("anim:"); + addMissingLines(lines, this.missingAnimationDetails, 3); + } else { + lines.add("anim: none"); + } + + if (this.missingModelDetails != null && !this.missingModelDetails.isEmpty()) { + lines.add("model:"); + addMissingLines(lines, this.missingModelDetails, 3); + } else { + lines.add("model: none"); + } + + return List.copyOf(lines); + } + + private static void addMissingLines(List out, List values, int maxEntries) { + if (values != null && !values.isEmpty()) { + int safeMax = Math.max(1, maxEntries); + int take = Math.min(safeMax, values.size()); + + for (int i = 0; i < take; i++) { + out.add(values.get(i)); + } + + int remaining = values.size() - take; + if (remaining > 0) { + out.add("+" + remaining + " more"); + } + } + } + } + + private enum ResourceStatus { + OK("config.needsofnature.loaded_animations.status.ok", -11141291), + PARTIAL("config.needsofnature.loaded_animations.status.partial", -8090), + MISSING("config.needsofnature.loaded_animations.status.missing", -43691); + + private final String translationKey; + private final int color; + + ResourceStatus(String translationKey, int color) { + this.translationKey = translationKey; + this.color = color; + } + + private int sortRank() { + return switch (this) { + case OK -> 2; + case PARTIAL -> 1; + case MISSING -> 0; + }; + } + } + + private enum SortMode { + ID, + STATUS, + MISSING, + ENTITIES; + } + + private enum StatusFilter { + ALL, + MISSING, + PARTIAL, + OK; + } + } + + static class OffspringCountConfigScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private boolean serverConfigEditable; + private Map appliedMap; + private NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList offspringList; + private final List rows = new ArrayList<>(); + private ButtonWidget applyButton; + private ButtonWidget doneButton; + + protected OffspringCountConfigScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.offspring_count_title")); + this.parent = parent; + this.config = config; + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + this.appliedMap = new LinkedHashMap<>(config.getEntityProfilesByEntity()); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + List preserved = this.snapshotRows(); + if (preserved == null) { + preserved = new ArrayList<>(); + + for (Entry entry : NonEntityProfiles.resolveEffectiveProfilesMap().entrySet()) { + NonConfig.EntityProfile profile = entry.getValue() == null + ? NonEntityProfiles.resolve(Identifier.tryParse(entry.getKey())).toConfigProfile() + : entry.getValue().sanitized(); + NonConfig.EntityProfile configOverride = this.config.getEntityProfilesByEntity().get(entry.getKey()); + preserved.add( + new NonModMenuDebugScreens.OffspringCountConfigScreen.RowData( + entry.getKey(), + configOverride != null && configOverride.pregnancyChancePercent() != null ? String.valueOf(configOverride.pregnancyChancePercent()) : "", + String.valueOf(profile.offspringMin() == null ? 1 : profile.offspringMin()), + String.valueOf(profile.offspringMax() == null ? 1 : profile.offspringMax()), + profile.birthEntity() == null ? entry.getKey() : profile.birthEntity(), + profile.birthMode() == null ? "direct" : profile.birthMode(), + profile.egg() == null ? NonConfig.EggProfile.defaults() : profile.egg() + ) + ); + } + } + + int listTop = 44; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + this.offspringList = new NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(this.offspringList); + this.rows.clear(); + + for (NonModMenuDebugScreens.OffspringCountConfigScreen.RowData data : preserved) { + this.addRow(data.id(), data.chance(), data.min(), data.max(), data.birthEntity(), data.birthMode(), data.eggProfile()); + } + + if (this.rows.isEmpty()) { + this.addRow("", "", "1", "1", "", "direct", NonConfig.EggProfile.defaults()); + } + + int centerX = this.width / 2; + ButtonWidget addButton = ButtonWidget.builder(Text.translatable("config.needsofnature.offspring_count_add"), button -> { + this.addRow("", "", "1", "1", "", "direct", NonConfig.EggProfile.defaults()); + this.updateApplyButton(); + }).dimensions(centerX - 100, this.height - 52, 96, 20).build(); + addButton.active = this.serverConfigEditable; + NonModMenuScreens.setTooltip(addButton, "config.needsofnature.tooltip.offspring_count_add"); + this.addDrawableChild(addButton); + ButtonWidget resetDefaultsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.offspring_count_reset"), + button -> MinecraftClient.getInstance() + .setScreen(new NonModMenuDebugScreens.OffspringCountConfigScreen.ResetOffspringCountConfirmScreen(this, () -> { + this.applyDefaultsToRows(); + this.updateApplyButton(); + MinecraftClient.getInstance().setScreen(this); + })) + ) + .dimensions(centerX + 4, this.height - 52, 96, 20) + .build(); + resetDefaultsButton.active = this.serverConfigEditable; + NonModMenuScreens.setTooltip(resetDefaultsButton, "config.needsofnature.tooltip.offspring_count_reset"); + this.addDrawableChild(resetDefaultsButton); + this.applyButton = ButtonWidget.builder(Text.translatable("config.needsofnature.apply"), button -> { + if (this.serverConfigEditable) { + Map next = this.collectMapFromFields(); + this.config.setEntityProfilesByEntity(next); + NonModMenuScreens.saveServerConfig(this.config); + this.appliedMap = new LinkedHashMap<>(this.config.getEntityProfilesByEntity()); + this.updateApplyButton(); + } + }).dimensions(centerX - 100, this.height - 28, 96, 20).build(); + NonModMenuScreens.setTooltip(this.applyButton, "config.needsofnature.tooltip.apply"); + this.addDrawableChild(this.applyButton); + this.doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 4, this.height - 28, 96, 20) + .build(); + NonModMenuScreens.setTooltip(this.doneButton, "config.needsofnature.tooltip.done_unsaved"); + this.addDrawableChild(this.doneButton); + this.updateApplyButton(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateApplyButton(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + this.drawHeader(context); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void drawHeader(DrawContext context) { + if (this.offspringList != null) { + int rowX = this.offspringList.getRowLeft(); + int rowWidth = this.offspringList.getRowWidth(); + int removeW = 20; + int chanceW = 44; + int minW = 34; + int maxW = 34; + int birthW = 140; + int modeW = 54; + int eggW = 42; + int idW = Math.max(105, rowWidth - removeW - chanceW - minW - maxW - birthW - modeW - eggW - 48); + int chanceX = rowX + idW + 6; + int minX = chanceX + chanceW + 6; + int maxX = minX + minW + 6; + int birthX = maxX + maxW + 6; + int modeX = birthX + birthW + 6; + int eggX = modeX + modeW + 6; + int y = 34; + int color = -4342339; + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.offspring_count_column.entity"), rowX + 4, y, color); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.entity_profile_column.chance"), chanceX + chanceW / 2, y, color + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.offspring_count_column.min"), minX + minW / 2, y, color + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.offspring_count_column.max"), maxX + maxW / 2, y, color + ); + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.entity_profile_column.birth_entity"), birthX + 4, y, color); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.entity_profile_column.birth_mode"), modeX + modeW / 2, y, color + ); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.entity_profile_column.egg"), eggX + eggW / 2, y, color + ); + } + } + + private void addRow( + String idText, String chanceText, String minText, String maxText, String birthEntityText, String birthModeText, NonConfig.EggProfile eggProfile + ) { + NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry[] holder = new NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry[1]; + NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry entry = new NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry( + this.textRenderer, idText, chanceText, minText, maxText, birthEntityText, birthModeText, eggProfile, () -> { + NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry current = holder[0]; + if (current != null) { + this.rows.removeIf(row -> row == current); + this.offspringList.removeEntryRow(current); + this.updateApplyButton(); + } + }, this::chanceSuggestionFor, this::openEggSettings + ); + holder[0] = entry; + entry.setOnChange(this::updateApplyButton); + entry.setEditable(this.serverConfigEditable); + this.rows.add(entry); + this.offspringList.addEntryRow(entry); + } + + private void updateApplyButton() { + if (this.applyButton != null) { + if (!this.serverConfigEditable) { + this.applyButton.active = false; + if (this.doneButton != null) { + this.doneButton.setMessage(Text.translatable("config.needsofnature.done")); + } + } else { + Map current = this.collectMapFromFields(); + boolean hasUnsavedChanges = !current.equals(this.appliedMap); + this.applyButton.active = hasUnsavedChanges; + if (this.doneButton != null) { + this.doneButton.setMessage(Text.translatable(hasUnsavedChanges ? "config.needsofnature.done_unsaved" : "config.needsofnature.done")); + } + } + } + } + + private void applyDefaultsToRows() { + if (this.offspringList != null) { + for (NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry entry : new ArrayList<>(this.rows)) { + this.offspringList.removeEntryRow(entry); + } + } + + this.rows.clear(); + + for (Entry entry : NonEntityProfiles.resolveDefaultProfilesMap().entrySet()) { + NonConfig.EntityProfile profile = entry.getValue(); + this.addRow( + entry.getKey(), + "", + String.valueOf(profile.offspringMin() == null ? 1 : profile.offspringMin()), + String.valueOf(profile.offspringMax() == null ? 1 : profile.offspringMax()), + profile.birthEntity() == null ? entry.getKey() : profile.birthEntity(), + profile.birthMode() == null ? "direct" : profile.birthMode(), + profile.egg() == null ? NonConfig.EggProfile.defaults() : profile.egg() + ); + } + + if (this.rows.isEmpty()) { + this.addRow("", "", "1", "1", "", "direct", NonConfig.EggProfile.defaults()); + } + } + + private Map collectMapFromFields() { + Map map = new LinkedHashMap<>(); + + for (NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry row : this.rows) { + if (row != null) { + String id = row.getIdText(); + if (id != null) { + id = id.trim(); + if (!id.isEmpty()) { + Identifier parsedId = Identifier.tryParse(id); + if (parsedId != null) { + String rawChance = row.getChanceText() == null ? "" : row.getChanceText().trim(); + int min = this.parseInt(row.getMinText(), 1); + int max = this.parseInt(row.getMaxText(), min); + if (max < min) { + max = min; + } + + String birthEntity = row.getBirthEntityText(); + if (birthEntity != null && !birthEntity.trim().isEmpty()) { + birthEntity = birthEntity.trim(); + } else { + birthEntity = id; + } + + if (Identifier.tryParse(birthEntity) == null) { + birthEntity = id; + } + + String birthMode = NonConfig.normalizeBirthMode(row.getBirthModeText()); + if (birthMode == null) { + birthMode = "direct"; + } + + NonConfig.EggProfile eggProfile = row.getEggProfile(); + NonConfig.EntityProfile existing = this.config.getEntityProfilesByEntity().get(id); + NonConfig.EntityProfile defaultProfile = NonEntityProfiles.resolveDefaultProfile(parsedId).toConfigProfile(); + int defaultChance = defaultProfile.pregnancyChancePercent() == null + ? this.config.getPregnancyChancePercent() + : defaultProfile.pregnancyChancePercent(); + Integer chanceOverride = rawChance.isEmpty() ? null : Math.max(0, Math.min(100, this.parseInt(rawChance, defaultChance))); + Integer minOverride = min == defaultProfile.offspringMin() ? null : min; + Integer maxOverride = max == defaultProfile.offspringMax() ? null : max; + String birthEntityOverride = birthEntity.equals(defaultProfile.birthEntity()) ? null : birthEntity; + String birthModeOverride = birthMode.equals(defaultProfile.birthMode()) ? null : birthMode; + NonConfig.EggProfile defaultEgg = defaultProfile.egg() == null ? NonConfig.EggProfile.defaults() : defaultProfile.egg(); + NonConfig.EggProfile eggOverride = Objects.equals(eggProfile, defaultEgg) ? null : eggProfile; + NonConfig.EntityProfile next = new NonConfig.EntityProfile( + chanceOverride != null && chanceOverride != defaultChance ? chanceOverride : null, + minOverride, + maxOverride, + birthEntityOverride, + birthModeOverride, + existing == null ? null : existing.genderSpawn(), + eggOverride, + existing == null ? null : existing.lookAtNearbyAnimations(), + existing == null ? null : existing.multiActorJoinChancePercent(), + existing == null ? null : existing.energyGainMultiplier() + ) + .sanitized(); + if (!next.isEmpty()) { + map.put(id, next); + } + } + } + } + } + } + + return map; + } + + private String chanceSuggestionFor(String idText) { + Identifier id = idText != null && !idText.isBlank() ? Identifier.tryParse(idText.trim()) : null; + int defaultChance = id == null ? this.config.getPregnancyChancePercent() : NonEntityProfiles.resolveDefaultProfile(id).pregnancyChancePercent(); + return String.valueOf(defaultChance); + } + + private List snapshotRows() { + if (this.rows.isEmpty()) { + return null; + } + + List data = new ArrayList<>(); + + for (NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry row : this.rows) { + data.add( + new NonModMenuDebugScreens.OffspringCountConfigScreen.RowData( + row.getIdText(), + row.getChanceText(), + row.getMinText(), + row.getMaxText(), + row.getBirthEntityText(), + row.getBirthModeText(), + row.getEggProfile() + ) + ); + } + + return data; + } + + private int parseInt(String value, int fallback) { + if (value == null) { + return fallback; + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private void openEggSettings(NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry row) { + if (row != null) { + Identifier id = Identifier.tryParse(row.getIdText() == null ? "" : row.getIdText().trim()); + NonConfig.EggProfile defaultEgg = id == null ? NonConfig.EggProfile.defaults() : NonEntityProfiles.resolveDefaultProfile(id).eggProfile(); + MinecraftClient.getInstance() + .setScreen(new NonModMenuDebugScreens.OffspringCountConfigScreen.EggProfileSettingsScreen(this, row.getEggProfile(), defaultEgg, updated -> { + row.setEggProfile(updated); + this.updateApplyButton(); + }, this.serverConfigEditable)); + } + } + + private static final class EggProfileSettingsScreen extends Screen { + private final Screen parent; + private final NonConfig.EggProfile initialProfile; + private final NonConfig.EggProfile defaultProfile; + private final Consumer onApply; + private final boolean editable; + private TextFieldWidget startSizeField; + private TextFieldWidget endSizeField; + private TextFieldWidget textureField; + private TextFieldWidget healthField; + + private EggProfileSettingsScreen( + Screen parent, NonConfig.EggProfile initialProfile, NonConfig.EggProfile defaultProfile, Consumer onApply, boolean editable + ) { + super(Text.translatable("config.needsofnature.egg_profile_title")); + this.parent = parent; + this.initialProfile = initialProfile == null ? NonConfig.EggProfile.defaults() : initialProfile.sanitized(); + this.defaultProfile = defaultProfile == null ? NonConfig.EggProfile.defaults() : defaultProfile.sanitized(); + this.onApply = onApply; + this.editable = editable; + } + + protected void init() { + int centerX = this.width / 2; + int labelX = centerX - 180; + int fieldX = centerX - 40; + int y = 58; + int textureFieldW = Math.min(460, Math.max(220, this.width - fieldX - 20)); + this.startSizeField = this.newTextField(70, formatFloat(valueOrDefault(this.initialProfile.startSize(), this.defaultProfile.startSize(), 0.5F))); + this.endSizeField = this.newTextField(70, formatFloat(valueOrDefault(this.initialProfile.endSize(), this.defaultProfile.endSize(), 1.0F))); + this.textureField = this.newTextField(textureFieldW, valueOrDefault(this.initialProfile.texture(), this.defaultProfile.texture(), "")); + this.healthField = this.newTextField(70, formatFloat(valueOrDefault(this.initialProfile.health(), this.defaultProfile.health(), 2.0F))); + this.addField(this.startSizeField, fieldX, y, "config.needsofnature.tooltip.egg_profile_start_size"); + y += 28; + this.addField(this.endSizeField, fieldX, y, "config.needsofnature.tooltip.egg_profile_end_size"); + y += 28; + this.addField(this.textureField, fieldX, y, "config.needsofnature.tooltip.egg_profile_texture"); + y += 28; + this.addField(this.healthField, fieldX, y, "config.needsofnature.tooltip.egg_profile_health"); + ButtonWidget resetButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset"), button -> { + this.startSizeField.setText(formatFloat(valueOrDefault(this.defaultProfile.startSize(), null, 0.5F))); + this.endSizeField.setText(formatFloat(valueOrDefault(this.defaultProfile.endSize(), null, 1.0F))); + this.textureField.setText(valueOrDefault(this.defaultProfile.texture(), null, "")); + this.healthField.setText(formatFloat(valueOrDefault(this.defaultProfile.health(), null, 2.0F))); + }).dimensions(centerX - 154, this.height - 52, 96, 20).build(); + resetButton.active = this.editable; + NonModMenuScreens.setTooltip(resetButton, "config.needsofnature.tooltip.egg_profile_reset"); + this.addDrawableChild(resetButton); + ButtonWidget applyButton = ButtonWidget.builder(Text.translatable("config.needsofnature.apply"), button -> { + if (this.editable) { + if (this.onApply != null) { + this.onApply.accept(this.collectProfile()); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + }).dimensions(centerX - 50, this.height - 52, 96, 20).build(); + applyButton.active = this.editable; + NonModMenuScreens.setTooltip(applyButton, "config.needsofnature.tooltip.apply"); + this.addDrawableChild(applyButton); + ButtonWidget cancelButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 54, this.height - 52, 96, 20) + .build(); + this.addDrawableChild(cancelButton); + this.startSizeField.setEditable(this.editable); + this.endSizeField.setEditable(this.editable); + this.textureField.setEditable(this.editable); + this.healthField.setEditable(this.editable); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + int centerX = this.width / 2; + int labelX = centerX - 180; + int y = 63; + context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, -1); + this.drawLabel(context, "config.needsofnature.egg_profile_start_size", labelX, y); + y += 28; + this.drawLabel(context, "config.needsofnature.egg_profile_end_size", labelX, y); + y += 28; + this.drawLabel(context, "config.needsofnature.egg_profile_texture", labelX, y); + y += 28; + this.drawLabel(context, "config.needsofnature.egg_profile_health", labelX, y); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void addField(TextFieldWidget field, int x, int y, String tooltipKey) { + field.setX(x); + field.setY(y); + NonModMenuScreens.setTooltip(field, tooltipKey); + this.addDrawableChild(field); + } + + private TextFieldWidget newTextField(int width, String value) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, width, 20, Text.literal("")); + field.setMaxLength(128); + field.setText(value == null ? "" : value); + return field; + } + + private void drawLabel(DrawContext context, String key, int x, int y) { + context.drawTextWithShadow(this.textRenderer, Text.translatable(key), x, y, -2039584); + } + + private NonConfig.EggProfile collectProfile() { + NonConfig.EggProfile raw = new NonConfig.EggProfile( + parseFloat(this.startSizeField.getText(), this.defaultProfile.startSize(), 0.5F), + parseFloat(this.endSizeField.getText(), this.defaultProfile.endSize(), 1.0F), + this.textureField.getText() != null && !this.textureField.getText().trim().isEmpty() ? this.textureField.getText().trim() : null, + parseFloat(this.healthField.getText(), this.defaultProfile.health(), 2.0F) + ); + return raw.resolvedOver(this.defaultProfile); + } + + private static Float parseFloat(String raw, Float fallback, float hardFallback) { + if (raw != null && !raw.trim().isEmpty()) { + try { + return Float.parseFloat(raw.trim()); + } catch (NumberFormatException e) { + return fallback == null ? hardFallback : fallback; + } + } else { + return fallback == null ? hardFallback : fallback; + } + } + + private static float valueOrDefault(Float value, Float fallback, float hardFallback) { + if (value != null) { + return value; + } else { + return fallback != null ? fallback : hardFallback; + } + } + + private static String valueOrDefault(String value, String fallback, String hardFallback) { + if (value != null && !value.isBlank()) { + return value; + } else { + return fallback != null && !fallback.isBlank() ? fallback : hardFallback; + } + } + + private static String formatFloat(float value) { + String formatted = String.format(Locale.ROOT, "%.3f", value); + + while (formatted.contains(".") && formatted.endsWith("0")) { + formatted = formatted.substring(0, formatted.length() - 1); + } + + if (formatted.endsWith(".")) { + formatted = formatted.substring(0, formatted.length() - 1); + } + + return formatted; + } + } + + private static final class OffspringCountList + extends ElementListWidget { + private OffspringCountList(MinecraftClient client, int width, int height, int top) { + super(client, width, height, top, 24); + this.centerListVertically = false; + } + + private void addEntryRow(NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry entry) { + super.addEntry(entry); + } + + private void removeEntryRow(NonModMenuDebugScreens.OffspringCountConfigScreen.OffspringCountList.OffspringEntry entry) { + super.removeEntry(entry); + } + + public int getRowWidth() { + return 620; + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + private static final class OffspringEntry + extends net.minecraft.client.gui.widget.ElementListWidget.Entry { + private final TextFieldWidget idField; + private final TextFieldWidget chanceField; + private final TextFieldWidget minField; + private final TextFieldWidget maxField; + private final TextFieldWidget birthEntityField; + private final TextFieldWidget birthModeField; + private final ButtonWidget eggButton; + private final ButtonWidget removeButton; + private final List widgets; + private final Function chanceSuggestionSupplier; + private NonConfig.EggProfile eggProfile; + private boolean editable; + private Runnable onChange; + + private OffspringEntry( + TextRenderer textRenderer, + String idText, + String chanceText, + String minText, + String maxText, + String birthEntityText, + String birthModeText, + NonConfig.EggProfile eggProfile, + Runnable onRemove, + Function chanceSuggestionSupplier, + Consumer onEggSettings + ) { + this.chanceSuggestionSupplier = chanceSuggestionSupplier; + this.eggProfile = eggProfile == null ? NonConfig.EggProfile.defaults() : eggProfile.sanitized(); + this.idField = new TextFieldWidget(textRenderer, 0, 0, 120, 20, Text.literal("")); + this.idField.setText(idText == null ? "" : idText); + this.idField.setMaxLength(80); + NonModMenuScreens.setTooltip(this.idField, "config.needsofnature.tooltip.offspring_count_entry_entity"); + this.chanceField = new TextFieldWidget(textRenderer, 0, 0, 44, 20, Text.literal("")); + this.chanceField.setText(chanceText == null ? "" : chanceText); + this.chanceField.setMaxLength(3); + this.updateChanceSuggestion(); + NonModMenuScreens.setTooltip(this.chanceField, "config.needsofnature.tooltip.entity_profile_entry_chance"); + this.minField = new TextFieldWidget(textRenderer, 0, 0, 34, 20, Text.literal("")); + this.minField.setText(minText == null ? "1" : minText); + this.minField.setMaxLength(3); + NonModMenuScreens.setTooltip(this.minField, "config.needsofnature.tooltip.offspring_count_entry_min"); + this.maxField = new TextFieldWidget(textRenderer, 0, 0, 34, 20, Text.literal("")); + this.maxField.setText(maxText == null ? "1" : maxText); + this.maxField.setMaxLength(3); + NonModMenuScreens.setTooltip(this.maxField, "config.needsofnature.tooltip.offspring_count_entry_max"); + this.birthEntityField = new TextFieldWidget(textRenderer, 0, 0, 140, 20, Text.literal("")); + this.birthEntityField.setText(birthEntityText == null ? "" : birthEntityText); + this.birthEntityField.setMaxLength(80); + NonModMenuScreens.setTooltip(this.birthEntityField, "config.needsofnature.tooltip.entity_profile_entry_birth_entity"); + this.birthModeField = new TextFieldWidget(textRenderer, 0, 0, 54, 20, Text.literal("")); + this.birthModeField.setText(birthModeText == null ? "direct" : birthModeText); + this.birthModeField.setMaxLength(6); + NonModMenuScreens.setTooltip(this.birthModeField, "config.needsofnature.tooltip.entity_profile_entry_birth_mode"); + this.eggButton = ButtonWidget.builder(Text.translatable("config.needsofnature.entity_profile_egg_button"), button -> { + if (onEggSettings != null) { + onEggSettings.accept(this); + } + }).dimensions(0, 0, 42, 20).build(); + NonModMenuScreens.setTooltip(this.eggButton, "config.needsofnature.tooltip.entity_profile_entry_egg"); + this.removeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.remove_icon"), button -> { + if (onRemove != null) { + onRemove.run(); + } + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(this.removeButton, "config.needsofnature.tooltip.remove"); + this.widgets = new ArrayList<>(8); + this.widgets.add(this.idField); + this.widgets.add(this.chanceField); + this.widgets.add(this.minField); + this.widgets.add(this.maxField); + this.widgets.add(this.birthEntityField); + this.widgets.add(this.birthModeField); + this.widgets.add(this.eggButton); + this.widgets.add(this.removeButton); + this.idField.setChangedListener(ignored -> { + this.updateChanceSuggestion(); + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.chanceField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.minField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.maxField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.birthEntityField.setChangedListener(ignored -> { + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.birthModeField.setChangedListener(ignored -> { + this.updateEggButtonActive(); + if (this.onChange != null) { + this.onChange.run(); + } + }); + this.updateEggButtonActive(); + } + + private void setOnChange(Runnable onChange) { + this.onChange = onChange; + } + + private void updateChanceSuggestion() { + String suggestion = this.chanceSuggestionSupplier == null ? null : this.chanceSuggestionSupplier.apply(this.idField.getText()); + this.chanceField.setPlaceholder(suggestion != null && !suggestion.isBlank() ? Text.literal(suggestion) : null); + } + + private void setEditable(boolean editable) { + this.editable = editable; + this.idField.setEditable(editable); + this.chanceField.setEditable(editable); + this.minField.setEditable(editable); + this.maxField.setEditable(editable); + this.birthEntityField.setEditable(editable); + this.birthModeField.setEditable(editable); + this.updateEggButtonActive(); + this.removeButton.active = editable; + } + + private void updateEggButtonActive() { + this.eggButton.active = this.editable && "egg".equals(NonConfig.normalizeBirthMode(this.birthModeField.getText())); + } + + private String getIdText() { + return this.idField.getText(); + } + + private String getMinText() { + return this.minField.getText(); + } + + private String getMaxText() { + return this.maxField.getText(); + } + + private String getChanceText() { + return this.chanceField.getText(); + } + + private String getBirthEntityText() { + return this.birthEntityField.getText(); + } + + private String getBirthModeText() { + return this.birthModeField.getText(); + } + + private NonConfig.EggProfile getEggProfile() { + return this.eggProfile == null ? NonConfig.EggProfile.defaults() : this.eggProfile.sanitized(); + } + + private void setEggProfile(NonConfig.EggProfile eggProfile) { + this.eggProfile = eggProfile == null ? NonConfig.EggProfile.defaults() : eggProfile.sanitized(); + if (this.onChange != null) { + this.onChange.run(); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int rowX = this.getContentX(); + int rowY = this.getContentY(); + int rowWidth = this.getContentWidth(); + int widgetY = rowY + 2; + int removeW = this.removeButton.getWidth(); + int chanceW = this.chanceField.getWidth(); + int minW = this.minField.getWidth(); + int maxW = this.maxField.getWidth(); + int birthW = this.birthEntityField.getWidth(); + int modeW = this.birthModeField.getWidth(); + int eggW = this.eggButton.getWidth(); + int idW = Math.max(105, rowWidth - removeW - chanceW - minW - maxW - birthW - modeW - eggW - 48); + int chanceX = rowX + idW + 6; + int minX = chanceX + chanceW + 6; + int maxX = minX + minW + 6; + int birthX = maxX + maxW + 6; + int modeX = birthX + birthW + 6; + int eggX = modeX + modeW + 6; + int removeX = eggX + eggW + 6; + this.idField.setX(rowX); + this.idField.setY(widgetY); + this.idField.setWidth(idW); + this.chanceField.setX(chanceX); + this.chanceField.setY(widgetY); + this.minField.setX(minX); + this.minField.setY(widgetY); + this.maxField.setX(maxX); + this.maxField.setY(widgetY); + this.birthEntityField.setX(birthX); + this.birthEntityField.setY(widgetY); + this.birthModeField.setX(modeX); + this.birthModeField.setY(widgetY); + this.eggButton.setX(eggX); + this.eggButton.setY(widgetY); + this.removeButton.setX(removeX); + this.removeButton.setY(widgetY); + + for (ClickableWidget widget : this.widgets) { + widget.render(context, mouseX, mouseY, deltaTicks); + } + } + + public List selectableChildren() { + return List.copyOf(this.widgets); + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + + public List children() { + return List.copyOf(this.widgets); + } + } + } + + private static final class ResetOffspringCountConfirmScreen extends Screen { + private final Screen parent; + private final Runnable onConfirm; + + private ResetOffspringCountConfirmScreen(Screen parent, Runnable onConfirm) { + super(Text.translatable("config.needsofnature.offspring_count_reset_title")); + this.parent = parent; + this.onConfirm = onConfirm; + } + + protected void init() { + int centerX = this.width / 2; + int buttonY = this.height / 2 + 20; + ButtonWidget yesButton = ButtonWidget.builder(Text.translatable("gui.yes"), button -> { + if (this.onConfirm != null) { + this.onConfirm.run(); + } + + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(centerX - 100, buttonY, 96, 20).build(); + this.addDrawableChild(yesButton); + ButtonWidget noButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 4, buttonY, 96, 20) + .build(); + this.addDrawableChild(noButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, this.height / 2 - 20, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.offspring_count_reset_body"), this.width / 2, this.height / 2 - 4, -4144960 + ); + } + } + + private record RowData(String id, String chance, String min, String max, String birthEntity, String birthMode, NonConfig.EggProfile eggProfile) { + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuExternalScreens.java b/src/main/java/com/nonid/integration/NonModMenuExternalScreens.java new file mode 100644 index 0000000..948020b --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuExternalScreens.java @@ -0,0 +1,17 @@ +package com.nonid.integration; + +import com.nonid.NeedsOfNature; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; + +public final class NonModMenuExternalScreens { + private NonModMenuExternalScreens() { + } + + public static void openPlayerPreferences(Screen parent) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + client.setScreen(new NonModMenuPlayerScreens.PlayerPreferencesScreen(parent, NeedsOfNature.getLocalConfig())); + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuGameplayScreens.java b/src/main/java/com/nonid/integration/NonModMenuGameplayScreens.java new file mode 100644 index 0000000..d67bfe3 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuGameplayScreens.java @@ -0,0 +1,2383 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import com.nonid.client.NonHudOverlay; +import java.util.Locale; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; + +final class NonModMenuGameplayScreens { + private NonModMenuGameplayScreens() { + } + + static class GameplayAnimationsScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private int loopSeconds; + private int peakLoopSeconds; + private int maleMalePercent; + private int femaleFemalePercent; + private int entityEntityPercent; + private int multiActorJoinPercent; + private TextFieldWidget loopField; + private TextFieldWidget peakLoopField; + private TextFieldWidget maleMaleField; + private TextFieldWidget femaleFemaleField; + private TextFieldWidget entityEntityField; + private TextFieldWidget multiActorJoinField; + private ButtonWidget resetLoopButton; + private ButtonWidget resetPeakLoopButton; + private ButtonWidget resetMaleMaleButton; + private ButtonWidget resetFemaleFemaleButton; + private ButtonWidget resetEntityEntityButton; + private ButtonWidget resetMultiActorJoinButton; + private boolean serverConfigEditable; + + protected GameplayAnimationsScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.section.animations")); + this.parent = parent; + this.config = config; + this.loopSeconds = NonHudOverlay.getRuntimeLoopSeconds(); + this.peakLoopSeconds = NonHudOverlay.getRuntimePeakLoopSeconds(); + this.maleMalePercent = config.getMaleMaleChancePercent(); + this.femaleFemalePercent = config.getFemaleFemaleChancePercent(); + this.entityEntityPercent = config.getEntityEntityChancePercent(); + this.multiActorJoinPercent = config.getMultiActorJoinChancePercent(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.loopField != null) { + this.loopSeconds = this.parseField(this.loopField, this.loopSeconds); + this.peakLoopSeconds = this.parseField(this.peakLoopField, this.peakLoopSeconds); + this.maleMalePercent = this.parseField(this.maleMaleField, this.maleMalePercent); + this.femaleFemalePercent = this.parseField(this.femaleFemaleField, this.femaleFemalePercent); + this.entityEntityPercent = this.parseField(this.entityEntityField, this.entityEntityPercent); + this.multiActorJoinPercent = this.parseField(this.multiActorJoinField, this.multiActorJoinPercent); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.animation_pacing"))); + this.loopField = this.newNumberField(fieldWidth, this.loopSeconds); + this.resetLoopButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), button -> this.loopField.setText(String.valueOf(this.defaults.getLoopProgressSeconds())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetLoopButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.loop_seconds"), + this.loopField, + this.resetLoopButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.loop_seconds") + ) + ); + this.peakLoopField = this.newNumberField(fieldWidth, this.peakLoopSeconds); + this.resetPeakLoopButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.peakLoopField.setText(String.valueOf(this.defaults.getPeakLoopProgressSeconds())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPeakLoopButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.peak_loop_seconds"), + this.peakLoopField, + this.resetPeakLoopButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.peak_loop_seconds") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.match_chances"))); + this.maleMaleField = this.newNumberField(fieldWidth, this.maleMalePercent); + this.resetMaleMaleButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.maleMaleField.setText(String.valueOf(this.defaults.getMaleMaleChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetMaleMaleButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.male_male_chance"), + this.maleMaleField, + this.resetMaleMaleButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.male_male_chance") + ) + ); + this.femaleFemaleField = this.newNumberField(fieldWidth, this.femaleFemalePercent); + this.resetFemaleFemaleButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.femaleFemaleField.setText(String.valueOf(this.defaults.getFemaleFemaleChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFemaleFemaleButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.female_female_chance"), + this.femaleFemaleField, + this.resetFemaleFemaleButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.female_female_chance") + ) + ); + this.entityEntityField = this.newNumberField(fieldWidth, this.entityEntityPercent); + this.resetEntityEntityButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.entityEntityField.setText(String.valueOf(this.defaults.getEntityEntityChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetEntityEntityButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.entity_entity_chance"), + this.entityEntityField, + this.resetEntityEntityButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.entity_entity_chance") + ) + ); + this.multiActorJoinField = this.newNumberField(fieldWidth, this.multiActorJoinPercent); + this.resetMultiActorJoinButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.multiActorJoinField.setText(String.valueOf(this.defaults.getMultiActorJoinChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetMultiActorJoinButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.multi_actor_join_chance"), + this.multiActorJoinField, + this.resetMultiActorJoinButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.multi_actor_join_chance") + ) + ); + this.loopField.setChangedListener(ignored -> this.updateResetButtons()); + this.peakLoopField.setChangedListener(ignored -> this.updateResetButtons()); + this.maleMaleField.setChangedListener(ignored -> this.updateResetButtons()); + this.femaleFemaleField.setChangedListener(ignored -> this.updateResetButtons()); + this.entityEntityField.setChangedListener(ignored -> this.updateResetButtons()); + this.multiActorJoinField.setChangedListener(ignored -> this.updateResetButtons()); + this.loopField.setEditable(this.serverConfigEditable); + this.peakLoopField.setEditable(this.serverConfigEditable); + this.maleMaleField.setEditable(this.serverConfigEditable); + this.femaleFemaleField.setEditable(this.serverConfigEditable); + this.entityEntityField.setEditable(this.serverConfigEditable); + this.multiActorJoinField.setEditable(this.serverConfigEditable); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip( + doneButton, this.serverConfigEditable ? "config.needsofnature.tooltip.done_save" : "config.needsofnature.tooltip.done_unsaved" + ); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.loopSeconds = this.clampLoopSeconds(this.parseField(this.loopField, this.loopSeconds)); + this.peakLoopSeconds = this.clampPeakLoopSeconds(this.parseField(this.peakLoopField, this.peakLoopSeconds)); + this.maleMalePercent = this.clampPercent(this.parseField(this.maleMaleField, this.maleMalePercent)); + this.femaleFemalePercent = this.clampPercent(this.parseField(this.femaleFemaleField, this.femaleFemalePercent)); + this.entityEntityPercent = this.clampPercent(this.parseField(this.entityEntityField, this.entityEntityPercent)); + this.multiActorJoinPercent = this.clampPercent(this.parseField(this.multiActorJoinField, this.multiActorJoinPercent)); + this.config.setLoopProgressSeconds(this.loopSeconds); + this.config.setPeakLoopProgressSeconds(this.peakLoopSeconds); + this.config.setMaleMaleChancePercent(this.maleMalePercent); + this.config.setFemaleFemaleChancePercent(this.femaleFemalePercent); + this.config.setEntityEntityChancePercent(this.entityEntityPercent); + this.config.setMultiActorJoinChancePercent(this.multiActorJoinPercent); + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private int clampLoopSeconds(int v) { + return Math.max(5, Math.min(300, v)); + } + + private int clampPeakLoopSeconds(int v) { + return Math.max(1, Math.min(300, v)); + } + + private int clampPercent(int v) { + return Math.max(0, Math.min(100, v)); + } + + private boolean isIntFieldChanged(TextFieldWidget field, int defaultValue) { + return field == null ? false : this.parseField(field, defaultValue) != defaultValue; + } + + private void updateResetButtons() { + if (this.resetLoopButton != null) { + this.resetLoopButton.active = this.serverConfigEditable && this.isIntFieldChanged(this.loopField, this.defaults.getLoopProgressSeconds()); + } + + if (this.resetPeakLoopButton != null) { + this.resetPeakLoopButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.peakLoopField, this.defaults.getPeakLoopProgressSeconds()); + } + + if (this.resetMaleMaleButton != null) { + this.resetMaleMaleButton.active = this.serverConfigEditable && this.isIntFieldChanged(this.maleMaleField, this.defaults.getMaleMaleChancePercent()); + } + + if (this.resetFemaleFemaleButton != null) { + this.resetFemaleFemaleButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.femaleFemaleField, this.defaults.getFemaleFemaleChancePercent()); + } + + if (this.resetEntityEntityButton != null) { + this.resetEntityEntityButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.entityEntityField, this.defaults.getEntityEntityChancePercent()); + } + + if (this.resetMultiActorJoinButton != null) { + this.resetMultiActorJoinButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.multiActorJoinField, this.defaults.getMultiActorJoinChancePercent()); + } + } + } + + static class GameplayCategoryScreen extends Screen { + private final Screen parent; + private final NonConfig config; + + protected GameplayCategoryScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.gameplay_title")); + this.parent = parent; + this.config = config; + } + + protected void init() { + int centerX = this.width / 2; + int y = this.height / 2 - 48; + int gap = 24; + ButtonWidget worldButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.section.world"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuGameplayScreens.GameplayWorldScreen(this, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + this.addDrawableChild(worldButton); + y += gap; + ButtonWidget energyButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.section.energy"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuGameplayScreens.GameplayEnergyScreen(this, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + this.addDrawableChild(energyButton); + y += gap; + ButtonWidget animationsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.section.animations"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuGameplayScreens.GameplayAnimationsScreen(this, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + this.addDrawableChild(animationsButton); + y += gap; + ButtonWidget difficultyButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.section.difficulty"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuGameplayScreens.GameplayDifficultyScreen(this, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + this.addDrawableChild(difficultyButton); + ButtonWidget doneButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent) + ) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + } + + static class GameplayDifficultyScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private int playerRequestMinEnergy; + private int attackHits; + private double attackDecay; + private int attackEscapeDamageDifficultyPercent; + private int pvpFullHealthEscapeBonusPercent; + private int pvpCriticalHealthMashStrengthPercent; + private int pvpMaxHeatEscapeStrengthPercent; + private int pvpZeroHeatAttackStrengthPercent; + private int attackEscapeInvulnerabilitySeconds; + private int attackEscapeAnimationProtectionSeconds; + private int postEscapeGatherMaxMobs; + private boolean attackCreativePlayers; + private boolean blockAnimationsWhileRidingLivingEntities; + private boolean blockAnimationsWhileRidingVehicles; + private int attackOutcomeFailsafeThreshold; + private boolean lastDefeatedEnabled; + private int lastDefeatedEnergyThreshold; + private int lastDefeatedSearchRadius; + private int lastDefeatedCooldownSeconds; + private boolean allowCraftingTableSkinRepair; + private boolean keepMessAfterDeath; + private boolean keepRippedSkinAfterDeath; + private boolean keepLiquidTankAfterDeath; + private boolean keepPregnancyAfterDeath; + private TextFieldWidget playerRequestMinEnergyField; + private TextFieldWidget attackHitsField; + private TextFieldWidget attackDecayField; + private TextFieldWidget attackEscapeDamageDifficultyPercentField; + private TextFieldWidget pvpFullHealthEscapeBonusPercentField; + private TextFieldWidget pvpCriticalHealthMashStrengthPercentField; + private TextFieldWidget pvpMaxHeatEscapeStrengthPercentField; + private TextFieldWidget pvpZeroHeatAttackStrengthPercentField; + private TextFieldWidget attackEscapeInvulnerabilitySecondsField; + private TextFieldWidget attackEscapeAnimationProtectionSecondsField; + private TextFieldWidget postEscapeGatherMaxMobsField; + private TextFieldWidget attackOutcomeFailsafeThresholdField; + private TextFieldWidget lastDefeatedEnergyThresholdField; + private TextFieldWidget lastDefeatedSearchRadiusField; + private TextFieldWidget lastDefeatedCooldownSecondsField; + private ButtonWidget resetPlayerRequestMinEnergyButton; + private ButtonWidget resetAttackHitsButton; + private ButtonWidget resetAttackDecayButton; + private ButtonWidget resetAttackEscapeDamageDifficultyPercentButton; + private ButtonWidget resetPvpFullHealthEscapeBonusPercentButton; + private ButtonWidget resetPvpCriticalHealthMashStrengthPercentButton; + private ButtonWidget resetPvpMaxHeatEscapeStrengthPercentButton; + private ButtonWidget resetPvpZeroHeatAttackStrengthPercentButton; + private ButtonWidget resetAttackEscapeInvulnerabilitySecondsButton; + private ButtonWidget resetAttackEscapeAnimationProtectionSecondsButton; + private ButtonWidget resetPostEscapeGatherMaxMobsButton; + private ButtonWidget attackCreativePlayersButton; + private ButtonWidget resetAttackCreativePlayersButton; + private ButtonWidget blockAnimationsWhileRidingLivingEntitiesButton; + private ButtonWidget resetBlockAnimationsWhileRidingLivingEntitiesButton; + private ButtonWidget blockAnimationsWhileRidingVehiclesButton; + private ButtonWidget resetBlockAnimationsWhileRidingVehiclesButton; + private ButtonWidget resetAttackOutcomeFailsafeThresholdButton; + private ButtonWidget lastDefeatedEnabledButton; + private ButtonWidget resetLastDefeatedEnabledButton; + private ButtonWidget resetLastDefeatedEnergyThresholdButton; + private ButtonWidget resetLastDefeatedSearchRadiusButton; + private ButtonWidget resetLastDefeatedCooldownSecondsButton; + private ButtonWidget allowCraftingTableSkinRepairButton; + private ButtonWidget resetAllowCraftingTableSkinRepairButton; + private ButtonWidget keepMessAfterDeathButton; + private ButtonWidget resetKeepMessAfterDeathButton; + private ButtonWidget keepRippedSkinAfterDeathButton; + private ButtonWidget resetKeepRippedSkinAfterDeathButton; + private ButtonWidget keepLiquidTankAfterDeathButton; + private ButtonWidget resetKeepLiquidTankAfterDeathButton; + private ButtonWidget keepPregnancyAfterDeathButton; + private ButtonWidget resetKeepPregnancyAfterDeathButton; + private boolean serverConfigEditable; + + protected GameplayDifficultyScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.section.difficulty")); + this.parent = parent; + this.config = config; + this.playerRequestMinEnergy = config.getPlayerRequestMinEnergy(); + this.attackHits = NonHudOverlay.getRuntimeAttackEscapeHits(); + this.attackDecay = NonHudOverlay.getRuntimeAttackDecayPerSecond(); + this.attackEscapeDamageDifficultyPercent = NonHudOverlay.getRuntimeAttackEscapeDamageDifficultyPercent(); + this.pvpFullHealthEscapeBonusPercent = config.getPvpFullHealthEscapeBonusPercent(); + this.pvpCriticalHealthMashStrengthPercent = config.getPvpCriticalHealthMashStrengthPercent(); + this.pvpMaxHeatEscapeStrengthPercent = config.getPvpMaxHeatEscapeStrengthPercent(); + this.pvpZeroHeatAttackStrengthPercent = config.getPvpZeroHeatAttackStrengthPercent(); + this.attackEscapeInvulnerabilitySeconds = config.getAttackEscapeInvulnerabilitySeconds(); + this.attackEscapeAnimationProtectionSeconds = config.getAttackEscapeAnimationProtectionSeconds(); + this.postEscapeGatherMaxMobs = config.getPostEscapeGatherMaxMobs(); + this.attackCreativePlayers = NonHudOverlay.getRuntimeAttackCreativePlayers(); + this.blockAnimationsWhileRidingLivingEntities = config.blockAnimationsWhileRidingLivingEntities(); + this.blockAnimationsWhileRidingVehicles = config.blockAnimationsWhileRidingVehicles(); + this.attackOutcomeFailsafeThreshold = config.getAttackOutcomeFailsafeThreshold(); + this.lastDefeatedEnabled = config.isLastDefeatedEnabled(); + this.lastDefeatedEnergyThreshold = config.getLastDefeatedEnergyThreshold(); + this.lastDefeatedSearchRadius = config.getLastDefeatedSearchRadius(); + this.lastDefeatedCooldownSeconds = config.getLastDefeatedCooldownSeconds(); + this.allowCraftingTableSkinRepair = config.isCraftingTableSkinRepairAllowed(); + this.keepMessAfterDeath = config.keepMessAfterDeath(); + this.keepRippedSkinAfterDeath = config.keepRippedSkinAfterDeath(); + this.keepLiquidTankAfterDeath = config.keepLiquidTankAfterDeath(); + this.keepPregnancyAfterDeath = config.keepPregnancyAfterDeath(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.attackHitsField != null) { + this.playerRequestMinEnergy = this.parseField(this.playerRequestMinEnergyField, this.playerRequestMinEnergy); + this.attackHits = this.parseField(this.attackHitsField, this.attackHits); + this.attackDecay = this.parseDecimalField(this.attackDecayField, this.attackDecay); + this.attackEscapeDamageDifficultyPercent = this.parseField(this.attackEscapeDamageDifficultyPercentField, this.attackEscapeDamageDifficultyPercent); + this.pvpFullHealthEscapeBonusPercent = this.parseField(this.pvpFullHealthEscapeBonusPercentField, this.pvpFullHealthEscapeBonusPercent); + this.pvpCriticalHealthMashStrengthPercent = this.parseField( + this.pvpCriticalHealthMashStrengthPercentField, this.pvpCriticalHealthMashStrengthPercent + ); + this.pvpMaxHeatEscapeStrengthPercent = this.parseField(this.pvpMaxHeatEscapeStrengthPercentField, this.pvpMaxHeatEscapeStrengthPercent); + this.pvpZeroHeatAttackStrengthPercent = this.parseField(this.pvpZeroHeatAttackStrengthPercentField, this.pvpZeroHeatAttackStrengthPercent); + this.attackEscapeInvulnerabilitySeconds = this.parseField(this.attackEscapeInvulnerabilitySecondsField, this.attackEscapeInvulnerabilitySeconds); + this.attackEscapeAnimationProtectionSeconds = this.parseField( + this.attackEscapeAnimationProtectionSecondsField, this.attackEscapeAnimationProtectionSeconds + ); + this.postEscapeGatherMaxMobs = this.parseField(this.postEscapeGatherMaxMobsField, this.postEscapeGatherMaxMobs); + this.attackOutcomeFailsafeThreshold = this.parseField(this.attackOutcomeFailsafeThresholdField, this.attackOutcomeFailsafeThreshold); + this.lastDefeatedEnergyThreshold = this.parseField(this.lastDefeatedEnergyThresholdField, this.lastDefeatedEnergyThreshold); + this.lastDefeatedSearchRadius = this.parseField(this.lastDefeatedSearchRadiusField, this.lastDefeatedSearchRadius); + this.lastDefeatedCooldownSeconds = this.parseField(this.lastDefeatedCooldownSecondsField, this.lastDefeatedCooldownSeconds); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.player_requests"))); + this.playerRequestMinEnergyField = this.newNumberField(fieldWidth, this.playerRequestMinEnergy); + this.resetPlayerRequestMinEnergyButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerRequestMinEnergyField.setText(String.valueOf(this.defaults.getPlayerRequestMinEnergy())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerRequestMinEnergyButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_request_min_energy"), + this.playerRequestMinEnergyField, + this.resetPlayerRequestMinEnergyButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_request_min_energy") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.attack_escape"))); + this.attackCreativePlayersButton = ButtonWidget.builder(this.onOffLabel(this.attackCreativePlayers), button -> { + this.attackCreativePlayers = !this.attackCreativePlayers; + button.setMessage(this.onOffLabel(this.attackCreativePlayers)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetAttackCreativePlayersButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.attackCreativePlayers = this.defaults.canAttackCreativePlayers(); + this.attackCreativePlayersButton.setMessage(this.onOffLabel(this.attackCreativePlayers)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetAttackCreativePlayersButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_creative_players"), + this.attackCreativePlayersButton, + this.resetAttackCreativePlayersButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_creative_players") + ) + ); + this.attackHitsField = this.newNumberField(fieldWidth, this.attackHits); + this.resetAttackHitsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.attackHitsField.setText(String.valueOf(this.defaults.getAttackEscapeHits())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetAttackHitsButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_hits"), + this.attackHitsField, + this.resetAttackHitsButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_hits") + ) + ); + this.attackDecayField = this.newDecimalField(fieldWidth, this.attackDecay); + this.resetAttackDecayButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.attackDecayField.setText(String.valueOf(this.defaults.getAttackDecayPerSecond())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetAttackDecayButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_decay"), + this.attackDecayField, + this.resetAttackDecayButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_decay") + ) + ); + this.attackEscapeDamageDifficultyPercentField = this.newNumberField(fieldWidth, this.attackEscapeDamageDifficultyPercent); + this.resetAttackEscapeDamageDifficultyPercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.attackEscapeDamageDifficultyPercentField.setText(String.valueOf(this.defaults.getAttackEscapeDamageDifficultyPercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetAttackEscapeDamageDifficultyPercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_escape_damage_difficulty_percent"), + this.attackEscapeDamageDifficultyPercentField, + this.resetAttackEscapeDamageDifficultyPercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_escape_damage_difficulty_percent") + ) + ); + settingsList.addEntryRow( + SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.player_attack_balance")) + ); + this.pvpFullHealthEscapeBonusPercentField = this.newNumberField(fieldWidth, this.pvpFullHealthEscapeBonusPercent); + this.resetPvpFullHealthEscapeBonusPercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pvpFullHealthEscapeBonusPercentField.setText(String.valueOf(this.defaults.getPvpFullHealthEscapeBonusPercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPvpFullHealthEscapeBonusPercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pvp_full_health_escape_bonus_percent"), + this.pvpFullHealthEscapeBonusPercentField, + this.resetPvpFullHealthEscapeBonusPercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pvp_full_health_escape_bonus_percent") + ) + ); + this.pvpCriticalHealthMashStrengthPercentField = this.newNumberField(fieldWidth, this.pvpCriticalHealthMashStrengthPercent); + this.resetPvpCriticalHealthMashStrengthPercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pvpCriticalHealthMashStrengthPercentField.setText(String.valueOf(this.defaults.getPvpCriticalHealthMashStrengthPercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPvpCriticalHealthMashStrengthPercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pvp_critical_health_mash_strength_percent"), + this.pvpCriticalHealthMashStrengthPercentField, + this.resetPvpCriticalHealthMashStrengthPercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pvp_critical_health_mash_strength_percent") + ) + ); + this.pvpMaxHeatEscapeStrengthPercentField = this.newNumberField(fieldWidth, this.pvpMaxHeatEscapeStrengthPercent); + this.resetPvpMaxHeatEscapeStrengthPercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pvpMaxHeatEscapeStrengthPercentField.setText(String.valueOf(this.defaults.getPvpMaxHeatEscapeStrengthPercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPvpMaxHeatEscapeStrengthPercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pvp_max_heat_escape_strength_percent"), + this.pvpMaxHeatEscapeStrengthPercentField, + this.resetPvpMaxHeatEscapeStrengthPercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pvp_max_heat_escape_strength_percent") + ) + ); + this.pvpZeroHeatAttackStrengthPercentField = this.newNumberField(fieldWidth, this.pvpZeroHeatAttackStrengthPercent); + this.resetPvpZeroHeatAttackStrengthPercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pvpZeroHeatAttackStrengthPercentField.setText(String.valueOf(this.defaults.getPvpZeroHeatAttackStrengthPercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPvpZeroHeatAttackStrengthPercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pvp_zero_heat_attack_strength_percent"), + this.pvpZeroHeatAttackStrengthPercentField, + this.resetPvpZeroHeatAttackStrengthPercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pvp_zero_heat_attack_strength_percent") + ) + ); + this.attackEscapeInvulnerabilitySecondsField = this.newNumberField(fieldWidth, this.attackEscapeInvulnerabilitySeconds); + this.resetAttackEscapeInvulnerabilitySecondsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.attackEscapeInvulnerabilitySecondsField.setText(String.valueOf(this.defaults.getAttackEscapeInvulnerabilitySeconds())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetAttackEscapeInvulnerabilitySecondsButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_escape_invulnerability_seconds"), + this.attackEscapeInvulnerabilitySecondsField, + this.resetAttackEscapeInvulnerabilitySecondsButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_escape_invulnerability_seconds") + ) + ); + this.attackEscapeAnimationProtectionSecondsField = this.newNumberField(fieldWidth, this.attackEscapeAnimationProtectionSeconds); + this.resetAttackEscapeAnimationProtectionSecondsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.attackEscapeAnimationProtectionSecondsField.setText(String.valueOf(this.defaults.getAttackEscapeAnimationProtectionSeconds())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetAttackEscapeAnimationProtectionSecondsButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_escape_animation_protection_seconds"), + this.attackEscapeAnimationProtectionSecondsField, + this.resetAttackEscapeAnimationProtectionSecondsButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_escape_animation_protection_seconds") + ) + ); + this.postEscapeGatherMaxMobsField = this.newNumberField(fieldWidth, this.postEscapeGatherMaxMobs); + this.resetPostEscapeGatherMaxMobsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.postEscapeGatherMaxMobsField.setText(String.valueOf(this.defaults.getPostEscapeGatherMaxMobs())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPostEscapeGatherMaxMobsButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.post_escape_gather_max_mobs"), + this.postEscapeGatherMaxMobsField, + this.resetPostEscapeGatherMaxMobsButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.post_escape_gather_max_mobs") + ) + ); + this.attackOutcomeFailsafeThresholdField = this.newNumberField(fieldWidth, this.attackOutcomeFailsafeThreshold); + this.resetAttackOutcomeFailsafeThresholdButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.attackOutcomeFailsafeThresholdField.setText(String.valueOf(this.defaults.getAttackOutcomeFailsafeThreshold())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetAttackOutcomeFailsafeThresholdButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.attack_outcome_failsafe_threshold"), + this.attackOutcomeFailsafeThresholdField, + this.resetAttackOutcomeFailsafeThresholdButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.attack_outcome_failsafe_threshold") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.mount_protection"))); + this.blockAnimationsWhileRidingLivingEntitiesButton = ButtonWidget.builder(this.onOffLabel(this.blockAnimationsWhileRidingLivingEntities), button -> { + this.blockAnimationsWhileRidingLivingEntities = !this.blockAnimationsWhileRidingLivingEntities; + button.setMessage(this.onOffLabel(this.blockAnimationsWhileRidingLivingEntities)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip( + this.blockAnimationsWhileRidingLivingEntitiesButton, "config.needsofnature.tooltip.block_animations_while_riding_living_entities" + ); + this.resetBlockAnimationsWhileRidingLivingEntitiesButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.blockAnimationsWhileRidingLivingEntities = this.defaults.blockAnimationsWhileRidingLivingEntities(); + this.blockAnimationsWhileRidingLivingEntitiesButton.setMessage(this.onOffLabel(this.blockAnimationsWhileRidingLivingEntities)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetBlockAnimationsWhileRidingLivingEntitiesButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.block_animations_while_riding_living_entities"), + this.blockAnimationsWhileRidingLivingEntitiesButton, + this.resetBlockAnimationsWhileRidingLivingEntitiesButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.block_animations_while_riding_living_entities") + ) + ); + this.blockAnimationsWhileRidingVehiclesButton = ButtonWidget.builder(this.onOffLabel(this.blockAnimationsWhileRidingVehicles), button -> { + this.blockAnimationsWhileRidingVehicles = !this.blockAnimationsWhileRidingVehicles; + button.setMessage(this.onOffLabel(this.blockAnimationsWhileRidingVehicles)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.blockAnimationsWhileRidingVehiclesButton, "config.needsofnature.tooltip.block_animations_while_riding_vehicles"); + this.resetBlockAnimationsWhileRidingVehiclesButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.blockAnimationsWhileRidingVehicles = this.defaults.blockAnimationsWhileRidingVehicles(); + this.blockAnimationsWhileRidingVehiclesButton.setMessage(this.onOffLabel(this.blockAnimationsWhileRidingVehicles)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetBlockAnimationsWhileRidingVehiclesButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.block_animations_while_riding_vehicles"), + this.blockAnimationsWhileRidingVehiclesButton, + this.resetBlockAnimationsWhileRidingVehiclesButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.block_animations_while_riding_vehicles") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.last_defeated"))); + this.lastDefeatedEnabledButton = ButtonWidget.builder(this.onOffLabel(this.lastDefeatedEnabled), button -> { + this.lastDefeatedEnabled = !this.lastDefeatedEnabled; + button.setMessage(this.onOffLabel(this.lastDefeatedEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetLastDefeatedEnabledButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.lastDefeatedEnabled = this.defaults.isLastDefeatedEnabled(); + this.lastDefeatedEnabledButton.setMessage(this.onOffLabel(this.lastDefeatedEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetLastDefeatedEnabledButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.last_defeated_enabled"), + this.lastDefeatedEnabledButton, + this.resetLastDefeatedEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.last_defeated_enabled") + ) + ); + this.lastDefeatedEnergyThresholdField = this.newNumberField(fieldWidth, this.lastDefeatedEnergyThreshold); + this.resetLastDefeatedEnergyThresholdButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.lastDefeatedEnergyThresholdField.setText(String.valueOf(this.defaults.getLastDefeatedEnergyThreshold())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetLastDefeatedEnergyThresholdButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.last_defeated_energy_threshold"), + this.lastDefeatedEnergyThresholdField, + this.resetLastDefeatedEnergyThresholdButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.last_defeated_energy_threshold") + ) + ); + this.lastDefeatedSearchRadiusField = this.newNumberField(fieldWidth, this.lastDefeatedSearchRadius); + this.resetLastDefeatedSearchRadiusButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.lastDefeatedSearchRadiusField.setText(String.valueOf(this.defaults.getLastDefeatedSearchRadius())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetLastDefeatedSearchRadiusButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.last_defeated_search_radius"), + this.lastDefeatedSearchRadiusField, + this.resetLastDefeatedSearchRadiusButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.last_defeated_search_radius") + ) + ); + this.lastDefeatedCooldownSecondsField = this.newNumberField(fieldWidth, this.lastDefeatedCooldownSeconds); + this.resetLastDefeatedCooldownSecondsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.lastDefeatedCooldownSecondsField.setText(String.valueOf(this.defaults.getLastDefeatedCooldownSeconds())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetLastDefeatedCooldownSecondsButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.last_defeated_cooldown_seconds"), + this.lastDefeatedCooldownSecondsField, + this.resetLastDefeatedCooldownSecondsButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.last_defeated_cooldown_seconds") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.ripped_skin_repair"))); + this.allowCraftingTableSkinRepairButton = ButtonWidget.builder(this.onOffLabel(this.allowCraftingTableSkinRepair), button -> { + this.allowCraftingTableSkinRepair = !this.allowCraftingTableSkinRepair; + button.setMessage(this.onOffLabel(this.allowCraftingTableSkinRepair)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.allowCraftingTableSkinRepairButton, "config.needsofnature.tooltip.allow_crafting_table_skin_repair"); + this.resetAllowCraftingTableSkinRepairButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.allowCraftingTableSkinRepair = this.defaults.isCraftingTableSkinRepairAllowed(); + this.allowCraftingTableSkinRepairButton.setMessage(this.onOffLabel(this.allowCraftingTableSkinRepair)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetAllowCraftingTableSkinRepairButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.allow_crafting_table_skin_repair"), + this.allowCraftingTableSkinRepairButton, + this.resetAllowCraftingTableSkinRepairButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.allow_crafting_table_skin_repair") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.death_persistence"))); + this.keepMessAfterDeathButton = ButtonWidget.builder(this.onOffLabel(this.keepMessAfterDeath), button -> { + this.keepMessAfterDeath = !this.keepMessAfterDeath; + button.setMessage(this.onOffLabel(this.keepMessAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.keepMessAfterDeathButton, "config.needsofnature.tooltip.keep_mess_after_death"); + this.resetKeepMessAfterDeathButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.keepMessAfterDeath = this.defaults.keepMessAfterDeath(); + this.keepMessAfterDeathButton.setMessage(this.onOffLabel(this.keepMessAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetKeepMessAfterDeathButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.keep_mess_after_death"), + this.keepMessAfterDeathButton, + this.resetKeepMessAfterDeathButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.keep_mess_after_death") + ) + ); + this.keepRippedSkinAfterDeathButton = ButtonWidget.builder(this.onOffLabel(this.keepRippedSkinAfterDeath), button -> { + this.keepRippedSkinAfterDeath = !this.keepRippedSkinAfterDeath; + button.setMessage(this.onOffLabel(this.keepRippedSkinAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.keepRippedSkinAfterDeathButton, "config.needsofnature.tooltip.keep_ripped_skin_after_death"); + this.resetKeepRippedSkinAfterDeathButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.keepRippedSkinAfterDeath = this.defaults.keepRippedSkinAfterDeath(); + this.keepRippedSkinAfterDeathButton.setMessage(this.onOffLabel(this.keepRippedSkinAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetKeepRippedSkinAfterDeathButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.keep_ripped_skin_after_death"), + this.keepRippedSkinAfterDeathButton, + this.resetKeepRippedSkinAfterDeathButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.keep_ripped_skin_after_death") + ) + ); + this.keepLiquidTankAfterDeathButton = ButtonWidget.builder(this.onOffLabel(this.keepLiquidTankAfterDeath), button -> { + this.keepLiquidTankAfterDeath = !this.keepLiquidTankAfterDeath; + button.setMessage(this.onOffLabel(this.keepLiquidTankAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.keepLiquidTankAfterDeathButton, "config.needsofnature.tooltip.keep_liquid_tank_after_death"); + this.resetKeepLiquidTankAfterDeathButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.keepLiquidTankAfterDeath = this.defaults.keepLiquidTankAfterDeath(); + this.keepLiquidTankAfterDeathButton.setMessage(this.onOffLabel(this.keepLiquidTankAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetKeepLiquidTankAfterDeathButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.keep_liquid_tank_after_death"), + this.keepLiquidTankAfterDeathButton, + this.resetKeepLiquidTankAfterDeathButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.keep_liquid_tank_after_death") + ) + ); + this.keepPregnancyAfterDeathButton = ButtonWidget.builder(this.onOffLabel(this.keepPregnancyAfterDeath), button -> { + this.keepPregnancyAfterDeath = !this.keepPregnancyAfterDeath; + button.setMessage(this.onOffLabel(this.keepPregnancyAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.keepPregnancyAfterDeathButton, "config.needsofnature.tooltip.keep_pregnancy_after_death"); + this.resetKeepPregnancyAfterDeathButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.keepPregnancyAfterDeath = this.defaults.keepPregnancyAfterDeath(); + this.keepPregnancyAfterDeathButton.setMessage(this.onOffLabel(this.keepPregnancyAfterDeath)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetKeepPregnancyAfterDeathButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.keep_pregnancy_after_death"), + this.keepPregnancyAfterDeathButton, + this.resetKeepPregnancyAfterDeathButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.keep_pregnancy_after_death") + ) + ); + this.playerRequestMinEnergyField.setChangedListener(ignored -> this.updateResetButtons()); + this.attackHitsField.setChangedListener(ignored -> this.updateResetButtons()); + this.attackDecayField.setChangedListener(ignored -> this.updateResetButtons()); + this.attackEscapeDamageDifficultyPercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.pvpFullHealthEscapeBonusPercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.pvpCriticalHealthMashStrengthPercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.pvpMaxHeatEscapeStrengthPercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.pvpZeroHeatAttackStrengthPercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.attackEscapeInvulnerabilitySecondsField.setChangedListener(ignored -> this.updateResetButtons()); + this.attackEscapeAnimationProtectionSecondsField.setChangedListener(ignored -> this.updateResetButtons()); + this.postEscapeGatherMaxMobsField.setChangedListener(ignored -> this.updateResetButtons()); + this.attackOutcomeFailsafeThresholdField.setChangedListener(ignored -> this.updateResetButtons()); + this.lastDefeatedEnergyThresholdField.setChangedListener(ignored -> this.updateResetButtons()); + this.lastDefeatedSearchRadiusField.setChangedListener(ignored -> this.updateResetButtons()); + this.lastDefeatedCooldownSecondsField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerRequestMinEnergyField.setEditable(this.serverConfigEditable); + this.attackHitsField.setEditable(this.serverConfigEditable); + this.attackDecayField.setEditable(this.serverConfigEditable); + this.attackEscapeDamageDifficultyPercentField.setEditable(this.serverConfigEditable); + this.pvpFullHealthEscapeBonusPercentField.setEditable(this.serverConfigEditable); + this.pvpCriticalHealthMashStrengthPercentField.setEditable(this.serverConfigEditable); + this.pvpMaxHeatEscapeStrengthPercentField.setEditable(this.serverConfigEditable); + this.pvpZeroHeatAttackStrengthPercentField.setEditable(this.serverConfigEditable); + this.attackEscapeInvulnerabilitySecondsField.setEditable(this.serverConfigEditable); + this.attackEscapeAnimationProtectionSecondsField.setEditable(this.serverConfigEditable); + this.postEscapeGatherMaxMobsField.setEditable(this.serverConfigEditable); + this.attackOutcomeFailsafeThresholdField.setEditable(this.serverConfigEditable); + this.lastDefeatedEnergyThresholdField.setEditable(this.serverConfigEditable); + this.lastDefeatedSearchRadiusField.setEditable(this.serverConfigEditable); + this.lastDefeatedCooldownSecondsField.setEditable(this.serverConfigEditable); + if (this.lastDefeatedEnabledButton != null) { + this.lastDefeatedEnabledButton.active = this.serverConfigEditable; + } + + if (this.attackCreativePlayersButton != null) { + this.attackCreativePlayersButton.active = this.serverConfigEditable; + } + + if (this.blockAnimationsWhileRidingLivingEntitiesButton != null) { + this.blockAnimationsWhileRidingLivingEntitiesButton.active = this.serverConfigEditable; + } + + if (this.blockAnimationsWhileRidingVehiclesButton != null) { + this.blockAnimationsWhileRidingVehiclesButton.active = this.serverConfigEditable; + } + + if (this.allowCraftingTableSkinRepairButton != null) { + this.allowCraftingTableSkinRepairButton.active = this.serverConfigEditable; + } + + if (this.keepMessAfterDeathButton != null) { + this.keepMessAfterDeathButton.active = this.serverConfigEditable; + } + + if (this.keepRippedSkinAfterDeathButton != null) { + this.keepRippedSkinAfterDeathButton.active = this.serverConfigEditable; + } + + if (this.keepLiquidTankAfterDeathButton != null) { + this.keepLiquidTankAfterDeathButton.active = this.serverConfigEditable; + } + + if (this.keepPregnancyAfterDeathButton != null) { + this.keepPregnancyAfterDeathButton.active = this.serverConfigEditable; + } + + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip( + doneButton, this.serverConfigEditable ? "config.needsofnature.tooltip.done_save" : "config.needsofnature.tooltip.done_unsaved" + ); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.playerRequestMinEnergy = this.clampPlayerRequestMinEnergy(this.parseField(this.playerRequestMinEnergyField, this.playerRequestMinEnergy)); + this.attackHits = this.clampAttackHits(this.parseField(this.attackHitsField, this.attackHits)); + this.attackDecay = this.clampAttackDecay(this.parseDecimalField(this.attackDecayField, this.attackDecay)); + this.attackEscapeDamageDifficultyPercent = this.clampAttackEscapeDamageDifficultyPercent( + this.parseField(this.attackEscapeDamageDifficultyPercentField, this.attackEscapeDamageDifficultyPercent) + ); + this.pvpFullHealthEscapeBonusPercent = this.clampPvpMashPercent( + this.parseField(this.pvpFullHealthEscapeBonusPercentField, this.pvpFullHealthEscapeBonusPercent) + ); + this.pvpCriticalHealthMashStrengthPercent = this.clampPvpMashPercent( + this.parseField(this.pvpCriticalHealthMashStrengthPercentField, this.pvpCriticalHealthMashStrengthPercent) + ); + this.pvpMaxHeatEscapeStrengthPercent = this.clampPvpMashPercent( + this.parseField(this.pvpMaxHeatEscapeStrengthPercentField, this.pvpMaxHeatEscapeStrengthPercent) + ); + this.pvpZeroHeatAttackStrengthPercent = this.clampPvpMashPercent( + this.parseField(this.pvpZeroHeatAttackStrengthPercentField, this.pvpZeroHeatAttackStrengthPercent) + ); + this.attackEscapeInvulnerabilitySeconds = this.clampAttackEscapeProtectionSeconds( + this.parseField(this.attackEscapeInvulnerabilitySecondsField, this.attackEscapeInvulnerabilitySeconds) + ); + this.attackEscapeAnimationProtectionSeconds = this.clampAttackEscapeProtectionSeconds( + this.parseField(this.attackEscapeAnimationProtectionSecondsField, this.attackEscapeAnimationProtectionSeconds) + ); + this.postEscapeGatherMaxMobs = this.clampPostEscapeGatherMaxMobs(this.parseField(this.postEscapeGatherMaxMobsField, this.postEscapeGatherMaxMobs)); + this.attackOutcomeFailsafeThreshold = this.clampAttackOutcomeFailsafeThreshold( + this.parseField(this.attackOutcomeFailsafeThresholdField, this.attackOutcomeFailsafeThreshold) + ); + this.lastDefeatedEnergyThreshold = this.clampLastDefeatedEnergyThreshold( + this.parseField(this.lastDefeatedEnergyThresholdField, this.lastDefeatedEnergyThreshold) + ); + this.lastDefeatedSearchRadius = this.clampLastDefeatedSearchRadius( + this.parseField(this.lastDefeatedSearchRadiusField, this.lastDefeatedSearchRadius) + ); + this.lastDefeatedCooldownSeconds = this.clampLastDefeatedCooldownSeconds( + this.parseField(this.lastDefeatedCooldownSecondsField, this.lastDefeatedCooldownSeconds) + ); + this.config.setPlayerRequestMinEnergy(this.playerRequestMinEnergy); + this.config.setAttackEscapeHits(this.attackHits); + this.config.setAttackDecayPerSecond(this.attackDecay); + this.config.setAttackEscapeDamageDifficultyPercent(this.attackEscapeDamageDifficultyPercent); + this.config.setPvpFullHealthEscapeBonusPercent(this.pvpFullHealthEscapeBonusPercent); + this.config.setPvpCriticalHealthMashStrengthPercent(this.pvpCriticalHealthMashStrengthPercent); + this.config.setPvpMaxHeatEscapeStrengthPercent(this.pvpMaxHeatEscapeStrengthPercent); + this.config.setPvpZeroHeatAttackStrengthPercent(this.pvpZeroHeatAttackStrengthPercent); + this.config.setAttackEscapeInvulnerabilitySeconds(this.attackEscapeInvulnerabilitySeconds); + this.config.setAttackEscapeAnimationProtectionSeconds(this.attackEscapeAnimationProtectionSeconds); + this.config.setPostEscapeGatherMaxMobs(this.postEscapeGatherMaxMobs); + this.config.setAttackCreativePlayers(this.attackCreativePlayers); + this.config.setBlockAnimationsWhileRidingLivingEntities(this.blockAnimationsWhileRidingLivingEntities); + this.config.setBlockAnimationsWhileRidingVehicles(this.blockAnimationsWhileRidingVehicles); + this.config.setAttackOutcomeFailsafeThreshold(this.attackOutcomeFailsafeThreshold); + this.config.setLastDefeatedEnabled(this.lastDefeatedEnabled); + this.config.setLastDefeatedEnergyThreshold(this.lastDefeatedEnergyThreshold); + this.config.setLastDefeatedSearchRadius(this.lastDefeatedSearchRadius); + this.config.setLastDefeatedCooldownSeconds(this.lastDefeatedCooldownSeconds); + this.config.setCraftingTableSkinRepairAllowed(this.allowCraftingTableSkinRepair); + this.config.setKeepMessAfterDeath(this.keepMessAfterDeath); + this.config.setKeepRippedSkinAfterDeath(this.keepRippedSkinAfterDeath); + this.config.setKeepLiquidTankAfterDeath(this.keepLiquidTankAfterDeath); + this.config.setKeepPregnancyAfterDeath(this.keepPregnancyAfterDeath); + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private TextFieldWidget newDecimalField(int w, double initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + int maxLen = 16; + field.setMaxLength(maxLen); + field.setText(this.trimToMax(this.formatDecimal(initial), maxLen)); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private double parseDecimalField(TextFieldWidget field, double fallback) { + try { + return Double.parseDouble(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private String formatDecimal(double value) { + String text = String.format(Locale.ROOT, "%.3f", value); + int dot = text.indexOf(46); + if (dot >= 0) { + int end = text.length(); + + while (end > dot + 1 && text.charAt(end - 1) == '0') { + end--; + } + + if (end == dot + 1) { + end = dot; + } + + text = text.substring(0, end); + } + + return text; + } + + private String trimToMax(String value, int max) { + if (value == null) { + return ""; + } else { + return value.length() > max ? value.substring(0, max) : value; + } + } + + private int clampAttackHits(int v) { + return Math.max(1, Math.min(50, v)); + } + + private int clampPlayerRequestMinEnergy(int v) { + return Math.max(0, Math.min(200, v)); + } + + private double clampAttackDecay(double v) { + return Math.max(0.0, Math.min(5.0, v)); + } + + private int clampAttackEscapeDamageDifficultyPercent(int v) { + return Math.max(0, Math.min(200, v)); + } + + private int clampPvpMashPercent(int v) { + return Math.max(0, Math.min(200, v)); + } + + private int clampAttackOutcomeFailsafeThreshold(int v) { + return Math.max(1, Math.min(50, v)); + } + + private int clampAttackEscapeProtectionSeconds(int v) { + return Math.max(0, Math.min(10, v)); + } + + private int clampPostEscapeGatherMaxMobs(int v) { + return Math.max(0, Math.min(64, v)); + } + + private int clampLastDefeatedEnergyThreshold(int v) { + return Math.max(0, Math.min(200, v)); + } + + private int clampLastDefeatedSearchRadius(int v) { + return Math.max(1, Math.min(32, v)); + } + + private int clampLastDefeatedCooldownSeconds(int v) { + return Math.max(0, Math.min(3600, v)); + } + + private Text onOffLabel(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private boolean isIntFieldChanged(TextFieldWidget field, int defaultValue) { + return field == null ? false : this.parseField(field, defaultValue) != defaultValue; + } + + private boolean isDoubleFieldChanged(TextFieldWidget field, double defaultValue) { + if (field == null) { + return false; + } + + double value = this.parseDecimalField(field, defaultValue); + return Math.abs(value - defaultValue) >= 1.0E-4; + } + + private void updateResetButtons() { + if (this.resetPlayerRequestMinEnergyButton != null) { + this.resetPlayerRequestMinEnergyButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.playerRequestMinEnergyField, this.defaults.getPlayerRequestMinEnergy()); + } + + if (this.resetAttackHitsButton != null) { + this.resetAttackHitsButton.active = this.serverConfigEditable && this.isIntFieldChanged(this.attackHitsField, this.defaults.getAttackEscapeHits()); + } + + if (this.resetAttackDecayButton != null) { + this.resetAttackDecayButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.attackDecayField, this.defaults.getAttackDecayPerSecond()); + } + + if (this.resetAttackEscapeDamageDifficultyPercentButton != null) { + this.resetAttackEscapeDamageDifficultyPercentButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.attackEscapeDamageDifficultyPercentField, this.defaults.getAttackEscapeDamageDifficultyPercent()); + } + + if (this.resetPvpFullHealthEscapeBonusPercentButton != null) { + this.resetPvpFullHealthEscapeBonusPercentButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pvpFullHealthEscapeBonusPercentField, this.defaults.getPvpFullHealthEscapeBonusPercent()); + } + + if (this.resetPvpCriticalHealthMashStrengthPercentButton != null) { + this.resetPvpCriticalHealthMashStrengthPercentButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pvpCriticalHealthMashStrengthPercentField, this.defaults.getPvpCriticalHealthMashStrengthPercent()); + } + + if (this.resetPvpMaxHeatEscapeStrengthPercentButton != null) { + this.resetPvpMaxHeatEscapeStrengthPercentButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pvpMaxHeatEscapeStrengthPercentField, this.defaults.getPvpMaxHeatEscapeStrengthPercent()); + } + + if (this.resetPvpZeroHeatAttackStrengthPercentButton != null) { + this.resetPvpZeroHeatAttackStrengthPercentButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pvpZeroHeatAttackStrengthPercentField, this.defaults.getPvpZeroHeatAttackStrengthPercent()); + } + + if (this.resetAttackEscapeInvulnerabilitySecondsButton != null) { + this.resetAttackEscapeInvulnerabilitySecondsButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.attackEscapeInvulnerabilitySecondsField, this.defaults.getAttackEscapeInvulnerabilitySeconds()); + } + + if (this.resetAttackEscapeAnimationProtectionSecondsButton != null) { + this.resetAttackEscapeAnimationProtectionSecondsButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.attackEscapeAnimationProtectionSecondsField, this.defaults.getAttackEscapeAnimationProtectionSeconds()); + } + + if (this.resetPostEscapeGatherMaxMobsButton != null) { + this.resetPostEscapeGatherMaxMobsButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.postEscapeGatherMaxMobsField, this.defaults.getPostEscapeGatherMaxMobs()); + } + + if (this.resetAttackOutcomeFailsafeThresholdButton != null) { + this.resetAttackOutcomeFailsafeThresholdButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.attackOutcomeFailsafeThresholdField, this.defaults.getAttackOutcomeFailsafeThreshold()); + } + + if (this.resetAttackCreativePlayersButton != null) { + this.resetAttackCreativePlayersButton.active = this.serverConfigEditable && this.attackCreativePlayers != this.defaults.canAttackCreativePlayers(); + } + + if (this.resetBlockAnimationsWhileRidingLivingEntitiesButton != null) { + this.resetBlockAnimationsWhileRidingLivingEntitiesButton.active = this.serverConfigEditable + && this.blockAnimationsWhileRidingLivingEntities != this.defaults.blockAnimationsWhileRidingLivingEntities(); + } + + if (this.resetBlockAnimationsWhileRidingVehiclesButton != null) { + this.resetBlockAnimationsWhileRidingVehiclesButton.active = this.serverConfigEditable + && this.blockAnimationsWhileRidingVehicles != this.defaults.blockAnimationsWhileRidingVehicles(); + } + + if (this.resetLastDefeatedEnabledButton != null) { + this.resetLastDefeatedEnabledButton.active = this.serverConfigEditable && this.lastDefeatedEnabled != this.defaults.isLastDefeatedEnabled(); + } + + if (this.resetLastDefeatedEnergyThresholdButton != null) { + this.resetLastDefeatedEnergyThresholdButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.lastDefeatedEnergyThresholdField, this.defaults.getLastDefeatedEnergyThreshold()); + } + + if (this.resetLastDefeatedSearchRadiusButton != null) { + this.resetLastDefeatedSearchRadiusButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.lastDefeatedSearchRadiusField, this.defaults.getLastDefeatedSearchRadius()); + } + + if (this.resetLastDefeatedCooldownSecondsButton != null) { + this.resetLastDefeatedCooldownSecondsButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.lastDefeatedCooldownSecondsField, this.defaults.getLastDefeatedCooldownSeconds()); + } + + if (this.resetAllowCraftingTableSkinRepairButton != null) { + this.resetAllowCraftingTableSkinRepairButton.active = this.serverConfigEditable + && this.allowCraftingTableSkinRepair != this.defaults.isCraftingTableSkinRepairAllowed(); + } + + if (this.resetKeepMessAfterDeathButton != null) { + this.resetKeepMessAfterDeathButton.active = this.serverConfigEditable && this.keepMessAfterDeath != this.defaults.keepMessAfterDeath(); + } + + if (this.resetKeepRippedSkinAfterDeathButton != null) { + this.resetKeepRippedSkinAfterDeathButton.active = this.serverConfigEditable + && this.keepRippedSkinAfterDeath != this.defaults.keepRippedSkinAfterDeath(); + } + + if (this.resetKeepLiquidTankAfterDeathButton != null) { + this.resetKeepLiquidTankAfterDeathButton.active = this.serverConfigEditable + && this.keepLiquidTankAfterDeath != this.defaults.keepLiquidTankAfterDeath(); + } + + if (this.resetKeepPregnancyAfterDeathButton != null) { + this.resetKeepPregnancyAfterDeathButton.active = this.serverConfigEditable + && this.keepPregnancyAfterDeath != this.defaults.keepPregnancyAfterDeath(); + } + + if (this.lastDefeatedEnabledButton != null) { + this.lastDefeatedEnabledButton.active = this.serverConfigEditable; + } + + if (this.blockAnimationsWhileRidingLivingEntitiesButton != null) { + this.blockAnimationsWhileRidingLivingEntitiesButton.active = this.serverConfigEditable; + } + + if (this.blockAnimationsWhileRidingVehiclesButton != null) { + this.blockAnimationsWhileRidingVehiclesButton.active = this.serverConfigEditable; + } + + if (this.allowCraftingTableSkinRepairButton != null) { + this.allowCraftingTableSkinRepairButton.active = this.serverConfigEditable; + } + + if (this.keepMessAfterDeathButton != null) { + this.keepMessAfterDeathButton.active = this.serverConfigEditable; + } + + if (this.keepRippedSkinAfterDeathButton != null) { + this.keepRippedSkinAfterDeathButton.active = this.serverConfigEditable; + } + + if (this.keepLiquidTankAfterDeathButton != null) { + this.keepLiquidTankAfterDeathButton.active = this.serverConfigEditable; + } + + if (this.keepPregnancyAfterDeathButton != null) { + this.keepPregnancyAfterDeathButton.active = this.serverConfigEditable; + } + } + } + + static class GameplayEnergyScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private double energyGainRate; + private double nearAnimationEnergyGainMult; + private double playerEnergyAuraMultLow; + private double playerEnergyAuraMultMid; + private double playerEnergyAuraMultHigh; + private double destroyedSkinAuraMultStage1; + private double destroyedSkinAuraMultStage2; + private double destroyedSkinAuraMultStage3; + private double destroyedSkinAuraMultStage4; + private int normalDamageDestroyedSkinChancePercent; + private int playerEnergyAuraRadiusMin; + private int playerEnergyAuraRadiusMax; + private int playerEnergyAuraPulseTicks; + private TextFieldWidget energyGainRateField; + private TextFieldWidget nearAnimationEnergyGainMultField; + private TextFieldWidget playerEnergyAuraMultLowField; + private TextFieldWidget playerEnergyAuraMultMidField; + private TextFieldWidget playerEnergyAuraMultHighField; + private TextFieldWidget destroyedSkinAuraMultStage1Field; + private TextFieldWidget destroyedSkinAuraMultStage2Field; + private TextFieldWidget destroyedSkinAuraMultStage3Field; + private TextFieldWidget destroyedSkinAuraMultStage4Field; + private TextFieldWidget normalDamageDestroyedSkinChanceField; + private TextFieldWidget playerEnergyAuraRadiusMinField; + private TextFieldWidget playerEnergyAuraRadiusMaxField; + private TextFieldWidget playerEnergyAuraPulseTicksField; + private ButtonWidget resetEnergyGainRateButton; + private ButtonWidget resetNearAnimationEnergyGainMultButton; + private ButtonWidget resetPlayerEnergyAuraMultLowButton; + private ButtonWidget resetPlayerEnergyAuraMultMidButton; + private ButtonWidget resetPlayerEnergyAuraMultHighButton; + private ButtonWidget resetDestroyedSkinAuraMultStage1Button; + private ButtonWidget resetDestroyedSkinAuraMultStage2Button; + private ButtonWidget resetDestroyedSkinAuraMultStage3Button; + private ButtonWidget resetDestroyedSkinAuraMultStage4Button; + private ButtonWidget resetNormalDamageDestroyedSkinChanceButton; + private ButtonWidget resetPlayerEnergyAuraRadiusMinButton; + private ButtonWidget resetPlayerEnergyAuraRadiusMaxButton; + private ButtonWidget resetPlayerEnergyAuraPulseTicksButton; + private boolean serverConfigEditable; + + protected GameplayEnergyScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.section.energy")); + this.parent = parent; + this.config = config; + this.energyGainRate = config.getEnergyGainRate(); + this.nearAnimationEnergyGainMult = config.getNearAnimationEnergyGainMult(); + this.playerEnergyAuraMultLow = config.getPlayerEnergyAuraMultLow(); + this.playerEnergyAuraMultMid = config.getPlayerEnergyAuraMultMid(); + this.playerEnergyAuraMultHigh = config.getPlayerEnergyAuraMultHigh(); + this.destroyedSkinAuraMultStage1 = config.getDestroyedSkinAuraMultStage1(); + this.destroyedSkinAuraMultStage2 = config.getDestroyedSkinAuraMultStage2(); + this.destroyedSkinAuraMultStage3 = config.getDestroyedSkinAuraMultStage3(); + this.destroyedSkinAuraMultStage4 = config.getDestroyedSkinAuraMultStage4(); + this.normalDamageDestroyedSkinChancePercent = config.getNormalDamageDestroyedSkinChancePercent(); + this.playerEnergyAuraRadiusMin = config.getPlayerEnergyAuraRadiusMin(); + this.playerEnergyAuraRadiusMax = config.getPlayerEnergyAuraRadiusMax(); + this.playerEnergyAuraPulseTicks = config.getPlayerEnergyAuraPulseTicks(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.energyGainRateField != null) { + this.energyGainRate = this.parseDecimalField(this.energyGainRateField, this.energyGainRate); + this.nearAnimationEnergyGainMult = this.parseDecimalField(this.nearAnimationEnergyGainMultField, this.nearAnimationEnergyGainMult); + this.playerEnergyAuraMultLow = this.parseDecimalField(this.playerEnergyAuraMultLowField, this.playerEnergyAuraMultLow); + this.playerEnergyAuraMultMid = this.parseDecimalField(this.playerEnergyAuraMultMidField, this.playerEnergyAuraMultMid); + this.playerEnergyAuraMultHigh = this.parseDecimalField(this.playerEnergyAuraMultHighField, this.playerEnergyAuraMultHigh); + this.destroyedSkinAuraMultStage1 = this.parseDecimalField(this.destroyedSkinAuraMultStage1Field, this.destroyedSkinAuraMultStage1); + this.destroyedSkinAuraMultStage2 = this.parseDecimalField(this.destroyedSkinAuraMultStage2Field, this.destroyedSkinAuraMultStage2); + this.destroyedSkinAuraMultStage3 = this.parseDecimalField(this.destroyedSkinAuraMultStage3Field, this.destroyedSkinAuraMultStage3); + this.destroyedSkinAuraMultStage4 = this.parseDecimalField(this.destroyedSkinAuraMultStage4Field, this.destroyedSkinAuraMultStage4); + this.normalDamageDestroyedSkinChancePercent = this.parseNumberField( + this.normalDamageDestroyedSkinChanceField, this.normalDamageDestroyedSkinChancePercent + ); + this.playerEnergyAuraRadiusMin = this.parseNumberField(this.playerEnergyAuraRadiusMinField, this.playerEnergyAuraRadiusMin); + this.playerEnergyAuraRadiusMax = this.parseNumberField(this.playerEnergyAuraRadiusMaxField, this.playerEnergyAuraRadiusMax); + this.playerEnergyAuraPulseTicks = this.parseNumberField(this.playerEnergyAuraPulseTicksField, this.playerEnergyAuraPulseTicks); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.energy_gain"))); + this.energyGainRateField = this.newDecimalField(fieldWidth, this.energyGainRate); + this.resetEnergyGainRateButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.energyGainRateField.setText(String.valueOf(this.defaults.getEnergyGainRate())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetEnergyGainRateButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.energy_gain_rate"), + this.energyGainRateField, + this.resetEnergyGainRateButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.energy_gain_rate") + ) + ); + this.nearAnimationEnergyGainMultField = this.newDecimalField(fieldWidth, this.nearAnimationEnergyGainMult); + this.resetNearAnimationEnergyGainMultButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.nearAnimationEnergyGainMultField.setText(String.valueOf(this.defaults.getNearAnimationEnergyGainMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetNearAnimationEnergyGainMultButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.near_animation_energy_gain_mult"), + this.nearAnimationEnergyGainMultField, + this.resetNearAnimationEnergyGainMultButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.near_animation_energy_gain_mult") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.player_energy_aura"))); + this.playerEnergyAuraMultLowField = this.newDecimalField(fieldWidth, this.playerEnergyAuraMultLow); + this.resetPlayerEnergyAuraMultLowButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerEnergyAuraMultLowField.setText(String.valueOf(this.defaults.getPlayerEnergyAuraMultLow())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerEnergyAuraMultLowButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_energy_aura_mult_low"), + this.playerEnergyAuraMultLowField, + this.resetPlayerEnergyAuraMultLowButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_energy_aura_mult_low") + ) + ); + this.playerEnergyAuraMultMidField = this.newDecimalField(fieldWidth, this.playerEnergyAuraMultMid); + this.resetPlayerEnergyAuraMultMidButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerEnergyAuraMultMidField.setText(String.valueOf(this.defaults.getPlayerEnergyAuraMultMid())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerEnergyAuraMultMidButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_energy_aura_mult_mid"), + this.playerEnergyAuraMultMidField, + this.resetPlayerEnergyAuraMultMidButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_energy_aura_mult_mid") + ) + ); + this.playerEnergyAuraMultHighField = this.newDecimalField(fieldWidth, this.playerEnergyAuraMultHigh); + this.resetPlayerEnergyAuraMultHighButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerEnergyAuraMultHighField.setText(String.valueOf(this.defaults.getPlayerEnergyAuraMultHigh())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerEnergyAuraMultHighButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_energy_aura_mult_high"), + this.playerEnergyAuraMultHighField, + this.resetPlayerEnergyAuraMultHighButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_energy_aura_mult_high") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.destroyed_skin"))); + this.destroyedSkinAuraMultStage1Field = this.newDecimalField(fieldWidth, this.destroyedSkinAuraMultStage1); + this.resetDestroyedSkinAuraMultStage1Button = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.destroyedSkinAuraMultStage1Field.setText(String.valueOf(this.defaults.getDestroyedSkinAuraMultStage1())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetDestroyedSkinAuraMultStage1Button, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.destroyed_skin_aura_mult_stage1"), + this.destroyedSkinAuraMultStage1Field, + this.resetDestroyedSkinAuraMultStage1Button, + this.destroyedSkinSettingTooltip("config.needsofnature.tooltip.destroyed_skin_aura_mult_stage1") + ) + ); + this.destroyedSkinAuraMultStage2Field = this.newDecimalField(fieldWidth, this.destroyedSkinAuraMultStage2); + this.resetDestroyedSkinAuraMultStage2Button = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.destroyedSkinAuraMultStage2Field.setText(String.valueOf(this.defaults.getDestroyedSkinAuraMultStage2())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetDestroyedSkinAuraMultStage2Button, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.destroyed_skin_aura_mult_stage2"), + this.destroyedSkinAuraMultStage2Field, + this.resetDestroyedSkinAuraMultStage2Button, + this.destroyedSkinSettingTooltip("config.needsofnature.tooltip.destroyed_skin_aura_mult_stage2") + ) + ); + this.destroyedSkinAuraMultStage3Field = this.newDecimalField(fieldWidth, this.destroyedSkinAuraMultStage3); + this.resetDestroyedSkinAuraMultStage3Button = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.destroyedSkinAuraMultStage3Field.setText(String.valueOf(this.defaults.getDestroyedSkinAuraMultStage3())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetDestroyedSkinAuraMultStage3Button, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.destroyed_skin_aura_mult_stage3"), + this.destroyedSkinAuraMultStage3Field, + this.resetDestroyedSkinAuraMultStage3Button, + this.destroyedSkinSettingTooltip("config.needsofnature.tooltip.destroyed_skin_aura_mult_stage3") + ) + ); + this.destroyedSkinAuraMultStage4Field = this.newDecimalField(fieldWidth, this.destroyedSkinAuraMultStage4); + this.resetDestroyedSkinAuraMultStage4Button = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.destroyedSkinAuraMultStage4Field.setText(String.valueOf(this.defaults.getDestroyedSkinAuraMultStage4())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetDestroyedSkinAuraMultStage4Button, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.destroyed_skin_aura_mult_stage4"), + this.destroyedSkinAuraMultStage4Field, + this.resetDestroyedSkinAuraMultStage4Button, + this.destroyedSkinSettingTooltip("config.needsofnature.tooltip.destroyed_skin_aura_mult_stage4") + ) + ); + this.normalDamageDestroyedSkinChanceField = this.newNumberField(fieldWidth, this.normalDamageDestroyedSkinChancePercent); + this.resetNormalDamageDestroyedSkinChanceButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.normalDamageDestroyedSkinChanceField.setText(String.valueOf(this.defaults.getNormalDamageDestroyedSkinChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetNormalDamageDestroyedSkinChanceButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.normal_damage_destroyed_skin_chance_percent"), + this.normalDamageDestroyedSkinChanceField, + this.resetNormalDamageDestroyedSkinChanceButton, + this.destroyedSkinSettingTooltip("config.needsofnature.tooltip.normal_damage_destroyed_skin_chance_percent") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.aura_range_timing"))); + this.playerEnergyAuraRadiusMinField = this.newNumberField(fieldWidth, this.playerEnergyAuraRadiusMin); + this.resetPlayerEnergyAuraRadiusMinButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerEnergyAuraRadiusMinField.setText(String.valueOf(this.defaults.getPlayerEnergyAuraRadiusMin())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerEnergyAuraRadiusMinButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_energy_aura_radius_min"), + this.playerEnergyAuraRadiusMinField, + this.resetPlayerEnergyAuraRadiusMinButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_energy_aura_radius_min") + ) + ); + this.playerEnergyAuraRadiusMaxField = this.newNumberField(fieldWidth, this.playerEnergyAuraRadiusMax); + this.resetPlayerEnergyAuraRadiusMaxButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerEnergyAuraRadiusMaxField.setText(String.valueOf(this.defaults.getPlayerEnergyAuraRadiusMax())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerEnergyAuraRadiusMaxButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_energy_aura_radius_max"), + this.playerEnergyAuraRadiusMaxField, + this.resetPlayerEnergyAuraRadiusMaxButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_energy_aura_radius_max") + ) + ); + this.playerEnergyAuraPulseTicksField = this.newNumberField(fieldWidth, this.playerEnergyAuraPulseTicks); + this.resetPlayerEnergyAuraPulseTicksButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.playerEnergyAuraPulseTicksField.setText(String.valueOf(this.defaults.getPlayerEnergyAuraPulseTicks())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPlayerEnergyAuraPulseTicksButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_energy_aura_pulse_ticks"), + this.playerEnergyAuraPulseTicksField, + this.resetPlayerEnergyAuraPulseTicksButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.player_energy_aura_pulse_ticks") + ) + ); + this.energyGainRateField.setChangedListener(ignored -> this.updateResetButtons()); + this.nearAnimationEnergyGainMultField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerEnergyAuraMultLowField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerEnergyAuraMultMidField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerEnergyAuraMultHighField.setChangedListener(ignored -> this.updateResetButtons()); + this.destroyedSkinAuraMultStage1Field.setChangedListener(ignored -> this.updateResetButtons()); + this.destroyedSkinAuraMultStage2Field.setChangedListener(ignored -> this.updateResetButtons()); + this.destroyedSkinAuraMultStage3Field.setChangedListener(ignored -> this.updateResetButtons()); + this.destroyedSkinAuraMultStage4Field.setChangedListener(ignored -> this.updateResetButtons()); + this.normalDamageDestroyedSkinChanceField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerEnergyAuraRadiusMinField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerEnergyAuraRadiusMaxField.setChangedListener(ignored -> this.updateResetButtons()); + this.playerEnergyAuraPulseTicksField.setChangedListener(ignored -> this.updateResetButtons()); + this.energyGainRateField.setEditable(this.serverConfigEditable); + this.nearAnimationEnergyGainMultField.setEditable(this.serverConfigEditable); + this.playerEnergyAuraMultLowField.setEditable(this.serverConfigEditable); + this.playerEnergyAuraMultMidField.setEditable(this.serverConfigEditable); + this.playerEnergyAuraMultHighField.setEditable(this.serverConfigEditable); + this.destroyedSkinAuraMultStage1Field.setEditable(this.destroyedSkinSettingsEditable()); + this.destroyedSkinAuraMultStage2Field.setEditable(this.destroyedSkinSettingsEditable()); + this.destroyedSkinAuraMultStage3Field.setEditable(this.destroyedSkinSettingsEditable()); + this.destroyedSkinAuraMultStage4Field.setEditable(this.destroyedSkinSettingsEditable()); + this.normalDamageDestroyedSkinChanceField.setEditable(this.destroyedSkinSettingsEditable()); + this.playerEnergyAuraRadiusMinField.setEditable(this.serverConfigEditable); + this.playerEnergyAuraRadiusMaxField.setEditable(this.serverConfigEditable); + this.playerEnergyAuraPulseTicksField.setEditable(this.serverConfigEditable); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip( + doneButton, this.serverConfigEditable ? "config.needsofnature.tooltip.done_save" : "config.needsofnature.tooltip.done_unsaved" + ); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.energyGainRate = this.clampEnergyGainRate(this.parseDecimalField(this.energyGainRateField, this.energyGainRate)); + this.nearAnimationEnergyGainMult = this.clampNearAnimationEnergyGainMult( + this.parseDecimalField(this.nearAnimationEnergyGainMultField, this.nearAnimationEnergyGainMult) + ); + this.playerEnergyAuraMultLow = this.clampPlayerEnergyAuraMult( + this.parseDecimalField(this.playerEnergyAuraMultLowField, this.playerEnergyAuraMultLow) + ); + this.playerEnergyAuraMultMid = this.clampPlayerEnergyAuraMult( + this.parseDecimalField(this.playerEnergyAuraMultMidField, this.playerEnergyAuraMultMid) + ); + this.playerEnergyAuraMultHigh = this.clampPlayerEnergyAuraMult( + this.parseDecimalField(this.playerEnergyAuraMultHighField, this.playerEnergyAuraMultHigh) + ); + this.destroyedSkinAuraMultStage1 = this.clampDestroyedSkinAuraMult( + this.parseDecimalField(this.destroyedSkinAuraMultStage1Field, this.destroyedSkinAuraMultStage1) + ); + this.destroyedSkinAuraMultStage2 = this.clampDestroyedSkinAuraMult( + this.parseDecimalField(this.destroyedSkinAuraMultStage2Field, this.destroyedSkinAuraMultStage2) + ); + this.destroyedSkinAuraMultStage3 = this.clampDestroyedSkinAuraMult( + this.parseDecimalField(this.destroyedSkinAuraMultStage3Field, this.destroyedSkinAuraMultStage3) + ); + this.destroyedSkinAuraMultStage4 = this.clampDestroyedSkinAuraMult( + this.parseDecimalField(this.destroyedSkinAuraMultStage4Field, this.destroyedSkinAuraMultStage4) + ); + this.normalDamageDestroyedSkinChancePercent = this.clampPercent( + this.parseNumberField(this.normalDamageDestroyedSkinChanceField, this.normalDamageDestroyedSkinChancePercent) + ); + this.playerEnergyAuraRadiusMin = this.clampPlayerEnergyAuraRadius( + this.parseNumberField(this.playerEnergyAuraRadiusMinField, this.playerEnergyAuraRadiusMin) + ); + this.playerEnergyAuraRadiusMax = Math.max( + this.playerEnergyAuraRadiusMin, + this.clampPlayerEnergyAuraRadius(this.parseNumberField(this.playerEnergyAuraRadiusMaxField, this.playerEnergyAuraRadiusMax)) + ); + this.playerEnergyAuraPulseTicks = this.clampPlayerEnergyAuraPulseTicks( + this.parseNumberField(this.playerEnergyAuraPulseTicksField, this.playerEnergyAuraPulseTicks) + ); + this.config.setEnergyGainRate(this.energyGainRate); + this.config.setNearAnimationEnergyGainMult(this.nearAnimationEnergyGainMult); + this.config.setPlayerEnergyAuraMultLow(this.playerEnergyAuraMultLow); + this.config.setPlayerEnergyAuraMultMid(this.playerEnergyAuraMultMid); + this.config.setPlayerEnergyAuraMultHigh(this.playerEnergyAuraMultHigh); + this.config.setDestroyedSkinAuraMultStage1(this.destroyedSkinAuraMultStage1); + this.config.setDestroyedSkinAuraMultStage2(this.destroyedSkinAuraMultStage2); + this.config.setDestroyedSkinAuraMultStage3(this.destroyedSkinAuraMultStage3); + this.config.setDestroyedSkinAuraMultStage4(this.destroyedSkinAuraMultStage4); + this.config.setNormalDamageDestroyedSkinChancePercent(this.normalDamageDestroyedSkinChancePercent); + this.config.setPlayerEnergyAuraRadiusMin(this.playerEnergyAuraRadiusMin); + this.config.setPlayerEnergyAuraRadiusMax(this.playerEnergyAuraRadiusMax); + this.config.setPlayerEnergyAuraPulseTicks(this.playerEnergyAuraPulseTicks); + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newDecimalField(int w, double initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + int maxLen = 16; + field.setMaxLength(maxLen); + field.setText(this.trimToMax(this.formatDecimal(initial), maxLen)); + field.setEditable(true); + return field; + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + int maxLen = 16; + field.setMaxLength(maxLen); + field.setText(this.trimToMax(String.valueOf(initial), maxLen)); + field.setEditable(true); + return field; + } + + private double parseDecimalField(TextFieldWidget field, double fallback) { + try { + return Double.parseDouble(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private int parseNumberField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private String formatDecimal(double value) { + String text = String.format(Locale.ROOT, "%.3f", value); + int dot = text.indexOf(46); + if (dot >= 0) { + int end = text.length(); + + while (end > dot + 1 && text.charAt(end - 1) == '0') { + end--; + } + + if (end == dot + 1) { + end = dot; + } + + text = text.substring(0, end); + } + + return text; + } + + private String trimToMax(String value, int max) { + if (value == null) { + return ""; + } else { + return value.length() > max ? value.substring(0, max) : value; + } + } + + private double clampEnergyGainRate(double v) { + return Math.max(0.1, Math.min(10.0, v)); + } + + private double clampNearAnimationEnergyGainMult(double v) { + return Math.max(1.0, Math.min(10.0, v)); + } + + private double clampPlayerEnergyAuraMult(double v) { + return Math.max(1.0, Math.min(20.0, v)); + } + + private double clampDestroyedSkinAuraMult(double v) { + return Math.max(1.0, Math.min(50.0, v)); + } + + private int clampPercent(int v) { + return Math.max(0, Math.min(100, v)); + } + + private int clampPlayerEnergyAuraRadius(int v) { + return Math.max(1, Math.min(64, v)); + } + + private int clampPlayerEnergyAuraPulseTicks(int v) { + return Math.max(1, Math.min(1200, v)); + } + + private boolean destroyedSkinSettingsEditable() { + return this.serverConfigEditable && this.config.isDestroyedSkinSystemEnabled(); + } + + private Tooltip destroyedSkinSettingTooltip(String enabledKey) { + return NonModMenuScreens.tooltip( + this.config.isDestroyedSkinSystemEnabled() ? enabledKey : "config.needsofnature.tooltip.destroyed_skin_system.disabled_setting" + ); + } + + private boolean isDoubleFieldChanged(TextFieldWidget field, double defaultValue) { + if (field == null) { + return false; + } + + double value = this.parseDecimalField(field, defaultValue); + return Math.abs(value - defaultValue) >= 1.0E-4; + } + + private boolean isIntFieldChanged(TextFieldWidget field, int defaultValue) { + return field == null ? false : this.parseNumberField(field, defaultValue) != defaultValue; + } + + private void updateResetButtons() { + if (this.resetEnergyGainRateButton != null) { + this.resetEnergyGainRateButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.energyGainRateField, this.defaults.getEnergyGainRate()); + } + + if (this.resetNearAnimationEnergyGainMultButton != null) { + this.resetNearAnimationEnergyGainMultButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.nearAnimationEnergyGainMultField, this.defaults.getNearAnimationEnergyGainMult()); + } + + if (this.resetPlayerEnergyAuraMultLowButton != null) { + this.resetPlayerEnergyAuraMultLowButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.playerEnergyAuraMultLowField, this.defaults.getPlayerEnergyAuraMultLow()); + } + + if (this.resetPlayerEnergyAuraMultMidButton != null) { + this.resetPlayerEnergyAuraMultMidButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.playerEnergyAuraMultMidField, this.defaults.getPlayerEnergyAuraMultMid()); + } + + if (this.resetPlayerEnergyAuraMultHighButton != null) { + this.resetPlayerEnergyAuraMultHighButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.playerEnergyAuraMultHighField, this.defaults.getPlayerEnergyAuraMultHigh()); + } + + if (this.resetDestroyedSkinAuraMultStage1Button != null) { + this.resetDestroyedSkinAuraMultStage1Button.active = this.destroyedSkinSettingsEditable() + && this.isDoubleFieldChanged(this.destroyedSkinAuraMultStage1Field, this.defaults.getDestroyedSkinAuraMultStage1()); + } + + if (this.resetDestroyedSkinAuraMultStage2Button != null) { + this.resetDestroyedSkinAuraMultStage2Button.active = this.destroyedSkinSettingsEditable() + && this.isDoubleFieldChanged(this.destroyedSkinAuraMultStage2Field, this.defaults.getDestroyedSkinAuraMultStage2()); + } + + if (this.resetDestroyedSkinAuraMultStage3Button != null) { + this.resetDestroyedSkinAuraMultStage3Button.active = this.destroyedSkinSettingsEditable() + && this.isDoubleFieldChanged(this.destroyedSkinAuraMultStage3Field, this.defaults.getDestroyedSkinAuraMultStage3()); + } + + if (this.resetDestroyedSkinAuraMultStage4Button != null) { + this.resetDestroyedSkinAuraMultStage4Button.active = this.destroyedSkinSettingsEditable() + && this.isDoubleFieldChanged(this.destroyedSkinAuraMultStage4Field, this.defaults.getDestroyedSkinAuraMultStage4()); + } + + if (this.resetNormalDamageDestroyedSkinChanceButton != null) { + this.resetNormalDamageDestroyedSkinChanceButton.active = this.destroyedSkinSettingsEditable() + && this.isIntFieldChanged(this.normalDamageDestroyedSkinChanceField, this.defaults.getNormalDamageDestroyedSkinChancePercent()); + } + + if (this.resetPlayerEnergyAuraRadiusMinButton != null) { + this.resetPlayerEnergyAuraRadiusMinButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.playerEnergyAuraRadiusMinField, this.defaults.getPlayerEnergyAuraRadiusMin()); + } + + if (this.resetPlayerEnergyAuraRadiusMaxButton != null) { + this.resetPlayerEnergyAuraRadiusMaxButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.playerEnergyAuraRadiusMaxField, this.defaults.getPlayerEnergyAuraRadiusMax()); + } + + if (this.resetPlayerEnergyAuraPulseTicksButton != null) { + this.resetPlayerEnergyAuraPulseTicksButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.playerEnergyAuraPulseTicksField, this.defaults.getPlayerEnergyAuraPulseTicks()); + } + } + } + + static class GameplayWorldScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private int initialEnergyMax; + private int genderMalePercent; + private int genderFemalePercent; + private int genderBothPercent; + private boolean allowPlayerGenderChangeAnytime; + private boolean requirePlayerGenderSelectionOnJoin; + private int allowedStartingGenderMask; + private TextFieldWidget initialEnergyField; + private TextFieldWidget genderMaleField; + private TextFieldWidget genderFemaleField; + private TextFieldWidget genderBothField; + private ButtonWidget allowPlayerGenderChangeButton; + private ButtonWidget requirePlayerGenderSelectionButton; + private ButtonWidget allowStartingMaleButton; + private ButtonWidget allowStartingFemaleButton; + private ButtonWidget allowStartingBothButton; + private ButtonWidget resetInitialEnergyButton; + private ButtonWidget resetGenderMaleButton; + private ButtonWidget resetGenderFemaleButton; + private ButtonWidget resetGenderBothButton; + private ButtonWidget resetAllowPlayerGenderChangeButton; + private ButtonWidget resetRequirePlayerGenderSelectionButton; + private ButtonWidget resetAllowedStartingGendersButton; + private boolean serverConfigEditable; + + protected GameplayWorldScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.section.world")); + this.parent = parent; + this.config = config; + this.initialEnergyMax = config.getInitialEnergyMax(); + this.genderMalePercent = config.getGenderMaleChancePercent(); + this.genderFemalePercent = config.getGenderFemaleChancePercent(); + this.genderBothPercent = config.getGenderBothChancePercent(); + this.allowPlayerGenderChangeAnytime = config.allowPlayerGenderChangeAnytime(); + this.requirePlayerGenderSelectionOnJoin = config.requirePlayerGenderSelectionOnJoin(); + this.allowedStartingGenderMask = config.getAllowedStartingGenderMask(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.initialEnergyField != null) { + this.initialEnergyMax = this.parseField(this.initialEnergyField, this.initialEnergyMax); + this.genderMalePercent = this.parseField(this.genderMaleField, this.genderMalePercent); + this.genderFemalePercent = this.parseField(this.genderFemaleField, this.genderFemalePercent); + this.genderBothPercent = this.parseField(this.genderBothField, this.genderBothPercent); + } + + int listTop = 32; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.mob_energy"))); + this.initialEnergyField = this.newNumberField(fieldWidth, this.initialEnergyMax); + this.resetInitialEnergyButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.initialEnergyField.setText(String.valueOf(this.defaults.getInitialEnergyMax())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetInitialEnergyButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.initial_energy_max"), + this.initialEnergyField, + this.resetInitialEnergyButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.initial_energy_max") + ) + ); + settingsList.addEntryRow( + SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.gender_spawn_chances")) + ); + this.genderMaleField = this.newNumberField(fieldWidth, this.genderMalePercent); + this.resetGenderMaleButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.genderMaleField.setText(String.valueOf(this.defaults.getGenderMaleChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetGenderMaleButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.gender_male"), + this.genderMaleField, + this.resetGenderMaleButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.gender_male") + ) + ); + this.genderFemaleField = this.newNumberField(fieldWidth, this.genderFemalePercent); + this.resetGenderFemaleButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.genderFemaleField.setText(String.valueOf(this.defaults.getGenderFemaleChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetGenderFemaleButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.gender_female"), + this.genderFemaleField, + this.resetGenderFemaleButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.gender_female") + ) + ); + this.genderBothField = this.newNumberField(fieldWidth, this.genderBothPercent); + this.resetGenderBothButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.genderBothField.setText(String.valueOf(this.defaults.getGenderBothChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetGenderBothButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.gender_both"), + this.genderBothField, + this.resetGenderBothButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.gender_both") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.player_gender_rules"))); + this.allowPlayerGenderChangeButton = ButtonWidget.builder(this.onOffLabel(this.allowPlayerGenderChangeAnytime), button -> { + this.allowPlayerGenderChangeAnytime = !this.allowPlayerGenderChangeAnytime; + button.setMessage(this.onOffLabel(this.allowPlayerGenderChangeAnytime)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetAllowPlayerGenderChangeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.allowPlayerGenderChangeAnytime = this.defaults.allowPlayerGenderChangeAnytime(); + this.allowPlayerGenderChangeButton.setMessage(this.onOffLabel(this.allowPlayerGenderChangeAnytime)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetAllowPlayerGenderChangeButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.allow_player_gender_change_anytime"), + this.allowPlayerGenderChangeButton, + this.resetAllowPlayerGenderChangeButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.allow_player_gender_change_anytime") + ) + ); + this.requirePlayerGenderSelectionButton = ButtonWidget.builder(this.onOffLabel(this.requirePlayerGenderSelectionOnJoin), button -> { + this.requirePlayerGenderSelectionOnJoin = !this.requirePlayerGenderSelectionOnJoin; + button.setMessage(this.onOffLabel(this.requirePlayerGenderSelectionOnJoin)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetRequirePlayerGenderSelectionButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.requirePlayerGenderSelectionOnJoin = this.defaults.requirePlayerGenderSelectionOnJoin(); + this.requirePlayerGenderSelectionButton.setMessage(this.onOffLabel(this.requirePlayerGenderSelectionOnJoin)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetRequirePlayerGenderSelectionButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.require_player_gender_selection_on_join"), + this.requirePlayerGenderSelectionButton, + this.resetRequirePlayerGenderSelectionButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.require_player_gender_selection_on_join") + ) + ); + this.allowStartingMaleButton = ButtonWidget.builder( + this.startingGenderToggleLabel("config.needsofnature.player_gender.male", this.isStartingGenderChoiceAllowed(1)), button -> { + this.allowedStartingGenderMask = this.toggleAllowedStartingGenderChoice(this.allowedStartingGenderMask, 1); + this.updateAllowedStartingGenderButtons(); + this.updateResetButtons(); + } + ) + .dimensions(0, 0, 64, 20) + .build(); + this.allowStartingFemaleButton = ButtonWidget.builder( + this.startingGenderToggleLabel("config.needsofnature.player_gender.female", this.isStartingGenderChoiceAllowed(2)), button -> { + this.allowedStartingGenderMask = this.toggleAllowedStartingGenderChoice(this.allowedStartingGenderMask, 2); + this.updateAllowedStartingGenderButtons(); + this.updateResetButtons(); + } + ) + .dimensions(0, 0, 70, 20) + .build(); + this.allowStartingBothButton = ButtonWidget.builder( + this.startingGenderToggleLabel("config.needsofnature.player_gender.both", this.isStartingGenderChoiceAllowed(4)), button -> { + this.allowedStartingGenderMask = this.toggleAllowedStartingGenderChoice(this.allowedStartingGenderMask, 4); + this.updateAllowedStartingGenderButtons(); + this.updateResetButtons(); + } + ) + .dimensions(0, 0, 98, 20) + .build(); + this.resetAllowedStartingGendersButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.allowedStartingGenderMask = this.defaults.getAllowedStartingGenderMask(); + this.updateAllowedStartingGenderButtons(); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.allowStartingMaleButton, "config.needsofnature.tooltip.allowed_starting_genders"); + NonModMenuScreens.setTooltip(this.allowStartingFemaleButton, "config.needsofnature.tooltip.allowed_starting_genders"); + NonModMenuScreens.setTooltip(this.allowStartingBothButton, "config.needsofnature.tooltip.allowed_starting_genders"); + NonModMenuScreens.setTooltip(this.resetAllowedStartingGendersButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.groupedField( + this.textRenderer, + Text.translatable("config.needsofnature.allowed_starting_genders"), + NonModMenuScreens.tooltip("config.needsofnature.tooltip.allowed_starting_genders"), + this.allowStartingMaleButton, + this.allowStartingFemaleButton, + this.allowStartingBothButton, + this.resetAllowedStartingGendersButton + ) + ); + this.initialEnergyField.setChangedListener(ignored -> this.updateResetButtons()); + this.genderMaleField.setChangedListener(ignored -> this.updateResetButtons()); + this.genderFemaleField.setChangedListener(ignored -> this.updateResetButtons()); + this.genderBothField.setChangedListener(ignored -> this.updateResetButtons()); + this.initialEnergyField.setEditable(this.serverConfigEditable); + this.genderMaleField.setEditable(this.serverConfigEditable); + this.genderFemaleField.setEditable(this.serverConfigEditable); + this.genderBothField.setEditable(this.serverConfigEditable); + this.allowPlayerGenderChangeButton.active = this.serverConfigEditable; + this.requirePlayerGenderSelectionButton.active = this.serverConfigEditable; + this.allowStartingMaleButton.active = this.serverConfigEditable; + this.allowStartingFemaleButton.active = this.serverConfigEditable; + this.allowStartingBothButton.active = this.serverConfigEditable; + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget genderSpawnButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.gender_spawn_button"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.GenderSpawnConfigScreen(this, this.config)) + ) + .dimensions(centerX - 100, this.height - 52, 200, 20) + .build(); + NonModMenuScreens.setTooltip(genderSpawnButton, "config.needsofnature.tooltip.gender_spawn_button"); + this.addDrawableChild(genderSpawnButton); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.initialEnergyMax = this.clampInitialEnergy(this.parseField(this.initialEnergyField, this.initialEnergyMax)); + this.genderMalePercent = this.clampPercent(this.parseField(this.genderMaleField, this.genderMalePercent)); + this.genderFemalePercent = this.clampPercent(this.parseField(this.genderFemaleField, this.genderFemalePercent)); + this.genderBothPercent = this.clampPercent(this.parseField(this.genderBothField, this.genderBothPercent)); + this.config.setInitialEnergyMax(this.initialEnergyMax); + this.config.setGlobalGenderSpawnChances(new NonConfig.GenderSpawnChances(this.genderMalePercent, this.genderFemalePercent, this.genderBothPercent)); + this.config.setAllowPlayerGenderChangeAnytime(this.allowPlayerGenderChangeAnytime); + this.config.setRequirePlayerGenderSelectionOnJoin(this.requirePlayerGenderSelectionOnJoin); + this.config.setAllowedStartingGenderMask(this.allowedStartingGenderMask); + } + + if (this.serverConfigEditable) { + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private int clampInitialEnergy(int v) { + return Math.max(0, Math.min(200, v)); + } + + private int clampPercent(int v) { + return Math.max(0, Math.min(100, v)); + } + + private boolean isIntFieldChanged(TextFieldWidget field, int defaultValue) { + return field == null ? false : this.parseField(field, defaultValue) != defaultValue; + } + + private Text onOffLabel(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private Text startingGenderToggleLabel(String labelKey, boolean enabled) { + return Text.translatable( + enabled ? "config.needsofnature.toggle_label.enabled" : "config.needsofnature.toggle_label.disabled", new Object[]{Text.translatable(labelKey)} + ); + } + + private boolean isStartingGenderChoiceAllowed(int choiceBit) { + return (this.normalizeAllowedStartingGenderMask(this.allowedStartingGenderMask) & choiceBit) != 0; + } + + private int normalizeAllowedStartingGenderMask(int mask) { + int normalized = mask & 7; + return normalized == 0 ? 7 : normalized; + } + + private int toggleAllowedStartingGenderChoice(int mask, int choiceBit) { + int normalized = this.normalizeAllowedStartingGenderMask(mask); + int next = normalized ^ choiceBit; + return next == 0 ? normalized : next; + } + + private void updateAllowedStartingGenderButtons() { + if (this.allowStartingMaleButton != null) { + this.allowStartingMaleButton + .setMessage(this.startingGenderToggleLabel("config.needsofnature.player_gender.male", this.isStartingGenderChoiceAllowed(1))); + } + + if (this.allowStartingFemaleButton != null) { + this.allowStartingFemaleButton + .setMessage(this.startingGenderToggleLabel("config.needsofnature.player_gender.female", this.isStartingGenderChoiceAllowed(2))); + } + + if (this.allowStartingBothButton != null) { + this.allowStartingBothButton + .setMessage(this.startingGenderToggleLabel("config.needsofnature.player_gender.both", this.isStartingGenderChoiceAllowed(4))); + } + } + + private void updateResetButtons() { + if (this.resetInitialEnergyButton != null) { + this.resetInitialEnergyButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.initialEnergyField, this.defaults.getInitialEnergyMax()); + } + + if (this.resetGenderMaleButton != null) { + this.resetGenderMaleButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.genderMaleField, this.defaults.getGenderMaleChancePercent()); + } + + if (this.resetGenderFemaleButton != null) { + this.resetGenderFemaleButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.genderFemaleField, this.defaults.getGenderFemaleChancePercent()); + } + + if (this.resetGenderBothButton != null) { + this.resetGenderBothButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.genderBothField, this.defaults.getGenderBothChancePercent()); + } + + if (this.resetAllowPlayerGenderChangeButton != null) { + this.resetAllowPlayerGenderChangeButton.active = this.serverConfigEditable + && this.allowPlayerGenderChangeAnytime != this.defaults.allowPlayerGenderChangeAnytime(); + } + + if (this.resetRequirePlayerGenderSelectionButton != null) { + this.resetRequirePlayerGenderSelectionButton.active = this.serverConfigEditable + && this.requirePlayerGenderSelectionOnJoin != this.defaults.requirePlayerGenderSelectionOnJoin(); + } + + if (this.resetAllowedStartingGendersButton != null) { + this.resetAllowedStartingGendersButton.active = this.serverConfigEditable + && this.allowedStartingGenderMask != this.defaults.getAllowedStartingGenderMask(); + } + + if (this.allowPlayerGenderChangeButton != null) { + this.allowPlayerGenderChangeButton.active = this.serverConfigEditable; + } + + if (this.requirePlayerGenderSelectionButton != null) { + this.requirePlayerGenderSelectionButton.active = this.serverConfigEditable; + } + + if (this.allowStartingMaleButton != null) { + this.allowStartingMaleButton.active = this.serverConfigEditable; + } + + if (this.allowStartingFemaleButton != null) { + this.allowStartingFemaleButton.active = this.serverConfigEditable; + } + + if (this.allowStartingBothButton != null) { + this.allowStartingBothButton.active = this.serverConfigEditable; + } + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuHudConfigScreen.java b/src/main/java/com/nonid/integration/NonModMenuHudConfigScreen.java new file mode 100644 index 0000000..2d4908f --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuHudConfigScreen.java @@ -0,0 +1,653 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import com.nonid.client.NonHudOverlay; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.input.KeyInput; +import net.minecraft.text.Text; + +final class NonModMenuHudConfigScreen { + private NonModMenuHudConfigScreen() { + } + + static class UiConfigScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private static final int UI_MIN = -999; + private static final int UI_MAX = 999; + private int energyX; + private int energyY; + private int energyHeartX; + private int energyHeartY; + private int attackX; + private int attackY; + private int promptX; + private int promptY; + private int liquidX; + private int liquidY; + private boolean energyVisible; + private boolean energyHeartVisible; + private boolean attackVisible; + private boolean promptVisible; + private boolean liquidVisible; + private int appliedEnergyX; + private int appliedEnergyY; + private int appliedEnergyHeartX; + private int appliedEnergyHeartY; + private int appliedAttackX; + private int appliedAttackY; + private int appliedPromptX; + private int appliedPromptY; + private int appliedLiquidX; + private int appliedLiquidY; + private boolean appliedEnergyVisible; + private boolean appliedEnergyHeartVisible; + private boolean appliedAttackVisible; + private boolean appliedPromptVisible; + private boolean appliedLiquidVisible; + private NonModMenuHudConfigScreen.UiConfigScreen.HudElement selectedElement = NonModMenuHudConfigScreen.UiConfigScreen.HudElement.LIQUID; + private NonModMenuHudConfigScreen.UiConfigScreen.HudElement draggedElement = null; + private double dragStartMouseX; + private double dragStartMouseY; + private int dragStartOffsetX; + private int dragStartOffsetY; + private ButtonWidget saveButton; + private ButtonWidget cancelButton; + private ButtonWidget toggleSelectedVisibilityButton; + private ButtonWidget resetSelectedButton; + private ButtonWidget resetAllButton; + private ButtonWidget toolbarVisibilityButton; + private boolean toolbarVisible = true; + private NonHudOverlay.UiPreviewBounds lastBounds = NonHudOverlay.UiPreviewBounds.empty(); + + protected UiConfigScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.ui_title")); + this.parent = parent; + this.config = config; + this.energyX = config.getUiEnergyOffsetX(); + this.energyY = config.getUiEnergyOffsetY(); + this.energyHeartX = config.getUiEnergyHeartOffsetX(); + this.energyHeartY = config.getUiEnergyHeartOffsetY(); + this.attackX = config.getUiAttackOffsetX(); + this.attackY = config.getUiAttackOffsetY(); + this.promptX = config.getUiPromptOffsetX(); + this.promptY = config.getUiPromptOffsetY(); + this.liquidX = config.getUiLiquidOffsetX(); + this.liquidY = config.getUiLiquidOffsetY(); + this.energyVisible = config.isUiEnergyVisible(); + this.energyHeartVisible = config.isUiEnergyHeartVisible(); + this.attackVisible = config.isUiAttackVisible(); + this.promptVisible = config.isUiPromptVisible(); + this.liquidVisible = config.isUiLiquidVisible(); + this.appliedEnergyX = this.energyX; + this.appliedEnergyY = this.energyY; + this.appliedEnergyHeartX = this.energyHeartX; + this.appliedEnergyHeartY = this.energyHeartY; + this.appliedAttackX = this.attackX; + this.appliedAttackY = this.attackY; + this.appliedPromptX = this.promptX; + this.appliedPromptY = this.promptY; + this.appliedLiquidX = this.liquidX; + this.appliedLiquidY = this.liquidY; + this.appliedEnergyVisible = this.energyVisible; + this.appliedEnergyHeartVisible = this.energyHeartVisible; + this.appliedAttackVisible = this.attackVisible; + this.appliedPromptVisible = this.promptVisible; + this.appliedLiquidVisible = this.liquidVisible; + } + + protected void init() { + NonHudOverlay.setUiPreviewEnabled(true); + this.updatePreviewOffsets(); + int buttonY = 8; + this.saveButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> { + this.saveOffsets(); + NonHudOverlay.setUiPreviewEnabled(false); + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(0, buttonY, 48, 18).build(); + NonModMenuScreens.setTooltip(this.saveButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(this.saveButton); + this.toggleSelectedVisibilityButton = ButtonWidget.builder(this.toggleSelectedVisibilityLabel(), button -> { + this.setVisible(this.selectedElement, !this.isVisible(this.selectedElement)); + this.updatePreviewOffsets(); + this.updateButtons(); + }).dimensions(0, buttonY, 86, 18).build(); + NonModMenuScreens.setTooltip(this.toggleSelectedVisibilityButton, "config.needsofnature.tooltip.ui_toggle_selected_visibility"); + this.addDrawableChild(this.toggleSelectedVisibilityButton); + this.resetSelectedButton = ButtonWidget.builder(Text.translatable("config.needsofnature.ui_reset_selected"), button -> { + this.resetElement(this.selectedElement); + this.updatePreviewOffsets(); + this.updateButtons(); + }).dimensions(0, buttonY, 86, 18).build(); + this.addDrawableChild(this.resetSelectedButton); + this.resetAllButton = ButtonWidget.builder(Text.translatable("config.needsofnature.ui_reset_all"), button -> { + this.resetElement(NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ENERGY); + this.resetElement(NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ENERGY_HEART); + this.resetElement(NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ATTACK); + this.resetElement(NonModMenuHudConfigScreen.UiConfigScreen.HudElement.PROMPT); + this.resetElement(NonModMenuHudConfigScreen.UiConfigScreen.HudElement.LIQUID); + this.updatePreviewOffsets(); + this.updateButtons(); + }).dimensions(0, buttonY, 62, 18).build(); + this.addDrawableChild(this.resetAllButton); + this.cancelButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> { + this.resetToApplied(); + NonHudOverlay.setUiPreviewEnabled(false); + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(0, buttonY, 54, 18).build(); + NonModMenuScreens.setTooltip(this.cancelButton, "config.needsofnature.tooltip.done_unsaved"); + this.addDrawableChild(this.cancelButton); + this.toolbarVisibilityButton = ButtonWidget.builder(this.toolbarVisibilityLabel(), button -> { + this.toolbarVisible = !this.toolbarVisible; + this.updateButtons(); + }).dimensions(0, buttonY, 18, 18).build(); + NonModMenuScreens.setTooltip(this.toolbarVisibilityButton, "config.needsofnature.tooltip.ui_toolbar_visibility"); + this.addDrawableChild(this.toolbarVisibilityButton); + this.updateButtons(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updatePreviewOffsets(); + super.render(context, mouseX, mouseY, deltaTicks); + this.lastBounds = NonHudOverlay.getUiPreviewBounds(context); + this.drawDragOverlays(context, mouseX, mouseY); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 12, 16777215); + Text help = Text.translatable("config.needsofnature.ui_drag_help"); + context.drawCenteredTextWithShadow(this.textRenderer, help, this.width / 2, 24, -2039584); + Text selected = this.isVisible(this.selectedElement) + ? Text.translatable("config.needsofnature.ui_selected", new Object[]{Text.translatable(this.selectedElement.labelKey)}) + : Text.translatable("config.needsofnature.ui_selected_hidden", new Object[]{Text.translatable(this.selectedElement.labelKey)}); + context.drawCenteredTextWithShadow(this.textRenderer, selected, this.width / 2, 36, this.selectedElement.color); + this.updateButtons(); + } + + public void renderBackground(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + } + + public boolean mouseClicked(Click click, boolean doubled) { + if (super.mouseClicked(click, doubled)) { + return true; + } + + if (click.button() != 0) { + return false; + } + + NonModMenuHudConfigScreen.UiConfigScreen.HudElement hit = this.hitTest(click.x(), click.y()); + if (hit == null) { + return false; + } + + this.selectedElement = hit; + this.draggedElement = hit; + this.dragStartMouseX = click.x(); + this.dragStartMouseY = click.y(); + this.dragStartOffsetX = this.offsetX(hit); + this.dragStartOffsetY = this.offsetY(hit); + this.updateButtons(); + return true; + } + + public boolean mouseDragged(Click click, double offsetX, double offsetY) { + if (this.draggedElement != null && click.button() == 0) { + int nextX = this.clampUiOffset(this.dragStartOffsetX + (int)Math.round(click.x() - this.dragStartMouseX)); + int nextY = this.clampUiOffset(this.dragStartOffsetY + (int)Math.round(click.y() - this.dragStartMouseY)); + this.setOffset(this.draggedElement, nextX, nextY); + this.updatePreviewOffsets(); + this.updateButtons(); + return true; + } else { + return super.mouseDragged(click, offsetX, offsetY); + } + } + + public boolean mouseReleased(Click click) { + if (click.button() == 0 && this.draggedElement != null) { + this.draggedElement = null; + this.updateButtons(); + return true; + } else { + return super.mouseReleased(click); + } + } + + public boolean keyPressed(KeyInput input) { + int step = (input.modifiers() & 1) != 0 ? 5 : 1; + int dx = 0; + int dy = 0; + int keyCode = input.key(); + if (keyCode == 263) { + dx = -step; + } + + if (keyCode == 262) { + dx = step; + } + + if (keyCode == 265) { + dy = -step; + } + + if (keyCode == 264) { + dy = step; + } + + if (dx == 0 && dy == 0) { + return super.keyPressed(input); + } + + this.setOffset( + this.selectedElement, this.clampUiOffset(this.offsetX(this.selectedElement) + dx), this.clampUiOffset(this.offsetY(this.selectedElement) + dy) + ); + this.updatePreviewOffsets(); + this.updateButtons(); + return true; + } + + public void close() { + this.resetToApplied(); + NonHudOverlay.setUiPreviewEnabled(false); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void drawDragOverlays(DrawContext context, int mouseX, int mouseY) { + this.drawElementBox(context, this.lastBounds.energy(), NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ENERGY, mouseX, mouseY); + this.drawElementBox(context, this.lastBounds.energyHeart(), NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ENERGY_HEART, mouseX, mouseY); + this.drawElementBox(context, this.lastBounds.attack(), NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ATTACK, mouseX, mouseY); + this.drawElementBox(context, this.lastBounds.prompt(), NonModMenuHudConfigScreen.UiConfigScreen.HudElement.PROMPT, mouseX, mouseY); + this.drawElementBox(context, this.lastBounds.liquid(), NonModMenuHudConfigScreen.UiConfigScreen.HudElement.LIQUID, mouseX, mouseY); + } + + private void drawElementBox( + DrawContext context, NonHudOverlay.Rect rawRect, NonModMenuHudConfigScreen.UiConfigScreen.HudElement element, int mouseX, int mouseY + ) { + NonHudOverlay.Rect rect = rawRect.padded(4); + boolean hovered = rect.contains(mouseX, mouseY); + boolean active = element == this.selectedElement || element == this.draggedElement || hovered; + boolean visible = this.isVisible(element); + if (active || !visible) { + int color = visible ? element.color : -8355712; + int fill = color & 16777215 | (!hovered && element != this.draggedElement ? (visible ? 402653184 : 570425344) : 855638016); + context.fill(rect.x(), rect.y(), rect.x() + rect.width(), rect.y() + rect.height(), fill); + this.drawOutline(context, rect, color); + Text label = visible + ? Text.translatable(element.labelKey) + : Text.translatable("config.needsofnature.ui_hidden_label", new Object[]{Text.translatable(element.labelKey)}); + context.drawTextWithShadow(this.textRenderer, label, rect.x(), Math.max(0, rect.y() - 10), color); + } + } + + private void drawOutline(DrawContext context, NonHudOverlay.Rect rect, int color) { + int x1 = rect.x(); + int y1 = rect.y(); + int x2 = rect.x() + rect.width(); + int y2 = rect.y() + rect.height(); + context.fill(x1, y1, x2, y1 + 1, color); + context.fill(x1, y2 - 1, x2, y2, color); + context.fill(x1, y1, x1 + 1, y2, color); + context.fill(x2 - 1, y1, x2, y2, color); + } + + private NonModMenuHudConfigScreen.UiConfigScreen.HudElement hitTest(double mouseX, double mouseY) { + if (this.lastBounds.liquid().padded(6).contains(mouseX, mouseY)) { + return NonModMenuHudConfigScreen.UiConfigScreen.HudElement.LIQUID; + } else if (this.lastBounds.prompt().padded(6).contains(mouseX, mouseY)) { + return NonModMenuHudConfigScreen.UiConfigScreen.HudElement.PROMPT; + } else if (this.lastBounds.attack().padded(6).contains(mouseX, mouseY)) { + return NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ATTACK; + } else if (this.lastBounds.energyHeart().padded(6).contains(mouseX, mouseY)) { + return NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ENERGY_HEART; + } else { + return this.lastBounds.energy().padded(6).contains(mouseX, mouseY) ? NonModMenuHudConfigScreen.UiConfigScreen.HudElement.ENERGY : null; + } + } + + private int offsetX(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + return switch (element) { + case ENERGY -> this.energyX; + case ENERGY_HEART -> this.energyHeartX; + case ATTACK -> this.attackX; + case PROMPT -> this.promptX; + case LIQUID -> this.liquidX; + }; + } + + private int offsetY(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + return switch (element) { + case ENERGY -> this.energyY; + case ENERGY_HEART -> this.energyHeartY; + case ATTACK -> this.attackY; + case PROMPT -> this.promptY; + case LIQUID -> this.liquidY; + }; + } + + private void setOffset(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element, int x, int y) { + switch (element) { + case ENERGY: + this.energyX = x; + this.energyY = y; + break; + case ENERGY_HEART: + this.energyHeartX = x; + this.energyHeartY = y; + break; + case ATTACK: + this.attackX = x; + this.attackY = y; + break; + case PROMPT: + this.promptX = x; + this.promptY = y; + break; + case LIQUID: + this.liquidX = x; + this.liquidY = y; + } + } + + private boolean isVisible(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + return switch (element) { + case ENERGY -> this.energyVisible; + case ENERGY_HEART -> this.energyHeartVisible; + case ATTACK -> this.attackVisible; + case PROMPT -> this.promptVisible; + case LIQUID -> this.liquidVisible; + }; + } + + private void setVisible(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element, boolean visible) { + switch (element) { + case ENERGY: + this.energyVisible = visible; + break; + case ENERGY_HEART: + this.energyHeartVisible = visible; + break; + case ATTACK: + this.attackVisible = visible; + break; + case PROMPT: + this.promptVisible = visible; + break; + case LIQUID: + this.liquidVisible = visible; + } + } + + private void resetElement(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + switch (element) { + case ENERGY: + this.setOffset(element, this.defaults.getUiEnergyOffsetX(), this.defaults.getUiEnergyOffsetY()); + this.setVisible(element, this.defaults.isUiEnergyVisible()); + break; + case ENERGY_HEART: + this.setOffset(element, this.defaults.getUiEnergyHeartOffsetX(), this.defaults.getUiEnergyHeartOffsetY()); + this.setVisible(element, this.defaults.isUiEnergyHeartVisible()); + break; + case ATTACK: + this.setOffset(element, this.defaults.getUiAttackOffsetX(), this.defaults.getUiAttackOffsetY()); + this.setVisible(element, this.defaults.isUiAttackVisible()); + break; + case PROMPT: + this.setOffset(element, this.defaults.getUiPromptOffsetX(), this.defaults.getUiPromptOffsetY()); + this.setVisible(element, this.defaults.isUiPromptVisible()); + break; + case LIQUID: + this.setOffset(element, this.defaults.getUiLiquidOffsetX(), this.defaults.getUiLiquidOffsetY()); + this.setVisible(element, this.defaults.isUiLiquidVisible()); + } + } + + private void saveOffsets() { + this.config.setUiEnergyOffsetX(this.energyX); + this.config.setUiEnergyOffsetY(this.energyY); + this.config.setUiEnergyHeartOffsetX(this.energyHeartX); + this.config.setUiEnergyHeartOffsetY(this.energyHeartY); + this.config.setUiAttackOffsetX(this.attackX); + this.config.setUiAttackOffsetY(this.attackY); + this.config.setUiPromptOffsetX(this.promptX); + this.config.setUiPromptOffsetY(this.promptY); + this.config.setUiLiquidOffsetX(this.liquidX); + this.config.setUiLiquidOffsetY(this.liquidY); + this.config.setUiEnergyVisible(this.energyVisible); + this.config.setUiEnergyHeartVisible(this.energyHeartVisible); + this.config.setUiAttackVisible(this.attackVisible); + this.config.setUiPromptVisible(this.promptVisible); + this.config.setUiLiquidVisible(this.liquidVisible); + this.config.save(); + this.appliedEnergyX = this.energyX; + this.appliedEnergyY = this.energyY; + this.appliedEnergyHeartX = this.energyHeartX; + this.appliedEnergyHeartY = this.energyHeartY; + this.appliedAttackX = this.attackX; + this.appliedAttackY = this.attackY; + this.appliedPromptX = this.promptX; + this.appliedPromptY = this.promptY; + this.appliedLiquidX = this.liquidX; + this.appliedLiquidY = this.liquidY; + this.appliedEnergyVisible = this.energyVisible; + this.appliedEnergyHeartVisible = this.energyHeartVisible; + this.appliedAttackVisible = this.attackVisible; + this.appliedPromptVisible = this.promptVisible; + this.appliedLiquidVisible = this.liquidVisible; + } + + private void resetToApplied() { + this.energyX = this.appliedEnergyX; + this.energyY = this.appliedEnergyY; + this.energyHeartX = this.appliedEnergyHeartX; + this.energyHeartY = this.appliedEnergyHeartY; + this.attackX = this.appliedAttackX; + this.attackY = this.appliedAttackY; + this.promptX = this.appliedPromptX; + this.promptY = this.appliedPromptY; + this.liquidX = this.appliedLiquidX; + this.liquidY = this.appliedLiquidY; + this.energyVisible = this.appliedEnergyVisible; + this.energyHeartVisible = this.appliedEnergyHeartVisible; + this.attackVisible = this.appliedAttackVisible; + this.promptVisible = this.appliedPromptVisible; + this.liquidVisible = this.appliedLiquidVisible; + this.updatePreviewOffsets(); + } + + private void updatePreviewOffsets() { + NonHudOverlay.setUiPreviewOffsets( + this.energyX, + this.energyY, + this.attackX, + this.attackY, + this.promptX, + this.promptY, + this.liquidX, + this.liquidY, + this.energyHeartX, + this.energyHeartY, + this.energyVisible, + this.energyHeartVisible, + this.attackVisible, + this.promptVisible, + this.liquidVisible + ); + } + + private boolean hasPendingUiChanges() { + return this.energyX != this.appliedEnergyX + || this.energyY != this.appliedEnergyY + || this.energyHeartX != this.appliedEnergyHeartX + || this.energyHeartY != this.appliedEnergyHeartY + || this.attackX != this.appliedAttackX + || this.attackY != this.appliedAttackY + || this.promptX != this.appliedPromptX + || this.promptY != this.appliedPromptY + || this.liquidX != this.appliedLiquidX + || this.liquidY != this.appliedLiquidY + || this.energyVisible != this.appliedEnergyVisible + || this.energyHeartVisible != this.appliedEnergyHeartVisible + || this.attackVisible != this.appliedAttackVisible + || this.promptVisible != this.appliedPromptVisible + || this.liquidVisible != this.appliedLiquidVisible; + } + + private Text toolbarVisibilityLabel() { + return Text.literal(this.toolbarVisible ? "H" : "S"); + } + + private Text toggleSelectedVisibilityLabel() { + return Text.translatable(this.isVisible(this.selectedElement) ? "config.needsofnature.ui_hide_selected" : "config.needsofnature.ui_show_selected"); + } + + private void updateButtons() { + this.updateToolbarLayout(); + boolean pending = this.hasPendingUiChanges(); + if (this.saveButton != null) { + this.saveButton.active = pending; + } + + if (this.toggleSelectedVisibilityButton != null) { + this.toggleSelectedVisibilityButton.setMessage(this.toggleSelectedVisibilityLabel()); + } + + if (this.resetSelectedButton != null) { + this.resetSelectedButton.active = this.offsetX(this.selectedElement) != this.defaultX(this.selectedElement) + || this.offsetY(this.selectedElement) != this.defaultY(this.selectedElement) + || this.isVisible(this.selectedElement) != this.defaultVisible(this.selectedElement); + } + + if (this.resetAllButton != null) { + this.resetAllButton.active = this.energyX != this.defaults.getUiEnergyOffsetX() + || this.energyY != this.defaults.getUiEnergyOffsetY() + || this.energyHeartX != this.defaults.getUiEnergyHeartOffsetX() + || this.energyHeartY != this.defaults.getUiEnergyHeartOffsetY() + || this.attackX != this.defaults.getUiAttackOffsetX() + || this.attackY != this.defaults.getUiAttackOffsetY() + || this.promptX != this.defaults.getUiPromptOffsetX() + || this.promptY != this.defaults.getUiPromptOffsetY() + || this.liquidX != this.defaults.getUiLiquidOffsetX() + || this.liquidY != this.defaults.getUiLiquidOffsetY() + || this.energyVisible != this.defaults.isUiEnergyVisible() + || this.energyHeartVisible != this.defaults.isUiEnergyHeartVisible() + || this.attackVisible != this.defaults.isUiAttackVisible() + || this.promptVisible != this.defaults.isUiPromptVisible() + || this.liquidVisible != this.defaults.isUiLiquidVisible(); + } + + if (this.saveButton != null) { + this.saveButton.visible = this.toolbarVisible; + } + + if (this.cancelButton != null) { + this.cancelButton.visible = this.toolbarVisible; + } + + if (this.toggleSelectedVisibilityButton != null) { + this.toggleSelectedVisibilityButton.visible = this.toolbarVisible; + } + + if (this.resetSelectedButton != null) { + this.resetSelectedButton.visible = this.toolbarVisible; + } + + if (this.resetAllButton != null) { + this.resetAllButton.visible = this.toolbarVisible; + } + + if (this.toolbarVisibilityButton != null) { + this.toolbarVisibilityButton.visible = true; + this.toolbarVisibilityButton.setMessage(this.toolbarVisibilityLabel()); + } + } + + private void updateToolbarLayout() { + if (this.toolbarVisibilityButton != null) { + int y = 8; + int gap = 3; + int eyeW = this.toolbarVisibilityButton.getWidth(); + int right = this.width - 8; + this.toolbarVisibilityButton.setPosition(right - eyeW, y); + if (this.toolbarVisible) { + int nextY = y + this.toolbarVisibilityButton.getHeight() + gap; + if (this.toggleSelectedVisibilityButton != null) { + this.toggleSelectedVisibilityButton.setPosition(right - this.toggleSelectedVisibilityButton.getWidth(), nextY); + nextY += this.toggleSelectedVisibilityButton.getHeight() + gap; + } + + if (this.resetSelectedButton != null) { + this.resetSelectedButton.setPosition(right - this.resetSelectedButton.getWidth(), nextY); + nextY += this.resetSelectedButton.getHeight() + gap; + } + + if (this.resetAllButton != null) { + this.resetAllButton.setPosition(right - this.resetAllButton.getWidth(), nextY); + nextY += this.resetAllButton.getHeight() + gap; + } + + if (this.saveButton != null) { + this.saveButton.setPosition(right - this.saveButton.getWidth(), nextY); + nextY += this.saveButton.getHeight() + gap; + } + + if (this.cancelButton != null) { + this.cancelButton.setPosition(right - this.cancelButton.getWidth(), nextY); + } + } + } + } + + private int defaultX(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + return switch (element) { + case ENERGY -> this.defaults.getUiEnergyOffsetX(); + case ENERGY_HEART -> this.defaults.getUiEnergyHeartOffsetX(); + case ATTACK -> this.defaults.getUiAttackOffsetX(); + case PROMPT -> this.defaults.getUiPromptOffsetX(); + case LIQUID -> this.defaults.getUiLiquidOffsetX(); + }; + } + + private int defaultY(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + return switch (element) { + case ENERGY -> this.defaults.getUiEnergyOffsetY(); + case ENERGY_HEART -> this.defaults.getUiEnergyHeartOffsetY(); + case ATTACK -> this.defaults.getUiAttackOffsetY(); + case PROMPT -> this.defaults.getUiPromptOffsetY(); + case LIQUID -> this.defaults.getUiLiquidOffsetY(); + }; + } + + private boolean defaultVisible(NonModMenuHudConfigScreen.UiConfigScreen.HudElement element) { + return switch (element) { + case ENERGY -> this.defaults.isUiEnergyVisible(); + case ENERGY_HEART -> this.defaults.isUiEnergyHeartVisible(); + case ATTACK -> this.defaults.isUiAttackVisible(); + case PROMPT -> this.defaults.isUiPromptVisible(); + case LIQUID -> this.defaults.isUiLiquidVisible(); + }; + } + + private int clampUiOffset(int value) { + return Math.max(-999, Math.min(999, value)); + } + + private enum HudElement { + ENERGY("config.needsofnature.ui_energy", -8532993), + ENERGY_HEART("config.needsofnature.ui_energy_heart", -33333), + ATTACK("config.needsofnature.ui_attack", -38037), + PROMPT("config.needsofnature.ui_prompt", -11930), + LIQUID("config.needsofnature.ui_liquid", -10616907); + + private final String labelKey; + private final int color; + + HudElement(String labelKey, int color) { + this.labelKey = labelKey; + this.color = color; + } + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuIntegration.java b/src/main/java/com/nonid/integration/NonModMenuIntegration.java new file mode 100644 index 0000000..4709f25 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuIntegration.java @@ -0,0 +1,10 @@ +package com.nonid.integration; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +public class NonModMenuIntegration implements ModMenuApi { + public ConfigScreenFactory getModConfigScreenFactory() { + return NonModMenuScreens::createConfigScreen; + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuIntifaceInstallScreen.java b/src/main/java/com/nonid/integration/NonModMenuIntifaceInstallScreen.java new file mode 100644 index 0000000..d39842e --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuIntifaceInstallScreen.java @@ -0,0 +1,97 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import com.nonid.client.intiface.NonIntifaceDependencyManager; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; + +final class NonModMenuIntifaceInstallScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final boolean enableAfterInstall; + private ButtonWidget downloadButton; + + NonModMenuIntifaceInstallScreen(Screen parent, NonConfig config, boolean enableAfterInstall) { + super(Text.translatable("config.needsofnature.intiface.libs.install_title")); + this.parent = parent; + this.config = config; + this.enableAfterInstall = enableAfterInstall; + } + + protected void init() { + int centerX = this.width / 2; + this.downloadButton = ButtonWidget.builder( + Text.translatable( + NonIntifaceDependencyManager.generatedBundleOutdated() + ? "config.needsofnature.intiface.libs.redownload" + : "config.needsofnature.intiface.libs.download" + ), + button -> this.startInstall() + ) + .dimensions(centerX - 155, this.height - 52, 150, 20) + .build(); + this.addDrawableChild(this.downloadButton); + ButtonWidget cancelButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 5, this.height - 52, 150, 20) + .build(); + this.addDrawableChild(cancelButton); + ButtonWidget doneButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent) + ) + .dimensions(centerX - 75, this.height - 28, 150, 20) + .build(); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 28, 16777215); + int y = 58; + this.drawCentered(context, Text.translatable("config.needsofnature.intiface.libs.install_line_1"), y); + y += 14; + this.drawCentered( + context, Text.translatable("config.needsofnature.intiface.libs.install_line_2", new Object[]{NonIntifaceDependencyManager.totalSizeMbText()}), y + ); + y += 14; + this.drawCentered(context, Text.translatable("config.needsofnature.intiface.libs.install_line_3"), y); + y += 22; + this.drawCentered(context, NonIntifaceDependencyManager.statusText(), y); + if (this.downloadButton != null) { + this.downloadButton + .setMessage( + Text.translatable( + NonIntifaceDependencyManager.generatedBundleOutdated() + ? "config.needsofnature.intiface.libs.redownload" + : "config.needsofnature.intiface.libs.download" + ) + ); + this.downloadButton.active = !NonIntifaceDependencyManager.installing() + && !NonIntifaceDependencyManager.restartRequired() + && NonIntifaceDependencyManager.installRequired(); + } + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void startInstall() { + if (this.downloadButton != null) { + this.downloadButton.active = false; + } + + NonIntifaceDependencyManager.installAsync(() -> { + if (this.enableAfterInstall && NonIntifaceDependencyManager.restartRequired()) { + this.config.setIntifaceEnabled(true); + this.config.save(); + } + }); + } + + private void drawCentered(DrawContext context, Text text, int y) { + context.drawCenteredTextWithShadow(this.textRenderer, text, this.width / 2, y, -2039584); + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuIntifaceScreen.java b/src/main/java/com/nonid/integration/NonModMenuIntifaceScreen.java new file mode 100644 index 0000000..45fdd28 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuIntifaceScreen.java @@ -0,0 +1,323 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import com.nonid.client.intiface.NonIntifaceBridge; +import com.nonid.client.intiface.NonIntifaceDependencyManager; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; + +final class NonModMenuIntifaceScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean enabled; + private boolean autoConnectOnWorldJoin; + private boolean stopOnDisconnect; + private String serverUrl; + private ButtonWidget enabledButton; + private ButtonWidget resetEnabledButton; + private ButtonWidget autoConnectButton; + private ButtonWidget resetAutoConnectButton; + private ButtonWidget stopOnDisconnectButton; + private ButtonWidget resetStopOnDisconnectButton; + private TextFieldWidget serverUrlField; + private ButtonWidget resetServerUrlButton; + private ButtonWidget statusButton; + private ButtonWidget connectButton; + private ButtonWidget scanButton; + private ButtonWidget testButton; + + NonModMenuIntifaceScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.intiface_title")); + this.parent = parent; + this.config = config; + this.loadFromConfig(); + } + + protected void init() { + if (this.serverUrlField != null) { + this.serverUrl = this.serverUrlField.getText(); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_connection"))); + boolean librariesUsable = NonIntifaceDependencyManager.librariesUsable(); + this.enabledButton = ButtonWidget.builder(this.toggleText(this.enabled), button -> { + if (!this.enabled && !librariesUsable) { + MinecraftClient.getInstance().setScreen(new NonModMenuIntifaceInstallScreen(this, this.config, true)); + } else { + this.enabled = !this.enabled; + button.setMessage(this.toggleText(this.enabled)); + this.updateResetButtons(); + } + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.enabledButton, "config.needsofnature.tooltip.intiface_enabled"); + this.resetEnabledButton = this.resetButton(() -> { + this.enabled = this.defaults.isIntifaceEnabled(); + this.enabledButton.setMessage(this.toggleText(this.enabled)); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_enabled"), + this.enabledButton, + this.resetEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_enabled") + ) + ); + if (!librariesUsable) { + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_libraries"))); + this.statusButton = ButtonWidget.builder(NonIntifaceDependencyManager.statusText(), button -> {}).dimensions(0, 0, 220, 20).build(); + this.statusButton.active = false; + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, this.statusButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_libraries_status") + ) + ); + ButtonWidget installButton = ButtonWidget.builder( + Text.translatable( + NonIntifaceDependencyManager.restartRequired() + ? "config.needsofnature.intiface.libs.restart_required" + : ( + NonIntifaceDependencyManager.generatedBundleOutdated() + ? "config.needsofnature.intiface.libs.redownload" + : "config.needsofnature.intiface.libs.install" + ) + ), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuIntifaceInstallScreen(this, this.config, false)) + ) + .dimensions(0, 0, 220, 20) + .build(); + installButton.active = !NonIntifaceDependencyManager.installing() + && !NonIntifaceDependencyManager.restartRequired() + && NonIntifaceDependencyManager.installRequired(); + NonModMenuScreens.setTooltip(installButton, "config.needsofnature.tooltip.intiface_libraries_install"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, installButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_libraries_install") + ) + ); + this.updateResetButtons(); + this.addDoneButton(); + } else { + this.serverUrlField = new TextFieldWidget(this.textRenderer, 0, 0, 180, 20, Text.literal("")); + this.serverUrlField.setMaxLength(128); + this.serverUrlField.setText(this.serverUrl); + this.serverUrlField.setChangedListener(value -> this.updateResetButtons()); + this.resetServerUrlButton = this.resetButton(() -> this.serverUrlField.setText(this.defaults.getIntifaceServerUrl())); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_server_url"), + this.serverUrlField, + this.resetServerUrlButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_server_url") + ) + ); + this.autoConnectButton = ButtonWidget.builder(this.toggleText(this.autoConnectOnWorldJoin), button -> { + this.autoConnectOnWorldJoin = !this.autoConnectOnWorldJoin; + button.setMessage(this.toggleText(this.autoConnectOnWorldJoin)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.autoConnectButton, "config.needsofnature.tooltip.intiface_auto_connect"); + this.resetAutoConnectButton = this.resetButton(() -> { + this.autoConnectOnWorldJoin = this.defaults.isIntifaceAutoConnectOnWorldJoin(); + this.autoConnectButton.setMessage(this.toggleText(this.autoConnectOnWorldJoin)); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_auto_connect"), + this.autoConnectButton, + this.resetAutoConnectButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_auto_connect") + ) + ); + this.stopOnDisconnectButton = ButtonWidget.builder(this.toggleText(this.stopOnDisconnect), button -> { + this.stopOnDisconnect = !this.stopOnDisconnect; + button.setMessage(this.toggleText(this.stopOnDisconnect)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.stopOnDisconnectButton, "config.needsofnature.tooltip.intiface_stop_on_disconnect"); + this.resetStopOnDisconnectButton = this.resetButton(() -> { + this.stopOnDisconnect = this.defaults.isIntifaceStopOnDisconnect(); + this.stopOnDisconnectButton.setMessage(this.toggleText(this.stopOnDisconnect)); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_stop_on_disconnect"), + this.stopOnDisconnectButton, + this.resetStopOnDisconnectButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_stop_on_disconnect") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_controls"))); + this.statusButton = ButtonWidget.builder(NonIntifaceBridge.statusText(this.config), button -> {}).dimensions(0, 0, 220, 20).build(); + this.statusButton.active = false; + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, this.statusButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_status") + ) + ); + this.connectButton = ButtonWidget.builder(Text.translatable("config.needsofnature.intiface_connect"), button -> { + if (NonIntifaceBridge.isConnected()) { + NonIntifaceBridge.disconnect(); + } else { + this.saveValues(); + NonIntifaceBridge.connect(this.config); + } + }).dimensions(0, 0, 106, 20).build(); + this.scanButton = ButtonWidget.builder(Text.translatable("config.needsofnature.intiface_scan"), button -> { + this.saveValues(); + NonIntifaceBridge.startScanning(this.config); + }).dimensions(0, 0, 106, 20).build(); + settingsList.addEntryRow( + SettingsList.RowEntry.groupedField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_connect_scan"), + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_connect_scan"), + this.connectButton, + this.scanButton + ) + ); + this.testButton = ButtonWidget.builder(Text.translatable("config.needsofnature.intiface_test"), button -> { + this.saveValues(); + NonIntifaceBridge.testPulse(this.config, false); + }).dimensions(0, 0, 216, 20).build(); + settingsList.addEntryRow( + SettingsList.RowEntry.groupedField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_test_pulse"), + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_test_pulse"), + this.testButton + ) + ); + settingsList.addEntryRow( + SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_toy_settings")) + ); + ButtonWidget tuningButton = ButtonWidget.builder(Text.translatable("config.needsofnature.intiface_toy_settings"), button -> { + this.saveValues(); + MinecraftClient.getInstance().setScreen(new NonModMenuIntifaceTuningScreen(this, this.config)); + }).dimensions(0, 0, 220, 20).build(); + NonModMenuScreens.setTooltip(tuningButton, "config.needsofnature.tooltip.intiface_toy_settings"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, tuningButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_toy_settings") + ) + ); + this.updateResetButtons(); + this.updateConnectionButtons(); + this.addDoneButton(); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + if (this.statusButton != null) { + this.statusButton + .setMessage(NonIntifaceDependencyManager.librariesUsable() ? NonIntifaceBridge.statusText(this.config) : NonIntifaceDependencyManager.statusText()); + } + + this.updateConnectionButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + this.saveValues(); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void saveValues() { + this.serverUrl = this.serverUrlField == null ? this.serverUrl : this.serverUrlField.getText(); + this.config.setIntifaceEnabled(this.enabled); + this.config.setIntifaceAutoConnectOnWorldJoin(this.autoConnectOnWorldJoin); + this.config.setIntifaceStopOnDisconnect(this.stopOnDisconnect); + this.config.setIntifaceServerUrl(this.serverUrl); + this.config.save(); + this.loadFromConfig(); + if (!this.enabled) { + NonIntifaceBridge.stopAll(); + NonIntifaceBridge.disconnect(); + } + } + + private void loadFromConfig() { + this.enabled = this.config.isIntifaceEnabled(); + this.autoConnectOnWorldJoin = this.config.isIntifaceAutoConnectOnWorldJoin(); + this.stopOnDisconnect = this.config.isIntifaceStopOnDisconnect(); + this.serverUrl = this.config.getIntifaceServerUrl(); + } + + private void addDoneButton() { + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + private ButtonWidget resetButton(Runnable action) { + ButtonWidget button = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), ignored -> { + if (action != null) { + action.run(); + } + + this.updateResetButtons(); + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(button, "config.needsofnature.tooltip.reset"); + return button; + } + + private void updateResetButtons() { + if (this.resetEnabledButton != null) { + this.resetEnabledButton.active = this.enabled != this.defaults.isIntifaceEnabled(); + } + + if (this.resetAutoConnectButton != null) { + this.resetAutoConnectButton.active = this.autoConnectOnWorldJoin != this.defaults.isIntifaceAutoConnectOnWorldJoin(); + } + + if (this.resetStopOnDisconnectButton != null) { + this.resetStopOnDisconnectButton.active = this.stopOnDisconnect != this.defaults.isIntifaceStopOnDisconnect(); + } + + if (this.resetServerUrlButton != null && this.serverUrlField != null) { + this.resetServerUrlButton.active = !this.serverUrlField.getText().equals(this.defaults.getIntifaceServerUrl()); + } + } + + private void updateConnectionButtons() { + boolean connected = NonIntifaceBridge.isConnected(); + if (this.connectButton != null) { + this.connectButton.setMessage(Text.translatable(connected ? "config.needsofnature.intiface_disconnect" : "config.needsofnature.intiface_connect")); + } + + if (this.scanButton != null) { + this.scanButton.active = connected; + } + + if (this.testButton != null) { + this.testButton.active = connected; + } + } + + private Text toggleText(boolean value) { + return Text.translatable(value ? "options.on" : "options.off"); + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuIntifaceStrokerOscillatorScreen.java b/src/main/java/com/nonid/integration/NonModMenuIntifaceStrokerOscillatorScreen.java new file mode 100644 index 0000000..a434aac --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuIntifaceStrokerOscillatorScreen.java @@ -0,0 +1,240 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import java.util.Locale; +import java.util.function.DoubleConsumer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; + +final class NonModMenuIntifaceStrokerOscillatorScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private int oscillatorRegularSpeedPercent; + private int oscillatorPeakSpeedPercent; + private int strokerMinDistancePercent; + private int strokerMaxDistancePercent; + private int strokerRegularMoveDurationMs; + private int strokerPeakMoveDurationMs; + private FloatValueSliderWidget oscillatorRegularSlider; + private ButtonWidget resetOscillatorRegularButton; + private FloatValueSliderWidget oscillatorPeakSlider; + private ButtonWidget resetOscillatorPeakButton; + private FloatValueSliderWidget strokerMinSlider; + private ButtonWidget resetStrokerMinButton; + private FloatValueSliderWidget strokerMaxSlider; + private ButtonWidget resetStrokerMaxButton; + private FloatValueSliderWidget strokerRegularSlider; + private ButtonWidget resetStrokerRegularButton; + private FloatValueSliderWidget strokerPeakSlider; + private ButtonWidget resetStrokerPeakButton; + + NonModMenuIntifaceStrokerOscillatorScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.intiface_stroker_oscillator_title")); + this.parent = parent; + this.config = config; + this.loadFromConfig(); + } + + protected void init() { + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_oscillator"))); + this.oscillatorRegularSlider = this.percentSlider(this.oscillatorRegularSpeedPercent, value -> { + this.oscillatorRegularSpeedPercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetOscillatorRegularButton = this.resetButton(() -> { + this.oscillatorRegularSpeedPercent = this.defaults.getIntifaceOscillatorRegularSpeedPercent(); + this.oscillatorRegularSlider.setFloatValue(this.oscillatorRegularSpeedPercent); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_oscillator_regular_speed"), + this.oscillatorRegularSlider, + this.resetOscillatorRegularButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_oscillator_regular_speed") + ) + ); + this.oscillatorPeakSlider = this.percentSlider(this.oscillatorPeakSpeedPercent, value -> { + this.oscillatorPeakSpeedPercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetOscillatorPeakButton = this.resetButton(() -> { + this.oscillatorPeakSpeedPercent = this.defaults.getIntifaceOscillatorPeakSpeedPercent(); + this.oscillatorPeakSlider.setFloatValue(this.oscillatorPeakSpeedPercent); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_oscillator_peak_speed"), + this.oscillatorPeakSlider, + this.resetOscillatorPeakButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_oscillator_peak_speed") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_stroker"))); + this.strokerMaxSlider = this.percentSlider(this.strokerMaxDistancePercent, value -> { + this.strokerMaxDistancePercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetStrokerMaxButton = this.resetButton(() -> { + this.strokerMaxDistancePercent = this.defaults.getIntifaceStrokerMaxDistancePercent(); + this.strokerMaxSlider.setFloatValue(this.strokerMaxDistancePercent); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_stroker_max_distance"), + this.strokerMaxSlider, + this.resetStrokerMaxButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_stroker_max_distance") + ) + ); + this.strokerMinSlider = this.percentSlider(this.strokerMinDistancePercent, value -> { + this.strokerMinDistancePercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetStrokerMinButton = this.resetButton(() -> { + this.strokerMinDistancePercent = this.defaults.getIntifaceStrokerMinDistancePercent(); + this.strokerMinSlider.setFloatValue(this.strokerMinDistancePercent); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_stroker_min_distance"), + this.strokerMinSlider, + this.resetStrokerMinButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_stroker_min_distance") + ) + ); + this.strokerRegularSlider = this.durationSlider(this.strokerRegularMoveDurationMs, value -> { + this.strokerRegularMoveDurationMs = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetStrokerRegularButton = this.resetButton(() -> { + this.strokerRegularMoveDurationMs = this.defaults.getIntifaceStrokerRegularMoveDurationMs(); + this.strokerRegularSlider.setFloatValue(this.strokerRegularMoveDurationMs); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_stroker_regular_duration"), + this.strokerRegularSlider, + this.resetStrokerRegularButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_stroker_regular_duration") + ) + ); + this.strokerPeakSlider = this.durationSlider(this.strokerPeakMoveDurationMs, value -> { + this.strokerPeakMoveDurationMs = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetStrokerPeakButton = this.resetButton(() -> { + this.strokerPeakMoveDurationMs = this.defaults.getIntifaceStrokerPeakMoveDurationMs(); + this.strokerPeakSlider.setFloatValue(this.strokerPeakMoveDurationMs); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_stroker_peak_duration"), + this.strokerPeakSlider, + this.resetStrokerPeakButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_stroker_peak_duration") + ) + ); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + this.saveValues(); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void saveValues() { + this.config.setIntifaceOscillatorRegularSpeedPercent(this.oscillatorRegularSpeedPercent); + this.config.setIntifaceOscillatorPeakSpeedPercent(this.oscillatorPeakSpeedPercent); + this.config.setIntifaceStrokerDistancesPercent(this.strokerMinDistancePercent, this.strokerMaxDistancePercent); + this.config.setIntifaceStrokerRegularMoveDurationMs(this.strokerRegularMoveDurationMs); + this.config.setIntifaceStrokerPeakMoveDurationMs(this.strokerPeakMoveDurationMs); + this.config.save(); + this.loadFromConfig(); + } + + private void loadFromConfig() { + this.oscillatorRegularSpeedPercent = this.config.getIntifaceOscillatorRegularSpeedPercent(); + this.oscillatorPeakSpeedPercent = this.config.getIntifaceOscillatorPeakSpeedPercent(); + this.strokerMinDistancePercent = this.config.getIntifaceStrokerMinDistancePercent(); + this.strokerMaxDistancePercent = this.config.getIntifaceStrokerMaxDistancePercent(); + this.strokerRegularMoveDurationMs = this.config.getIntifaceStrokerRegularMoveDurationMs(); + this.strokerPeakMoveDurationMs = this.config.getIntifaceStrokerPeakMoveDurationMs(); + } + + private ButtonWidget resetButton(Runnable action) { + ButtonWidget button = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), ignored -> { + if (action != null) { + action.run(); + } + + this.updateResetButtons(); + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(button, "config.needsofnature.tooltip.reset"); + return button; + } + + private FloatValueSliderWidget percentSlider(int initial, DoubleConsumer listener) { + return new FloatValueSliderWidget(0, 0, 130, 20, initial, 0.0F, 100.0F, 1.0F, listener, value -> String.format(Locale.ROOT, "%d%%", Math.round(value))); + } + + private FloatValueSliderWidget durationSlider(int initial, DoubleConsumer listener) { + return new FloatValueSliderWidget(0, 0, 130, 20, initial, 0.0F, 5000.0F, 10.0F, listener, value -> String.format(Locale.ROOT, "%dms", Math.round(value))); + } + + private void updateResetButtons() { + if (this.resetOscillatorRegularButton != null) { + this.resetOscillatorRegularButton.active = this.oscillatorRegularSpeedPercent != this.defaults.getIntifaceOscillatorRegularSpeedPercent(); + } + + if (this.resetOscillatorPeakButton != null) { + this.resetOscillatorPeakButton.active = this.oscillatorPeakSpeedPercent != this.defaults.getIntifaceOscillatorPeakSpeedPercent(); + } + + if (this.resetStrokerMinButton != null) { + this.resetStrokerMinButton.active = this.strokerMinDistancePercent != this.defaults.getIntifaceStrokerMinDistancePercent(); + } + + if (this.resetStrokerMaxButton != null) { + this.resetStrokerMaxButton.active = this.strokerMaxDistancePercent != this.defaults.getIntifaceStrokerMaxDistancePercent(); + } + + if (this.resetStrokerRegularButton != null) { + this.resetStrokerRegularButton.active = this.strokerRegularMoveDurationMs != this.defaults.getIntifaceStrokerRegularMoveDurationMs(); + } + + if (this.resetStrokerPeakButton != null) { + this.resetStrokerPeakButton.active = this.strokerPeakMoveDurationMs != this.defaults.getIntifaceStrokerPeakMoveDurationMs(); + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuIntifaceTuningScreen.java b/src/main/java/com/nonid/integration/NonModMenuIntifaceTuningScreen.java new file mode 100644 index 0000000..6dc8e84 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuIntifaceTuningScreen.java @@ -0,0 +1,111 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import java.util.Locale; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; + +final class NonModMenuIntifaceTuningScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private int latencyCompensationMs; + private FloatValueSliderWidget latencyCompensationSlider; + private ButtonWidget resetLatencyCompensationButton; + + NonModMenuIntifaceTuningScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.intiface_toy_title")); + this.parent = parent; + this.config = config; + this.loadFromConfig(); + } + + protected void init() { + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_timing"))); + this.latencyCompensationSlider = new FloatValueSliderWidget(0, 0, 130, 20, this.latencyCompensationMs, 0.0F, 1000.0F, 10.0F, value -> { + this.latencyCompensationMs = (int)Math.round(value); + this.updateResetButton(); + }, value -> String.format(Locale.ROOT, "%dms early", Math.round(value))); + this.resetLatencyCompensationButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), ignored -> { + this.latencyCompensationMs = this.defaults.getIntifaceLatencyCompensationMs(); + this.latencyCompensationSlider.setFloatValue(this.latencyCompensationMs); + this.updateResetButton(); + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(this.resetLatencyCompensationButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_latency_compensation"), + this.latencyCompensationSlider, + this.resetLatencyCompensationButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_latency_compensation") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_toy_settings"))); + ButtonWidget vibratorButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.intiface_vibrator_settings"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuIntifaceVibratorScreen(this, this.config)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(vibratorButton, "config.needsofnature.tooltip.intiface_vibrator_settings"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, vibratorButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_vibrator_settings") + ) + ); + ButtonWidget strokerOscillatorButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.intiface_stroker_oscillator_settings"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuIntifaceStrokerOscillatorScreen(this, this.config)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(strokerOscillatorButton, "config.needsofnature.tooltip.intiface_stroker_oscillator_settings"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, strokerOscillatorButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_stroker_oscillator_settings") + ) + ); + int centerX = this.width / 2; + this.updateResetButton(); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButton(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + this.config.setIntifaceLatencyCompensationMs(this.latencyCompensationMs); + this.config.save(); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void loadFromConfig() { + this.latencyCompensationMs = this.config.getIntifaceLatencyCompensationMs(); + } + + private void updateResetButton() { + if (this.resetLatencyCompensationButton != null) { + this.resetLatencyCompensationButton.active = this.latencyCompensationMs != this.defaults.getIntifaceLatencyCompensationMs(); + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuIntifaceVibratorScreen.java b/src/main/java/com/nonid/integration/NonModMenuIntifaceVibratorScreen.java new file mode 100644 index 0000000..cfe2f1b --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuIntifaceVibratorScreen.java @@ -0,0 +1,290 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import java.util.Locale; +import java.util.function.DoubleConsumer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; + +final class NonModMenuIntifaceVibratorScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private int reactiveStrengthPercent; + private int reactiveDurationMs; + private int peakStrengthPercent; + private int peakDurationMs; + private int cooldownMs; + private int animationBaselineStrengthPercent; + private final int[] energizedBasePercent = new int[3]; + private final int[] energizedPulsePercent = new int[3]; + private FloatValueSliderWidget reactiveStrengthSlider; + private TextFieldWidget reactiveDurationField; + private ButtonWidget resetReactiveButton; + private FloatValueSliderWidget peakStrengthSlider; + private TextFieldWidget peakDurationField; + private ButtonWidget resetPeakButton; + private TextFieldWidget cooldownField; + private ButtonWidget resetCooldownButton; + private FloatValueSliderWidget animationBaselineSlider; + private ButtonWidget resetAnimationBaselineButton; + private final FloatValueSliderWidget[] energizedBaseSliders = new FloatValueSliderWidget[3]; + private final FloatValueSliderWidget[] energizedPulseSliders = new FloatValueSliderWidget[3]; + private final ButtonWidget[] resetEnergizedButtons = new ButtonWidget[3]; + + NonModMenuIntifaceVibratorScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.intiface_vibrator_title")); + this.parent = parent; + this.config = config; + this.loadFromConfig(); + } + + protected void init() { + if (this.reactiveDurationField != null) { + this.reactiveDurationMs = this.parseField(this.reactiveDurationField, this.reactiveDurationMs); + this.peakDurationMs = this.parseField(this.peakDurationField, this.peakDurationMs); + this.cooldownMs = this.parseField(this.cooldownField, this.cooldownMs); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + settingsList.addEntryRow( + SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_reactiveimpact")) + ); + this.reactiveStrengthSlider = this.percentSlider(this.reactiveStrengthPercent, value -> { + this.reactiveStrengthPercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.reactiveDurationField = this.newNumberField(70, this.reactiveDurationMs); + this.reactiveDurationField.setChangedListener(ignored -> this.updateResetButtons()); + this.resetReactiveButton = this.resetButton(() -> { + this.reactiveStrengthPercent = this.defaults.getIntifaceReactiveImpactStrengthPercent(); + this.reactiveDurationMs = this.defaults.getIntifaceReactiveImpactDurationMs(); + this.reactiveStrengthSlider.setFloatValue(this.reactiveStrengthPercent); + this.reactiveDurationField.setText(String.valueOf(this.reactiveDurationMs)); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.groupedField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_reactiveimpact"), + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_reactiveimpact"), + this.reactiveStrengthSlider, + this.reactiveDurationField, + this.resetReactiveButton + ) + ); + this.peakStrengthSlider = this.percentSlider(this.peakStrengthPercent, value -> { + this.peakStrengthPercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.peakDurationField = this.newNumberField(70, this.peakDurationMs); + this.peakDurationField.setChangedListener(ignored -> this.updateResetButtons()); + this.resetPeakButton = this.resetButton(() -> { + this.peakStrengthPercent = this.defaults.getIntifacePeakReactiveImpactStrengthPercent(); + this.peakDurationMs = this.defaults.getIntifacePeakReactiveImpactDurationMs(); + this.peakStrengthSlider.setFloatValue(this.peakStrengthPercent); + this.peakDurationField.setText(String.valueOf(this.peakDurationMs)); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.groupedField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_peak_reactiveimpact"), + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_peak_reactiveimpact"), + this.peakStrengthSlider, + this.peakDurationField, + this.resetPeakButton + ) + ); + this.cooldownField = this.newNumberField(70, this.cooldownMs); + this.cooldownField.setChangedListener(ignored -> this.updateResetButtons()); + this.resetCooldownButton = this.resetButton(() -> { + this.cooldownMs = this.defaults.getIntifaceCooldownMs(); + this.cooldownField.setText(String.valueOf(this.cooldownMs)); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_cooldown"), + this.cooldownField, + this.resetCooldownButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_cooldown") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_baseline"))); + this.animationBaselineSlider = this.percentSlider(this.animationBaselineStrengthPercent, value -> { + this.animationBaselineStrengthPercent = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetAnimationBaselineButton = this.resetButton(() -> { + this.animationBaselineStrengthPercent = this.defaults.getIntifaceAnimationBaselineStrengthPercent(); + this.animationBaselineSlider.setFloatValue(this.animationBaselineStrengthPercent); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_animation_baseline"), + this.animationBaselineSlider, + this.resetAnimationBaselineButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_animation_baseline") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.intiface_energized"))); + + for (int level = 1; level <= 3; level++) { + int index = level - 1; + int rowLevel = level; + this.energizedBaseSliders[index] = this.percentSlider(this.energizedBasePercent[index], value -> { + this.energizedBasePercent[rowLevel - 1] = (int)Math.round(value); + this.updateResetButtons(); + }); + this.energizedPulseSliders[index] = this.percentSlider(this.energizedPulsePercent[index], value -> { + this.energizedPulsePercent[rowLevel - 1] = (int)Math.round(value); + this.updateResetButtons(); + }); + this.resetEnergizedButtons[index] = this.resetButton(() -> { + this.energizedBasePercent[rowLevel - 1] = this.defaults.getIntifaceEnergizedBasePercent(rowLevel); + this.energizedPulsePercent[rowLevel - 1] = this.defaults.getIntifaceEnergizedPulsePercent(rowLevel); + this.energizedBaseSliders[rowLevel - 1].setFloatValue(this.energizedBasePercent[rowLevel - 1]); + this.energizedPulseSliders[rowLevel - 1].setFloatValue(this.energizedPulsePercent[rowLevel - 1]); + }); + settingsList.addEntryRow( + SettingsList.RowEntry.groupedField( + this.textRenderer, + Text.translatable("config.needsofnature.intiface_energized_level", new Object[]{level}), + NonModMenuScreens.tooltip("config.needsofnature.tooltip.intiface_energized"), + this.energizedBaseSliders[index], + this.energizedPulseSliders[index], + this.resetEnergizedButtons[index] + ) + ); + } + + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + this.saveValues(); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void saveValues() { + this.reactiveDurationMs = this.parseField(this.reactiveDurationField, this.reactiveDurationMs); + this.peakDurationMs = this.parseField(this.peakDurationField, this.peakDurationMs); + this.cooldownMs = this.parseField(this.cooldownField, this.cooldownMs); + this.config.setIntifaceReactiveImpactStrengthPercent(this.reactiveStrengthPercent); + this.config.setIntifaceReactiveImpactDurationMs(this.reactiveDurationMs); + this.config.setIntifacePeakReactiveImpactStrengthPercent(this.peakStrengthPercent); + this.config.setIntifacePeakReactiveImpactDurationMs(this.peakDurationMs); + this.config.setIntifaceCooldownMs(this.cooldownMs); + this.config.setIntifaceAnimationBaselineStrengthPercent(this.animationBaselineStrengthPercent); + + for (int level = 1; level <= 3; level++) { + this.config.setIntifaceEnergizedBasePercent(level, this.energizedBasePercent[level - 1]); + this.config.setIntifaceEnergizedPulsePercent(level, this.energizedPulsePercent[level - 1]); + } + + this.config.save(); + this.loadFromConfig(); + } + + private void loadFromConfig() { + this.reactiveStrengthPercent = this.config.getIntifaceReactiveImpactStrengthPercent(); + this.reactiveDurationMs = this.config.getIntifaceReactiveImpactDurationMs(); + this.peakStrengthPercent = this.config.getIntifacePeakReactiveImpactStrengthPercent(); + this.peakDurationMs = this.config.getIntifacePeakReactiveImpactDurationMs(); + this.cooldownMs = this.config.getIntifaceCooldownMs(); + this.animationBaselineStrengthPercent = this.config.getIntifaceAnimationBaselineStrengthPercent(); + + for (int level = 1; level <= 3; level++) { + this.energizedBasePercent[level - 1] = this.config.getIntifaceEnergizedBasePercent(level); + this.energizedPulsePercent[level - 1] = this.config.getIntifaceEnergizedPulsePercent(level); + } + } + + private ButtonWidget resetButton(Runnable action) { + ButtonWidget button = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), ignored -> { + if (action != null) { + action.run(); + } + + this.updateResetButtons(); + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(button, "config.needsofnature.tooltip.reset"); + return button; + } + + private FloatValueSliderWidget percentSlider(int initial, DoubleConsumer listener) { + return new FloatValueSliderWidget(0, 0, 130, 20, initial, 0.0F, 100.0F, 1.0F, listener, value -> String.format(Locale.ROOT, "%d%%", Math.round(value))); + } + + private TextFieldWidget newNumberField(int width, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, width, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + if (field == null) { + return fallback; + } + + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private void updateResetButtons() { + if (this.resetReactiveButton != null) { + this.resetReactiveButton.active = this.reactiveStrengthPercent != this.defaults.getIntifaceReactiveImpactStrengthPercent() + || this.parseField(this.reactiveDurationField, this.reactiveDurationMs) != this.defaults.getIntifaceReactiveImpactDurationMs(); + } + + if (this.resetPeakButton != null) { + this.resetPeakButton.active = this.peakStrengthPercent != this.defaults.getIntifacePeakReactiveImpactStrengthPercent() + || this.parseField(this.peakDurationField, this.peakDurationMs) != this.defaults.getIntifacePeakReactiveImpactDurationMs(); + } + + if (this.resetCooldownButton != null) { + this.resetCooldownButton.active = this.parseField(this.cooldownField, this.cooldownMs) != this.defaults.getIntifaceCooldownMs(); + } + + if (this.resetAnimationBaselineButton != null) { + this.resetAnimationBaselineButton.active = this.animationBaselineStrengthPercent != this.defaults.getIntifaceAnimationBaselineStrengthPercent(); + } + + for (int level = 1; level <= 3; level++) { + ButtonWidget reset = this.resetEnergizedButtons[level - 1]; + if (reset != null) { + reset.active = this.energizedBasePercent[level - 1] != this.defaults.getIntifaceEnergizedBasePercent(level) + || this.energizedPulsePercent[level - 1] != this.defaults.getIntifaceEnergizedPulsePercent(level); + } + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuPlayerScreens.java b/src/main/java/com/nonid/integration/NonModMenuPlayerScreens.java new file mode 100644 index 0000000..77151c1 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuPlayerScreens.java @@ -0,0 +1,923 @@ +package com.nonid.integration; + +import com.nonid.GenderHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NeedsOfNatureClient; +import com.nonid.NonConfig; +import com.nonid.client.NonDestroyedSkinClient; +import com.nonid.client.NonWildfireGenderSync; +import java.io.IOException; +import java.util.Locale; +import java.util.function.DoubleFunction; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +final class NonModMenuPlayerScreens { + private NonModMenuPlayerScreens() { + } + + static class DestroyedSkinTintPickerScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final String initialHex; + private Identifier skinTexture; + private NativeImage skinImage; + private int selectedRgb = 13803641; + private int skinX; + private int skinY; + private int skinSize; + private int selectedPixelX = -1; + private int selectedPixelY = -1; + private TextFieldWidget tintField; + + protected DestroyedSkinTintPickerScreen(Screen parent, NonConfig config, String initialHex) { + super(Text.translatable("config.needsofnature.destroyed_skin_tint.pick_title")); + this.parent = parent; + this.config = config; + this.initialHex = initialHex == null ? "" : initialHex; + this.selectedRgb = parseHexOrDefault(this.initialHex, this.selectedRgb); + } + + protected void init() { + this.loadSkinImage(); + this.skinSize = Math.min(320, Math.max(128, Math.min(this.width - 40, this.height - 136))); + this.skinX = (this.width - this.skinSize) / 2; + this.skinY = 40; + int centerX = this.width / 2; + this.tintField = new TextFieldWidget(this.textRenderer, centerX - 15, this.height - 62, 80, 20, Text.literal("")); + this.tintField.setMaxLength(7); + this.tintField.setText(formatRgb(this.selectedRgb)); + this.tintField.setChangedListener(ignored -> this.updateSelectedRgbFromField()); + this.addDrawableChild(this.tintField); + ButtonWidget useButton = ButtonWidget.builder(Text.translatable("config.needsofnature.destroyed_skin_tint.use"), button -> { + String pickedHex = this.sanitizeHexInput(this.tintField, formatRgb(this.selectedRgb)); + this.selectedRgb = parseHexOrDefault(pickedHex, this.selectedRgb); + this.config.setDestroyedSkinTintHex(pickedHex); + this.config.save(); + NonDestroyedSkinClient.uploadLocalSkinIfPresent(); + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(centerX - 155, this.height - 28, 100, 20).build(); + this.addDrawableChild(useButton); + ButtonWidget autoButton = ButtonWidget.builder(Text.translatable("config.needsofnature.destroyed_skin_tint.auto"), button -> { + this.setSelectedRgb(NonDestroyedSkinClient.detectSkinTintFromCurrentPlayer(this.selectedRgb)); + this.selectedPixelX = -1; + this.selectedPixelY = -1; + }).dimensions(centerX - 50, this.height - 28, 100, 20).build(); + NonModMenuScreens.setTooltip(autoButton, "config.needsofnature.tooltip.destroyed_skin_tint.auto"); + this.addDrawableChild(autoButton); + ButtonWidget cancelButton = ButtonWidget.builder(Text.translatable("gui.cancel"), button -> MinecraftClient.getInstance().setScreen(this.parent)) + .dimensions(centerX + 55, this.height - 28, 100, 20) + .build(); + this.addDrawableChild(cancelButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 16777215); + if (this.skinTexture != null) { + context.fill(this.skinX, this.skinY, this.skinX + this.skinSize, this.skinY + this.skinSize, 0xFF000000 | this.selectedRgb); + context.drawTexture( + RenderPipelines.GUI_TEXTURED, this.skinTexture, this.skinX, this.skinY, 0.0F, 0.0F, this.skinSize, this.skinSize, 64, 64, 64, 64 + ); + context.fill(this.skinX - 1, this.skinY - 1, this.skinX + this.skinSize + 1, this.skinY, -1); + context.fill(this.skinX - 1, this.skinY + this.skinSize, this.skinX + this.skinSize + 1, this.skinY + this.skinSize + 1, -1); + context.fill(this.skinX - 1, this.skinY - 1, this.skinX, this.skinY + this.skinSize + 1, -1); + context.fill(this.skinX + this.skinSize, this.skinY - 1, this.skinX + this.skinSize + 1, this.skinY + this.skinSize + 1, -1); + } else { + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.translatable("config.needsofnature.destroyed_skin_tint.no_skin"), + this.width / 2, + this.skinY + this.skinSize / 2, + 16744576 + ); + } + + if (this.selectedPixelX >= 0 && this.selectedPixelY >= 0) { + int px = this.skinX + this.selectedPixelX * this.skinSize / 64; + int py = this.skinY + this.selectedPixelY * this.skinSize / 64; + int cell = Math.max(1, this.skinSize / 64); + context.fill(px - 1, py - 1, px + cell + 1, py, -256); + context.fill(px - 1, py + cell, px + cell + 1, py + cell + 1, -256); + context.fill(px - 1, py - 1, px, py + cell + 1, -256); + context.fill(px + cell, py - 1, px + cell + 1, py + cell + 1, -256); + } + + int previewY = this.height - 58; + int previewX = this.width / 2 - 78; + context.fill(previewX, previewY, previewX + 24, previewY + 24, 0xFF000000 | this.selectedRgb); + context.fill(previewX - 1, previewY - 1, previewX + 25, previewY, -1); + context.fill(previewX - 1, previewY + 24, previewX + 25, previewY + 25, -1); + context.fill(previewX - 1, previewY - 1, previewX, previewY + 25, -1); + context.fill(previewX + 24, previewY - 1, previewX + 25, previewY + 25, -1); + context.drawTextWithShadow(this.textRenderer, Text.translatable("config.needsofnature.destroyed_skin_tint"), previewX + 34, previewY + 8, 16777215); + } + + public boolean mouseClicked(Click click, boolean doubled) { + return this.tryPickColor(click.x(), click.y()) ? true : super.mouseClicked(click, doubled); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + public void removed() { + if (this.skinImage != null) { + this.skinImage.close(); + this.skinImage = null; + } + } + + private void loadSkinImage() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + this.skinTexture = client.player.getSkin().body().texturePath(); + + try { + this.skinImage = NonDestroyedSkinClient.readTextureImage(client, this.skinTexture); + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.warn("[NoN] Failed to read player skin for tint picker.", e); + this.skinImage = null; + } + } + } + + private boolean tryPickColor(double mouseX, double mouseY) { + if (this.skinImage == null) { + return false; + } + + if (!(mouseX < this.skinX) && !(mouseX >= this.skinX + this.skinSize) && !(mouseY < this.skinY) && !(mouseY >= this.skinY + this.skinSize)) { + int pixelX = Math.max(0, Math.min(63, (int)((mouseX - this.skinX) * 64.0 / this.skinSize))); + int pixelY = Math.max(0, Math.min(63, (int)((mouseY - this.skinY) * 64.0 / this.skinSize))); + if (pixelX < this.skinImage.getWidth() && pixelY < this.skinImage.getHeight()) { + int argb = this.skinImage.getColorArgb(pixelX, pixelY); + if ((argb >>> 24 & 0xFF) < 32) { + return true; + } + + this.setSelectedRgb(argb & 16777215); + this.selectedPixelX = pixelX; + this.selectedPixelY = pixelY; + return true; + } else { + return false; + } + } else { + return false; + } + } + + private void setSelectedRgb(int rgb) { + this.selectedRgb = rgb & 16777215; + if (this.tintField != null) { + this.tintField.setText(formatRgb(this.selectedRgb)); + } + } + + private void updateSelectedRgbFromField() { + this.selectedRgb = parseHexOrDefault(this.sanitizeHexInput(this.tintField, formatRgb(this.selectedRgb)), this.selectedRgb); + } + + private String sanitizeHexInput(TextFieldWidget field, String fallback) { + if (field == null) { + return fallback == null ? "" : fallback; + } + + String value = field.getText() == null ? "" : field.getText().trim(); + if (value.startsWith("#")) { + value = value.substring(1); + } + + value = value.toUpperCase(Locale.ROOT); + if (value.isEmpty()) { + return ""; + } else { + return value.matches("[0-9A-F]{6}") ? value : (fallback == null ? "" : fallback); + } + } + + private static int parseHexOrDefault(String hex, int fallback) { + if (hex == null) { + return fallback; + } + + String value = hex.trim(); + if (value.startsWith("#")) { + value = value.substring(1); + } + + if (!value.matches("[0-9a-fA-F]{6}")) { + return fallback; + } + + try { + return Integer.parseUnsignedInt(value, 16) & 16777215; + } catch (NumberFormatException e) { + return fallback; + } + } + + private static String formatRgb(int rgb) { + return String.format(Locale.ROOT, "%06X", rgb & 16777215); + } + } + + static class FemaleGenderDestroyedOverridesScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean overridesEnabled; + private ButtonWidget overridesButton; + private ButtonWidget resetOverridesButton; + private NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.ProfileFields lowFields; + private NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.ProfileFields highFields; + + protected FemaleGenderDestroyedOverridesScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.destroyed_skin_female_gender_profiles_title")); + this.parent = parent; + this.config = config; + this.overridesEnabled = config.isDestroyedSkinFemaleGenderOverridesEnabled(); + } + + protected void init() { + int listTop = 156; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + this.overridesButton = ButtonWidget.builder(toggleText(this.overridesEnabled), button -> { + this.overridesEnabled = !this.overridesEnabled; + this.updateOverrideControls(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.overridesButton, "config.needsofnature.tooltip.destroyed_skin_female_gender_overrides"); + this.resetOverridesButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.overridesEnabled = this.defaults.isDestroyedSkinFemaleGenderOverridesEnabled(); + this.updateOverrideControls(); + }).dimensions(0, 0, 20, 20).build(); + NonModMenuScreens.setTooltip(this.resetOverridesButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.destroyed_skin_female_gender_overrides"), + this.overridesButton, + this.resetOverridesButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.destroyed_skin_female_gender_overrides") + ) + ); + ButtonWidget breastTextureEditorButton = ButtonWidget.builder(Text.translatable("wildfire_gender.uv_editor"), button -> { + this.saveValues(); + NonWildfireGenderSync.openBreastTextureEditor(this); + }).dimensions(0, 0, 220, 20).build(); + breastTextureEditorButton.active = NonWildfireGenderSync.canOpenBreastTextureEditor(); + NonModMenuScreens.setTooltip(breastTextureEditorButton, "config.needsofnature.tooltip.female_gender.breast_texture_editor"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, breastTextureEditorButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.female_gender.breast_texture_editor") + ) + ); + this.lowFields = new NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.ProfileFields(this.config.getFemaleGenderDestroyedLow()); + this.highFields = new NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.ProfileFields(this.config.getFemaleGenderDestroyedHigh()); + this.lowFields.addRows(settingsList, this.textRenderer, Text.translatable("config.needsofnature.destroyed_skin_female_gender_profile.low")); + this.highFields.addRows(settingsList, this.textRenderer, Text.translatable("config.needsofnature.destroyed_skin_female_gender_profile.high")); + this.updateOverrideControls(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> { + this.saveValues(); + MinecraftClient.getInstance().setScreen(this.parent); + }).dimensions(centerX - 100, this.height - 28, 200, 20).build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + this.renderPreviewPanel(context, mouseX, mouseY); + } + + public void close() { + this.saveValues(); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void saveValues() { + this.config.setDestroyedSkinFemaleGenderOverridesEnabled(this.overridesEnabled); + if (this.lowFields != null) { + this.config.setFemaleGenderDestroyedLow(this.lowFields.toProfile(this.config.getFemaleGenderDestroyedLow())); + } + + if (this.highFields != null) { + this.config.setFemaleGenderDestroyedHigh(this.highFields.toProfile(this.config.getFemaleGenderDestroyedHigh())); + } + + this.config.save(); + NonWildfireGenderSync.syncDestroyedSkinOverrides(this.config); + } + + private void updateOverrideControls() { + if (this.overridesButton != null) { + this.overridesButton.setMessage(toggleText(this.overridesEnabled)); + } + + if (this.resetOverridesButton != null) { + this.resetOverridesButton.active = this.overridesEnabled != this.defaults.isDestroyedSkinFemaleGenderOverridesEnabled(); + } + + if (this.lowFields != null) { + this.lowFields.setActive(this.overridesEnabled); + } + + if (this.highFields != null) { + this.highFields.setActive(this.overridesEnabled); + } + } + + private static Text toggleText(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private void renderPreviewPanel(DrawContext context, int mouseX, int mouseY) { + int panelTop = 34; + int panelBottom = 148; + int panelWidth = 160; + int gap = 14; + int leftX = this.width / 2 - panelWidth - gap / 2; + int rightX = this.width / 2 + gap / 2; + this.renderSinglePreview( + context, + mouseX, + mouseY, + leftX, + panelTop, + panelWidth, + panelBottom - panelTop, + Text.translatable("config.needsofnature.destroyed_skin_female_gender_profile.low"), + this.lowFields == null ? this.config.getFemaleGenderDestroyedLow() : this.lowFields.toProfile(this.config.getFemaleGenderDestroyedLow()) + ); + this.renderSinglePreview( + context, + mouseX, + mouseY, + rightX, + panelTop, + panelWidth, + panelBottom - panelTop, + Text.translatable("config.needsofnature.destroyed_skin_female_gender_profile.high"), + this.highFields == null ? this.config.getFemaleGenderDestroyedHigh() : this.highFields.toProfile(this.config.getFemaleGenderDestroyedHigh()) + ); + } + + private void renderSinglePreview( + DrawContext context, int mouseX, int mouseY, int x, int y, int w, int h, Text label, NonConfig.FemaleGenderDestroyedProfile profile + ) { + context.fill(x, y, x + w, y + h, 1711276032); + context.fill(x, y, x + w, y + 1, -1996488705); + context.fill(x, y + h - 1, x + w, y + h, -2013265920); + context.fill(x, y, x + 1, y + h, -1996488705); + context.fill(x + w - 1, y, x + w, y + h, -2013265920); + context.drawCenteredTextWithShadow(this.textRenderer, label, x + w / 2, y + 6, -2039584); + boolean rendered = NonWildfireGenderSync.renderBreastPreview(this, context, profile, x + w / 2, y + h - 12, mouseX, mouseY); + if (!rendered) { + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("config.needsofnature.female_gender.preview_unavailable"), x + w / 2, y + h / 2, -32640 + ); + } + } + + private final class ProfileFields { + private final FloatValueSliderWidget breastSize; + private final FloatValueSliderWidget separation; + private final FloatValueSliderWidget height; + private final FloatValueSliderWidget depth; + private final FloatValueSliderWidget rotation; + private final ButtonWidget breastPhysics; + private final ButtonWidget dualPhysics; + private final FloatValueSliderWidget intensity; + private final FloatValueSliderWidget momentum; + private boolean breastPhysicsValue; + private boolean dualPhysicsValue; + + private ProfileFields(NonConfig.FemaleGenderDestroyedProfile profile) { + this.breastSize = this.newFloatSlider(profile.breastSize(), 0.0F, 1.6F, 0.008F, value -> this.formatPercent(value * 125.0)); + this.breastSize.setWarningAbove(0.8F); + this.separation = this.newFloatSlider(profile.separation(), -1.0F, 1.0F, 0.1F, value -> this.formatPlain(value * 10.0)); + this.height = this.newFloatSlider(profile.height(), -2.0F, 1.0F, 0.1F, value -> this.formatPlain(value * 10.0)); + this.depth = this.newFloatSlider(profile.depth(), -1.0F, 0.0F, 0.1F, value -> this.formatPlain(value * 10.0)); + this.rotation = this.newFloatSlider(profile.rotation(), 0.0F, 0.1F, 0.01F, value -> this.formatPlain(value * 100.0) + "°"); + this.breastPhysicsValue = profile.breastPhysics(); + this.dualPhysicsValue = profile.dualPhysics(); + this.breastPhysics = ButtonWidget.builder( + NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.toggleText(this.breastPhysicsValue), button -> { + this.breastPhysicsValue = !this.breastPhysicsValue; + button.setMessage(NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.toggleText(this.breastPhysicsValue)); + } + ) + .dimensions(0, 0, 70, 20) + .build(); + this.dualPhysics = ButtonWidget.builder(NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.toggleText(this.dualPhysicsValue), button -> { + this.dualPhysicsValue = !this.dualPhysicsValue; + button.setMessage(NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen.toggleText(this.dualPhysicsValue)); + }).dimensions(0, 0, 70, 20).build(); + this.intensity = this.newFloatSlider(profile.intensity(), 0.0F, 0.5F, 0.0033333334F, value -> this.formatPercent(value * 300.0)); + this.momentum = this.newFloatSlider(profile.momentum(), 0.25F, 1.0F, 0.0075F, value -> this.formatPercent((value - 0.25) / 0.75 * 100.0)); + } + + private void addRows(SettingsList settingsList, TextRenderer textRenderer, Text title) { + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(textRenderer, title)); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(textRenderer, Text.translatable("config.needsofnature.female_gender.customization"))); + this.addFloatRow( + settingsList, textRenderer, "breast_size", this.breastSize, "0% - 200%", "config.needsofnature.tooltip.female_gender.breast_size_extended" + ); + this.addFloatRow(settingsList, textRenderer, "separation", this.separation, "-10 - 10"); + this.addFloatRow(settingsList, textRenderer, "height", this.height, "-20 - 10"); + this.addFloatRow(settingsList, textRenderer, "depth", this.depth, "-10 - 0"); + this.addFloatRow(settingsList, textRenderer, "rotation", this.rotation, "0° - 10°"); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(textRenderer, Text.translatable("config.needsofnature.female_gender.physics"))); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + textRenderer, + Text.translatable("config.needsofnature.female_gender.breast_physics"), + this.breastPhysics, + null, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.female_gender.breast_physics") + ) + ); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + textRenderer, + Text.translatable("config.needsofnature.female_gender.dual_physics"), + this.dualPhysics, + null, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.female_gender.dual_physics") + ) + ); + this.addFloatRow(settingsList, textRenderer, "intensity", this.intensity, "0% - 150%"); + this.addFloatRow(settingsList, textRenderer, "momentum", this.momentum, "0% - 100%"); + } + + private void addFloatRow(SettingsList settingsList, TextRenderer textRenderer, String key, FloatValueSliderWidget field, String range) { + this.addFloatRow(settingsList, textRenderer, key, field, range, null); + } + + private void addFloatRow( + SettingsList settingsList, TextRenderer textRenderer, String key, FloatValueSliderWidget field, String range, String warningKey + ) { + MutableText tooltipText = Text.translatable("config.needsofnature.tooltip.female_gender.range", new Object[]{range}); + if (warningKey != null) { + tooltipText.append("\n").append(Text.translatable(warningKey).formatted(Formatting.YELLOW)); + } + + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + textRenderer, Text.translatable("config.needsofnature.female_gender." + key), field, null, NonModMenuScreens.tooltip(tooltipText) + ) + ); + } + + private void setActive(boolean active) { + this.breastSize.active = active; + this.separation.active = active; + this.height.active = active; + this.depth.active = active; + this.rotation.active = active; + this.breastPhysics.active = active; + this.dualPhysics.active = active; + this.intensity.active = active; + this.momentum.active = active; + } + + private NonConfig.FemaleGenderDestroyedProfile toProfile(NonConfig.FemaleGenderDestroyedProfile fallback) { + return new NonConfig.FemaleGenderDestroyedProfile( + this.breastSize.getFloatValue(), + this.separation.getFloatValue(), + this.height.getFloatValue(), + this.depth.getFloatValue(), + this.rotation.getFloatValue(), + this.breastPhysicsValue, + this.dualPhysicsValue, + this.intensity.getFloatValue(), + this.momentum.getFloatValue() + ); + } + + private FloatValueSliderWidget newFloatSlider(float initial, float min, float max, float step, DoubleFunction formatter) { + return new FloatValueSliderWidget(0, 0, 160, 20, initial, min, max, step, ignored -> {}, formatter); + } + + private String formatPercent(double value) { + return this.formatPlain(value) + "%"; + } + + private String formatPlain(double value) { + return String.format(Locale.ROOT, "%.0f", value); + } + } + } + + static class PlayerPreferencesScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private NonConfig.PlayerGenderSelection playerGenderSelection; + private boolean syncFemaleGenderModWithNonGender; + private boolean armorShedEffectEnabled; + private boolean autoSwitchThirdPersonOnAnimationStart; + private int actionSoundVolumePercent; + private int liquidPuddleDespawnSeconds; + private ButtonWidget playerGenderButton; + private ButtonWidget resetPlayerGenderButton; + private ButtonWidget syncFemaleGenderModWithNonGenderButton; + private ButtonWidget resetSyncFemaleGenderModWithNonGenderButton; + private ButtonWidget armorShedEffectButton; + private ButtonWidget resetArmorShedEffectButton; + private ButtonWidget autoSwitchThirdPersonButton; + private ButtonWidget resetAutoSwitchThirdPersonButton; + private PercentSliderWidget actionSoundVolumeSlider; + private ButtonWidget resetActionSoundVolumeButton; + private TextFieldWidget liquidPuddleDespawnField; + private ButtonWidget resetLiquidPuddleDespawnButton; + private boolean playerGenderEditable; + + protected PlayerPreferencesScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.player_preferences_title")); + this.parent = parent; + this.config = config; + this.playerGenderSelection = config.getPlayerGenderSelection(); + this.syncFemaleGenderModWithNonGender = config.isSyncFemaleGenderModWithNonGender(); + this.armorShedEffectEnabled = config.isArmorShedEffectEnabled(); + this.autoSwitchThirdPersonOnAnimationStart = config.autoSwitchThirdPersonOnAnimationStart(); + this.actionSoundVolumePercent = config.getActionSoundVolumePercent(); + this.liquidPuddleDespawnSeconds = config.getLiquidPuddleDespawnSeconds(); + } + + protected void init() { + if (this.liquidPuddleDespawnField != null) { + this.liquidPuddleDespawnSeconds = this.clampLiquidPuddleDespawnSeconds( + this.parseField(this.liquidPuddleDespawnField, this.liquidPuddleDespawnSeconds) + ); + } + + int listTop = 32; + int bottomArea = 40; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + this.playerGenderEditable = this.config.allowPlayerGenderChangeAnytime(); + if (!this.playerGenderEditable) { + this.playerGenderSelection = this.currentPlayerGenderSelection(this.playerGenderSelection); + } + + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.player_identity"))); + this.playerGenderButton = ButtonWidget.builder(this.playerGenderText(this.playerGenderSelection), button -> { + this.playerGenderSelection = this.playerGenderSelection.next(); + button.setMessage(this.playerGenderText(this.playerGenderSelection)); + this.updateResetButtons(); + }).dimensions(0, 0, 140, 20).build(); + this.playerGenderButton.active = this.playerGenderEditable; + NonModMenuScreens.setTooltip( + this.playerGenderButton, + this.playerGenderEditable ? "config.needsofnature.tooltip.player_gender" : "config.needsofnature.tooltip.player_gender.locked" + ); + this.resetPlayerGenderButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.playerGenderSelection = this.defaults.getPlayerGenderSelection(); + if (this.playerGenderButton != null) { + this.playerGenderButton.setMessage(this.playerGenderText(this.playerGenderSelection)); + } + + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetPlayerGenderButton, "config.needsofnature.tooltip.reset"); + this.resetPlayerGenderButton.active = this.playerGenderEditable; + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.player_gender"), + this.playerGenderButton, + this.resetPlayerGenderButton, + NonModMenuScreens.tooltip( + this.playerGenderEditable ? "config.needsofnature.tooltip.player_gender" : "config.needsofnature.tooltip.player_gender.locked" + ) + ) + ); + ButtonWidget playerProfilesButton = ButtonWidget.builder( + Text.translatable("gui.needsofnature.player_profiles.title"), + button -> MinecraftClient.getInstance().setScreen(new NonPlayerProfilesScreen(this)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(playerProfilesButton, "gui.needsofnature.player_profiles.tooltip"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, playerProfilesButton, NonModMenuScreens.tooltip("gui.needsofnature.player_profiles.tooltip") + ) + ); + if (NonWildfireGenderSync.isAvailable()) { + this.syncFemaleGenderModWithNonGenderButton = ButtonWidget.builder(this.toggleText(this.syncFemaleGenderModWithNonGender), button -> { + this.syncFemaleGenderModWithNonGender = !this.syncFemaleGenderModWithNonGender; + button.setMessage(this.toggleText(this.syncFemaleGenderModWithNonGender)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.syncFemaleGenderModWithNonGenderButton, "config.needsofnature.tooltip.sync_female_gender_mod"); + this.resetSyncFemaleGenderModWithNonGenderButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.syncFemaleGenderModWithNonGender = this.defaults.isSyncFemaleGenderModWithNonGender(); + if (this.syncFemaleGenderModWithNonGenderButton != null) { + this.syncFemaleGenderModWithNonGenderButton.setMessage(this.toggleText(this.syncFemaleGenderModWithNonGender)); + } + + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetSyncFemaleGenderModWithNonGenderButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.sync_female_gender_mod"), + this.syncFemaleGenderModWithNonGenderButton, + this.resetSyncFemaleGenderModWithNonGenderButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.sync_female_gender_mod") + ) + ); + ButtonWidget destroyedSkinFemaleGenderProfilesButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.destroyed_skin_female_gender_profiles"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuPlayerScreens.FemaleGenderDestroyedOverridesScreen(this, this.config)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(destroyedSkinFemaleGenderProfilesButton, "config.needsofnature.tooltip.destroyed_skin_female_gender_profiles"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, + destroyedSkinFemaleGenderProfilesButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.destroyed_skin_female_gender_profiles") + ) + ); + } + + boolean destroyedSkinSystemEnabled = this.config.isDestroyedSkinSystemEnabled(); + boolean hasCustomDestroyedSkin = NonDestroyedSkinClient.hasLocalCustomDestroyedSkin(); + boolean hasDefaultDestroyedSkin = NonDestroyedSkinClient.hasCurrentDefaultDestroyedSkin(); + String destroyedSkinTintTooltip = !destroyedSkinSystemEnabled + ? "config.needsofnature.tooltip.destroyed_skin_tint.disabled_system" + : ( + hasCustomDestroyedSkin + ? "config.needsofnature.tooltip.destroyed_skin_tint.custom_skin" + : ( + hasDefaultDestroyedSkin + ? "config.needsofnature.tooltip.destroyed_skin_tint.default_skin" + : "config.needsofnature.tooltip.destroyed_skin_tint.pick" + ) + ); + ButtonWidget pickerButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.destroyed_skin_tint.pick"), + button -> MinecraftClient.getInstance() + .setScreen(new NonModMenuPlayerScreens.DestroyedSkinTintPickerScreen(this, this.config, this.config.getDestroyedSkinTintHex())) + ) + .dimensions(0, 0, 220, 20) + .build(); + pickerButton.active = destroyedSkinSystemEnabled && !hasCustomDestroyedSkin && !hasDefaultDestroyedSkin; + NonModMenuScreens.setTooltip(pickerButton, destroyedSkinTintTooltip); + settingsList.addEntryRow(SettingsList.RowEntry.buttonWithReset(this.textRenderer, pickerButton, NonModMenuScreens.tooltip(destroyedSkinTintTooltip))); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.local_visuals"))); + this.autoSwitchThirdPersonButton = ButtonWidget.builder(this.toggleText(this.autoSwitchThirdPersonOnAnimationStart), button -> { + this.autoSwitchThirdPersonOnAnimationStart = !this.autoSwitchThirdPersonOnAnimationStart; + button.setMessage(this.toggleText(this.autoSwitchThirdPersonOnAnimationStart)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.autoSwitchThirdPersonButton, "config.needsofnature.tooltip.animation.auto_switch_third_person"); + this.resetAutoSwitchThirdPersonButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.autoSwitchThirdPersonOnAnimationStart = this.defaults.autoSwitchThirdPersonOnAnimationStart(); + this.autoSwitchThirdPersonButton.setMessage(this.toggleText(this.autoSwitchThirdPersonOnAnimationStart)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetAutoSwitchThirdPersonButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.animation.auto_switch_third_person"), + this.autoSwitchThirdPersonButton, + this.resetAutoSwitchThirdPersonButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.animation.auto_switch_third_person") + ) + ); + this.armorShedEffectButton = ButtonWidget.builder(this.toggleText(this.armorShedEffectEnabled), button -> { + this.armorShedEffectEnabled = !this.armorShedEffectEnabled; + button.setMessage(this.toggleText(this.armorShedEffectEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.armorShedEffectButton, "config.needsofnature.tooltip.armor_shed_effect"); + this.resetArmorShedEffectButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.armorShedEffectEnabled = this.defaults.isArmorShedEffectEnabled(); + if (this.armorShedEffectButton != null) { + this.armorShedEffectButton.setMessage(this.toggleText(this.armorShedEffectEnabled)); + } + + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetArmorShedEffectButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.armor_shed_effect"), + this.armorShedEffectButton, + this.resetArmorShedEffectButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.armor_shed_effect") + ) + ); + this.actionSoundVolumeSlider = new PercentSliderWidget(0, 0, 180, 20, this.actionSoundVolumePercent, value -> { + this.actionSoundVolumePercent = value; + this.updateResetButtons(); + }); + NonModMenuScreens.setTooltip(this.actionSoundVolumeSlider, "config.needsofnature.tooltip.action_sound_volume"); + this.resetActionSoundVolumeButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.actionSoundVolumePercent = this.defaults.getActionSoundVolumePercent(); + if (this.actionSoundVolumeSlider != null) { + this.actionSoundVolumeSlider.setPercent(this.actionSoundVolumePercent); + } + + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetActionSoundVolumeButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.action_sound_volume"), + this.actionSoundVolumeSlider, + this.resetActionSoundVolumeButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.action_sound_volume") + ) + ); + this.liquidPuddleDespawnField = this.newNumberField(fieldWidth, this.liquidPuddleDespawnSeconds); + this.resetLiquidPuddleDespawnButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.liquidPuddleDespawnField.setText(String.valueOf(this.defaults.getLiquidPuddleDespawnSeconds())); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetLiquidPuddleDespawnButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.liquid_puddle_despawn_seconds"), + this.liquidPuddleDespawnField, + this.resetLiquidPuddleDespawnButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.liquid_puddle_despawn_seconds") + ) + ); + this.liquidPuddleDespawnField.setChangedListener(ignored -> this.updateResetButtons()); + ButtonWidget intifaceButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.intiface"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuIntifaceScreen(this, this.config)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(intifaceButton, "config.needsofnature.tooltip.category.intiface"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, intifaceButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.category.intiface") + ) + ); + ButtonWidget interfaceButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.interface"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuHudConfigScreen.UiConfigScreen(this, this.config)) + ) + .dimensions(0, 0, 220, 20) + .build(); + NonModMenuScreens.setTooltip(interfaceButton, "config.needsofnature.tooltip.category.interface"); + settingsList.addEntryRow( + SettingsList.RowEntry.buttonWithReset( + this.textRenderer, interfaceButton, NonModMenuScreens.tooltip("config.needsofnature.tooltip.category.interface") + ) + ); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + this.liquidPuddleDespawnSeconds = this.clampLiquidPuddleDespawnSeconds(this.parseField(this.liquidPuddleDespawnField, this.liquidPuddleDespawnSeconds)); + if (this.playerGenderEditable) { + this.config.setPlayerGenderSelection(this.playerGenderSelection); + } + + this.config.setSyncFemaleGenderModWithNonGender(this.syncFemaleGenderModWithNonGender); + this.config.setArmorShedEffectEnabled(this.armorShedEffectEnabled); + this.config.setAutoSwitchThirdPersonOnAnimationStart(this.autoSwitchThirdPersonOnAnimationStart); + this.config.setActionSoundVolumePercent(this.actionSoundVolumePercent); + this.config.setLiquidPuddleDespawnSeconds(this.liquidPuddleDespawnSeconds); + this.config.save(); + if (this.playerGenderEditable) { + NeedsOfNatureClient.pushConfiguredPlayerGenderToServer(this.config); + } + + NonWildfireGenderSync.syncDestroyedSkinOverrides(this.config); + MinecraftClient.getInstance().setScreen(this.parent); + } + + private Text playerGenderText(NonConfig.PlayerGenderSelection selection) { + NonConfig.PlayerGenderSelection resolved = selection == null ? NonConfig.PlayerGenderSelection.FEMALE : selection; + + return switch (resolved) { + case MALE -> Text.translatable("config.needsofnature.player_gender.male"); + case FEMALE -> Text.translatable("config.needsofnature.player_gender.female"); + case BOTH -> Text.translatable("config.needsofnature.player_gender.both"); + }; + } + + private Text toggleText(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private NonConfig.PlayerGenderSelection currentPlayerGenderSelection(NonConfig.PlayerGenderSelection fallback) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player instanceof GenderHolder holder) { + int mask = holder.getGenderMask() & 3; + if (mask != 0) { + return NonConfig.PlayerGenderSelection.fromMask(mask, fallback); + } + } + + return fallback == null ? NonConfig.PlayerGenderSelection.FEMALE : fallback; + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + if (field == null) { + return fallback; + } + + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private int clampLiquidPuddleDespawnSeconds(int v) { + return Math.max(1, Math.min(300, v)); + } + + private void updateResetButtons() { + if (this.resetPlayerGenderButton != null) { + this.resetPlayerGenderButton.active = this.playerGenderEditable && this.playerGenderSelection != this.defaults.getPlayerGenderSelection(); + } + + if (this.playerGenderButton != null) { + this.playerGenderButton.active = this.playerGenderEditable; + } + + if (this.resetSyncFemaleGenderModWithNonGenderButton != null) { + this.resetSyncFemaleGenderModWithNonGenderButton.active = this.syncFemaleGenderModWithNonGender + != this.defaults.isSyncFemaleGenderModWithNonGender(); + } + + if (this.resetArmorShedEffectButton != null) { + this.resetArmorShedEffectButton.active = this.armorShedEffectEnabled != this.defaults.isArmorShedEffectEnabled(); + } + + if (this.resetAutoSwitchThirdPersonButton != null) { + this.resetAutoSwitchThirdPersonButton.active = this.autoSwitchThirdPersonOnAnimationStart != this.defaults.autoSwitchThirdPersonOnAnimationStart(); + } + + if (this.resetActionSoundVolumeButton != null) { + this.resetActionSoundVolumeButton.active = this.actionSoundVolumePercent != this.defaults.getActionSoundVolumePercent(); + } + + if (this.resetLiquidPuddleDespawnButton != null) { + this.resetLiquidPuddleDespawnButton.active = this.parseField(this.liquidPuddleDespawnField, this.defaults.getLiquidPuddleDespawnSeconds()) + != this.defaults.getLiquidPuddleDespawnSeconds(); + } + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuScreens.java b/src/main/java/com/nonid/integration/NonModMenuScreens.java new file mode 100644 index 0000000..8371822 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuScreens.java @@ -0,0 +1,277 @@ +package com.nonid.integration; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.client.NonServerSettingsClient; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Text; + +final class NonModMenuScreens { + private static final Map SERVER_DRAFT_REVISIONS = Collections.synchronizedMap(new WeakHashMap<>()); + + private NonModMenuScreens() { + } + + static Tooltip tooltip(String key) { + return Tooltip.of(Text.translatable(key)); + } + + static Tooltip tooltip(Text text) { + return Tooltip.of(text); + } + + static void setTooltip(ClickableWidget widget, String key) { + if (widget != null) { + widget.setTooltip(tooltip(key)); + widget.setTooltipDelay(Duration.ofMillis(500L)); + } + } + + static void setTooltip(ClickableWidget widget, Tooltip tooltip) { + if (widget != null) { + widget.setTooltip(tooltip); + widget.setTooltipDelay(Duration.ofMillis(500L)); + } + } + + static boolean canEditServerGameplaySettings() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return true; + } else { + return client.world == null ? true : NonServerSettingsClient.hasConfigSnapshot() && (client.getServer() != null || NonServerSettingsClient.canEdit()); + } + } + + static boolean canEditLocalPackLoadOrder() { + MinecraftClient client = MinecraftClient.getInstance(); + return client == null || client.world == null || client.getServer() != null; + } + + static NonConfig serverConfigForScreen() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null) { + NonConfig draft = NonServerSettingsClient.createServerConfigDraft(); + SERVER_DRAFT_REVISIONS.put(draft, NonServerSettingsClient.configRevision()); + return draft; + } else { + return NeedsOfNature.getLocalConfig(); + } + } + + static void saveServerConfig(NonConfig config) { + if (config != null) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null || client.world == null) { + config.save(); + } else if (canEditServerGameplaySettings()) { + long expectedRevision = SERVER_DRAFT_REVISIONS.getOrDefault(config, NonServerSettingsClient.configRevision()); + NonServerSettingsClient.sendConfigUpdate(config, expectedRevision); + SERVER_DRAFT_REVISIONS.put(config, expectedRevision + 1L); + } + } + } + + static Screen createConfigScreen(Screen parent) { + NonServerSettingsClient.requestRefresh(); + return new NonModMenuScreens.ConfigCategoryScreen(parent, NeedsOfNature.getLocalConfig()); + } + + private static class ConfigCategoryScreen extends Screen { + private final Screen parent; + private final NonConfig config; + + protected ConfigCategoryScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.categories_title")); + this.parent = parent; + this.config = config; + } + + protected void init() { + int centerX = this.width / 2; + int y = this.height / 2 - 36; + int gap = 24; + ButtonWidget togglesButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.toggles"), + button -> MinecraftClient.getInstance() + .setScreen(new NonModMenuSystemSettingsScreens.ToggleSettingsScreen(this, NonModMenuScreens.serverConfigForScreen())) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + NonModMenuScreens.setTooltip(togglesButton, "config.needsofnature.tooltip.category.toggles"); + this.addDrawableChild(togglesButton); + y += gap; + ButtonWidget playerButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.player_preferences"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuPlayerScreens.PlayerPreferencesScreen(this, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + NonModMenuScreens.setTooltip(playerButton, "config.needsofnature.tooltip.category.player_preferences"); + this.addDrawableChild(playerButton); + y += gap; + ButtonWidget settingsButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.settings"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuScreens.SettingsCategoryScreen(this, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + NonModMenuScreens.setTooltip(settingsButton, "config.needsofnature.tooltip.category.settings"); + this.addDrawableChild(settingsButton); + ButtonWidget doneButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent) + ) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + } + + static final class FlatTextButton extends ClickableWidget { + private final NonModMenuScreens.FlatTextButton.FlatPressAction onPress; + + FlatTextButton(int width, Text text, NonModMenuScreens.FlatTextButton.FlatPressAction onPress) { + super(0, 0, width, 12, text); + this.onPress = onPress; + } + + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + TextRenderer renderer = MinecraftClient.getInstance().textRenderer; + if (renderer != null) { + int rgb = this.active ? (this.isSelected() ? 16777215 : 13684944) : 8421504; + int alpha = Math.max(0, Math.min(255, (int)(this.getAlpha() * 255.0F))); + int color = alpha << 24 | rgb; + int textX = this.getX() + (this.getWidth() - renderer.getWidth(this.getMessage())) / 2; + int textY = this.getY() + (this.getHeight() - 8) / 2; + context.drawTextWithShadow(renderer, this.getMessage(), textX, textY, color); + } + } + + public void onClick(Click click, boolean doubled) { + super.onClick(click, doubled); + if (this.active && this.onPress != null) { + this.onPress.onPress(this); + } + } + + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + this.appendDefaultNarrations(builder); + } + + @FunctionalInterface + interface FlatPressAction { + void onPress(NonModMenuScreens.FlatTextButton var1); + } + } + + private static class SettingsCategoryScreen extends Screen { + private final Screen parent; + private final NonConfig config; + + protected SettingsCategoryScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.settings_title")); + this.parent = parent; + this.config = config; + } + + protected void init() { + NonConfig serverConfig = NonModMenuScreens.serverConfigForScreen(); + int centerX = this.width / 2; + int y = this.height / 2 - 58; + int gap = 24; + ButtonWidget gameplayButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.gameplay"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuGameplayScreens.GameplayCategoryScreen(this, serverConfig)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + NonModMenuScreens.setTooltip(gameplayButton, "config.needsofnature.tooltip.category.gameplay"); + this.addDrawableChild(gameplayButton); + y += gap; + ButtonWidget vanillaOverridesButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.vanilla_overrides"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuSystemSettingsScreens.VanillaOverrideScreen(this, serverConfig)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + vanillaOverridesButton.active = serverConfig.isVanillaOverridesEnabled(); + NonModMenuScreens.setTooltip( + vanillaOverridesButton, + vanillaOverridesButton.active + ? "config.needsofnature.tooltip.category.vanilla_overrides" + : "config.needsofnature.tooltip.category.disabled_in_toggles" + ); + this.addDrawableChild(vanillaOverridesButton); + y += gap; + ButtonWidget liquidButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.liquid"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuSystemSettingsScreens.LiquidSettingsScreen(this, serverConfig)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + liquidButton.active = serverConfig.isLiquidTankEnabled(); + NonModMenuScreens.setTooltip( + liquidButton, liquidButton.active ? "config.needsofnature.tooltip.category.liquid" : "config.needsofnature.tooltip.category.disabled_in_toggles" + ); + this.addDrawableChild(liquidButton); + y += gap; + ButtonWidget pregnancyButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.pregnancy"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuSystemSettingsScreens.PregnancySettingsScreen(this, serverConfig)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + pregnancyButton.active = serverConfig.isPregnancyEnabled(); + NonModMenuScreens.setTooltip( + pregnancyButton, + pregnancyButton.active ? "config.needsofnature.tooltip.category.pregnancy" : "config.needsofnature.tooltip.category.disabled_in_toggles" + ); + this.addDrawableChild(pregnancyButton); + y += gap; + ButtonWidget debugButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.category.debug_advanced"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.DebugAdvancedScreen(this, serverConfig, this.config)) + ) + .dimensions(centerX - 100, y, 200, 20) + .build(); + NonModMenuScreens.setTooltip(debugButton, "config.needsofnature.tooltip.category.debug_advanced"); + this.addDrawableChild(debugButton); + ButtonWidget doneButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.done"), button -> MinecraftClient.getInstance().setScreen(this.parent) + ) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + } +} diff --git a/src/main/java/com/nonid/integration/NonModMenuSystemSettingsScreens.java b/src/main/java/com/nonid/integration/NonModMenuSystemSettingsScreens.java new file mode 100644 index 0000000..1976907 --- /dev/null +++ b/src/main/java/com/nonid/integration/NonModMenuSystemSettingsScreens.java @@ -0,0 +1,1394 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import java.util.Locale; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; + +final class NonModMenuSystemSettingsScreens { + private NonModMenuSystemSettingsScreens() { + } + + static class LiquidSettingsScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean serverConfigEditable; + private int liquidTankCapacityMl; + private double liquidDecay; + private double peakXpPerMl; + private double filledStageOneSpeed; + private double filledStageTwoSpeed; + private double filledStageThreeSpeed; + private double filledStageOneJump; + private double filledStageTwoJump; + private double filledStageThreeJump; + private TextFieldWidget liquidTankCapacityField; + private TextFieldWidget liquidDecayField; + private TextFieldWidget peakXpPerMlField; + private TextFieldWidget filledStageOneSpeedField; + private TextFieldWidget filledStageTwoSpeedField; + private TextFieldWidget filledStageThreeSpeedField; + private TextFieldWidget filledStageOneJumpField; + private TextFieldWidget filledStageTwoJumpField; + private TextFieldWidget filledStageThreeJumpField; + private ButtonWidget resetLiquidTankCapacityButton; + private ButtonWidget resetLiquidDecayButton; + private ButtonWidget resetPeakXpPerMlButton; + private ButtonWidget resetFilledStageOneSpeedButton; + private ButtonWidget resetFilledStageTwoSpeedButton; + private ButtonWidget resetFilledStageThreeSpeedButton; + private ButtonWidget resetFilledStageOneJumpButton; + private ButtonWidget resetFilledStageTwoJumpButton; + private ButtonWidget resetFilledStageThreeJumpButton; + + protected LiquidSettingsScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.liquid_settings_title")); + this.parent = parent; + this.config = config; + this.liquidTankCapacityMl = config.getLiquidTankCapacityMl(); + this.liquidDecay = config.getLiquidDecayPerSecond(); + this.peakXpPerMl = config.getPeakXpPerMl(); + this.filledStageOneSpeed = config.getFilledStageOneSpeedMult(); + this.filledStageTwoSpeed = config.getFilledStageTwoSpeedMult(); + this.filledStageThreeSpeed = config.getFilledStageThreeSpeedMult(); + this.filledStageOneJump = config.getFilledStageOneJumpMult(); + this.filledStageTwoJump = config.getFilledStageTwoJumpMult(); + this.filledStageThreeJump = config.getFilledStageThreeJumpMult(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.liquidTankCapacityField != null) { + this.liquidTankCapacityMl = this.parseField(this.liquidTankCapacityField, this.liquidTankCapacityMl); + this.liquidDecay = this.parseDecimalField(this.liquidDecayField, this.liquidDecay); + this.peakXpPerMl = this.parseDecimalField(this.peakXpPerMlField, this.peakXpPerMl); + this.filledStageOneSpeed = this.parseDecimalField(this.filledStageOneSpeedField, this.filledStageOneSpeed); + this.filledStageTwoSpeed = this.parseDecimalField(this.filledStageTwoSpeedField, this.filledStageTwoSpeed); + this.filledStageThreeSpeed = this.parseDecimalField(this.filledStageThreeSpeedField, this.filledStageThreeSpeed); + this.filledStageOneJump = this.parseDecimalField(this.filledStageOneJumpField, this.filledStageOneJump); + this.filledStageTwoJump = this.parseDecimalField(this.filledStageTwoJumpField, this.filledStageTwoJump); + this.filledStageThreeJump = this.parseDecimalField(this.filledStageThreeJumpField, this.filledStageThreeJump); + } + + int listTop = 32; + int bottomArea = 88; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.liquid_tank"))); + this.liquidTankCapacityField = this.newNumberField(fieldWidth, this.liquidTankCapacityMl); + this.resetLiquidTankCapacityButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.liquidTankCapacityField.setText(String.valueOf(this.defaults.getLiquidTankCapacityMl())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetLiquidTankCapacityButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.liquid_tank_capacity"), + this.liquidTankCapacityField, + this.resetLiquidTankCapacityButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.liquid_tank_capacity") + ) + ); + this.liquidDecayField = this.newDecimalField(fieldWidth, this.liquidDecay); + this.resetLiquidDecayButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.liquidDecayField.setText(String.valueOf(this.defaults.getLiquidDecayPerSecond())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetLiquidDecayButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.liquid_decay"), + this.liquidDecayField, + this.resetLiquidDecayButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.liquid_decay") + ) + ); + this.peakXpPerMlField = this.newDecimalField(fieldWidth, this.peakXpPerMl); + this.resetPeakXpPerMlButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), button -> this.peakXpPerMlField.setText(String.valueOf(this.defaults.getPeakXpPerMl())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPeakXpPerMlButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.peak_xp_per_ml"), + this.peakXpPerMlField, + this.resetPeakXpPerMlButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.peak_xp_per_ml") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.filled_penalties"))); + this.filledStageOneSpeedField = this.newDecimalField(fieldWidth, this.filledStageOneSpeed); + this.resetFilledStageOneSpeedButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.filledStageOneSpeedField.setText(String.valueOf(this.defaults.getFilledStageOneSpeedMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFilledStageOneSpeedButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.filled_stage_one_speed"), + this.filledStageOneSpeedField, + this.resetFilledStageOneSpeedButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.filled_stage_one_speed") + ) + ); + this.filledStageTwoSpeedField = this.newDecimalField(fieldWidth, this.filledStageTwoSpeed); + this.resetFilledStageTwoSpeedButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.filledStageTwoSpeedField.setText(String.valueOf(this.defaults.getFilledStageTwoSpeedMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFilledStageTwoSpeedButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.filled_stage_two_speed"), + this.filledStageTwoSpeedField, + this.resetFilledStageTwoSpeedButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.filled_stage_two_speed") + ) + ); + this.filledStageThreeSpeedField = this.newDecimalField(fieldWidth, this.filledStageThreeSpeed); + this.resetFilledStageThreeSpeedButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.filledStageThreeSpeedField.setText(String.valueOf(this.defaults.getFilledStageThreeSpeedMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFilledStageThreeSpeedButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.filled_stage_three_speed"), + this.filledStageThreeSpeedField, + this.resetFilledStageThreeSpeedButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.filled_stage_three_speed") + ) + ); + this.filledStageOneJumpField = this.newDecimalField(fieldWidth, this.filledStageOneJump); + this.resetFilledStageOneJumpButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.filledStageOneJumpField.setText(String.valueOf(this.defaults.getFilledStageOneJumpMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFilledStageOneJumpButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.filled_stage_one_jump"), + this.filledStageOneJumpField, + this.resetFilledStageOneJumpButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.filled_stage_one_jump") + ) + ); + this.filledStageTwoJumpField = this.newDecimalField(fieldWidth, this.filledStageTwoJump); + this.resetFilledStageTwoJumpButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.filledStageTwoJumpField.setText(String.valueOf(this.defaults.getFilledStageTwoJumpMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFilledStageTwoJumpButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.filled_stage_two_jump"), + this.filledStageTwoJumpField, + this.resetFilledStageTwoJumpButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.filled_stage_two_jump") + ) + ); + this.filledStageThreeJumpField = this.newDecimalField(fieldWidth, this.filledStageThreeJump); + this.resetFilledStageThreeJumpButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.filledStageThreeJumpField.setText(String.valueOf(this.defaults.getFilledStageThreeJumpMult())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetFilledStageThreeJumpButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.filled_stage_three_jump"), + this.filledStageThreeJumpField, + this.resetFilledStageThreeJumpButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.filled_stage_three_jump") + ) + ); + this.liquidTankCapacityField.setEditable(this.serverConfigEditable); + this.liquidDecayField.setEditable(this.serverConfigEditable); + this.peakXpPerMlField.setEditable(this.serverConfigEditable); + this.filledStageOneSpeedField.setEditable(this.serverConfigEditable); + this.filledStageTwoSpeedField.setEditable(this.serverConfigEditable); + this.filledStageThreeSpeedField.setEditable(this.serverConfigEditable); + this.filledStageOneJumpField.setEditable(this.serverConfigEditable); + this.filledStageTwoJumpField.setEditable(this.serverConfigEditable); + this.filledStageThreeJumpField.setEditable(this.serverConfigEditable); + this.liquidTankCapacityField.setChangedListener(ignored -> this.updateResetButtons()); + this.liquidDecayField.setChangedListener(ignored -> this.updateResetButtons()); + this.peakXpPerMlField.setChangedListener(ignored -> this.updateResetButtons()); + this.filledStageOneSpeedField.setChangedListener(ignored -> this.updateResetButtons()); + this.filledStageTwoSpeedField.setChangedListener(ignored -> this.updateResetButtons()); + this.filledStageThreeSpeedField.setChangedListener(ignored -> this.updateResetButtons()); + this.filledStageOneJumpField.setChangedListener(ignored -> this.updateResetButtons()); + this.filledStageTwoJumpField.setChangedListener(ignored -> this.updateResetButtons()); + this.filledStageThreeJumpField.setChangedListener(ignored -> this.updateResetButtons()); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget liquidGainButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.liquid_gain_button"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.LiquidGainConfigScreen(this, this.config)) + ) + .dimensions(centerX - 100, this.height - 52, 200, 20) + .build(); + NonModMenuScreens.setTooltip(liquidGainButton, "config.needsofnature.tooltip.liquid_gain_button"); + this.addDrawableChild(liquidGainButton); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip(doneButton, "config.needsofnature.tooltip.done_save"); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + this.liquidTankCapacityMl = this.clampLiquidTankCapacityMl(this.parseField(this.liquidTankCapacityField, this.liquidTankCapacityMl)); + this.liquidDecay = this.clampLiquidDecay(this.parseDecimalField(this.liquidDecayField, this.liquidDecay)); + this.peakXpPerMl = this.clampPeakXpPerMl(this.parseDecimalField(this.peakXpPerMlField, this.peakXpPerMl)); + this.filledStageOneSpeed = this.clampMultiplier(this.parseDecimalField(this.filledStageOneSpeedField, this.filledStageOneSpeed)); + this.filledStageTwoSpeed = this.clampMultiplier(this.parseDecimalField(this.filledStageTwoSpeedField, this.filledStageTwoSpeed)); + this.filledStageThreeSpeed = this.clampMultiplier(this.parseDecimalField(this.filledStageThreeSpeedField, this.filledStageThreeSpeed)); + this.filledStageOneJump = this.clampMultiplier(this.parseDecimalField(this.filledStageOneJumpField, this.filledStageOneJump)); + this.filledStageTwoJump = this.clampMultiplier(this.parseDecimalField(this.filledStageTwoJumpField, this.filledStageTwoJump)); + this.filledStageThreeJump = this.clampMultiplier(this.parseDecimalField(this.filledStageThreeJumpField, this.filledStageThreeJump)); + if (this.serverConfigEditable) { + this.config.setLiquidTankCapacityMl(this.liquidTankCapacityMl); + this.config.setLiquidDecayPerSecond(this.liquidDecay); + this.config.setPeakXpPerMl(this.peakXpPerMl); + this.config.setFilledStageOneSpeedMult((float)this.filledStageOneSpeed); + this.config.setFilledStageTwoSpeedMult((float)this.filledStageTwoSpeed); + this.config.setFilledStageThreeSpeedMult((float)this.filledStageThreeSpeed); + this.config.setFilledStageOneJumpMult((float)this.filledStageOneJump); + this.config.setFilledStageTwoJumpMult((float)this.filledStageTwoJump); + this.config.setFilledStageThreeJumpMult((float)this.filledStageThreeJump); + } + + if (this.serverConfigEditable) { + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private TextFieldWidget newDecimalField(int w, double initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + int maxLen = 16; + field.setMaxLength(maxLen); + field.setText(this.trimToMax(this.formatDecimal(initial), maxLen)); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private double parseDecimalField(TextFieldWidget field, double fallback) { + try { + return Double.parseDouble(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private String formatDecimal(double value) { + String text = String.format(Locale.ROOT, "%.3f", value); + int dot = text.indexOf(46); + if (dot >= 0) { + int end = text.length(); + + while (end > dot + 1 && text.charAt(end - 1) == '0') { + end--; + } + + if (end == dot + 1) { + end = dot; + } + + text = text.substring(0, end); + } + + return text; + } + + private String trimToMax(String value, int max) { + if (value == null) { + return ""; + } else { + return value.length() > max ? value.substring(0, max) : value; + } + } + + private double clampLiquidDecay(double v) { + return Math.max(0.0, Math.min(20.0, v)); + } + + private double clampPeakXpPerMl(double v) { + return Math.max(0.0, Math.min(10.0, v)); + } + + private int clampLiquidTankCapacityMl(int v) { + return Math.max(1, Math.min(10000, v)); + } + + private double clampMultiplier(double v) { + return Math.max(0.1, Math.min(1.0, v)); + } + + private boolean isIntFieldChanged(TextFieldWidget field, int defaultValue) { + return field == null ? false : this.parseField(field, defaultValue) != defaultValue; + } + + private boolean isDoubleFieldChanged(TextFieldWidget field, double defaultValue) { + if (field == null) { + return false; + } + + double value = this.parseDecimalField(field, defaultValue); + return Math.abs(value - defaultValue) >= 1.0E-4; + } + + private void updateResetButtons() { + if (this.resetLiquidTankCapacityButton != null) { + this.resetLiquidTankCapacityButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.liquidTankCapacityField, this.defaults.getLiquidTankCapacityMl()); + } + + if (this.resetLiquidDecayButton != null) { + this.resetLiquidDecayButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.liquidDecayField, this.defaults.getLiquidDecayPerSecond()); + } + + if (this.resetPeakXpPerMlButton != null) { + this.resetPeakXpPerMlButton.active = this.serverConfigEditable && this.isDoubleFieldChanged(this.peakXpPerMlField, this.defaults.getPeakXpPerMl()); + } + + if (this.resetFilledStageOneSpeedButton != null) { + this.resetFilledStageOneSpeedButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.filledStageOneSpeedField, this.defaults.getFilledStageOneSpeedMult()); + } + + if (this.resetFilledStageTwoSpeedButton != null) { + this.resetFilledStageTwoSpeedButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.filledStageTwoSpeedField, this.defaults.getFilledStageTwoSpeedMult()); + } + + if (this.resetFilledStageThreeSpeedButton != null) { + this.resetFilledStageThreeSpeedButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.filledStageThreeSpeedField, this.defaults.getFilledStageThreeSpeedMult()); + } + + if (this.resetFilledStageOneJumpButton != null) { + this.resetFilledStageOneJumpButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.filledStageOneJumpField, this.defaults.getFilledStageOneJumpMult()); + } + + if (this.resetFilledStageTwoJumpButton != null) { + this.resetFilledStageTwoJumpButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.filledStageTwoJumpField, this.defaults.getFilledStageTwoJumpMult()); + } + + if (this.resetFilledStageThreeJumpButton != null) { + this.resetFilledStageThreeJumpButton.active = this.serverConfigEditable + && this.isDoubleFieldChanged(this.filledStageThreeJumpField, this.defaults.getFilledStageThreeJumpMult()); + } + } + } + + static class PregnancySettingsScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean serverConfigEditable; + private int pregnancyChancePercent; + private int pregnancyDurationMinutes; + private int pregnancyEggHatchMinutes; + private boolean pregnancyAutoTameMobs; + private boolean pregnancyFriendlyMobsIgnorePlayers; + private boolean liquidBasedPregnancy; + private int pregnancyRiskyStartDay; + private int pregnancyOvulationEndDay; + private int pregnancySafeDayChancePercent; + private int pregnancyRiskyChancePercent; + private int pregnancyOvulationChancePercent; + private TextFieldWidget pregnancyChanceField; + private TextFieldWidget pregnancyDurationField; + private TextFieldWidget pregnancyEggHatchMinutesField; + private TextFieldWidget pregnancyRiskyStartDayField; + private TextFieldWidget pregnancyOvulationEndDayField; + private TextFieldWidget pregnancySafeDayChancePercentField; + private TextFieldWidget pregnancyRiskyChancePercentField; + private TextFieldWidget pregnancyOvulationChancePercentField; + private ButtonWidget pregnancyAutoTameMobsButton; + private ButtonWidget pregnancyFriendlyMobsIgnorePlayersButton; + private ButtonWidget liquidBasedPregnancyButton; + private ButtonWidget resetPregnancyChanceButton; + private ButtonWidget resetPregnancyDurationButton; + private ButtonWidget resetPregnancyEggHatchMinutesButton; + private ButtonWidget resetPregnancyAutoTameMobsButton; + private ButtonWidget resetPregnancyFriendlyMobsIgnorePlayersButton; + private ButtonWidget resetLiquidBasedPregnancyButton; + private ButtonWidget resetPregnancyRiskyStartDayButton; + private ButtonWidget resetPregnancyOvulationEndDayButton; + private ButtonWidget resetPregnancySafeDayChancePercentButton; + private ButtonWidget resetPregnancyRiskyChancePercentButton; + private ButtonWidget resetPregnancyOvulationChancePercentButton; + + protected PregnancySettingsScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.pregnancy_title")); + this.parent = parent; + this.config = config; + this.pregnancyChancePercent = config.getPregnancyChancePercent(); + this.pregnancyDurationMinutes = config.getPregnancyDurationMinutes(); + this.pregnancyEggHatchMinutes = this.eggHatchSecondsToMinutes(config.getPregnancyEggDefaultHatchSeconds()); + this.pregnancyAutoTameMobs = config.isPregnancyAutoTameMobs(); + this.pregnancyFriendlyMobsIgnorePlayers = config.pregnancyFriendlyMobsIgnorePlayers(); + this.liquidBasedPregnancy = config.isLiquidBasedPregnancyEnabled(); + this.pregnancyRiskyStartDay = config.getPregnancyRiskyStartDay(); + this.pregnancyOvulationEndDay = config.getPregnancyOvulationEndDay(); + this.pregnancySafeDayChancePercent = config.getPregnancySafeDayChancePercent(); + this.pregnancyRiskyChancePercent = config.getPregnancyRiskyChancePercent(); + this.pregnancyOvulationChancePercent = config.getPregnancyOvulationChancePercent(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + if (this.pregnancyChanceField != null) { + this.pregnancyChancePercent = this.parseField(this.pregnancyChanceField, this.pregnancyChancePercent); + this.pregnancyDurationMinutes = this.parseField(this.pregnancyDurationField, this.pregnancyDurationMinutes); + this.pregnancyEggHatchMinutes = this.parseField(this.pregnancyEggHatchMinutesField, this.pregnancyEggHatchMinutes); + this.pregnancyRiskyStartDay = this.parseField(this.pregnancyRiskyStartDayField, this.pregnancyRiskyStartDay); + this.pregnancyOvulationEndDay = this.parseField(this.pregnancyOvulationEndDayField, this.pregnancyOvulationEndDay); + this.pregnancySafeDayChancePercent = this.parseField(this.pregnancySafeDayChancePercentField, this.pregnancySafeDayChancePercent); + this.pregnancyRiskyChancePercent = this.parseField(this.pregnancyRiskyChancePercentField, this.pregnancyRiskyChancePercent); + this.pregnancyOvulationChancePercent = this.parseField(this.pregnancyOvulationChancePercentField, this.pregnancyOvulationChancePercent); + } + + int listTop = 32; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int fieldWidth = 50; + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.pregnancy_timing"))); + this.pregnancyChanceField = this.newNumberField(fieldWidth, this.pregnancyChancePercent); + this.resetPregnancyChanceButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyChanceField.setText(String.valueOf(this.defaults.getPregnancyChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyChanceButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_chance_percent"), + this.pregnancyChanceField, + this.resetPregnancyChanceButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_chance_percent") + ) + ); + this.pregnancyDurationField = this.newNumberField(fieldWidth, this.pregnancyDurationMinutes); + this.resetPregnancyDurationButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyDurationField.setText(String.valueOf(this.defaults.getPregnancyDurationMinutes())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyDurationButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_duration_minutes"), + this.pregnancyDurationField, + this.resetPregnancyDurationButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_duration_minutes") + ) + ); + this.pregnancyEggHatchMinutesField = this.newNumberField(fieldWidth, this.pregnancyEggHatchMinutes); + this.resetPregnancyEggHatchMinutesButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyEggHatchMinutesField + .setText(String.valueOf(this.eggHatchSecondsToMinutes(this.defaults.getPregnancyEggDefaultHatchSeconds()))) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyEggHatchMinutesButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_egg_hatch_minutes"), + this.pregnancyEggHatchMinutesField, + this.resetPregnancyEggHatchMinutesButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_egg_hatch_minutes") + ) + ); + settingsList.addEntryRow( + SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.liquid_based_pregnancy")) + ); + this.liquidBasedPregnancyButton = ButtonWidget.builder(this.toggleText(this.liquidBasedPregnancy), button -> { + this.liquidBasedPregnancy = !this.liquidBasedPregnancy; + button.setMessage(this.toggleText(this.liquidBasedPregnancy)); + this.updateLiquidPregnancyControlState(); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.liquidBasedPregnancyButton, "config.needsofnature.tooltip.liquid_based_pregnancy"); + this.resetLiquidBasedPregnancyButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.liquidBasedPregnancy = this.defaults.isLiquidBasedPregnancyEnabled(); + this.liquidBasedPregnancyButton.setMessage(this.toggleText(this.liquidBasedPregnancy)); + this.updateLiquidPregnancyControlState(); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetLiquidBasedPregnancyButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.liquid_based_pregnancy"), + this.liquidBasedPregnancyButton, + this.resetLiquidBasedPregnancyButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.liquid_based_pregnancy") + ) + ); + this.pregnancyRiskyStartDayField = this.newNumberField(fieldWidth, this.pregnancyRiskyStartDay); + this.resetPregnancyRiskyStartDayButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyRiskyStartDayField.setText(String.valueOf(this.defaults.getPregnancyRiskyStartDay())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyRiskyStartDayButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_risky_start_day"), + this.pregnancyRiskyStartDayField, + this.resetPregnancyRiskyStartDayButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_risky_start_day") + ) + ); + this.pregnancyOvulationEndDayField = this.newNumberField(fieldWidth, this.pregnancyOvulationEndDay); + this.resetPregnancyOvulationEndDayButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyOvulationEndDayField.setText(String.valueOf(this.defaults.getPregnancyOvulationEndDay())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyOvulationEndDayButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_ovulation_end_day"), + this.pregnancyOvulationEndDayField, + this.resetPregnancyOvulationEndDayButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_ovulation_end_day") + ) + ); + this.pregnancySafeDayChancePercentField = this.newNumberField(fieldWidth, this.pregnancySafeDayChancePercent); + this.resetPregnancySafeDayChancePercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancySafeDayChancePercentField.setText(String.valueOf(this.defaults.getPregnancySafeDayChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancySafeDayChancePercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_safe_day_chance_percent"), + this.pregnancySafeDayChancePercentField, + this.resetPregnancySafeDayChancePercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_safe_day_chance_percent") + ) + ); + this.pregnancyRiskyChancePercentField = this.newNumberField(fieldWidth, this.pregnancyRiskyChancePercent); + this.resetPregnancyRiskyChancePercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyRiskyChancePercentField.setText(String.valueOf(this.defaults.getPregnancyRiskyChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyRiskyChancePercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_risky_chance_percent"), + this.pregnancyRiskyChancePercentField, + this.resetPregnancyRiskyChancePercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_risky_chance_percent") + ) + ); + this.pregnancyOvulationChancePercentField = this.newNumberField(fieldWidth, this.pregnancyOvulationChancePercent); + this.resetPregnancyOvulationChancePercentButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.reset_icon"), + button -> this.pregnancyOvulationChancePercentField.setText(String.valueOf(this.defaults.getPregnancyOvulationChancePercent())) + ) + .dimensions(0, 0, resetW, 20) + .build(); + NonModMenuScreens.setTooltip(this.resetPregnancyOvulationChancePercentButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_ovulation_chance_percent"), + this.pregnancyOvulationChancePercentField, + this.resetPregnancyOvulationChancePercentButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_ovulation_chance_percent") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.pregnancy_behavior"))); + this.pregnancyAutoTameMobsButton = ButtonWidget.builder(this.toggleText(this.pregnancyAutoTameMobs), button -> { + this.pregnancyAutoTameMobs = !this.pregnancyAutoTameMobs; + button.setMessage(this.toggleText(this.pregnancyAutoTameMobs)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetPregnancyAutoTameMobsButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.pregnancyAutoTameMobs = this.defaults.isPregnancyAutoTameMobs(); + this.pregnancyAutoTameMobsButton.setMessage(this.toggleText(this.pregnancyAutoTameMobs)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetPregnancyAutoTameMobsButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_auto_tame_mobs"), + this.pregnancyAutoTameMobsButton, + this.resetPregnancyAutoTameMobsButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_auto_tame_mobs") + ) + ); + this.pregnancyFriendlyMobsIgnorePlayersButton = ButtonWidget.builder(this.toggleText(this.pregnancyFriendlyMobsIgnorePlayers), button -> { + this.pregnancyFriendlyMobsIgnorePlayers = !this.pregnancyFriendlyMobsIgnorePlayers; + button.setMessage(this.toggleText(this.pregnancyFriendlyMobsIgnorePlayers)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetPregnancyFriendlyMobsIgnorePlayersButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.pregnancyFriendlyMobsIgnorePlayers = this.defaults.pregnancyFriendlyMobsIgnorePlayers(); + this.pregnancyFriendlyMobsIgnorePlayersButton.setMessage(this.toggleText(this.pregnancyFriendlyMobsIgnorePlayers)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetPregnancyFriendlyMobsIgnorePlayersButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.pregnancy_friendly_mobs_ignore_players"), + this.pregnancyFriendlyMobsIgnorePlayersButton, + this.resetPregnancyFriendlyMobsIgnorePlayersButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.pregnancy_friendly_mobs_ignore_players") + ) + ); + this.pregnancyChanceField.setEditable(this.serverConfigEditable); + this.pregnancyDurationField.setEditable(this.serverConfigEditable); + this.pregnancyEggHatchMinutesField.setEditable(this.serverConfigEditable); + this.pregnancyAutoTameMobsButton.active = this.serverConfigEditable; + this.pregnancyFriendlyMobsIgnorePlayersButton.active = this.serverConfigEditable; + this.updateLiquidPregnancyControlState(); + this.pregnancyChanceField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancyDurationField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancyEggHatchMinutesField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancyRiskyStartDayField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancyOvulationEndDayField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancySafeDayChancePercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancyRiskyChancePercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.pregnancyOvulationChancePercentField.setChangedListener(ignored -> this.updateResetButtons()); + this.updateResetButtons(); + int centerX = this.width / 2; + ButtonWidget offspringCountButton = ButtonWidget.builder( + Text.translatable("config.needsofnature.offspring_count_button"), + button -> MinecraftClient.getInstance().setScreen(new NonModMenuDebugScreens.OffspringCountConfigScreen(this, this.config)) + ) + .dimensions(centerX - 100, this.height - 52, 200, 20) + .build(); + NonModMenuScreens.setTooltip(offspringCountButton, "config.needsofnature.tooltip.offspring_count_button"); + this.addDrawableChild(offspringCountButton); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip( + doneButton, this.serverConfigEditable ? "config.needsofnature.tooltip.done_save" : "config.needsofnature.tooltip.done_unsaved" + ); + this.addDrawableChild(doneButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.pregnancyChancePercent = this.clampPercent(this.parseField(this.pregnancyChanceField, this.pregnancyChancePercent)); + this.pregnancyDurationMinutes = this.clampPregnancyDurationMinutes(this.parseField(this.pregnancyDurationField, this.pregnancyDurationMinutes)); + this.pregnancyEggHatchMinutes = this.clampPregnancyEggHatchMinutes( + this.parseField(this.pregnancyEggHatchMinutesField, this.pregnancyEggHatchMinutes) + ); + this.pregnancyRiskyStartDay = this.clampPregnancyRiskyStartDay(this.parseField(this.pregnancyRiskyStartDayField, this.pregnancyRiskyStartDay)); + this.pregnancyOvulationEndDay = Math.max( + this.pregnancyRiskyStartDay + 2, this.clampPregnancyCycleDay(this.parseField(this.pregnancyOvulationEndDayField, this.pregnancyOvulationEndDay)) + ); + this.pregnancySafeDayChancePercent = this.clampPregnancyCycleChancePercent( + this.parseField(this.pregnancySafeDayChancePercentField, this.pregnancySafeDayChancePercent) + ); + this.pregnancyRiskyChancePercent = this.clampPregnancyCycleChancePercent( + this.parseField(this.pregnancyRiskyChancePercentField, this.pregnancyRiskyChancePercent) + ); + this.pregnancyOvulationChancePercent = this.clampPregnancyCycleChancePercent( + this.parseField(this.pregnancyOvulationChancePercentField, this.pregnancyOvulationChancePercent) + ); + this.config.setPregnancyChancePercent(this.pregnancyChancePercent); + this.config.setPregnancyDurationMinutes(this.pregnancyDurationMinutes); + this.config.setPregnancyEggDefaultHatchSeconds(this.eggHatchMinutesToSeconds(this.pregnancyEggHatchMinutes)); + this.config.setPregnancyAutoTameMobs(this.pregnancyAutoTameMobs); + this.config.setPregnancyFriendlyMobsIgnorePlayers(this.pregnancyFriendlyMobsIgnorePlayers); + this.config.setLiquidBasedPregnancyEnabled(this.liquidBasedPregnancy); + this.config.setPregnancyRiskyStartDay(this.pregnancyRiskyStartDay); + this.config.setPregnancyOvulationEndDay(this.pregnancyOvulationEndDay); + this.config.setPregnancySafeDayChancePercent(this.pregnancySafeDayChancePercent); + this.config.setPregnancyRiskyChancePercent(this.pregnancyRiskyChancePercent); + this.config.setPregnancyOvulationChancePercent(this.pregnancyOvulationChancePercent); + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private TextFieldWidget newNumberField(int w, int initial) { + TextFieldWidget field = new TextFieldWidget(this.textRenderer, 0, 0, w, 20, Text.literal("")); + field.setText(String.valueOf(initial)); + field.setMaxLength(8); + field.setEditable(true); + return field; + } + + private int parseField(TextFieldWidget field, int fallback) { + try { + return Integer.parseInt(field.getText().trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + private int clampPercent(int v) { + return Math.max(0, Math.min(100, v)); + } + + private int clampPregnancyDurationMinutes(int v) { + return Math.max(1, Math.min(5256000, v)); + } + + private int clampPregnancyEggHatchMinutes(int v) { + return Math.max(1, Math.min(1200, v)); + } + + private int clampPregnancyCycleDay(int value) { + return Math.max(1, Math.min(30, value)); + } + + private int clampPregnancyRiskyStartDay(int value) { + return Math.max(1, Math.min(28, value)); + } + + private int clampPregnancyCycleChancePercent(int value) { + return Math.max(0, Math.min(1000, value)); + } + + private int eggHatchSecondsToMinutes(int seconds) { + return Math.max(1, (int)Math.ceil(seconds / 60.0)); + } + + private int eggHatchMinutesToSeconds(int minutes) { + int clamped = this.clampPregnancyEggHatchMinutes(minutes); + return Math.max(1, Math.min(72000, clamped * 60)); + } + + private boolean isIntFieldChanged(TextFieldWidget field, int defaultValue) { + return field == null ? false : this.parseField(field, defaultValue) != defaultValue; + } + + private Text toggleText(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + + private void updateLiquidPregnancyControlState() { + boolean systemsEnabled = this.config.isPregnancyEnabled() && this.config.isLiquidTankEnabled(); + boolean detailsEditable = this.serverConfigEditable && systemsEnabled && this.liquidBasedPregnancy; + if (this.liquidBasedPregnancyButton != null) { + this.liquidBasedPregnancyButton.active = this.serverConfigEditable && systemsEnabled; + } + + if (this.pregnancyRiskyStartDayField != null) { + this.pregnancyRiskyStartDayField.setEditable(detailsEditable); + } + + if (this.pregnancyOvulationEndDayField != null) { + this.pregnancyOvulationEndDayField.setEditable(detailsEditable); + } + + if (this.pregnancySafeDayChancePercentField != null) { + this.pregnancySafeDayChancePercentField.setEditable(detailsEditable); + } + + if (this.pregnancyRiskyChancePercentField != null) { + this.pregnancyRiskyChancePercentField.setEditable(detailsEditable); + } + + if (this.pregnancyOvulationChancePercentField != null) { + this.pregnancyOvulationChancePercentField.setEditable(detailsEditable); + } + } + + private void updateResetButtons() { + if (this.resetPregnancyChanceButton != null) { + this.resetPregnancyChanceButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pregnancyChanceField, this.defaults.getPregnancyChancePercent()); + } + + if (this.resetPregnancyDurationButton != null) { + this.resetPregnancyDurationButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pregnancyDurationField, this.defaults.getPregnancyDurationMinutes()); + } + + if (this.resetPregnancyEggHatchMinutesButton != null) { + this.resetPregnancyEggHatchMinutesButton.active = this.serverConfigEditable + && this.isIntFieldChanged(this.pregnancyEggHatchMinutesField, this.eggHatchSecondsToMinutes(this.defaults.getPregnancyEggDefaultHatchSeconds())); + } + + if (this.resetPregnancyAutoTameMobsButton != null) { + this.resetPregnancyAutoTameMobsButton.active = this.serverConfigEditable && this.pregnancyAutoTameMobs != this.defaults.isPregnancyAutoTameMobs(); + } + + if (this.resetPregnancyFriendlyMobsIgnorePlayersButton != null) { + this.resetPregnancyFriendlyMobsIgnorePlayersButton.active = this.serverConfigEditable + && this.pregnancyFriendlyMobsIgnorePlayers != this.defaults.pregnancyFriendlyMobsIgnorePlayers(); + } + + if (this.resetLiquidBasedPregnancyButton != null) { + this.resetLiquidBasedPregnancyButton.active = this.serverConfigEditable + && this.liquidBasedPregnancy != this.defaults.isLiquidBasedPregnancyEnabled(); + } + + boolean liquidPregnancyDetailsEditable = this.serverConfigEditable + && this.config.isPregnancyEnabled() + && this.config.isLiquidTankEnabled() + && this.liquidBasedPregnancy; + if (this.resetPregnancyRiskyStartDayButton != null) { + this.resetPregnancyRiskyStartDayButton.active = liquidPregnancyDetailsEditable + && this.isIntFieldChanged(this.pregnancyRiskyStartDayField, this.defaults.getPregnancyRiskyStartDay()); + } + + if (this.resetPregnancyOvulationEndDayButton != null) { + this.resetPregnancyOvulationEndDayButton.active = liquidPregnancyDetailsEditable + && this.isIntFieldChanged(this.pregnancyOvulationEndDayField, this.defaults.getPregnancyOvulationEndDay()); + } + + if (this.resetPregnancySafeDayChancePercentButton != null) { + this.resetPregnancySafeDayChancePercentButton.active = liquidPregnancyDetailsEditable + && this.isIntFieldChanged(this.pregnancySafeDayChancePercentField, this.defaults.getPregnancySafeDayChancePercent()); + } + + if (this.resetPregnancyRiskyChancePercentButton != null) { + this.resetPregnancyRiskyChancePercentButton.active = liquidPregnancyDetailsEditable + && this.isIntFieldChanged(this.pregnancyRiskyChancePercentField, this.defaults.getPregnancyRiskyChancePercent()); + } + + if (this.resetPregnancyOvulationChancePercentButton != null) { + this.resetPregnancyOvulationChancePercentButton.active = liquidPregnancyDetailsEditable + && this.isIntFieldChanged(this.pregnancyOvulationChancePercentField, this.defaults.getPregnancyOvulationChancePercent()); + } + } + } + + static class ToggleSettingsScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean pregnancyEnabled; + private boolean liquidTankEnabled; + private boolean messSystemEnabled; + private boolean destroyedSkinSystemEnabled; + private boolean vanillaOverridesEnabled; + private ButtonWidget pregnancyEnabledButton; + private ButtonWidget resetPregnancyEnabledButton; + private ButtonWidget liquidTankEnabledButton; + private ButtonWidget resetLiquidTankEnabledButton; + private ButtonWidget messSystemEnabledButton; + private ButtonWidget resetMessSystemEnabledButton; + private ButtonWidget destroyedSkinSystemEnabledButton; + private ButtonWidget resetDestroyedSkinSystemEnabledButton; + private ButtonWidget vanillaOverridesEnabledButton; + private ButtonWidget resetVanillaOverridesEnabledButton; + private boolean serverConfigEditable; + + protected ToggleSettingsScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.toggles_title")); + this.parent = parent; + this.config = config; + this.pregnancyEnabled = config.isPregnancyEnabled(); + this.liquidTankEnabled = config.isLiquidTankEnabled(); + this.messSystemEnabled = config.isMessSystemEnabled(); + this.destroyedSkinSystemEnabled = config.isDestroyedSkinSystemEnabled(); + this.vanillaOverridesEnabled = config.isVanillaOverridesEnabled(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + int centerX = this.width / 2; + int listTop = 32; + int bottomArea = 64; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.core_systems"))); + this.pregnancyEnabledButton = ButtonWidget.builder(this.toggleWithValue(this.pregnancyEnabled), button -> { + this.pregnancyEnabled = !this.pregnancyEnabled; + button.setMessage(this.toggleWithValue(this.pregnancyEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.pregnancyEnabledButton, "config.needsofnature.tooltip.toggle.pregnancy"); + this.resetPregnancyEnabledButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.pregnancyEnabled = this.defaults.isPregnancyEnabled(); + this.pregnancyEnabledButton.setMessage(this.toggleWithValue(this.pregnancyEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetPregnancyEnabledButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.toggle.pregnancy"), + this.pregnancyEnabledButton, + this.resetPregnancyEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.toggle.pregnancy") + ) + ); + this.liquidTankEnabledButton = ButtonWidget.builder(this.toggleWithValue(this.liquidTankEnabled), button -> { + this.liquidTankEnabled = !this.liquidTankEnabled; + button.setMessage(this.toggleWithValue(this.liquidTankEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.liquidTankEnabledButton, "config.needsofnature.tooltip.toggle.liquid_tank"); + this.resetLiquidTankEnabledButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.liquidTankEnabled = this.defaults.isLiquidTankEnabled(); + this.liquidTankEnabledButton.setMessage(this.toggleWithValue(this.liquidTankEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetLiquidTankEnabledButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.toggle.liquid_tank"), + this.liquidTankEnabledButton, + this.resetLiquidTankEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.toggle.liquid_tank") + ) + ); + this.messSystemEnabledButton = ButtonWidget.builder(this.toggleWithValue(this.messSystemEnabled), button -> { + this.messSystemEnabled = !this.messSystemEnabled; + button.setMessage(this.toggleWithValue(this.messSystemEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.messSystemEnabledButton, "config.needsofnature.tooltip.toggle.mess_system"); + this.resetMessSystemEnabledButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.messSystemEnabled = this.defaults.isMessSystemEnabled(); + this.messSystemEnabledButton.setMessage(this.toggleWithValue(this.messSystemEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetMessSystemEnabledButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.toggle.mess_system"), + this.messSystemEnabledButton, + this.resetMessSystemEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.toggle.mess_system") + ) + ); + this.destroyedSkinSystemEnabledButton = ButtonWidget.builder(this.toggleWithValue(this.destroyedSkinSystemEnabled), button -> { + this.destroyedSkinSystemEnabled = !this.destroyedSkinSystemEnabled; + button.setMessage(this.toggleWithValue(this.destroyedSkinSystemEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.destroyedSkinSystemEnabledButton, "config.needsofnature.tooltip.toggle.destroyed_skin_system"); + this.resetDestroyedSkinSystemEnabledButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.destroyedSkinSystemEnabled = this.defaults.isDestroyedSkinSystemEnabled(); + this.destroyedSkinSystemEnabledButton.setMessage(this.toggleWithValue(this.destroyedSkinSystemEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetDestroyedSkinSystemEnabledButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.toggle.destroyed_skin_system"), + this.destroyedSkinSystemEnabledButton, + this.resetDestroyedSkinSystemEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.toggle.destroyed_skin_system") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.vanilla_integration"))); + this.vanillaOverridesEnabledButton = ButtonWidget.builder(this.toggleWithValue(this.vanillaOverridesEnabled), button -> { + this.vanillaOverridesEnabled = !this.vanillaOverridesEnabled; + button.setMessage(this.toggleWithValue(this.vanillaOverridesEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.vanillaOverridesEnabledButton, "config.needsofnature.tooltip.toggle.vanilla_overrides"); + this.resetVanillaOverridesEnabledButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.vanillaOverridesEnabled = this.defaults.isVanillaOverridesEnabled(); + this.vanillaOverridesEnabledButton.setMessage(this.toggleWithValue(this.vanillaOverridesEnabled)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetVanillaOverridesEnabledButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.toggle.vanilla_overrides"), + this.vanillaOverridesEnabledButton, + this.resetVanillaOverridesEnabledButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.toggle.vanilla_overrides") + ) + ); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip( + doneButton, this.serverConfigEditable ? "config.needsofnature.tooltip.done_save" : "config.needsofnature.tooltip.done_unsaved" + ); + this.addDrawableChild(doneButton); + if (this.pregnancyEnabledButton != null) { + this.pregnancyEnabledButton.active = this.serverConfigEditable; + } + + if (this.liquidTankEnabledButton != null) { + this.liquidTankEnabledButton.active = this.serverConfigEditable; + } + + if (this.messSystemEnabledButton != null) { + this.messSystemEnabledButton.active = this.serverConfigEditable; + } + + if (this.destroyedSkinSystemEnabledButton != null) { + this.destroyedSkinSystemEnabledButton.active = this.serverConfigEditable; + } + + if (this.vanillaOverridesEnabledButton != null) { + this.vanillaOverridesEnabledButton.active = this.serverConfigEditable; + } + + this.updateResetButtons(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.config.setPregnancyEnabled(this.pregnancyEnabled); + this.config.setLiquidTankEnabled(this.liquidTankEnabled); + this.config.setMessSystemEnabled(this.messSystemEnabled); + this.config.setDestroyedSkinSystemEnabled(this.destroyedSkinSystemEnabled); + this.config.setVanillaOverridesEnabled(this.vanillaOverridesEnabled); + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void updateResetButtons() { + if (this.resetPregnancyEnabledButton != null) { + this.resetPregnancyEnabledButton.active = this.serverConfigEditable && this.pregnancyEnabled != this.defaults.isPregnancyEnabled(); + } + + if (this.resetLiquidTankEnabledButton != null) { + this.resetLiquidTankEnabledButton.active = this.serverConfigEditable && this.liquidTankEnabled != this.defaults.isLiquidTankEnabled(); + } + + if (this.resetMessSystemEnabledButton != null) { + this.resetMessSystemEnabledButton.active = this.serverConfigEditable && this.messSystemEnabled != this.defaults.isMessSystemEnabled(); + } + + if (this.resetDestroyedSkinSystemEnabledButton != null) { + this.resetDestroyedSkinSystemEnabledButton.active = this.serverConfigEditable + && this.destroyedSkinSystemEnabled != this.defaults.isDestroyedSkinSystemEnabled(); + } + + if (this.resetVanillaOverridesEnabledButton != null) { + this.resetVanillaOverridesEnabledButton.active = this.serverConfigEditable + && this.vanillaOverridesEnabled != this.defaults.isVanillaOverridesEnabled(); + } + + if (this.pregnancyEnabledButton != null) { + this.pregnancyEnabledButton.active = this.serverConfigEditable; + } + + if (this.liquidTankEnabledButton != null) { + this.liquidTankEnabledButton.active = this.serverConfigEditable; + } + + if (this.messSystemEnabledButton != null) { + this.messSystemEnabledButton.active = this.serverConfigEditable; + } + + if (this.destroyedSkinSystemEnabledButton != null) { + this.destroyedSkinSystemEnabledButton.active = this.serverConfigEditable; + } + + if (this.vanillaOverridesEnabledButton != null) { + this.vanillaOverridesEnabledButton.active = this.serverConfigEditable; + } + } + + private Text toggleWithValue(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + } + + static class VanillaOverrideScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final NonConfig defaults = new NonConfig(); + private boolean useAnimationBreeding; + private boolean requireMaleFemaleForBreeding; + private boolean femaleCowOnlyMilking; + private ButtonWidget useAnimationBreedingButton; + private ButtonWidget resetUseAnimationBreedingButton; + private ButtonWidget requireMaleFemaleForBreedingButton; + private ButtonWidget resetRequireMaleFemaleForBreedingButton; + private ButtonWidget femaleCowOnlyMilkingButton; + private ButtonWidget resetFemaleCowOnlyMilkingButton; + private boolean serverConfigEditable; + + protected VanillaOverrideScreen(Screen parent, NonConfig config) { + super(Text.translatable("config.needsofnature.vanilla_override_title")); + this.parent = parent; + this.config = config; + this.useAnimationBreeding = config.useAnimationBreeding(); + this.requireMaleFemaleForBreeding = config.requireMaleFemaleForBreeding(); + this.femaleCowOnlyMilking = config.femaleCowOnlyMilking(); + } + + protected void init() { + this.serverConfigEditable = NonModMenuScreens.canEditServerGameplaySettings(); + int centerX = this.width / 2; + int listTop = 32; + int bottomArea = 88; + int listHeight = Math.max(0, this.height - listTop - bottomArea); + SettingsList settingsList = new SettingsList(this.client, this.width, listHeight, listTop); + this.addDrawableChild(settingsList); + int resetW = 20; + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.breeding"))); + this.useAnimationBreedingButton = ButtonWidget.builder(this.toggleWithValue(this.useAnimationBreeding), button -> { + this.useAnimationBreeding = !this.useAnimationBreeding; + button.setMessage(this.toggleWithValue(this.useAnimationBreeding)); + this.updateDependentTooltips(); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.useAnimationBreedingButton, "config.needsofnature.tooltip.use_animation_breeding"); + this.resetUseAnimationBreedingButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.useAnimationBreeding = this.defaults.useAnimationBreeding(); + this.useAnimationBreedingButton.setMessage(this.toggleWithValue(this.useAnimationBreeding)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetUseAnimationBreedingButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.use_animation_breeding"), + this.useAnimationBreedingButton, + this.resetUseAnimationBreedingButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.use_animation_breeding") + ) + ); + this.requireMaleFemaleForBreedingButton = ButtonWidget.builder(this.toggleWithValue(this.requireMaleFemaleForBreeding), button -> { + this.requireMaleFemaleForBreeding = !this.requireMaleFemaleForBreeding; + button.setMessage(this.toggleWithValue(this.requireMaleFemaleForBreeding)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + this.resetRequireMaleFemaleForBreedingButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.requireMaleFemaleForBreeding = this.defaults.requireMaleFemaleForBreeding(); + this.requireMaleFemaleForBreedingButton.setMessage(this.toggleWithValue(this.requireMaleFemaleForBreeding)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetRequireMaleFemaleForBreedingButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.require_male_female_breeding"), + this.requireMaleFemaleForBreedingButton, + this.resetRequireMaleFemaleForBreedingButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.require_male_female_breeding") + ) + ); + settingsList.addEntryRow(SettingsList.RowEntry.sectionHeader(this.textRenderer, Text.translatable("config.needsofnature.section.milking"))); + this.femaleCowOnlyMilkingButton = ButtonWidget.builder(this.toggleWithValue(this.femaleCowOnlyMilking), button -> { + this.femaleCowOnlyMilking = !this.femaleCowOnlyMilking; + button.setMessage(this.toggleWithValue(this.femaleCowOnlyMilking)); + this.updateResetButtons(); + }).dimensions(0, 0, 100, 20).build(); + NonModMenuScreens.setTooltip(this.femaleCowOnlyMilkingButton, "config.needsofnature.tooltip.female_cow_only_milking"); + this.resetFemaleCowOnlyMilkingButton = ButtonWidget.builder(Text.translatable("config.needsofnature.reset_icon"), button -> { + this.femaleCowOnlyMilking = this.defaults.femaleCowOnlyMilking(); + this.femaleCowOnlyMilkingButton.setMessage(this.toggleWithValue(this.femaleCowOnlyMilking)); + this.updateResetButtons(); + }).dimensions(0, 0, resetW, 20).build(); + NonModMenuScreens.setTooltip(this.resetFemaleCowOnlyMilkingButton, "config.needsofnature.tooltip.reset"); + settingsList.addEntryRow( + SettingsList.RowEntry.labeledField( + this.textRenderer, + Text.translatable("config.needsofnature.female_cow_only_milking"), + this.femaleCowOnlyMilkingButton, + this.resetFemaleCowOnlyMilkingButton, + NonModMenuScreens.tooltip("config.needsofnature.tooltip.female_cow_only_milking") + ) + ); + ButtonWidget doneButton = ButtonWidget.builder(Text.translatable("config.needsofnature.done"), button -> this.saveAndClose()) + .dimensions(centerX - 100, this.height - 28, 200, 20) + .build(); + NonModMenuScreens.setTooltip( + doneButton, this.serverConfigEditable ? "config.needsofnature.tooltip.done_save" : "config.needsofnature.tooltip.done_unsaved" + ); + this.addDrawableChild(doneButton); + if (this.useAnimationBreedingButton != null) { + this.useAnimationBreedingButton.active = this.serverConfigEditable; + } + + if (this.requireMaleFemaleForBreedingButton != null) { + this.requireMaleFemaleForBreedingButton.active = this.serverConfigEditable && this.useAnimationBreeding; + } + + if (this.femaleCowOnlyMilkingButton != null) { + this.femaleCowOnlyMilkingButton.active = this.serverConfigEditable; + } + + this.updateDependentTooltips(); + this.updateResetButtons(); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + this.updateResetButtons(); + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 20, 16777215); + } + + public void close() { + this.saveAndClose(); + } + + private void saveAndClose() { + if (this.serverConfigEditable) { + this.config.setUseAnimationBreeding(this.useAnimationBreeding); + this.config.setRequireMaleFemaleForBreeding(this.requireMaleFemaleForBreeding); + this.config.setFemaleCowOnlyMilking(this.femaleCowOnlyMilking); + NonModMenuScreens.saveServerConfig(this.config); + } + + MinecraftClient.getInstance().setScreen(this.parent); + } + + private void updateResetButtons() { + if (this.resetUseAnimationBreedingButton != null) { + this.resetUseAnimationBreedingButton.active = this.serverConfigEditable && this.useAnimationBreeding != this.defaults.useAnimationBreeding(); + } + + if (this.resetRequireMaleFemaleForBreedingButton != null) { + this.resetRequireMaleFemaleForBreedingButton.active = this.serverConfigEditable + && this.useAnimationBreeding + && this.requireMaleFemaleForBreeding != this.defaults.requireMaleFemaleForBreeding(); + } + + if (this.resetFemaleCowOnlyMilkingButton != null) { + this.resetFemaleCowOnlyMilkingButton.active = this.serverConfigEditable && this.femaleCowOnlyMilking != this.defaults.femaleCowOnlyMilking(); + } + + if (this.useAnimationBreedingButton != null) { + this.useAnimationBreedingButton.active = this.serverConfigEditable; + } + + if (this.requireMaleFemaleForBreedingButton != null) { + this.requireMaleFemaleForBreedingButton.active = this.serverConfigEditable && this.useAnimationBreeding; + } + + if (this.femaleCowOnlyMilkingButton != null) { + this.femaleCowOnlyMilkingButton.active = this.serverConfigEditable; + } + + this.updateDependentTooltips(); + } + + private void updateDependentTooltips() { + if (this.requireMaleFemaleForBreedingButton != null) { + NonModMenuScreens.setTooltip( + this.requireMaleFemaleForBreedingButton, + this.useAnimationBreeding + ? "config.needsofnature.tooltip.require_male_female_breeding" + : "config.needsofnature.tooltip.require_male_female_breeding.disabled_animation_breeding" + ); + } + } + + private Text toggleWithValue(boolean enabled) { + return Text.translatable(enabled ? "options.on" : "options.off"); + } + } +} diff --git a/src/main/java/com/nonid/integration/NonPlayerProfilesScreen.java b/src/main/java/com/nonid/integration/NonPlayerProfilesScreen.java new file mode 100644 index 0000000..f9f466a --- /dev/null +++ b/src/main/java/com/nonid/integration/NonPlayerProfilesScreen.java @@ -0,0 +1,367 @@ +package com.nonid.integration; + +import com.nonid.NonConfig; +import com.nonid.client.NonPlayerInteractionClient; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.gui.widget.ElementListWidget.Entry; +import net.minecraft.text.Text; + +final class NonPlayerProfilesScreen extends Screen { + private static final String[] MODES = new String[]{"deny_all", "requests_only", "requests_and_attacks", "auto_accept"}; + private final Screen parent; + private ButtonWidget defaultModeButton; + private ButtonWidget typedModeButton; + private TextFieldWidget playerNameField; + private NonPlayerProfilesScreen.ProfileList profileList; + private String typedMode = "requests_and_attacks"; + private int renderedRevision = -1; + + NonPlayerProfilesScreen(Screen parent) { + super(Text.translatable("gui.needsofnature.player_profiles.title")); + this.parent = parent; + } + + protected void init() { + int centerX = this.width / 2; + int top = 34; + this.defaultModeButton = ButtonWidget.builder(Text.literal(""), ignored -> { + NonPlayerInteractionClient.ProfileSnapshot snapshot = NonPlayerInteractionClient.profileSnapshot(); + String next = nextAllowedMode(snapshot.default_mode(), snapshot.allowed_mask()); + NonPlayerInteractionClient.setDefaultProfile(next); + }).dimensions(centerX + 20, top, 180, 20).build(); + this.addDrawableChild(this.defaultModeButton); + this.playerNameField = new TextFieldWidget( + this.textRenderer, centerX - 220, top + 30, 185, 20, Text.translatable("gui.needsofnature.player_profiles.player_name") + ); + this.playerNameField.setMaxLength(64); + this.playerNameField.setPlaceholder(Text.translatable("gui.needsofnature.player_profiles.player_name_hint")); + this.addDrawableChild(this.playerNameField); + this.typedModeButton = ButtonWidget.builder(modeText(this.typedMode), ignored -> { + this.typedMode = nextAllowedMode(this.typedMode, NonPlayerInteractionClient.profileSnapshot().allowed_mask()); + this.typedModeButton.setMessage(modeText(this.typedMode)); + }).dimensions(centerX - 29, top + 30, 174, 20).build(); + this.addDrawableChild(this.typedModeButton); + this.addDrawableChild(ButtonWidget.builder(Text.translatable("gui.needsofnature.player_profiles.apply"), ignored -> { + String name = this.playerNameField.getText().trim(); + if (!name.isEmpty()) { + NonPlayerInteractionClient.setPlayerProfile(name, this.typedMode); + } + }).dimensions(centerX + 151, top + 30, 69, 20).build()); + int listTop = top + 60; + int bottom = 38; + this.profileList = new NonPlayerProfilesScreen.ProfileList( + this.client, + this.width, + Math.max(40, this.height - listTop - bottom), + listTop, + this.textRenderer, + this::setProfile, + NonPlayerInteractionClient::removePlayerProfile + ); + this.addDrawableChild(this.profileList); + if (NonModMenuScreens.canEditServerGameplaySettings()) { + this.addDrawableChild( + ButtonWidget.builder( + Text.translatable("gui.needsofnature.player_profiles.allowed_profiles"), + ignored -> MinecraftClient.getInstance() + .setScreen(new NonPlayerProfilesScreen.AllowedProfilesScreen(this, NonModMenuScreens.serverConfigForScreen())) + ) + .dimensions(centerX - 220, this.height - 28, 135, 20) + .build() + ); + } + + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.done"), ignored -> this.close()).dimensions(centerX - 75, this.height - 28, 150, 20).build() + ); + NonPlayerInteractionClient.requestProfiles(); + this.refreshFromSnapshot(true); + } + + public void tick() { + super.tick(); + this.refreshFromSnapshot(false); + } + + private void refreshFromSnapshot(boolean force) { + int revision = NonPlayerInteractionClient.profileRevision(); + if (force || revision != this.renderedRevision) { + this.renderedRevision = revision; + NonPlayerInteractionClient.ProfileSnapshot snapshot = NonPlayerInteractionClient.profileSnapshot(); + if (this.defaultModeButton != null) { + this.defaultModeButton.setMessage(modeText(snapshot.default_mode())); + } + + if (!isAllowed(this.typedMode, snapshot.allowed_mask())) { + this.typedMode = firstAllowedMode(snapshot.allowed_mask()); + if (this.typedModeButton != null) { + this.typedModeButton.setMessage(modeText(this.typedMode)); + } + } + + if (this.profileList != null) { + this.profileList.setSnapshot(snapshot); + } + } + } + + private void setProfile(NonPlayerProfilesScreen.ProfileRow row) { + String next = nextAllowedMode(row.mode(), NonPlayerInteractionClient.profileSnapshot().allowed_mask()); + NonPlayerInteractionClient.setPlayerProfile(row.uuid().toString(), next); + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 14, -1); + context.drawTextWithShadow(this.textRenderer, Text.translatable("gui.needsofnature.player_profiles.default"), this.width / 2 - 220, 40, -3092272); + } + + public void close() { + MinecraftClient.getInstance().setScreen(this.parent); + } + + private static Text modeText(String mode) { + return Text.translatable("gui.needsofnature.player_profiles.mode." + normalizeMode(mode)); + } + + private static String normalizeMode(String mode) { + for (String candidate : MODES) { + if (candidate.equals(mode)) { + return candidate; + } + } + + return "requests_and_attacks"; + } + + private static String nextAllowedMode(String current, int allowedMask) { + String normalized = normalizeMode(current); + int currentIndex = 0; + + for (int index = 0; index < MODES.length; index++) { + if (MODES[index].equals(normalized)) { + currentIndex = index; + } + } + + for (int offset = 1; offset <= MODES.length; offset++) { + String candidate = MODES[(currentIndex + offset) % MODES.length]; + if (isAllowed(candidate, allowedMask)) { + return candidate; + } + } + + return "requests_and_attacks"; + } + + private static String firstAllowedMode(int allowedMask) { + for (String mode : MODES) { + if (isAllowed(mode, allowedMask)) { + return mode; + } + } + + return "requests_and_attacks"; + } + + private static boolean isAllowed(String mode, int allowedMask) { + int index = List.of(MODES).indexOf(normalizeMode(mode)); + return index >= 0 && (allowedMask & 1 << index) != 0; + } + + private static final class AllowedProfilesScreen extends Screen { + private final Screen parent; + private final NonConfig config; + private final List modeButtons = new ArrayList<>(); + private int mask; + + private AllowedProfilesScreen(Screen parent, NonConfig config) { + super(Text.translatable("gui.needsofnature.player_profiles.allowed_profiles")); + this.parent = parent; + this.config = config; + this.mask = config.getAllowedPlayerInteractionProfilesMask(); + } + + protected void init() { + this.modeButtons.clear(); + int width = Math.min(340, this.width - 40); + int startY = Math.max(42, this.height / 2 - 62); + + for (int index = 0; index < NonPlayerProfilesScreen.MODES.length; index++) { + int bit = 1 << index; + String mode = NonPlayerProfilesScreen.MODES[index]; + ButtonWidget button = ButtonWidget.builder(Text.literal(""), ignored -> this.toggle(bit)) + .dimensions(this.width / 2 - width / 2, startY + index * 24, width, 20) + .build(); + this.modeButtons.add(button); + this.addDrawableChild(button); + } + + this.addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.done"), ignored -> this.close()).dimensions(this.width / 2 - 75, this.height - 28, 150, 20).build() + ); + this.updateButtons(); + } + + private void toggle(int bit) { + int next = this.mask ^ bit; + if ((next & 15) != 0) { + this.mask = next & 15; + this.updateButtons(); + } + } + + private void updateButtons() { + for (int index = 0; index < this.modeButtons.size(); index++) { + boolean enabled = (this.mask & 1 << index) != 0; + this.modeButtons + .get(index) + .setMessage( + Text.translatable( + enabled ? "gui.needsofnature.player_profiles.allowed" : "gui.needsofnature.player_profiles.disallowed", + new Object[]{NonPlayerProfilesScreen.modeText(NonPlayerProfilesScreen.MODES[index])} + ) + ); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, -1); + context.drawCenteredTextWithShadow( + this.textRenderer, Text.translatable("gui.needsofnature.player_profiles.allowed_profiles_hint"), this.width / 2, 29, -5592406 + ); + } + + public void close() { + this.config.setAllowedPlayerInteractionProfilesMask(this.mask); + NonModMenuScreens.saveServerConfig(this.config); + MinecraftClient.getInstance().setScreen(this.parent); + } + } + + private static final class ProfileList extends ElementListWidget { + private final TextRenderer textRenderer; + private final Consumer onCycle; + private final Consumer onRemove; + + private ProfileList( + MinecraftClient client, + int width, + int height, + int top, + TextRenderer textRenderer, + Consumer onCycle, + Consumer onRemove + ) { + super(client, width, height, top, 34); + this.textRenderer = textRenderer; + this.onCycle = onCycle; + this.onRemove = onRemove; + this.centerListVertically = false; + } + + private void setSnapshot(NonPlayerInteractionClient.ProfileSnapshot snapshot) { + this.clearEntries(); + Map overrides = new HashMap<>(); + + for (NonPlayerInteractionClient.ProfileEntry override : snapshot.overrides()) { + overrides.put(override.uuid(), override); + } + + for (NonPlayerInteractionClient.KnownPlayerEntry known : snapshot.known_players()) { + try { + UUID uuid = UUID.fromString(known.uuid()); + NonPlayerInteractionClient.ProfileEntry override = overrides.get(known.uuid()); + String mode = override == null ? snapshot.default_mode() : override.mode(); + this.addEntry( + new NonPlayerProfilesScreen.ProfileList.RowEntry( + new NonPlayerProfilesScreen.ProfileRow(uuid, known.name(), mode, override != null), this.textRenderer, this.onCycle, this.onRemove + ) + ); + } catch (IllegalArgumentException var8) { + } + } + } + + public int getRowWidth() { + return Math.min(440, Math.max(260, this.getWidth() - 30)); + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + protected int getScrollbarX() { + return this.getWidth() - 8; + } + + private static final class RowEntry extends Entry { + private final NonPlayerProfilesScreen.ProfileRow row; + private final TextRenderer textRenderer; + private final ButtonWidget modeButton; + private final ButtonWidget removeButton; + private final List widgets; + + private RowEntry( + NonPlayerProfilesScreen.ProfileRow row, TextRenderer textRenderer, Consumer onCycle, Consumer onRemove + ) { + this.row = row; + this.textRenderer = textRenderer; + this.modeButton = ButtonWidget.builder(NonPlayerProfilesScreen.modeText(row.mode()), ignored -> onCycle.accept(row)) + .dimensions(0, 0, 170, 20) + .build(); + this.removeButton = ButtonWidget.builder(Text.literal("x"), ignored -> onRemove.accept(row.uuid().toString())).dimensions(0, 0, 20, 20).build(); + this.removeButton.active = row.overridden(); + this.widgets = List.of(this.modeButton, this.removeButton); + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int x = this.getContentX() + 3; + int y = this.getContentY() + 4; + NonPlayerInteractionClient.drawPlayerFace(context, this.row.uuid(), x, y, 24); + int right = this.getContentX() + this.getContentWidth() - 3; + this.removeButton.setX(right - 20); + this.removeButton.setY(y + 2); + this.modeButton.setX(right - 196); + this.modeButton.setY(y + 2); + this.modeButton.render(context, mouseX, mouseY, deltaTicks); + this.removeButton.render(context, mouseX, mouseY, deltaTicks); + int textWidth = Math.max(30, this.modeButton.getX() - (x + 31) - 6); + context.drawTextWithShadow( + this.textRenderer, this.textRenderer.trimToWidth(this.row.name(), textWidth), x + 31, y + 8, this.row.overridden() ? -1 : -5592406 + ); + } + + public List selectableChildren() { + return this.widgets; + } + + public List children() { + return this.widgets; + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + } + } + + private record ProfileRow(UUID uuid, String name, String mode, boolean overridden) { + } +} diff --git a/src/main/java/com/nonid/integration/NonWildfireGenderConfigCompat.java b/src/main/java/com/nonid/integration/NonWildfireGenderConfigCompat.java new file mode 100644 index 0000000..db9f41f --- /dev/null +++ b/src/main/java/com/nonid/integration/NonWildfireGenderConfigCompat.java @@ -0,0 +1,52 @@ +package com.nonid.integration; + +import com.nonid.NeedsOfNature; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import net.fabricmc.loader.api.FabricLoader; + +public final class NonWildfireGenderConfigCompat { + private static final String MOD_ID = "wildfire_gender"; + private static final float NATIVE_BREAST_SIZE_MAX = 0.8F; + private static boolean initialized; + private static float supportedBreastSizeMax = 0.8F; + + private NonWildfireGenderConfigCompat() { + } + + public static synchronized void initialize() { + if (!initialized) { + initialized = true; + if (FabricLoader.getInstance().isModLoaded("wildfire_gender")) { + try { + Class configurationClass = Class.forName("com.wildfire.main.config.Configuration"); + Object breastSizeKey = configurationClass.getField("BUST_SIZE").get(null); + Method getMaxInclusive = breastSizeKey.getClass().getMethod("getMaxInclusive"); + float currentMax = ((Number)getMaxInclusive.invoke(breastSizeKey)).floatValue(); + float targetMax = Math.max(currentMax, 1.6F); + if (currentMax < targetMax) { + Class numberConfigKeyClass = Class.forName("com.wildfire.main.config.types.NumberConfigKey"); + Field maxInclusive = numberConfigKeyClass.getDeclaredField("maxInclusive"); + maxInclusive.setAccessible(true); + maxInclusive.set(breastSizeKey, targetMax); + } + + float appliedMax = ((Number)getMaxInclusive.invoke(breastSizeKey)).floatValue(); + if (appliedMax < targetMax) { + throw new IllegalStateException("Female Gender Mod breast-size maximum remained at " + appliedMax); + } + + supportedBreastSizeMax = appliedMax; + } catch (ReflectiveOperationException | RuntimeException e) { + NeedsOfNature.LOGGER + .warn("[NoN] Could not extend Female Gender Mod's breast-size range. Values above 100% will be clamped for compatibility.", e); + } + } + } + } + + public static float clampBreastSize(float breastSize) { + initialize(); + return Math.clamp(breastSize, 0.0F, supportedBreastSizeMax); + } +} diff --git a/src/main/java/com/nonid/integration/PercentSliderWidget.java b/src/main/java/com/nonid/integration/PercentSliderWidget.java new file mode 100644 index 0000000..567226d --- /dev/null +++ b/src/main/java/com/nonid/integration/PercentSliderWidget.java @@ -0,0 +1,42 @@ +package com.nonid.integration; + +import java.util.function.IntConsumer; +import net.minecraft.client.gui.widget.SliderWidget; +import net.minecraft.text.Text; + +final class PercentSliderWidget extends SliderWidget { + private final IntConsumer listener; + private int percent; + + PercentSliderWidget(int x, int y, int width, int height, int initialPercent, IntConsumer listener) { + super(x, y, width, height, Text.literal(""), clampPercent(initialPercent) / 200.0); + this.listener = listener; + this.percent = clampPercent(initialPercent); + this.updateMessage(); + } + + void setPercent(int value) { + this.percent = clampPercent(value); + this.value = this.percent / 200.0; + this.updateMessage(); + if (this.listener != null) { + this.listener.accept(this.percent); + } + } + + protected void updateMessage() { + this.percent = clampPercent((int)Math.round(this.value * 200.0)); + this.setMessage(Text.translatable("config.needsofnature.action_sound_volume.value", new Object[]{this.percent})); + } + + protected void applyValue() { + this.percent = clampPercent((int)Math.round(this.value * 200.0)); + if (this.listener != null) { + this.listener.accept(this.percent); + } + } + + private static int clampPercent(int value) { + return Math.max(0, Math.min(200, value)); + } +} diff --git a/src/main/java/com/nonid/integration/SettingsList.java b/src/main/java/com/nonid/integration/SettingsList.java new file mode 100644 index 0000000..7444d83 --- /dev/null +++ b/src/main/java/com/nonid/integration/SettingsList.java @@ -0,0 +1,202 @@ +package com.nonid.integration; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.gui.widget.TextWidget; +import net.minecraft.client.gui.widget.ElementListWidget.Entry; +import net.minecraft.text.Text; + +final class SettingsList extends ElementListWidget { + SettingsList(MinecraftClient client, int width, int height, int top) { + super(client, width, height, top, 24); + this.centerListVertically = false; + } + + void addEntryRow(SettingsList.RowEntry entry) { + super.addEntry(entry); + } + + public int getRowWidth() { + return 440; + } + + public int getRowLeft() { + return (this.getWidth() - this.getRowWidth()) / 2; + } + + static final class RowEntry extends Entry { + private final TextRenderer textRenderer; + private final Text label; + private final TextWidget labelWidget; + private final ClickableWidget primary; + private final ClickableWidget reset; + private final SettingsList.RowEntry.RowType type; + private final List widgets; + + private RowEntry( + TextRenderer textRenderer, Text label, ClickableWidget primary, ClickableWidget reset, SettingsList.RowEntry.RowType type, Tooltip tooltip + ) { + this.textRenderer = textRenderer; + this.label = label; + this.labelWidget = label == null ? null : new TextWidget(0, 0, 0, 20, label, textRenderer); + this.primary = primary; + this.reset = reset; + this.type = type; + this.widgets = new ArrayList<>(2); + if (primary != null) { + this.widgets.add(primary); + } + + if (reset != null) { + this.widgets.add(reset); + } + + if (tooltip != null) { + if (this.labelWidget != null) { + NonModMenuScreens.setTooltip(this.labelWidget, tooltip); + } + + if (this.primary != null) { + NonModMenuScreens.setTooltip(this.primary, tooltip); + } + } + } + + static SettingsList.RowEntry labeledField(TextRenderer textRenderer, Text label, ClickableWidget field, ClickableWidget reset, Tooltip tooltip) { + return new SettingsList.RowEntry(textRenderer, label, field, reset, SettingsList.RowEntry.RowType.LABELED_FIELD, tooltip); + } + + static SettingsList.RowEntry groupedField(TextRenderer textRenderer, Text label, Tooltip tooltip, ClickableWidget... fields) { + SettingsList.RowEntry entry = new SettingsList.RowEntry(textRenderer, label, null, null, SettingsList.RowEntry.RowType.GROUPED_FIELD, tooltip); + if (fields != null) { + for (ClickableWidget field : fields) { + if (field != null) { + entry.widgets.add(field); + } + } + } + + return entry; + } + + static SettingsList.RowEntry buttonWithReset(TextRenderer textRenderer, ClickableWidget button, Tooltip tooltip) { + return new SettingsList.RowEntry(textRenderer, null, button, null, SettingsList.RowEntry.RowType.BUTTON_RESET, tooltip); + } + + static SettingsList.RowEntry sectionHeader(TextRenderer textRenderer, Text label) { + return new SettingsList.RowEntry(textRenderer, label, null, null, SettingsList.RowEntry.RowType.SECTION_HEADER, null); + } + + public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float deltaTicks) { + int rowX = this.getContentX(); + int rowY = this.getContentY(); + int rowWidth = this.getContentWidth(); + int widgetY = rowY + 2; + if (this.type == SettingsList.RowEntry.RowType.SECTION_HEADER) { + if (this.label != null) { + int bandY = rowY + 3; + int bandH = 18; + int centerX = rowX + rowWidth / 2; + int textY = rowY + 8; + int lineY = rowY + 12; + int textW = this.textRenderer.getWidth(this.label); + int leftEnd = centerX - textW / 2 - 10; + int rightStart = centerX + textW / 2 + 10; + context.fill(rowX + 2, bandY, rowX + rowWidth - 2, bandY + bandH, 855638016); + context.fill(rowX + 8, lineY, Math.max(rowX + 8, leftEnd), lineY + 1, 1717986918); + context.fill(Math.min(rowX + rowWidth - 8, rightStart), lineY, rowX + rowWidth - 8, lineY + 1, 1717986918); + context.drawCenteredTextWithShadow(this.textRenderer, this.label, centerX, textY, -3158065); + } + } else { + if (this.type == SettingsList.RowEntry.RowType.GROUPED_FIELD) { + int totalW = 0; + + for (ClickableWidget widget : this.widgets) { + totalW += widget.getWidth(); + } + + totalW += Math.max(0, this.widgets.size() - 1) * 4; + int x = rowX + rowWidth - totalW; + if (this.labelWidget != null) { + int labelWidth = Math.max(20, x - rowX - 8); + this.labelWidget.setX(rowX + 4); + this.labelWidget.setY(rowY + 6); + this.labelWidget.setWidth(labelWidth); + } + + for (ClickableWidget widget : this.widgets) { + widget.setX(x); + widget.setY(widgetY); + x += widget.getWidth() + 4; + } + } else if (this.primary != null) { + if (this.type == SettingsList.RowEntry.RowType.LABELED_FIELD) { + int resetW = this.reset == null ? 0 : this.reset.getWidth(); + int fieldW = this.primary.getWidth(); + int resetX = rowX + rowWidth - resetW; + int fieldX = resetX - 6 - fieldW; + this.primary.setX(fieldX); + this.primary.setY(widgetY); + if (this.reset != null) { + this.reset.setX(resetX); + this.reset.setY(widgetY); + } + + if (this.labelWidget != null) { + int labelWidth = Math.max(20, fieldX - rowX - 8); + this.labelWidget.setX(rowX + 4); + this.labelWidget.setY(rowY + 6); + this.labelWidget.setWidth(labelWidth); + } + } else { + int resetW = this.reset == null ? 0 : this.reset.getWidth(); + int buttonW = Math.max(20, rowWidth - resetW - 6); + this.primary.setX(rowX); + this.primary.setY(widgetY); + this.primary.setWidth(buttonW); + if (this.reset != null) { + this.reset.setX(rowX + rowWidth - resetW); + this.reset.setY(widgetY); + } + } + } + + if (this.labelWidget != null) { + this.labelWidget.render(context, mouseX, mouseY, deltaTicks); + } + + for (ClickableWidget widget : this.widgets) { + widget.render(context, mouseX, mouseY, deltaTicks); + } + } + } + + public List selectableChildren() { + return List.copyOf(this.widgets); + } + + public void forEachChild(Consumer consumer) { + this.widgets.forEach(consumer); + } + + public List children() { + return List.copyOf(this.widgets); + } + + private enum RowType { + LABELED_FIELD, + GROUPED_FIELD, + BUTTON_RESET, + SECTION_HEADER; + } + } +} diff --git a/src/main/java/com/nonid/integration/jade/NonJadePlugin.java b/src/main/java/com/nonid/integration/jade/NonJadePlugin.java new file mode 100644 index 0000000..85ee228 --- /dev/null +++ b/src/main/java/com/nonid/integration/jade/NonJadePlugin.java @@ -0,0 +1,58 @@ +package com.nonid.integration.jade; + +import com.nonid.GenderHolder; +import com.nonid.NeedsOfNature; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import snownee.jade.api.EntityAccessor; +import snownee.jade.api.IEntityComponentProvider; +import snownee.jade.api.ITooltip; +import snownee.jade.api.IWailaClientRegistration; +import snownee.jade.api.IWailaPlugin; +import snownee.jade.api.WailaPlugin; +import snownee.jade.api.config.IPluginConfig; + +@WailaPlugin +public final class NonJadePlugin implements IWailaPlugin { + private static final Identifier GENDER = NeedsOfNature.id("gender"); + + public void registerClient(IWailaClientRegistration registration) { + registration.addConfig(GENDER, true); + registration.registerEntityComponent(NonJadePlugin.GenderProvider.INSTANCE, MobEntity.class); + registration.markAsClientFeature(GENDER); + } + + private enum GenderProvider implements IEntityComponentProvider { + INSTANCE; + + public void appendTooltip(ITooltip tooltip, EntityAccessor accessor, IPluginConfig config) { + String symbol = genderSymbol(accessor.getEntity()); + if (!symbol.isEmpty()) { + tooltip.append(Text.literal(" " + symbol), NonJadePlugin.GENDER); + } + } + + public Identifier getUid() { + return NonJadePlugin.GENDER; + } + + public int getDefaultPriority() { + return -10099; + } + + private static String genderSymbol(Entity entity) { + if (entity instanceof GenderHolder holder) { + return switch (holder.getGenderMask() & 3) { + case 1 -> "♂"; + case 2 -> "♀"; + case 3 -> "♂♀"; + default -> ""; + }; + } else { + return ""; + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/AnimationEngine.java b/src/main/java/com/nonid/internal/animation/AnimationEngine.java new file mode 100644 index 0000000..3641040 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/AnimationEngine.java @@ -0,0 +1,326 @@ +package com.nonid.internal.animation; + +import com.nonid.NeedsOfNature; +import com.nonid.NonDebugChatCategory; +import com.nonid.NonDebugChatMode; +import com.nonid.NonDiagnostics; +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.api.NonAnimationApiBridge; +import com.nonid.internal.animation.data.AnimationCueIndex; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AdjustAnimationSpeedC2SPayload; +import com.nonid.internal.animation.network.AdvanceAnimationStageS2CPayload; +import com.nonid.internal.animation.network.AnimationSpeedUpdateS2CPayload; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.network.DebugAdvanceStageC2SPayload; +import com.nonid.internal.animation.network.DebugChatPreferenceC2SPayload; +import com.nonid.internal.animation.network.DebugJoinAnimationC2SPayload; +import com.nonid.internal.animation.network.DebugStartAnimationC2SPayload; +import com.nonid.internal.animation.network.DebugStopAllAnimationsC2SPayload; +import com.nonid.internal.animation.network.DebugStopAnimationC2SPayload; +import com.nonid.internal.animation.network.StartAnimationS2CPayload; +import com.nonid.internal.animation.network.StopAllAnimationsS2CPayload; +import com.nonid.internal.animation.network.StopAnimationS2CPayload; +import com.nonid.internal.animation.server.AnimationStartContext; +import com.nonid.internal.animation.server.ServerAnimationController; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStarted; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Join; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.PlayerConfigEntry; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.slf4j.Logger; + +public final class AnimationEngine { + public static final String PACK_NAMESPACE = "animationframework"; + public static final String RUNTIME_NAMESPACE = "needsofnature"; + public static final Logger LOGGER = NeedsOfNature.LOGGER; + + private AnimationEngine() { + } + + public static Identifier packId(String path) { + return Identifier.of("animationframework", path); + } + + public static Identifier runtimeId(String path) { + return Identifier.of("needsofnature", "animation/" + path); + } + + public static void initialize() { + PayloadTypeRegistry.playC2S().register(DebugStartAnimationC2SPayload.ID, DebugStartAnimationC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DebugStopAllAnimationsC2SPayload.ID, DebugStopAllAnimationsC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DebugStopAnimationC2SPayload.ID, DebugStopAnimationC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DebugChatPreferenceC2SPayload.ID, DebugChatPreferenceC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DebugJoinAnimationC2SPayload.ID, DebugJoinAnimationC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(DebugAdvanceStageC2SPayload.ID, DebugAdvanceStageC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(AdjustAnimationSpeedC2SPayload.ID, AdjustAnimationSpeedC2SPayload.CODEC); + PayloadTypeRegistry.playS2C().register(StartAnimationS2CPayload.ID, StartAnimationS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(AdvanceAnimationStageS2CPayload.ID, AdvanceAnimationStageS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(StopAllAnimationsS2CPayload.ID, StopAllAnimationsS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(StopAnimationS2CPayload.ID, StopAnimationS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(AnimationSpeedUpdateS2CPayload.ID, AnimationSpeedUpdateS2CPayload.CODEC); + NonAnimationApiBridge.initialize(); + AnimationDefinitions.registerReloadListener(); + AnimationCueIndex.registerReloadListener(); + ServerAnimationController.init(); + registerStartupResourceReload(); + ServerPlayNetworking.registerGlobalReceiver(DebugStartAnimationC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (requireDebugControlPermission(player, "start")) { + NonAnimationDamageBehavior behavior = NonAnimationDamageBehavior.fromId(payload.damageBehaviorId(), NonAnimationDamageBehavior.STOP_ON_DAMAGE); + handleDebugStartRequest(player, payload.actorEntityIds(), behavior, payload.ignoreAttackers(), payload.anchorEntityId()); + } + })); + ServerPlayNetworking.registerGlobalReceiver(DebugStopAllAnimationsC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (requireDebugControlPermission(player, "stop_all")) { + if (player.getEntityWorld() instanceof ServerWorld world) { + AnimationService.stopAll(world); + sendDebugChat(player, NonDebugChatCategory.ALWAYS, Text.translatable("debug.needsofnature.animation.stop_all_broadcast_queued"), true); + } + } + })); + ServerPlayNetworking.registerGlobalReceiver(DebugStopAnimationC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (requireDebugControlPermission(player, "stop_instance")) { + ServerAnimationController.stopInstanceAndBroadcast(player, payload.instanceId()); + } + })); + ServerPlayNetworking.registerGlobalReceiver(DebugChatPreferenceC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + UUID uuid = context.player().getUuid(); + NonDebugChatMode mode = NonDebugChatMode.fromId(payload.modeId(), NonDebugChatMode.SETUP_ERRORS); + NonDiagnostics.setDebugChatPreference(uuid, mode); + })); + ServerPlayNetworking.registerGlobalReceiver(DebugJoinAnimationC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (requireDebugControlPermission(player, "join")) { + handleDebugJoinRequest(player, payload.instanceId(), payload.actorEntityIds()); + } + })); + ServerPlayNetworking.registerGlobalReceiver(DebugAdvanceStageC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (requireDebugControlPermission(player, "advance_stage")) { + if (player.getEntityWorld() instanceof ServerWorld world) { + ServerAnimationController.advanceStage(world, payload.instanceId()); + } + } + })); + ServerPlayNetworking.registerGlobalReceiver(AdjustAnimationSpeedC2SPayload.ID, (payload, context) -> context.server().execute(() -> { + ServerPlayerEntity player = context.player(); + if (requireDebugControlPermission(player, "adjust_speed")) { + if (player.getEntityWorld() instanceof ServerWorld world) { + ServerAnimationController.adjustSpeed(world, payload.instanceId(), payload.multiplier(), player); + } + } + })); + ServerPlayConnectionEvents.JOIN + .register((Join)(handler, sender, server) -> server.execute(() -> ServerAnimationController.sendActiveInstancesToPlayer(handler.player))); + LOGGER.info("[NoN] Animation engine initialized"); + } + + private static void registerStartupResourceReload() { + ServerLifecycleEvents.SERVER_STARTED.register((ServerStarted)server -> server.execute(() -> { + try { + LOGGER.info("[NoN] Reloading startup resources so generated and default packs are active."); + server.reloadResources(server.getDataPackManager().getEnabledIds()).exceptionally(throwable -> { + LOGGER.warn("[NoN] Startup resource reload failed.", throwable); + return null; + }); + } catch (RuntimeException e) { + LOGGER.warn("[NoN] Failed to schedule startup resource reload.", e); + } + })); + } + + public static void logSetupWarning(String template, Object... args) { + NonDiagnostics.logSetupWarning(template, args); + } + + public static void logRuntime(String template, Object... args) { + NonDiagnostics.logRuntime(template, args); + } + + public static void logSetupError(String template, Object... args) { + NonDiagnostics.logSetupError(template, args); + } + + public static void queueSetupChat(String message) { + NonDiagnostics.queueSetupChat(message); + } + + public static String formatLogTemplate(String template, Object... args) { + return NonDiagnostics.formatLogTemplate(template, args); + } + + public static boolean shouldSendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category) { + return NonDiagnostics.shouldSendDebugChat(player, category); + } + + public static boolean shouldSendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, boolean force) { + return NonDiagnostics.shouldSendDebugChat(player, category, force); + } + + public static void sendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, String message, boolean force) { + sendDebugChat(player, category, Text.literal(message), force); + } + + public static void sendDebugChat(ServerPlayerEntity player, NonDebugChatCategory category, Text message, boolean force) { + NonDiagnostics.sendDebugChat(player, category, message, force); + } + + private static boolean requireDebugControlPermission(ServerPlayerEntity player, String action) { + if (player != null + && player.getEntityWorld() instanceof ServerWorld world + && world.getServer().getPlayerManager().isOperator(new PlayerConfigEntry(player.getGameProfile()))) { + return true; + } + + if (player != null) { + LOGGER.warn("Denied animation debug control '{}' from non-OP player {}", action, player.getName().getString()); + sendDebugChat(player, NonDebugChatCategory.WARNING, Text.literal("[NoN 动画] 调试控制需要管理员权限。"), true); + } + + return false; + } + + private static void handleDebugStartRequest( + ServerPlayerEntity player, List requestedActorIds, NonAnimationDamageBehavior damageBehavior, boolean ignoreAttackers, int anchorEntityId + ) { + if (!requestedActorIds.isEmpty()) { + List resolved = new ArrayList<>(); + Entity anchorEntity = null; + + for (int entityId : requestedActorIds) { + Entity entity = player.getEntityWorld().getEntityById(entityId); + if (entity instanceof LivingEntity) { + resolved.add(entity); + if (anchorEntity == null && entityId == anchorEntityId) { + anchorEntity = entity; + } + } + } + + if (!resolved.isEmpty()) { + resolved.sort(Comparator.comparingInt(Entity::getId)); + NonAnimationDamageBehavior safeBehavior = damageBehavior == null ? NonAnimationDamageBehavior.STOP_ON_DAMAGE : damageBehavior; + UUID anchorUuid = anchorEntity != null ? anchorEntity.getUuid() : null; + ServerAnimationController.MatchedStartRequest start = ServerAnimationController.startEligibleMatchedNow( + player.getEntityWorld(), player, resolved, Set.of(), safeBehavior, ignoreAttackers, anchorUuid, AnimationStartContext.empty(), true + ); + String chosenStr = start == null ? "(none)" : start.animationId().toString(); + List actorKeys = start == null ? List.of() : start.actorKeys(); + String keysStr = actorKeys.isEmpty() ? "none" : actorKeys.toString(); + MutableText msg = Text.translatable("debug.needsofnature.animation.server_eval", new Object[]{resolved.size(), chosenStr, keysStr}); + sendDebugChat(player, NonDebugChatCategory.ALWAYS, msg, true); + if (start != null) { + logRuntime("Debug request: player={} chosen={} actors={}", player.getName().getString(), start.animationId(), resolved.size()); + } + } + } + } + + private static void handleDebugJoinRequest(ServerPlayerEntity player, UUID instanceId, List requestedActorIds) { + if (player != null && requestedActorIds != null && !requestedActorIds.isEmpty()) { + if (player.getEntityWorld() instanceof ServerWorld world) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + if (snapshot == null) { + sendDebugChat(player, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.animation.join_failed_no_active_instance"), true); + } else { + AnimationStageInfo currentStage = ServerAnimationController.getCurrentStage(world, instanceId); + if (currentStage != null && currentStage.allowJoin()) { + List combined = new ArrayList<>(); + Set existingActorUuids = new HashSet<>(snapshot.actorUuids()); + + for (UUID actorUuid : snapshot.actorUuids()) { + Entity actor = world.getEntity(actorUuid); + if (actor != null) { + combined.add(actor); + } + } + + if (combined.size() != snapshot.actorUuids().size()) { + sendDebugChat( + player, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.animation.join_failed_no_active_instance"), true + ); + } else { + List additions = new ArrayList<>(); + + for (int id : requestedActorIds) { + Entity e = world.getEntityById(id); + if (e instanceof MobEntity && existingActorUuids.add(e.getUuid())) { + additions.add(e); + } + } + + if (!additions.isEmpty()) { + combined.addAll(additions); + combined.sort(Comparator.comparingInt(Entity::getId)); + AnimationDefinitions.MatchResult match = AnimationDefinitions.match(combined); + AnimationDefinitions.Definition definition = match.chosen(); + if (definition == null) { + sendDebugChat( + player, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.animation.join_failed_no_expanded_definition"), true + ); + } else { + List actorKeys = AnimationDefinitions.resolveActorKeys(definition, combined, world.getRandom()); + if (actorKeys != null && actorKeys.size() == combined.size()) { + AnimationStartContext replacementContext = snapshot.startContext().withJoinPredecessor(snapshot.animationId()); + if (!ServerAnimationController.stopInstance(world, instanceId, false)) { + sendDebugChat( + player, + NonDebugChatCategory.WARNING, + Text.translatable("debug.needsofnature.animation.join_failed_no_active_instance"), + true + ); + } else { + ServerAnimationController.startNowWithContext( + world, + player, + definition.id(), + combined, + actorKeys, + definition.stages(), + snapshot.damageBehavior(), + snapshot.ignoreAttackers(), + null, + replacementContext, + true, + false + ); + } + } else { + sendDebugChat( + player, + NonDebugChatCategory.WARNING, + Text.translatable("debug.needsofnature.animation.join_failed_no_expanded_definition"), + true + ); + } + } + } + } + } else { + sendDebugChat(player, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.animation.join_denied_stage_disallows"), true); + } + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/AnimationEngineClient.java b/src/main/java/com/nonid/internal/animation/AnimationEngineClient.java new file mode 100644 index 0000000..3be9e01 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/AnimationEngineClient.java @@ -0,0 +1,519 @@ +package com.nonid.internal.animation; + +import com.nonid.NeedsOfNature; +import com.nonid.NonDebugChatCategory; +import com.nonid.NonDiagnostics; +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.client.NonClientDiagnostics; +import com.nonid.internal.animation.client.camera.AnimatedCameraPoseTracker; +import com.nonid.internal.animation.client.camera.AnimationCameraZoom; +import com.nonid.internal.animation.client.diagnostics.AnimationAssetDiagnostics; +import com.nonid.internal.animation.client.render.gecko.BoneTextureOverrides; +import com.nonid.internal.animation.client.render.gecko.VanillaTextureResolver; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import com.nonid.internal.animation.client.sound.AnimationSoundEffects; +import com.nonid.internal.animation.network.AdjustAnimationSpeedC2SPayload; +import com.nonid.internal.animation.network.AdvanceAnimationStageS2CPayload; +import com.nonid.internal.animation.network.AnimationSpeedUpdateS2CPayload; +import com.nonid.internal.animation.network.DebugAdvanceStageC2SPayload; +import com.nonid.internal.animation.network.DebugChatPreferenceC2SPayload; +import com.nonid.internal.animation.network.DebugJoinAnimationC2SPayload; +import com.nonid.internal.animation.network.DebugStartAnimationC2SPayload; +import com.nonid.internal.animation.network.DebugStopAllAnimationsC2SPayload; +import com.nonid.internal.animation.network.DebugStopAnimationC2SPayload; +import com.nonid.internal.animation.network.StartAnimationS2CPayload; +import com.nonid.internal.animation.network.StopAllAnimationsS2CPayload; +import com.nonid.internal.animation.network.StopAnimationS2CPayload; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.Disconnect; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.Join; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.option.Perspective; +import net.minecraft.client.option.KeyBinding.Category; +import net.minecraft.client.util.InputUtil; +import net.minecraft.client.util.InputUtil.Type; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.math.Vec3d; + +public final class AnimationEngineClient { + private static final Category KEY_CATEGORY = Category.create(Identifier.of("needsofnature", "animation")); + private static final KeyBinding SELECT_ACTOR_KEY = KeyBindingHelper.registerKeyBinding(createUnboundDebugKey("key.needsofnature.animation.select_actor")); + private static final KeyBinding SELECT_SELF_KEY = KeyBindingHelper.registerKeyBinding(createUnboundDebugKey("key.needsofnature.animation.select_self")); + private static final KeyBinding START_DEBUG_KEY = KeyBindingHelper.registerKeyBinding( + createUnboundDebugKey("key.needsofnature.animation.start_debug_animation") + ); + private static final KeyBinding STOP_ALL_DEBUG_KEY = KeyBindingHelper.registerKeyBinding(createUnboundDebugKey("key.needsofnature.animation.stop_all_debug")); + private static final KeyBinding STOP_INSTANCE_DEBUG_KEY = KeyBindingHelper.registerKeyBinding( + createUnboundDebugKey("key.needsofnature.animation.stop_instance_debug") + ); + private static final KeyBinding NEXT_STAGE_DEBUG_KEY = KeyBindingHelper.registerKeyBinding( + createUnboundDebugKey("key.needsofnature.animation.next_stage_debug") + ); + private static final KeyBinding SPEED_UP_DEBUG_KEY = KeyBindingHelper.registerKeyBinding(createUnboundDebugKey("key.needsofnature.animation.speed_up_debug")); + private static final KeyBinding SPEED_DOWN_DEBUG_KEY = KeyBindingHelper.registerKeyBinding( + createUnboundDebugKey("key.needsofnature.animation.speed_down_debug") + ); + private static final IntArrayList SELECTED_ACTOR_IDS = new IntArrayList(); + private static UUID SELECTED_INSTANCE_ID = null; + private static boolean wasSelfAnimating = false; + private static boolean ownsCameraPerspective = false; + private static Perspective preAnimationPerspective = null; + private static final double PLAYER_ANCHOR_EPSILON_SQ = 4.0E-4; + + public static void sendClientDebugChat(NonDebugChatCategory category, Text message) { + NonClientDiagnostics.send(category, message, false); + } + + public static void sendClientSetupWarning(String message) { + NonClientDiagnostics.sendSetupWarning(message, false); + } + + private static KeyBinding createUnboundDebugKey(String translationKey) { + return new KeyBinding(translationKey, Type.KEYSYM, InputUtil.UNKNOWN_KEY.getCode(), KEY_CATEGORY); + } + + private AnimationEngineClient() { + } + + public static void initialize() { + VanillaTextureResolver.registerReloadListener(); + BoneTextureOverrides.registerReloadListener(); + AnimationSoundEffects.registerReloadListener(); + AnimationAssetDiagnostics.registerReloadListener(); + ClientPlayConnectionEvents.JOIN.register((Join)(handler, sender, client) -> sendDebugChatPreference()); + ClientPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, client) -> { + ClientAnimationRuntime.stopAllNow(); + AnimatedCameraPoseTracker.clear(); + AnimationCameraZoom.reset(); + resetCameraPerspective(client); + }); + ClientPlayNetworking.registerGlobalReceiver( + StartAnimationS2CPayload.ID, + (payload, context) -> context.client() + .execute( + () -> { + ClientAnimationRuntime.queueStart( + payload.animationId(), + payload.modelRootsByActor(), + payload.instanceId(), + payload.actorUuids(), + payload.actorKeys(), + payload.stages(), + payload.startTick(), + payload.stageIndex(), + payload.stageStartTick(), + payload.stageEndTick(), + payload.speed(), + payload.lockOrientation(), + payload.lockedYaw(), + payload.lockedHeadYaw(), + payload.lockedPitch(), + payload.cameraOrbitTarget() + ); + MinecraftClient client = context.client(); + PlayerEntity player = client.player; + if (player != null && NonClientDiagnostics.shouldSend(NonDebugChatCategory.INFO)) { + String keysStr = payload.actorKeys().isEmpty() ? "none" : payload.actorKeys().toString(); + player.sendMessage(tr("start_received", payload.animationId(), payload.actorUuids().size(), keysStr), false); + } + } + ) + ); + ClientPlayNetworking.registerGlobalReceiver(StopAllAnimationsS2CPayload.ID, (payload, context) -> context.client().execute(() -> { + ClientAnimationRuntime.queueStopAll(payload.stopTick()); + MinecraftClient client = context.client(); + PlayerEntity player = client.player; + if (player != null && NonClientDiagnostics.shouldSend(NonDebugChatCategory.INFO)) { + player.sendMessage(tr("stop_all_received"), false); + } + })); + ClientPlayNetworking.registerGlobalReceiver( + AdvanceAnimationStageS2CPayload.ID, + (payload, context) -> context.client() + .execute(() -> ClientAnimationRuntime.queueStageAdvance(payload.instanceId(), payload.advanceTick(), payload.stageEndTick(), payload.stageIndex())) + ); + ClientPlayNetworking.registerGlobalReceiver( + AnimationSpeedUpdateS2CPayload.ID, + (payload, context) -> context.client() + .execute(() -> ClientAnimationRuntime.queueSpeedUpdate(payload.instanceId(), payload.speed(), payload.stageStartTick(), payload.stageEndTick())) + ); + ClientPlayNetworking.registerGlobalReceiver(StopAnimationS2CPayload.ID, (payload, context) -> context.client().execute(() -> { + ClientAnimationRuntime.queueStop(payload.instanceId(), payload.stopTick()); + MinecraftClient client = context.client(); + PlayerEntity player = client.player; + if (player != null && NonClientDiagnostics.shouldSend(NonDebugChatCategory.INFO)) { + player.sendMessage(tr("stop_instance_received"), false); + } + })); + ClientTickEvents.END_CLIENT_TICK.register(AnimationEngineClient::onClientTick); + } + + public static void sendDebugChatPreference() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null && ClientPlayNetworking.canSend(DebugChatPreferenceC2SPayload.ID)) { + ClientPlayNetworking.send(new DebugChatPreferenceC2SPayload(NeedsOfNature.getConfig().debugChatMode().id())); + } + } + + private static void onClientTick(MinecraftClient client) { + if (client.player != null && client.world != null) { + if (client.currentScreen == null) { + while (SELECT_ACTOR_KEY.wasPressed()) { + toggleTargetedActorSelection(client); + } + + while (SELECT_SELF_KEY.wasPressed()) { + toggleSelfSelection(client); + } + + while (START_DEBUG_KEY.wasPressed()) { + requestDebugStart(client); + } + + while (STOP_ALL_DEBUG_KEY.wasPressed()) { + requestStopAll(client); + } + + while (STOP_INSTANCE_DEBUG_KEY.wasPressed()) { + requestStopInstance(client); + } + + while (NEXT_STAGE_DEBUG_KEY.wasPressed()) { + requestNextStage(client); + } + + while (SPEED_UP_DEBUG_KEY.wasPressed()) { + requestSpeedAdjust(client, 1.1); + } + + while (SPEED_DOWN_DEBUG_KEY.wasPressed()) { + requestSpeedAdjust(client, 0.9); + } + } + + ClientAnimationRuntime.clientTick(client); + AnimationCameraZoom.clientTick(client); + AnimationAssetDiagnostics.validateLoadedDefinitionsOnce(); + AnimatedCameraPoseTracker.prune(client.world.getTime()); + handleCompletedStages(client); + syncCameraPerspective(client); + syncLocalPlayerToAnchor(client); + } + } + + private static void requestStopAll(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null) { + if (!ClientPlayNetworking.canSend(DebugStopAllAnimationsC2SPayload.ID)) { + sendManualDebug(player, tr("server_cannot_receive_stop_all")); + ClientAnimationRuntime.stopAllNow(); + } else { + sendManualDebug(player, tr("stop_all_requested")); + ClientPlayNetworking.send(new DebugStopAllAnimationsC2SPayload()); + } + } + } + + private static void requestStopInstance(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null) { + UUID self = player.getUuid(); + UUID instanceToStop = ClientAnimationRuntime.findLatestActiveInstanceContaining(self); + if (instanceToStop == null) { + instanceToStop = ClientAnimationRuntime.findLatestActiveInstance(); + } + + if (instanceToStop == null) { + sendManualDebug(player, tr("no_active_instance_to_stop")); + } else if (!ClientPlayNetworking.canSend(DebugStopAnimationC2SPayload.ID)) { + sendManualDebug(player, tr("server_cannot_receive_stop_instance")); + } else { + sendManualDebug(player, tr("stop_instance_requested")); + ClientPlayNetworking.send(new DebugStopAnimationC2SPayload(instanceToStop)); + } + } + } + + private static void toggleTargetedActorSelection(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null) { + if (client.crosshairTarget instanceof EntityHitResult entityHit) { + Entity target = entityHit.getEntity(); + if (!(target instanceof LivingEntity)) { + sendManualDebug(player, tr("target_not_living")); + } else { + int id = target.getId(); + Identifier typeId = Registries.ENTITY_TYPE.getId(target.getType()); + UUID instId = ClientAnimationRuntime.findLatestActiveInstanceContaining(target.getUuid()); + if (instId != null) { + if (SELECTED_INSTANCE_ID != null && SELECTED_INSTANCE_ID.equals(instId)) { + SELECTED_INSTANCE_ID = null; + SELECTED_ACTOR_IDS.clear(); + sendManualDebug(player, tr("animation_deselected")); + } else { + SELECTED_INSTANCE_ID = instId; + SELECTED_ACTOR_IDS.clear(); + sendManualDebug(player, tr("animation_selected_add_actors_for_join")); + } + } else { + int existingIndex = indexOfSelectedActor(id); + if (existingIndex >= 0) { + SELECTED_ACTOR_IDS.removeInt(existingIndex); + sendManualDebug(player, tr("entity_deselected_actor_count", typeId.getPath(), SELECTED_ACTOR_IDS.size())); + } else { + SELECTED_ACTOR_IDS.add(id); + sendManualDebug(player, tr("entity_selected_actor_count", typeId.getPath(), SELECTED_ACTOR_IDS.size())); + } + } + } + } else { + sendManualDebug(player, tr("no_entity_targeted")); + } + } + } + + private static void toggleSelfSelection(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null) { + int id = player.getId(); + int existingIndex = indexOfSelectedActor(id); + if (existingIndex >= 0) { + SELECTED_ACTOR_IDS.removeInt(existingIndex); + sendManualDebug(player, tr("self_deselected_actor_count", SELECTED_ACTOR_IDS.size())); + } else { + SELECTED_ACTOR_IDS.add(id); + sendManualDebug(player, tr("self_selected_actor_count", SELECTED_ACTOR_IDS.size())); + } + } + } + + private static void requestDebugStart(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null) { + if (SELECTED_INSTANCE_ID == null && SELECTED_ACTOR_IDS.isEmpty()) { + sendManualDebug(player, tr("no_actors_selected")); + } else if (SELECTED_INSTANCE_ID != null) { + List actorIds = new ArrayList<>(SELECTED_ACTOR_IDS.size()); + + for (int i = 0; i < SELECTED_ACTOR_IDS.size(); i++) { + actorIds.add(SELECTED_ACTOR_IDS.getInt(i)); + } + + if (actorIds.isEmpty()) { + sendManualDebug(player, tr("no_extra_actors_for_join")); + } else if (!ClientPlayNetworking.canSend(DebugJoinAnimationC2SPayload.ID)) { + sendManualDebug(player, tr("server_cannot_receive_join")); + } else { + ClientPlayNetworking.send(new DebugJoinAnimationC2SPayload(SELECTED_INSTANCE_ID, actorIds)); + sendManualDebug(player, tr("join_requested_instance_extras", SELECTED_INSTANCE_ID, actorIds.size())); + SELECTED_INSTANCE_ID = null; + SELECTED_ACTOR_IDS.clear(); + } + } else if (!ClientPlayNetworking.canSend(DebugStartAnimationC2SPayload.ID)) { + sendManualDebug(player, tr("server_cannot_receive_debug_packets")); + } else { + List actorIds = new ArrayList<>(SELECTED_ACTOR_IDS.size()); + + for (int i = 0; i < SELECTED_ACTOR_IDS.size(); i++) { + actorIds.add(SELECTED_ACTOR_IDS.getInt(i)); + } + + sendManualDebug(player, tr("start_requested_actor_count", actorIds.size())); + ClientPlayNetworking.send(new DebugStartAnimationC2SPayload(actorIds, NonAnimationDamageBehavior.STOP_ON_DAMAGE.id(), false, -1)); + SELECTED_ACTOR_IDS.clear(); + } + } + } + + private static void requestNextStage(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null) { + UUID instanceToAdvance = ClientAnimationRuntime.findLatestActiveInstance(); + if (instanceToAdvance == null) { + sendManualDebug(player, tr("no_active_instance_to_advance")); + } else { + if (ClientPlayNetworking.canSend(DebugAdvanceStageC2SPayload.ID)) { + ClientPlayNetworking.send(new DebugAdvanceStageC2SPayload(instanceToAdvance)); + sendManualDebug(player, tr("next_stage_requested_server")); + } else { + ClientAnimationRuntime.requestStageAdvance(instanceToAdvance); + sendManualDebug(player, tr("next_stage_requested_local")); + } + } + } + } + + private static void requestSpeedAdjust(MinecraftClient client, double multiplier) { + PlayerEntity player = client.player; + if (player != null) { + UUID targetInstance = ClientAnimationRuntime.findLatestActiveInstanceContaining(player.getUuid()); + if (targetInstance == null) { + targetInstance = ClientAnimationRuntime.findLatestActiveInstance(); + } + + if (targetInstance == null) { + sendManualDebug(player, tr("no_active_instance_to_adjust_speed")); + } else { + if (ClientPlayNetworking.canSend(AdjustAnimationSpeedC2SPayload.ID)) { + ClientPlayNetworking.send(new AdjustAnimationSpeedC2SPayload(targetInstance, multiplier)); + double percent = (multiplier - 1.0) * 100.0; + String delta = (percent >= 0.0 ? "+" : "") + String.format(Locale.ROOT, "%.0f%%", percent); + sendManualDebug(player, tr("speed_change_requested", delta)); + } else { + sendManualDebug(player, tr("server_cannot_receive_speed")); + } + } + } + } + + private static void handleCompletedStages(MinecraftClient client) { + List completed = ClientAnimationRuntime.drainCompletedInstances(); + if (!completed.isEmpty()) { + PlayerEntity player = client.player; + boolean canSend = ClientPlayNetworking.canSend(DebugStopAnimationC2SPayload.ID); + + for (UUID instanceId : completed) { + if (canSend) { + ClientPlayNetworking.send(new DebugStopAnimationC2SPayload(instanceId)); + } else if (player != null && NonClientDiagnostics.shouldSend(NonDebugChatCategory.ERROR)) { + sendCategoryDebug(player, NonDebugChatCategory.ERROR, tr("stage_sequence_ended_server_stop_unavailable")); + } + } + } + } + + private static void syncCameraPerspective(MinecraftClient client) { + if (client.player != null && client.options != null && client.world != null) { + if (!NeedsOfNature.getConfig().autoSwitchThirdPersonOnAnimationStart()) { + restoreCameraPerspective(client); + } else { + UUID selfId = client.player.getUuid(); + boolean shouldThirdPerson = ClientAnimationRuntime.isActorPendingOrActive(selfId); + if (shouldThirdPerson) { + if (!wasSelfAnimating) { + preAnimationPerspective = client.options.getPerspective(); + ownsCameraPerspective = true; + if (client.options.getPerspective() != Perspective.THIRD_PERSON_BACK) { + client.options.setPerspective(Perspective.THIRD_PERSON_BACK); + } + } else if (ownsCameraPerspective && client.options.getPerspective() != Perspective.THIRD_PERSON_BACK) { + preAnimationPerspective = null; + ownsCameraPerspective = false; + } + + wasSelfAnimating = true; + } else { + if (ownsCameraPerspective || wasSelfAnimating) { + restoreCameraPerspective(client); + } + } + } + } + } + + private static void resetCameraPerspective(MinecraftClient client) { + if (client != null && client.options != null) { + restoreCameraPerspective(client); + } + } + + private static void restoreCameraPerspective(MinecraftClient client) { + if (client != null && client.options != null) { + if (ownsCameraPerspective && preAnimationPerspective != null) { + client.options.setPerspective(preAnimationPerspective); + } + + wasSelfAnimating = false; + ownsCameraPerspective = false; + preAnimationPerspective = null; + } + } + + private static void syncLocalPlayerToAnchor(MinecraftClient client) { + PlayerEntity player = client.player; + if (player != null && client.world != null) { + UUID selfId = player.getUuid(); + if (ClientAnimationRuntime.isActorPendingOrActive(selfId)) { + UUID anchorId = ClientAnimationRuntime.findAnchorUuidForActor(selfId); + if (anchorId == null) { + return; + } + + Entity anchor = client.world.getEntity(anchorId); + if (anchor == null) { + return; + } + + double dx = player.getX() - anchor.getX(); + double dy = player.getY() - anchor.getY(); + double dz = player.getZ() - anchor.getZ(); + if (dx * dx + dy * dy + dz * dz > 4.0E-4) { + player.setPos(anchor.getX(), anchor.getY(), anchor.getZ()); + player.setVelocity(Vec3d.ZERO); + player.fallDistance = 0.0; + } + } + } + } + + private static int indexOfSelectedActor(int value) { + for (int i = 0; i < SELECTED_ACTOR_IDS.size(); i++) { + if (SELECTED_ACTOR_IDS.getInt(i) == value) { + return i; + } + } + + return -1; + } + + private static Text tr(String key, Object... args) { + return Text.translatable("debug.needsofnature.animation." + key, sanitizeTranslatableArgs(args)); + } + + private static Object[] sanitizeTranslatableArgs(Object... args) { + if (args != null && args.length != 0) { + Object[] safe = new Object[args.length]; + + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (arg instanceof Identifier id) { + safe[i] = id.toString(); + } else { + safe[i] = !(arg instanceof Text) && arg != null ? String.valueOf(arg) : arg; + } + } + + return safe; + } else { + return new Object[0]; + } + } + + private static void sendManualDebug(PlayerEntity player, Text message) { + if (player != null) { + player.sendMessage(message.copy().formatted(Formatting.DARK_GRAY), false); + } + } + + private static void sendCategoryDebug(PlayerEntity player, NonDebugChatCategory category, Text message) { + if (player != null) { + player.sendMessage(message.copy().formatted(NonDiagnostics.chatColor(category)), false); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/api/AnimationLifecycleEvents.java b/src/main/java/com/nonid/internal/animation/api/AnimationLifecycleEvents.java new file mode 100644 index 0000000..9b0226b --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/api/AnimationLifecycleEvents.java @@ -0,0 +1,103 @@ +package com.nonid.internal.animation.api; + +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.server.AnimationStartContext; +import com.nonid.internal.animation.server.ServerAnimationController; +import java.util.List; +import java.util.UUID; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class AnimationLifecycleEvents { + public static final Event ALLOW_START = EventFactory.createArrayBacked( + AnimationLifecycleEvents.AllowStart.class, callbacks -> context -> { + for (AnimationLifecycleEvents.AllowStart callback : callbacks) { + if (!callback.allowStart(context)) { + return false; + } + } + + return true; + } + ); + public static final Event START = EventFactory.createArrayBacked( + AnimationLifecycleEvents.Start.class, callbacks -> context -> { + for (AnimationLifecycleEvents.Start callback : callbacks) { + callback.onStart(context); + } + } + ); + public static final Event STOP = EventFactory.createArrayBacked( + AnimationLifecycleEvents.Stop.class, callbacks -> context -> { + for (AnimationLifecycleEvents.Stop callback : callbacks) { + callback.onStop(context); + } + } + ); + public static final Event STAGE_ADVANCE = EventFactory.createArrayBacked( + AnimationLifecycleEvents.StageAdvance.class, callbacks -> context -> { + for (AnimationLifecycleEvents.StageAdvance callback : callbacks) { + callback.onStageAdvance(context); + } + } + ); + + private AnimationLifecycleEvents() { + } + + @FunctionalInterface + public interface AllowStart { + boolean allowStart(AnimationLifecycleEvents.AllowStartContext var1); + } + + public record AllowStartContext( + ServerWorld world, + Identifier animationId, + List actorUuids, + List actorKeys, + List stages, + @Nullable ServerPlayerEntity requester, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext + ) { + public AllowStartContext { + actorUuids = actorUuids == null ? List.of() : List.copyOf(actorUuids); + actorKeys = actorKeys == null ? List.of() : List.copyOf(actorKeys); + stages = stages == null ? List.of() : List.copyOf(stages); + startContext = startContext == null ? AnimationStartContext.empty() : startContext; + } + } + + @FunctionalInterface + public interface StageAdvance { + void onStageAdvance(AnimationLifecycleEvents.StageChangedContext var1); + } + + public record StageChangedContext( + ServerWorld world, ServerAnimationController.ActiveInstanceSnapshot snapshot, AnimationStageInfo currentStage, long advanceTick + ) { + } + + @FunctionalInterface + public interface Start { + void onStart(AnimationLifecycleEvents.StartedContext var1); + } + + public record StartedContext(ServerWorld world, ServerAnimationController.ActiveInstanceSnapshot snapshot, @Nullable ServerPlayerEntity requester) { + } + + @FunctionalInterface + public interface Stop { + void onStop(AnimationLifecycleEvents.StoppedContext var1); + } + + public record StoppedContext(ServerWorld world, ServerAnimationController.ActiveInstanceSnapshot snapshot) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/api/AnimationService.java b/src/main/java/com/nonid/internal/animation/api/AnimationService.java new file mode 100644 index 0000000..288c802 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/api/AnimationService.java @@ -0,0 +1,508 @@ +package com.nonid.internal.animation.api; + +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.network.StopAllAnimationsS2CPayload; +import com.nonid.internal.animation.server.AnimationStartContext; +import com.nonid.internal.animation.server.ServerAnimationController; +import com.nonid.internal.animation.util.WeightedPicker; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationService { + private AnimationService() { + } + + public static boolean hasEnabledGenericAnimationForEntityType(@Nullable EntityType type, boolean baby) { + return type == null ? false : hasEnabledGenericAnimationForEntityType(Registries.ENTITY_TYPE.getId(type), baby); + } + + public static boolean hasEnabledGenericAnimationForEntityType(@Nullable Identifier entityTypeId, boolean baby) { + return AnimationDefinitions.hasEnabledGenericAnimationForEntityType(entityTypeId, baby); + } + + @Nullable + public static UUID startAnimationNow( + ServerWorld world, Identifier animationId, List actors, @Nullable List actorKeys, @Nullable List stages + ) { + return startAnimationNow(world, animationId, actors, actorKeys, stages, AnimationService.StartOptions.defaults()); + } + + @Nullable + public static UUID startAnimationNow( + ServerWorld world, + Identifier animationId, + List actors, + @Nullable List actorKeys, + @Nullable List stages, + @Nullable AnimationService.StartOptions options + ) { + AnimationService.StartOptions safeOptions = options == null ? AnimationService.StartOptions.defaults() : options; + return startAnimationNowWithContext( + world, animationId, actors, actorKeys, stages, safeOptions, AnimationStartContext.addonMetadata(sanitizeMetadata(safeOptions.metadata())) + ); + } + + @Nullable + public static UUID startAnimationNowWithContext( + ServerWorld world, + Identifier animationId, + List actors, + @Nullable List actorKeys, + @Nullable List stages, + @Nullable AnimationService.StartOptions options, + @Nullable AnimationStartContext startContext + ) { + if (world == null || animationId == null || actors == null || actors.isEmpty()) { + return null; + } + + if (!AnimationDefinitions.isAnimationEnabled(animationId)) { + return null; + } + + AnimationService.StartOptions safeOptions = options == null ? AnimationService.StartOptions.defaults() : options; + List actorList = new ArrayList<>(actors.size()); + actorList.addAll(actors); + actorList.sort(Comparator.comparingInt(Entity::getId)); + UUID positionAnchorUuid = null; + Entity positionAnchor = safeOptions.positionAnchor(); + if (positionAnchor != null && positionAnchor.getEntityWorld() == world) { + UUID candidate = positionAnchor.getUuid(); + boolean anchorIsActor = actorList.stream().anyMatch(e -> e.getUuid().equals(candidate)); + if (anchorIsActor) { + positionAnchorUuid = candidate; + } + } + + return ServerAnimationController.startNowWithContext( + world, + safeOptions.requester(), + animationId, + actorList, + actorKeys, + stages, + safeOptions.damageBehavior() == null ? NonAnimationDamageBehavior.IGNORE_DAMAGE : safeOptions.damageBehavior(), + safeOptions.ignoreAttackers(), + positionAnchorUuid, + startContext == null ? AnimationStartContext.empty() : startContext, + safeOptions.forceChat(), + safeOptions.allowMultiplePlayers() + ); + } + + @Nullable + public static Identifier findMatchedAnimationId(List actors, @Nullable Set requiredAnimationTags) { + if (actors != null && !actors.isEmpty()) { + List actorList = new ArrayList<>(actors.size()); + actorList.addAll(actors); + actorList.sort(Comparator.comparingInt(Entity::getId)); + AnimationDefinitions.MatchResult match = AnimationDefinitions.match(actorList, requiredAnimationTags == null ? Set.of() : requiredAnimationTags); + AnimationDefinitions.Definition chosen = match.chosen(); + return chosen == null ? null : chosen.id(); + } else { + return null; + } + } + + @Nullable + public static AnimationService.MatchedAnimation findAnimationForActors( + ServerWorld world, List actors, @Nullable AnimationService.MatchOptions options + ) { + if (world != null && actors != null && !actors.isEmpty()) { + AnimationService.MatchOptions safeOptions = options == null ? AnimationService.MatchOptions.defaults() : options; + List actorList = new ArrayList<>(actors.size()); + actorList.addAll(actors); + actorList.sort(Comparator.comparingInt(Entity::getId)); + Set safeTags = safeOptions.requiredAnimationTags() == null ? Set.of() : safeOptions.requiredAnimationTags(); + Set excluded = safeOptions.excludedAnimationIds() == null ? Set.of() : safeOptions.excludedAnimationIds(); + UUID positionAnchorUuid = null; + Entity positionAnchor = safeOptions.positionAnchor(); + if (positionAnchor != null && positionAnchor.getEntityWorld() == world) { + UUID candidate = positionAnchor.getUuid(); + boolean anchorIsActor = actors.stream().anyMatch(e -> e != null && e.getUuid().equals(candidate)); + if (anchorIsActor) { + positionAnchorUuid = candidate; + } + } + + List candidates = AnimationDefinitions.findMatchingCandidates(actorList, safeTags); + if (candidates != null && !candidates.isEmpty()) { + List eligible = new ArrayList<>(); + + for (AnimationDefinitions.Definition candidate : candidates) { + if (candidate != null + && !excluded.contains(candidate.id()) + && AnimationDefinitions.isJoinCandidateAllowed(safeOptions.joinPredecessorAnimationId(), candidate)) { + Entity activeActor = safeOptions.requiredActiveActor(); + if (( + activeActor == null + || AnimationDefinitions.canResolveActorAsActivity(candidate, actorList, activeActor, AnimationDefinitions.ActorActivity.ACTIVE) + ) + && ( + !safeOptions.requireStartEligibility() + || ServerAnimationController.isDefinitionStartEligible(world, candidate, actorList, positionAnchorUuid) + )) { + AnimationService.AnimationCandidateFilter candidateFilter = safeOptions.candidateFilter(); + if (candidateFilter == null || candidateFilter.test(world, candidate, List.copyOf(actorList))) { + eligible.add(candidate); + } + } + } + } + + return pickWeightedMatchedAnimation(world, actorList, eligible); + } else { + return null; + } + } else { + return null; + } + } + + public static List findEligibleAnimationsForActors( + ServerWorld world, List actors, @Nullable AnimationService.MatchOptions options + ) { + if (world != null && actors != null && !actors.isEmpty()) { + AnimationService.MatchOptions safeOptions = options == null ? AnimationService.MatchOptions.defaults() : options; + List actorList = new ArrayList<>(actors.size()); + actorList.addAll(actors); + actorList.sort(Comparator.comparingInt(Entity::getId)); + Set safeTags = safeOptions.requiredAnimationTags() == null ? Set.of() : safeOptions.requiredAnimationTags(); + Set excluded = safeOptions.excludedAnimationIds() == null ? Set.of() : safeOptions.excludedAnimationIds(); + Entity positionAnchor = safeOptions.positionAnchor(); + UUID positionAnchorUuid = positionAnchor != null + && positionAnchor.getEntityWorld() == world + && actorList.stream().anyMatch(entity -> entity.getUuid().equals(positionAnchor.getUuid())) + ? positionAnchor.getUuid() + : null; + List result = new ArrayList<>(); + + for (AnimationDefinitions.Definition candidate : AnimationDefinitions.findMatchingCandidates(actorList, safeTags)) { + if (candidate != null + && !excluded.contains(candidate.id()) + && AnimationDefinitions.isJoinCandidateAllowed(safeOptions.joinPredecessorAnimationId(), candidate)) { + Entity activeActor = safeOptions.requiredActiveActor(); + if (activeActor == null + || AnimationDefinitions.canResolveActorAsActivity(candidate, actorList, activeActor, AnimationDefinitions.ActorActivity.ACTIVE)) { + AnimationService.AnimationCandidateFilter filter = safeOptions.candidateFilter(); + if (filter == null || filter.test(world, candidate, List.copyOf(actorList))) { + for (List actorKeys : AnimationDefinitions.resolveAllActorKeyAssignments(candidate, actorList)) { + if (!safeOptions.requireStartEligibility() + || ServerAnimationController.isDefinitionStartEligible(world, candidate, actorList, actorKeys, positionAnchorUuid)) { + AnimationDefinitions.AnimationPackInfo pack = candidate.packInfo(); + result.add( + new AnimationService.EligibleAnimation( + candidate.id(), + List.copyOf(actorKeys), + candidate.stages() == null ? List.of() : List.copyOf(candidate.stages()), + candidate.contentTags() == null ? List.of() : List.copyOf(candidate.contentTags()), + pack == null ? null : pack.id(), + pack == null ? null : pack.name(), + pack == null ? null : pack.author(), + candidate.displayName() + ) + ); + } + } + } + } + } + } + + result.sort( + Comparator.comparing(candidatex -> candidatex.animationId().toString()) + .thenComparing(candidatex -> String.join("\u0000", candidatex.actorKeys())) + ); + return List.copyOf(result); + } else { + return List.of(); + } + } + + @Nullable + private static AnimationService.MatchedAnimation pickWeightedMatchedAnimation( + ServerWorld world, List actors, List definitions + ) { + if (world != null && actors != null && definitions != null && !definitions.isEmpty()) { + AnimationDefinitions.Definition picked = WeightedPicker.pick(definitions, AnimationDefinitions.Definition::weight, world.getRandom().nextDouble()); + if (picked == null) { + return null; + } + + List actorKeys = AnimationDefinitions.resolveActorKeys(picked, actors, world.getRandom()); + return actorKeys != null && actorKeys.size() == actors.size() + ? new AnimationService.MatchedAnimation(picked.id(), List.copyOf(actorKeys), picked.stages() == null ? List.of() : List.copyOf(picked.stages())) + : null; + } else { + return null; + } + } + + public static boolean isAnimationStartEligible( + ServerWorld world, @Nullable AnimationDefinitions.Definition definition, List actors, @Nullable Entity positionAnchor + ) { + if (world != null && actors != null && !actors.isEmpty()) { + UUID positionAnchorUuid = null; + if (positionAnchor != null && positionAnchor.getEntityWorld() == world) { + UUID candidate = positionAnchor.getUuid(); + boolean anchorIsActor = actors.stream().anyMatch(e -> e != null && e.getUuid().equals(candidate)); + if (anchorIsActor) { + positionAnchorUuid = candidate; + } + } + + return ServerAnimationController.isDefinitionStartEligible(world, definition, actors, positionAnchorUuid); + } else { + return false; + } + } + + public static boolean isAnimationStartEligible( + ServerWorld world, + @Nullable AnimationDefinitions.Definition definition, + List actors, + List actorKeys, + @Nullable Entity positionAnchor + ) { + if (world != null && actors != null && !actors.isEmpty()) { + UUID positionAnchorUuid = null; + if (positionAnchor != null && positionAnchor.getEntityWorld() == world) { + UUID candidate = positionAnchor.getUuid(); + boolean anchorIsActor = actors.stream().anyMatch(e -> e != null && e.getUuid().equals(candidate)); + if (anchorIsActor) { + positionAnchorUuid = candidate; + } + } + + return ServerAnimationController.isDefinitionStartEligible(world, definition, actors, actorKeys, positionAnchorUuid); + } else { + return false; + } + } + + public static boolean advanceStage(ServerWorld world, UUID instanceId) { + return ServerAnimationController.advanceStage(world, instanceId); + } + + public static boolean advanceToStage(ServerWorld world, UUID instanceId, int stageNumber) { + return ServerAnimationController.advanceToStage(world, instanceId, stageNumber); + } + + public static void adjustSpeedSilently(ServerWorld world, UUID instanceId, double multiplier) { + ServerAnimationController.adjustSpeedSilently(world, instanceId, multiplier); + } + + @Nullable + public static AnimationStageInfo getCurrentStage(ServerWorld world, UUID instanceId) { + return ServerAnimationController.getCurrentStage(world, instanceId); + } + + @Nullable + public static Identifier getActiveAnimationId(ServerWorld world, UUID instanceId) { + ServerAnimationController.ActiveInstanceSnapshot snapshot = ServerAnimationController.getActiveInstanceSnapshot(world, instanceId); + return snapshot == null ? null : snapshot.animationId(); + } + + public static boolean isJoinTransitionAllowed(@Nullable Identifier predecessorAnimationId, @Nullable Identifier successorAnimationId) { + return predecessorAnimationId != null && successorAnimationId != null + ? AnimationDefinitions.isJoinCandidateAllowed(predecessorAnimationId, AnimationDefinitions.getDefinition(successorAnimationId)) + : false; + } + + public static boolean stopAnimation(ServerWorld world, UUID instanceId) { + return ServerAnimationController.stopInstance(world, instanceId); + } + + public static boolean enqueueAnimation( + ServerWorld world, + Identifier animationId, + List actors, + int insertIndex, + @Nullable NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Map metadata, + @Nullable ServerPlayerEntity requester + ) { + return ServerAnimationController.enqueueForPlayerWithContext( + world, + requester, + animationId, + actors, + insertIndex, + damageBehavior == null ? NonAnimationDamageBehavior.IGNORE_DAMAGE : damageBehavior, + ignoreAttackers, + AnimationStartContext.addonMetadata(sanitizeMetadata(metadata)), + false + ); + } + + public static void clearQueuedAnimations(ServerWorld world, ServerPlayerEntity player) { + ServerAnimationController.clearPlayerQueue(world, player); + } + + public static boolean isActorPendingOrActive(ServerWorld world, UUID actorUuid) { + return ServerAnimationController.isActorPendingOrActive(world, actorUuid); + } + + public static boolean shouldIgnoreAttackers(ServerWorld world, LivingEntity actor) { + return world != null && actor != null ? ServerAnimationController.shouldIgnoreAttackers(world, actor) : false; + } + + public static void setAnimationStartProtection(ServerPlayerEntity player, int ticks) { + ServerAnimationController.setAnimationStartProtection(player, ticks); + } + + public static void clearAnimationStartProtection(ServerPlayerEntity player) { + ServerAnimationController.clearAnimationStartProtection(player); + } + + public static boolean hasAnimationStartProtection(ServerWorld world, ServerPlayerEntity player) { + return ServerAnimationController.hasAnimationStartProtection(world, player); + } + + public static void stopAll(ServerWorld world) { + long stopTick = world.getTime() + 1L; + StopAllAnimationsS2CPayload payload = new StopAllAnimationsS2CPayload(stopTick); + + for (ServerPlayerEntity player : Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayerList()) { + if (player.getEntityWorld() == world) { + ServerPlayNetworking.send(player, payload); + } + } + + ServerAnimationController.clearAllInstancesInWorld(world); + } + + private static Map sanitizeMetadata(@Nullable Map metadata) { + if (metadata != null && !metadata.isEmpty()) { + LinkedHashMap out = new LinkedHashMap<>(); + + for (Entry entry : metadata.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key != null && !key.isBlank() && value != null) { + out.put(key, value); + } + } + } + + return out.isEmpty() ? Map.of() : Map.copyOf(out); + } else { + return Map.of(); + } + } + + @FunctionalInterface + public interface AnimationCandidateFilter { + boolean test(ServerWorld var1, AnimationDefinitions.Definition var2, List var3); + } + + public record EligibleAnimation( + Identifier animationId, + List actorKeys, + List stages, + List contentTags, + @Nullable String packId, + @Nullable String packName, + @Nullable String packAuthor, + @Nullable String displayName + ) { + public EligibleAnimation( + Identifier animationId, + List actorKeys, + List stages, + List contentTags, + @Nullable String packId, + @Nullable String packName, + @Nullable String packAuthor + ) { + this(animationId, actorKeys, stages, contentTags, packId, packName, packAuthor, null); + } + } + + public record MatchOptions( + @Nullable Set requiredAnimationTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable AnimationService.AnimationCandidateFilter candidateFilter, + @Nullable Identifier joinPredecessorAnimationId + ) { + public MatchOptions( + @Nullable Set requiredAnimationTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds, + @Nullable AnimationService.AnimationCandidateFilter candidateFilter + ) { + this(requiredAnimationTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, candidateFilter, null); + } + + public MatchOptions( + @Nullable Set requiredAnimationTags, + @Nullable Entity positionAnchor, + boolean requireStartEligibility, + @Nullable Entity requiredActiveActor, + @Nullable Set excludedAnimationIds + ) { + this(requiredAnimationTags, positionAnchor, requireStartEligibility, requiredActiveActor, excludedAnimationIds, null); + } + + public static AnimationService.MatchOptions defaults() { + return new AnimationService.MatchOptions(Set.of(), null, false, null, Set.of(), null); + } + + public static AnimationService.MatchOptions startEligible(@Nullable Entity positionAnchor) { + return new AnimationService.MatchOptions(Set.of(), positionAnchor, true, null, Set.of(), null); + } + } + + public record MatchedAnimation(Identifier animationId, List actorKeys, List stages) { + } + + public record StartOptions( + @Nullable NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Entity positionAnchor, + @Nullable ServerPlayerEntity requester, + @Nullable Map metadata, + boolean forceChat, + boolean allowMultiplePlayers + ) { + public StartOptions( + @Nullable NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable Entity positionAnchor, + @Nullable ServerPlayerEntity requester, + @Nullable Map metadata, + boolean forceChat + ) { + this(damageBehavior, ignoreAttackers, positionAnchor, requester, metadata, forceChat, false); + } + + public static AnimationService.StartOptions defaults() { + return new AnimationService.StartOptions(NonAnimationDamageBehavior.IGNORE_DAMAGE, false, null, null, Map.of(), false, false); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/api/NonAnimationApiBridge.java b/src/main/java/com/nonid/internal/animation/api/NonAnimationApiBridge.java new file mode 100644 index 0000000..12c9c0e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/api/NonAnimationApiBridge.java @@ -0,0 +1,74 @@ +package com.nonid.internal.animation.api; + +import com.nonid.api.animation.NonAnimationEvents; +import com.nonid.internal.animation.server.ServerAnimationController; + +public final class NonAnimationApiBridge { + private static boolean initialized; + + private NonAnimationApiBridge() { + } + + public static synchronized void initialize() { + if (!initialized) { + initialized = true; + AnimationLifecycleEvents.ALLOW_START + .register( + (AnimationLifecycleEvents.AllowStart)context -> ((NonAnimationEvents.BeforeStart)NonAnimationEvents.BEFORE_START.invoker()) + .allow( + new NonAnimationEvents.BeforeStartContext( + context.world(), + context.animationId(), + context.actorUuids(), + context.actorKeys(), + NonAnimationViews.stages(context.stages()), + context.requester(), + context.damageBehavior(), + context.ignoreAttackers(), + context.startContext().addonMetadata() + ) + ) + ); + AnimationLifecycleEvents.START + .register( + (AnimationLifecycleEvents.Start)context -> ((NonAnimationEvents.Started)NonAnimationEvents.STARTED.invoker()) + .onStarted(new NonAnimationEvents.StartedContext(context.world(), NonAnimationViews.session(context.snapshot()), context.requester())) + ); + AnimationLifecycleEvents.STOP + .register( + (AnimationLifecycleEvents.Stop)context -> { + ServerAnimationController.ActiveInstanceSnapshot snapshot = context.snapshot(); + ((NonAnimationEvents.Stopped)NonAnimationEvents.STOPPED.invoker()) + .onStopped( + new NonAnimationEvents.StoppedContext( + context.world(), + snapshot.instanceId(), + snapshot.animationId(), + snapshot.actorUuids(), + snapshot.actorKeys(), + NonAnimationViews.stages(snapshot.stages()) + ) + ); + } + ); + AnimationLifecycleEvents.STAGE_ADVANCE + .register( + (AnimationLifecycleEvents.StageAdvance)context -> { + ServerAnimationController.ActiveInstanceSnapshot snapshot = context.snapshot(); + ((NonAnimationEvents.StageChanged)NonAnimationEvents.STAGE_CHANGED.invoker()) + .onStageChanged( + new NonAnimationEvents.StageChangedContext( + context.world(), + snapshot.instanceId(), + snapshot.animationId(), + snapshot.actorUuids(), + snapshot.actorKeys(), + context.advanceTick(), + NonAnimationViews.stage(context.currentStage()) + ) + ); + } + ); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/api/NonAnimationViews.java b/src/main/java/com/nonid/internal/animation/api/NonAnimationViews.java new file mode 100644 index 0000000..1473ed9 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/api/NonAnimationViews.java @@ -0,0 +1,56 @@ +package com.nonid.internal.animation.api; + +import com.nonid.api.animation.NonAnimationSession; +import com.nonid.api.animation.NonAnimationStage; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.server.ServerAnimationController; +import java.util.List; +import org.jetbrains.annotations.Nullable; + +public final class NonAnimationViews { + private NonAnimationViews() { + } + + @Nullable + public static NonAnimationStage stage(@Nullable AnimationStageInfo stage) { + return stage == null + ? null + : new NonAnimationStage( + stage.animationId(), + stage.loop(), + stage.lengthTicks(), + stage.allowJoin(), + stage.speed(), + stage.playbackAnimationId(), + stage.actorProps() + .stream() + .map(prop -> new NonAnimationStage.ActorProp(prop.actorKey(), prop.propLeftItemId(), prop.propRightItemId(), prop.propFloorItemId())) + .toList(), + stage.cycleMidpointOffsetSeconds(), + stage.cycleTicks() + ); + } + + public static List stages(@Nullable List stages) { + return stages != null && !stages.isEmpty() ? stages.stream().map(NonAnimationViews::stage).toList() : List.of(); + } + + public static NonAnimationSession session(ServerAnimationController.ActiveInstanceSnapshot snapshot) { + List stages = snapshot.stages(); + NonAnimationStage currentStage = null; + if (stages != null && !stages.isEmpty()) { + int index = Math.max(0, Math.min(snapshot.stageIndex(), stages.size() - 1)); + currentStage = stage(stages.get(index)); + } + + return new NonAnimationSession( + snapshot.instanceId(), + snapshot.animationId(), + snapshot.actorUuids(), + currentStage, + snapshot.damageBehavior(), + snapshot.ignoreAttackers(), + snapshot.metadata() + ); + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/camera/AnimatedCameraPoseTracker.java b/src/main/java/com/nonid/internal/animation/client/camera/AnimatedCameraPoseTracker.java new file mode 100644 index 0000000..7e138cb --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/camera/AnimatedCameraPoseTracker.java @@ -0,0 +1,76 @@ +package com.nonid.internal.animation.client.camera; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +public final class AnimatedCameraPoseTracker { + private static final long HOLD_TICKS = 20L; + private static final Map SAMPLES = new ConcurrentHashMap<>(); + + private AnimatedCameraPoseTracker() { + } + + public static void capture( + UUID actorUuid, + UUID instanceId, + Identifier animationId, + Vec3d worldPos, + Vec3d bodyWorldPos, + Vec3d headForward, + Vec3d headUp, + Vec3d headRight, + Vec3d bodyForward, + Vec3d bodyUp, + Vec3d bodyRight, + long tick + ) { + if (actorUuid != null && worldPos != null) { + SAMPLES.put( + actorUuid, + new AnimatedCameraPoseTracker.PoseSample( + instanceId, animationId, worldPos, bodyWorldPos, headForward, headUp, headRight, bodyForward, bodyUp, bodyRight, tick + ) + ); + } + } + + @Nullable + public static AnimatedCameraPoseTracker.PoseSample resolve(UUID actorUuid, long nowTick) { + AnimatedCameraPoseTracker.PoseSample sample = SAMPLES.get(actorUuid); + if (sample == null) { + return null; + } else if (nowTick - sample.tick() > 20L) { + SAMPLES.remove(actorUuid, sample); + return null; + } else { + return sample; + } + } + + public static void prune(long nowTick) { + SAMPLES.entrySet().removeIf(entry -> nowTick - entry.getValue().tick() > 20L); + } + + public static void clear() { + SAMPLES.clear(); + } + + public record PoseSample( + UUID instanceId, + Identifier animationId, + Vec3d worldPos, + Vec3d bodyWorldPos, + Vec3d headForward, + Vec3d headUp, + Vec3d headRight, + Vec3d bodyForward, + Vec3d bodyUp, + Vec3d bodyRight, + long tick + ) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/camera/AnimationCameraZoom.java b/src/main/java/com/nonid/internal/animation/client/camera/AnimationCameraZoom.java new file mode 100644 index 0000000..0138058 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/camera/AnimationCameraZoom.java @@ -0,0 +1,64 @@ +package com.nonid.internal.animation.client.camera; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.Perspective; +import net.minecraft.util.math.MathHelper; + +public final class AnimationCameraZoom { + private static final float DEFAULT_DISTANCE = 4.0F; + private static final float MIN_DISTANCE = 0.5F; + private static final float MAX_DISTANCE = 8.0F; + private static final float SCROLL_STEP = 0.25F; + private static final float SMOOTHING_PER_TICK = 0.35F; + private static final float SNAP_EPSILON = 0.005F; + private static float previousDistance = 4.0F; + private static float currentDistance = 4.0F; + private static float targetDistance = 4.0F; + + private AnimationCameraZoom() { + } + + public static boolean handleScroll(MinecraftClient client, double verticalScroll) { + if (verticalScroll != 0.0 && isThirdPersonZoomAvailable(client)) { + float direction = verticalScroll > 0.0 ? -1.0F : 1.0F; + targetDistance = MathHelper.clamp(targetDistance + direction * 0.25F, 0.5F, 8.0F); + return true; + } else { + return false; + } + } + + public static void clientTick(MinecraftClient client) { + if (!isLocalPlayerAnimating(client)) { + reset(); + } else { + previousDistance = currentDistance; + float delta = targetDistance - currentDistance; + if (Math.abs(delta) <= 0.005F) { + currentDistance = targetDistance; + } else { + currentDistance = MathHelper.lerp(0.35F, currentDistance, targetDistance); + } + } + } + + public static float distance(float tickDelta) { + float progress = MathHelper.clamp(tickDelta, 0.0F, 1.0F); + return MathHelper.lerp(progress, previousDistance, currentDistance); + } + + public static void reset() { + previousDistance = 4.0F; + currentDistance = 4.0F; + targetDistance = 4.0F; + } + + public static boolean isLocalPlayerAnimating(MinecraftClient client) { + return client != null && client.player != null && ClientAnimationRuntime.isActorPendingOrActive(client.player.getUuid()); + } + + private static boolean isThirdPersonZoomAvailable(MinecraftClient client) { + return isLocalPlayerAnimating(client) && client.currentScreen == null && client.options.getPerspective() != Perspective.FIRST_PERSON; + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/camera/CameraPositionAccess.java b/src/main/java/com/nonid/internal/animation/client/camera/CameraPositionAccess.java new file mode 100644 index 0000000..9bf09a0 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/camera/CameraPositionAccess.java @@ -0,0 +1,7 @@ +package com.nonid.internal.animation.client.camera; + +import net.minecraft.util.math.Vec3d; + +public interface CameraPositionAccess { + Vec3d afw$getPos(); +} diff --git a/src/main/java/com/nonid/internal/animation/client/camera/FirstPersonCameraCollision.java b/src/main/java/com/nonid/internal/animation/client/camera/FirstPersonCameraCollision.java new file mode 100644 index 0000000..cb9446e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/camera/FirstPersonCameraCollision.java @@ -0,0 +1,95 @@ +package com.nonid.internal.animation.client.camera; + +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.CollisionView; +import org.jetbrains.annotations.Nullable; + +public final class FirstPersonCameraCollision { + private static final double CAMERA_BOX_SIZE = 0.25; + private static final double PUSH_EPSILON = 0.005; + private static final int MAX_PUSH_ITERATIONS = 8; + + private FirstPersonCameraCollision() { + } + + public static Vec3d resolve(CollisionView world, Vec3d desiredPos) { + if (world != null && desiredPos != null) { + Vec3d resolvedPos = desiredPos; + + for (int i = 0; i < 8; i++) { + Box cameraBox = Box.of(resolvedPos, 0.25, 0.25, 0.25); + if (world.isSpaceEmpty(cameraBox)) { + return resolvedPos; + } + + Vec3d push = computeMinimalPush(world, cameraBox); + if (push == null || push.lengthSquared() < 1.0E-10) { + return resolvedPos; + } + + resolvedPos = resolvedPos.add(push); + } + + return resolvedPos; + } else { + return desiredPos; + } + } + + @Nullable + private static Vec3d computeMinimalPush(CollisionView world, Box cameraBox) { + double bestDistanceSq = Double.POSITIVE_INFINITY; + Vec3d bestPush = null; + + for (VoxelShape shape : world.getBlockCollisions(null, cameraBox.expand(0.005))) { + for (Box blockBox : shape.getBoundingBoxes()) { + if (cameraBox.intersects(blockBox)) { + Vec3d candidate = smallestSeparatingPush(cameraBox, blockBox); + if (candidate != null) { + double distanceSq = candidate.lengthSquared(); + if (distanceSq < bestDistanceSq) { + bestDistanceSq = distanceSq; + bestPush = candidate; + } + } + } + } + } + + return bestPush; + } + + @Nullable + private static Vec3d smallestSeparatingPush(Box cameraBox, Box blockBox) { + double pushNegX = blockBox.minX - cameraBox.maxX - 0.005; + double pushPosX = blockBox.maxX - cameraBox.minX + 0.005; + double pushNegY = blockBox.minY - cameraBox.maxY - 0.005; + double pushPosY = blockBox.maxY - cameraBox.minY + 0.005; + double pushNegZ = blockBox.minZ - cameraBox.maxZ - 0.005; + double pushPosZ = blockBox.maxZ - cameraBox.minZ + 0.005; + Vec3d[] candidates = new Vec3d[]{ + new Vec3d(pushNegX, 0.0, 0.0), + new Vec3d(pushPosX, 0.0, 0.0), + new Vec3d(0.0, pushNegY, 0.0), + new Vec3d(0.0, pushPosY, 0.0), + new Vec3d(0.0, 0.0, pushNegZ), + new Vec3d(0.0, 0.0, pushPosZ) + }; + Vec3d best = null; + double bestDistanceSq = Double.POSITIVE_INFINITY; + + for (Vec3d candidate : candidates) { + if (!(candidate.lengthSquared() <= 1.0E-10)) { + double distanceSq = candidate.lengthSquared(); + if (distanceSq < bestDistanceSq) { + bestDistanceSq = distanceSq; + best = candidate; + } + } + } + + return best; + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/camera/FirstPersonLookConstraint.java b/src/main/java/com/nonid/internal/animation/client/camera/FirstPersonLookConstraint.java new file mode 100644 index 0000000..3c25203 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/camera/FirstPersonLookConstraint.java @@ -0,0 +1,142 @@ +package com.nonid.internal.animation.client.camera; + +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +public final class FirstPersonLookConstraint { + private static final double BACKWARD_HARD_DEGREES = 160.0; + private static final double DOWNWARD_HARD_DEGREES = 60.0; + private static final double BACKWARD_FULL_LIMIT_UPWARD_PITCH_DEGREES = 20.0; + private static final double BACKWARD_DISABLE_UPWARD_PITCH_DEGREES = 40.0; + private static final double MIN_HORIZONTAL_LENGTH_FOR_YAW = 0.001; + + private FirstPersonLookConstraint() { + } + + public static Vec3d constrain(Vec3d desiredLook, @Nullable AnimatedCameraPoseTracker.PoseSample pose, @Nullable Vec3d previousConstrainedLook) { + if (desiredLook == null) { + return Vec3d.ZERO; + } + + Vec3d normalizedLook = desiredLook.normalize(); + if (pose == null) { + return normalizedLook; + } + + Vec3d bodyForward = normalize(pose.bodyForward()); + Vec3d bodyUp = normalize(pose.bodyUp()); + Vec3d bodyRight = normalize(pose.bodyRight()); + if (bodyForward != null && bodyUp != null && bodyRight != null) { + FirstPersonLookConstraint.LocalAngles desiredAngles = toLocalAngles(normalizedLook, bodyForward, bodyUp, bodyRight); + if (desiredAngles == null) { + return normalizedLook; + } + + double localYaw = desiredAngles.yawDegrees(); + double localPitch = desiredAngles.pitchDegrees(); + FirstPersonLookConstraint.LocalAngles previousAngles = toLocalAngles(previousConstrainedLook, bodyForward, bodyUp, bodyRight); + if (previousAngles != null) { + localYaw = unwrapNear(localYaw, previousAngles.yawDegrees()); + } + + double constrainedPitch = constrainDownward(localPitch, 60.0); + double effectiveBackwardLimit = computeEffectiveBackwardLimit(localPitch); + double constrainedYaw = constrainSymmetric(localYaw, effectiveBackwardLimit); + if (Math.abs(constrainedYaw - localYaw) < 1.0E-4 && Math.abs(constrainedPitch - localPitch) < 1.0E-4) { + return normalizedLook; + } + + double yawRad = Math.toRadians(constrainedYaw); + double pitchRad = Math.toRadians(constrainedPitch); + double correctedHorizontal = Math.cos(pitchRad); + Vec3d correctedLocal = new Vec3d(Math.sin(yawRad) * correctedHorizontal, Math.sin(pitchRad), Math.cos(yawRad) * correctedHorizontal); + Vec3d correctedWorld = bodyRight.multiply(correctedLocal.x).add(bodyUp.multiply(correctedLocal.y)).add(bodyForward.multiply(correctedLocal.z)); + return correctedWorld.normalize(); + } else { + return normalizedLook; + } + } + + public static float toMinecraftYaw(Vec3d lookVector, float fallbackYaw) { + Vec3d normalized = lookVector == null ? Vec3d.ZERO : lookVector.normalize(); + return normalized.horizontalLengthSquared() <= 0.001 ? fallbackYaw : (float)Math.toDegrees(Math.atan2(-normalized.x, normalized.z)); + } + + public static float toMinecraftPitch(Vec3d lookVector) { + Vec3d normalized = lookVector == null ? Vec3d.ZERO : lookVector.normalize(); + double clampedY = MathHelper.clamp(normalized.y, -1.0, 1.0); + return (float)(-Math.toDegrees(Math.asin(clampedY))); + } + + private static double constrainSymmetric(double angleDegrees, double hardLimit) { + double sign = Math.signum(angleDegrees); + double absolute = Math.abs(angleDegrees); + return absolute <= hardLimit ? angleDegrees : sign * hardLimit; + } + + private static double constrainDownward(double pitchDegrees, double hardLimit) { + double downward = Math.max(0.0, -pitchDegrees); + return downward <= hardLimit ? pitchDegrees : -hardLimit; + } + + private static double computeEffectiveBackwardLimit(double localPitchDegrees) { + if (localPitchDegrees <= 20.0) { + return 160.0; + } + + if (localPitchDegrees >= 40.0) { + return 180.0; + } + + double t = (localPitchDegrees - 20.0) / Math.max(1.0E-6, 20.0); + return MathHelper.lerp(t, 160.0, 180.0); + } + + @Nullable + private static Vec3d normalize(@Nullable Vec3d vector) { + if (vector == null) { + return null; + } + + if (vector.lengthSquared() < 1.0E-8) { + return null; + } + + Vec3d normalized = vector.normalize(); + return !normalized.isFinite() ? null : normalized; + } + + @Nullable + private static FirstPersonLookConstraint.LocalAngles toLocalAngles(@Nullable Vec3d look, Vec3d bodyForward, Vec3d bodyUp, Vec3d bodyRight) { + Vec3d normalizedLook = normalize(look); + if (normalizedLook == null) { + return null; + } + + double localX = normalizedLook.dotProduct(bodyRight); + double localY = normalizedLook.dotProduct(bodyUp); + double localZ = normalizedLook.dotProduct(bodyForward); + double horizontalLength = Math.sqrt(localX * localX + localZ * localZ); + double localYaw = Math.toDegrees(Math.atan2(localX, localZ)); + double localPitch = Math.toDegrees(Math.atan2(localY, Math.max(1.0E-6, horizontalLength))); + return new FirstPersonLookConstraint.LocalAngles(localYaw, localPitch); + } + + private static double unwrapNear(double angleDegrees, double referenceDegrees) { + double unwrapped = angleDegrees; + + while (unwrapped - referenceDegrees > 180.0) { + unwrapped -= 360.0; + } + + while (unwrapped - referenceDegrees < -180.0) { + unwrapped += 360.0; + } + + return unwrapped; + } + + private record LocalAngles(double yawDegrees, double pitchDegrees) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderCompat.java b/src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderCompat.java new file mode 100644 index 0000000..bb83831 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderCompat.java @@ -0,0 +1,955 @@ +package com.nonid.internal.animation.client.compat.wildfire; + +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.mixin.client.EntityRenderManagerAccessor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.Map.Entry; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.BipedEntityRenderState; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.EntityPose; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public final class WildfireGenderCompat { + private static final String MOD_ID = "wildfire_gender"; + private static final WildfireGenderState.UvRect DEFAULT_LEFT_BASE_UV = WildfireGenderState.UvRect.pixels(20, 20, 24, 26); + private static final WildfireGenderState.UvRect DEFAULT_RIGHT_BASE_UV = WildfireGenderState.UvRect.pixels(24, 20, 28, 26); + private static final WildfireGenderState.UvRect DEFAULT_LEFT_OVERLAY_UV = WildfireGenderState.UvRect.pixels(20, 36, 24, 42); + private static final WildfireGenderState.UvRect DEFAULT_RIGHT_OVERLAY_UV = WildfireGenderState.UvRect.pixels(24, 36, 28, 42); + private static final float POSITION_X_MIN = -1.25F; + private static final float POSITION_X_MAX = 1.25F; + private static final float POSITION_Y_MIN = -1.25F; + private static final float POSITION_Y_MAX = 1.75F; + private static final float POSITION_REFERENCE_DELTA = 0.08F; + private static final float POSITION_RESPONSE_EXPONENT = 0.55F; + private static final float PHYSICS_TARGET_RESPONSE = 0.1F; + private static final float GRAVITY_MOVEMENT_BOOST = 0.8F; + private static final float GRAVITY_SIDE_POSITION_SCALE = 0.35F; + private static final float GRAVITY_FORWARD_POSITION_SCALE = 0.45F; + private static final float LOCAL_FORWARD_VERTICAL_SCALE = 0.4F; + private static final float ROTATION_YAW_DIVISOR = 15.0F; + private static final float ROTATION_VERTICAL_SCALE = 18.0F; + private static final float ROTATION_LATERAL_SCALE = 16.0F; + private static final float ROTATION_FORWARD_SCALE = 20.0F; + private static final float GRAVITY_SIDE_ROTATION_SCALE = 4.0F; + private static final float GRAVITY_FORWARD_ROTATION_SCALE = 6.0F; + private static final float ROTATION_MIN = -25.0F; + private static final float ROTATION_MAX = 25.0F; + private static final float CHEST_SAMPLE_LOCAL_X = 0.0F; + private static final float CHEST_SAMPLE_LOCAL_Y = 0.3F; + private static final float CHEST_SAMPLE_LOCAL_Z = -0.35F; + private static final float BOUNCE_INTENSITY_SCALE = 12.0F; + private static final float BOUNCE_INTENSITY_MIN = 0.15F; + private static final float BOUNCE_INTENSITY_MAX = 3.0F; + private static final float RENDER_GRAVITY_POSITION_X_SCALE = 0.45F; + private static final float RENDER_GRAVITY_POSITION_Y_SCALE = 0.85F; + private static final float RENDER_GRAVITY_ROTATION_SIDE_SCALE = 10.0F; + private static final float RENDER_GRAVITY_ROTATION_FORWARD_SCALE = 2.0F; + private static final float RENDER_GRAVITY_POSITION_X_LIMIT = 0.8F; + private static final float RENDER_GRAVITY_POSITION_Y_LIMIT = 1.2F; + private static final float RENDER_GRAVITY_ROTATION_LIMIT = 10.0F; + private static boolean initialized; + private static boolean available; + private static boolean loggedFailure; + private static Method getRenderState; + private static Field genderField; + private static Field bustSizeField; + private static Field armorPhysicsOverrideField; + private static Field showBreastsInArmorField; + private static Field hasJacketLayerField; + private static Field breastsField; + private static Field leftPhysicsField; + private static Field rightPhysicsField; + private static Field leftUvField; + private static Field rightUvField; + private static Field leftOverlayUvField; + private static Field rightOverlayUvField; + private static Method genderCanHaveBreasts; + private static Field breastXOffsetField; + private static Field breastYOffsetField; + private static Field breastZOffsetField; + private static Field breastCleavageField; + private static Field breastUniboobField; + private static Method physicsPositionXMethod; + private static Method physicsPositionYMethod; + private static Method physicsBreastSizeMethod; + private static Method physicsBounceRotationMethod; + private static Field physicsPrePositionXField; + private static Field physicsPositionXField; + private static Field physicsPrePositionYField; + private static Field physicsPositionYField; + private static Field physicsPreBounceRotationField; + private static Field physicsBounceRotationField; + private static Method uvGetAllSidesMethod; + private static Method uvQuadX1Method; + private static Method uvQuadY1Method; + private static Method uvQuadX2Method; + private static Method uvQuadY2Method; + private static Class genderLayerClass; + private static Method genderLayerSubmitMethod; + private static Method contextModelMethod; + private static Field modelBodyField; + private static Field bodyOriginXField; + private static Field bodyOriginYField; + private static Field bodyOriginZField; + private static Field bodyPitchField; + private static Field bodyYawField; + private static Field bodyRollField; + private static Method entityConfigGetEntityMethod; + private static Constructor breastPhysicsConstructor; + private static Method breastPhysicsFinishTickMethod; + private static Field breastPhysicsTargetBounceXField; + private static Field breastPhysicsTargetBounceYField; + private static Field breastPhysicsTargetRotVelField; + private static Field breastPhysicsPrePositionXField; + private static Field breastPhysicsPositionXField; + private static Field breastPhysicsPrePositionYField; + private static Field breastPhysicsPositionYField; + private static Field breastPhysicsPreBounceRotationField; + private static Field breastPhysicsBounceRotationField; + private static final Map GENDER_LAYERS = new IdentityHashMap<>(); + private static final Map ANIMATION_PHYSICS = new HashMap<>(); + + private WildfireGenderCompat() { + } + + public static WildfireGenderState resolve(LivingEntity entity, LivingEntityRenderState renderState) { + if (entity == null || renderState == null) { + return null; + } + + if (!isAvailable()) { + return null; + } + + try { + Object wildfireState = getRenderState.invoke(null, renderState); + if (wildfireState == null) { + return null; + } + + Object gender = genderField.get(wildfireState); + if (!canHaveBreasts(gender)) { + return null; + } + + Object breasts = breastsField.get(wildfireState); + if (breasts == null) { + return null; + } + + cacheAnimationPhysics(entity); + float bustSize = getFloat(bustSizeField, wildfireState, 0.0F); + if (bustSize <= 0.02F) { + return null; + } + + boolean hasJacketLayer = getBoolean(hasJacketLayerField, wildfireState, false); + float xOffset = getFloat(breastXOffsetField, breasts, 0.0F); + float yOffset = getFloat(breastYOffsetField, breasts, 0.0F); + float zOffset = getFloat(breastZOffsetField, breasts, 0.0F); + float cleavage = getFloat(breastCleavageField, breasts, 0.05F); + boolean uniboob = getBoolean(breastUniboobField, breasts, false); + return new WildfireGenderState( + bustSize, + xOffset, + yOffset, + zOffset, + cleavage, + uniboob, + hasJacketLayer, + readSide( + leftPhysicsField.get(wildfireState), + leftUvField.get(wildfireState), + leftOverlayUvField.get(wildfireState), + DEFAULT_LEFT_BASE_UV, + DEFAULT_LEFT_OVERLAY_UV + ), + readSide( + rightPhysicsField.get(wildfireState), + rightUvField.get(wildfireState), + rightOverlayUvField.get(wildfireState), + DEFAULT_RIGHT_BASE_UV, + DEFAULT_RIGHT_OVERLAY_UV + ) + ); + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure("Failed to read Female Gender render state.", e); + return null; + } + } + + public static boolean submitOriginalLayer(LivingEntityRenderState renderState, MatrixStack matrices, OrderedRenderCommandQueue queue, int packedLight) { + if (renderState == null || matrices == null || queue == null) { + return false; + } + + if (!isAvailable()) { + return false; + } + + try { + Object layer = getOrCreateOriginalLayer(renderState); + if (layer != null && genderLayerSubmitMethod != null) { + Object wildfireState = getRenderState.invoke(null, renderState); + Boolean previousShowBreastsInArmor = forceShowBreastsInArmor(wildfireState); + Boolean previousArmorPhysicsOverride = forceArmorPhysicsOverride(wildfireState); + WildfireGenderCompat.RenderPoseSnapshot renderPose = neutralizeRenderPose(renderState); + WildfireGenderCompat.BodyTransformSnapshot bodyTransform = neutralizeContextBodyTransform(layer); + + try { + genderLayerSubmitMethod.invoke(layer, matrices, queue, packedLight, renderState, 0.0F, 0.0F); + return true; + } finally { + restoreContextBodyTransform(bodyTransform); + restoreRenderPose(renderState, renderPose); + restoreShowBreastsInArmor(wildfireState, previousShowBreastsInArmor); + restoreArmorPhysicsOverride(wildfireState, previousArmorPhysicsOverride); + } + } else { + return false; + } + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure("Failed to submit Female Gender original render layer.", e); + return false; + } + } + + @Nullable + private static Boolean forceShowBreastsInArmor(@Nullable Object wildfireState) throws IllegalAccessException { + if (wildfireState != null && showBreastsInArmorField != null) { + boolean previous = showBreastsInArmorField.getBoolean(wildfireState); + showBreastsInArmorField.setBoolean(wildfireState, true); + return previous; + } else { + return null; + } + } + + private static void restoreShowBreastsInArmor(@Nullable Object wildfireState, @Nullable Boolean previous) throws IllegalAccessException { + if (wildfireState != null && previous != null && showBreastsInArmorField != null) { + showBreastsInArmorField.setBoolean(wildfireState, previous); + } + } + + @Nullable + private static Boolean forceArmorPhysicsOverride(@Nullable Object wildfireState) throws IllegalAccessException { + if (wildfireState != null && armorPhysicsOverrideField != null) { + boolean previous = armorPhysicsOverrideField.getBoolean(wildfireState); + armorPhysicsOverrideField.setBoolean(wildfireState, true); + return previous; + } else { + return null; + } + } + + private static void restoreArmorPhysicsOverride(@Nullable Object wildfireState, @Nullable Boolean previous) throws IllegalAccessException { + if (wildfireState != null && previous != null && armorPhysicsOverrideField != null) { + armorPhysicsOverrideField.setBoolean(wildfireState, previous); + } + } + + public static void overrideRenderPhysics(UUID playerUuid, LivingEntityRenderState renderState, MatrixStack rootPose, MatrixStack bonePose) { + if (playerUuid != null && renderState != null && rootPose != null && bonePose != null) { + if (isAvailable()) { + try { + Object wildfireState = getRenderState.invoke(null, renderState); + if (wildfireState == null) { + return; + } + + WildfireGenderCompat.AnimationPhysicsPair physicsPair = ANIMATION_PHYSICS.get(playerUuid); + if (physicsPair == null) { + return; + } + + WildfireGenderCompat.BonePhysicsSample sample = WildfireGenderCompat.BonePhysicsSample.from(rootPose, bonePose); + if (sample == null) { + return; + } + + long worldTick = currentClientWorldTick(); + if (physicsPair.lastSample == null) { + physicsPair.lastSample = sample; + physicsPair.lastTick = worldTick; + } else if (physicsPair.lastTick != worldTick) { + stepAnimationPhysics(wildfireState, physicsPair, physicsPair.lastSample, sample); + physicsPair.lastSample = sample; + physicsPair.lastTick = worldTick; + } + + Object leftPhysics = leftPhysicsField.get(wildfireState); + Object rightPhysics = rightPhysicsField.get(wildfireState); + copyRealPhysicsToRenderState(leftPhysics, physicsPair.left, physicsPair.previousLeftBias, physicsPair.leftBias); + copyRealPhysicsToRenderState(rightPhysics, physicsPair.right, physicsPair.previousRightBias, physicsPair.rightBias); + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure("Failed to override Female Gender render physics from animated bone motion.", e); + } + } + } + } + + private static void stepAnimationPhysics( + Object wildfireState, + WildfireGenderCompat.AnimationPhysicsPair physicsPair, + WildfireGenderCompat.BonePhysicsSample previous, + WildfireGenderCompat.BonePhysicsSample current + ) throws ReflectiveOperationException { + float dx = current.chestX() - previous.chestX(); + float dy = current.chestY() - previous.chestY(); + float dz = current.chestZ() - previous.chestZ(); + Vec3d delta = new Vec3d(dx, dy, dz); + float lateral = (float)delta.dotProduct(previous.right()); + float localUp = (float)delta.dotProduct(previous.up()); + float localForward = (float)delta.dotProduct(previous.forward()); + float yawDelta = signedYawDelta(previous.forward(), current.forward()); + WildfireGenderCompat.GravityBias gravity = WildfireGenderCompat.GravityBias.from(current); + float movementBoost = 1.0F + gravity.tiltInfluence() * 0.8F; + float verticalMovement = localUp + localForward * gravity.forwardDown() * 0.4F; + float bustSize = getFloat(bustSizeField, wildfireState, 0.0F); + float bounceIntensity = clamp(bustSize * 12.0F, 0.15F, 3.0F); + float targetX = shapedImpulse(lateral * movementBoost, 0.08F, -1.25F, 1.25F, 0.55F); + float targetY = shapedImpulse(-verticalMovement * movementBoost, 0.08F, -1.25F, 1.75F, 0.55F); + targetX = clamp(targetX + gravity.sidePull() * 0.35F, -1.25F, 1.25F); + targetY = clamp(targetY + gravity.forwardDown() * 0.45F, -1.25F, 1.75F); + float targetRotVel = clamp( + -(yawDelta / 15.0F) * bounceIntensity + + -localUp * 18.0F * bounceIntensity + + lateral * 16.0F * bounceIntensity + + localForward * gravity.forwardDown() * 20.0F * bounceIntensity + + gravity.sidePull() * 4.0F * bounceIntensity + + gravity.forwardDown() * 6.0F * bounceIntensity, + -25.0F, + 25.0F + ); + boolean uniboob = isUniboob(wildfireState); + physicsPair.stepRenderBias(gravity, uniboob); + WildfireGenderCompat.TargetImpulse smoothedTarget = physicsPair.smoothTarget(targetX, targetY, targetRotVel); + if (uniboob) { + driveRealPhysics(physicsPair.left, smoothedTarget.x(), smoothedTarget.y(), 0.0F); + driveRealPhysics(physicsPair.right, smoothedTarget.x(), smoothedTarget.y(), 0.0F); + } else { + driveRealPhysics(physicsPair.left, smoothedTarget.x(), smoothedTarget.y(), smoothedTarget.rotation()); + driveRealPhysics(physicsPair.right, -smoothedTarget.x() * 0.85F, smoothedTarget.y() * 0.95F, -smoothedTarget.rotation() * 0.85F); + } + } + + private static void cacheAnimationPhysics(LivingEntity entity) throws ReflectiveOperationException { + if (entity != null && entityConfigGetEntityMethod != null) { + Object config = entityConfigGetEntityMethod.invoke(null, entity); + if (config != null) { + UUID uuid = entity.getUuid(); + WildfireGenderCompat.AnimationPhysicsPair existing = ANIMATION_PHYSICS.get(uuid); + if (existing == null || existing.config != config) { + Object left = breastPhysicsConstructor.newInstance(config); + Object right = breastPhysicsConstructor.newInstance(config); + ANIMATION_PHYSICS.put(uuid, new WildfireGenderCompat.AnimationPhysicsPair(config, left, right)); + } + } + } + } + + private static void driveRealPhysics(Object physics, float targetX, float targetY, float targetRotVel) throws ReflectiveOperationException { + if (physics != null) { + breastPhysicsPrePositionXField.setFloat(physics, breastPhysicsPositionXField.getFloat(physics)); + breastPhysicsPrePositionYField.setFloat(physics, breastPhysicsPositionYField.getFloat(physics)); + breastPhysicsPreBounceRotationField.setFloat(physics, breastPhysicsBounceRotationField.getFloat(physics)); + breastPhysicsTargetBounceXField.setFloat(physics, targetX); + breastPhysicsTargetBounceYField.setFloat(physics, targetY); + breastPhysicsTargetRotVelField.setFloat(physics, targetRotVel); + breastPhysicsFinishTickMethod.invoke(physics); + } + } + + private static void copyRealPhysicsToRenderState( + Object renderPhysics, Object realPhysics, WildfireGenderCompat.SideRenderBias previousBias, WildfireGenderCompat.SideRenderBias bias + ) throws IllegalAccessException { + if (renderPhysics != null && realPhysics != null) { + if (physicsPositionXField != null && physicsPrePositionXField != null) { + physicsPrePositionXField.setFloat(renderPhysics, breastPhysicsPrePositionXField.getFloat(realPhysics) + previousBias.x()); + physicsPositionXField.setFloat(renderPhysics, breastPhysicsPositionXField.getFloat(realPhysics) + bias.x()); + } + + if (physicsPositionYField != null && physicsPrePositionYField != null) { + physicsPrePositionYField.setFloat(renderPhysics, breastPhysicsPrePositionYField.getFloat(realPhysics) + previousBias.y()); + physicsPositionYField.setFloat(renderPhysics, breastPhysicsPositionYField.getFloat(realPhysics) + bias.y()); + } + + if (physicsBounceRotationField != null && physicsPreBounceRotationField != null) { + physicsPreBounceRotationField.setFloat(renderPhysics, breastPhysicsPreBounceRotationField.getFloat(realPhysics) + previousBias.rotation()); + physicsBounceRotationField.setFloat(renderPhysics, breastPhysicsBounceRotationField.getFloat(realPhysics) + bias.rotation()); + } + } + } + + private static long currentClientWorldTick() { + MinecraftClient client = MinecraftClient.getInstance(); + return client != null && client.world != null ? client.world.getTime() : Long.MIN_VALUE; + } + + private static boolean isUniboob(Object wildfireState) throws ReflectiveOperationException { + Object breasts = breastsField.get(wildfireState); + return breasts != null && getBoolean(breastUniboobField, breasts, false); + } + + private static Object getOrCreateOriginalLayer(LivingEntityRenderState renderState) throws ReflectiveOperationException { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.getEntityRenderDispatcher() != null) { + if (!(client.getEntityRenderDispatcher() instanceof EntityRenderManagerAccessor accessor)) { + return null; + } else { + EntityRenderer renderer = accessor.afw$getRenderer(renderState); + if (renderer == null) { + return null; + } + + Object cached = GENDER_LAYERS.get(renderer); + if (cached != null) { + return cached; + } + + Constructor constructor = null; + + for (Constructor candidate : genderLayerClass.getConstructors()) { + if (candidate.getParameterCount() == 1) { + Class parameter = candidate.getParameterTypes()[0]; + if (parameter.isAssignableFrom(renderer.getClass())) { + candidate.setAccessible(true); + constructor = candidate; + break; + } + } + } + + if (constructor == null) { + return null; + } + + Object created = constructor.newInstance(renderer); + GENDER_LAYERS.put(renderer, created); + return created; + } + } else { + return null; + } + } + + @Nullable + private static WildfireGenderCompat.RenderPoseSnapshot neutralizeRenderPose(LivingEntityRenderState renderState) { + if (renderState == null) { + return null; + } + + WildfireGenderCompat.RenderPoseSnapshot snapshot = new WildfireGenderCompat.RenderPoseSnapshot( + renderState.pose, renderState instanceof BipedEntityRenderState biped && biped.isInSneakingPose + ); + renderState.pose = EntityPose.STANDING; + if (renderState instanceof BipedEntityRenderState biped) { + biped.isInSneakingPose = false; + } + + return snapshot; + } + + private static void restoreRenderPose(LivingEntityRenderState renderState, @Nullable WildfireGenderCompat.RenderPoseSnapshot snapshot) { + if (renderState != null && snapshot != null) { + renderState.pose = snapshot.pose(); + if (renderState instanceof BipedEntityRenderState biped) { + biped.isInSneakingPose = snapshot.sneaking(); + } + } + } + + private static WildfireGenderCompat.BodyTransformSnapshot neutralizeContextBodyTransform(Object layer) throws IllegalAccessException { + Object body = getContextBody(layer); + if (body != null + && bodyOriginXField != null + && bodyOriginYField != null + && bodyOriginZField != null + && bodyPitchField != null + && bodyYawField != null + && bodyRollField != null) { + WildfireGenderCompat.BodyTransformSnapshot snapshot = new WildfireGenderCompat.BodyTransformSnapshot( + body, + bodyOriginXField.getFloat(body), + bodyOriginYField.getFloat(body), + bodyOriginZField.getFloat(body), + bodyPitchField.getFloat(body), + bodyYawField.getFloat(body), + bodyRollField.getFloat(body) + ); + bodyOriginXField.setFloat(body, 0.0F); + bodyOriginYField.setFloat(body, 0.0F); + bodyOriginZField.setFloat(body, 0.0F); + bodyPitchField.setFloat(body, 0.0F); + bodyYawField.setFloat(body, 0.0F); + bodyRollField.setFloat(body, 0.0F); + return snapshot; + } else { + return null; + } + } + + private static void restoreContextBodyTransform(WildfireGenderCompat.BodyTransformSnapshot snapshot) throws IllegalAccessException { + if (snapshot != null) { + bodyOriginXField.setFloat(snapshot.body(), snapshot.originX()); + bodyOriginYField.setFloat(snapshot.body(), snapshot.originY()); + bodyOriginZField.setFloat(snapshot.body(), snapshot.originZ()); + bodyPitchField.setFloat(snapshot.body(), snapshot.pitch()); + bodyYawField.setFloat(snapshot.body(), snapshot.yaw()); + bodyRollField.setFloat(snapshot.body(), snapshot.roll()); + } + } + + private static Object getContextBody(Object layer) { + try { + if (contextModelMethod == null) { + contextModelMethod = findNoArgMethod(layer.getClass(), "getContextModel", "method_17165"); + } + + if (contextModelMethod == null) { + return null; + } + + Object model = contextModelMethod.invoke(layer); + if (model == null) { + return null; + } + + if (modelBodyField == null) { + modelBodyField = findField(model.getClass(), "body", "field_3391"); + } + + if (modelBodyField == null) { + return null; + } + + Object body = modelBodyField.get(model); + if (body == null) { + return null; + } + + Class bodyClass = body.getClass(); + if (bodyOriginXField == null) { + bodyOriginXField = findField(bodyClass, "originX", "pivotX", "field_3657"); + } + + if (bodyOriginYField == null) { + bodyOriginYField = findField(bodyClass, "originY", "pivotY", "field_3656"); + } + + if (bodyOriginZField == null) { + bodyOriginZField = findField(bodyClass, "originZ", "pivotZ", "field_3655"); + } + + if (bodyPitchField == null) { + bodyPitchField = findField(bodyClass, "pitch", "xRot", "field_3674"); + } + + if (bodyYawField == null) { + bodyYawField = findField(bodyClass, "yaw", "yRot", "field_3675"); + } + + if (bodyRollField == null) { + bodyRollField = findField(bodyClass, "roll", "zRot", "field_3654"); + } + + return body; + } catch (ReflectiveOperationException | RuntimeException e) { + logFailure("Failed to neutralize Female Gender vanilla body transform.", e); + return null; + } + } + + private static WildfireGenderState.Side readSide( + Object physics, Object baseUv, Object overlayUv, WildfireGenderState.UvRect defaultBase, WildfireGenderState.UvRect defaultOverlay + ) throws ReflectiveOperationException { + return new WildfireGenderState.Side( + invokeFloat(physicsPositionXMethod, physics, 0.0F), + invokeFloat(physicsPositionYMethod, physics, 0.0F), + invokeFloat(physicsBreastSizeMethod, physics, 0.0F), + invokeFloat(physicsBounceRotationMethod, physics, 0.0F), + readNorthUv(baseUv, defaultBase), + readNorthUv(overlayUv, defaultOverlay) + ); + } + + private static WildfireGenderState.UvRect readNorthUv(Object uvLayout, WildfireGenderState.UvRect fallback) throws ReflectiveOperationException { + if (uvLayout != null && uvGetAllSidesMethod != null) { + if (uvGetAllSidesMethod.invoke(uvLayout) instanceof Map map) { + for (Entry entry : map.entrySet()) { + Object direction = entry.getKey(); + Object quad = entry.getValue(); + if (direction != null && quad != null && "NORTH".equals(String.valueOf(direction))) { + int x1 = (Integer)uvQuadX1Method.invoke(quad); + int y1 = (Integer)uvQuadY1Method.invoke(quad); + int x2 = (Integer)uvQuadX2Method.invoke(quad); + int y2 = (Integer)uvQuadY2Method.invoke(quad); + return WildfireGenderState.UvRect.pixels(x1, y1, x2, y2); + } + } + + return fallback; + } else { + return fallback; + } + } else { + return fallback; + } + } + + private static boolean canHaveBreasts(Object gender) throws ReflectiveOperationException { + if (gender == null) { + return false; + } else { + return genderCanHaveBreasts != null && genderCanHaveBreasts.invoke(gender) instanceof Boolean bool ? bool : !"MALE".equals(String.valueOf(gender)); + } + } + + private static boolean isAvailable() { + if (initialized) { + return available; + } + + initialized = true; + if (!FabricLoader.getInstance().isModLoaded("wildfire_gender")) { + available = false; + return false; + } + + try { + Class renderStateClass = Class.forName("com.wildfire.render.GenderRenderState"); + genderLayerClass = Class.forName("com.wildfire.render.GenderLayer"); + getRenderState = findStaticMethod(renderStateClass, "get", 1); + genderLayerSubmitMethod = findInstanceMethod(genderLayerClass, "submit", 6); + genderField = field(renderStateClass, "gender"); + bustSizeField = field(renderStateClass, "bustSize"); + armorPhysicsOverrideField = field(renderStateClass, "armorPhysicsOverride"); + showBreastsInArmorField = field(renderStateClass, "showBreastsInArmor"); + hasJacketLayerField = field(renderStateClass, "hasJacketLayer"); + breastsField = field(renderStateClass, "breasts"); + leftPhysicsField = field(renderStateClass, "leftBreastPhysics"); + rightPhysicsField = field(renderStateClass, "rightBreastPhysics"); + leftUvField = field(renderStateClass, "leftBreastUVLayout"); + rightUvField = field(renderStateClass, "rightBreastUVLayout"); + leftOverlayUvField = field(renderStateClass, "leftBreastOverlayUVLayout"); + rightOverlayUvField = field(renderStateClass, "rightBreastOverlayUVLayout"); + Class genderClass = Class.forName("com.wildfire.main.config.enums.Gender"); + genderCanHaveBreasts = methodOrNull(genderClass, "canHaveBreasts"); + Class breastStateClass = Class.forName("com.wildfire.render.GenderRenderState$BreastState"); + breastXOffsetField = field(breastStateClass, "xOffset"); + breastYOffsetField = field(breastStateClass, "yOffset"); + breastZOffsetField = field(breastStateClass, "zOffset"); + breastCleavageField = field(breastStateClass, "cleavage"); + breastUniboobField = field(breastStateClass, "uniboob"); + Class physicsClass = Class.forName("com.wildfire.render.GenderRenderState$BreastPhysicsState"); + physicsPositionXMethod = methodOrNull(physicsClass, "getPositionX"); + physicsPositionYMethod = methodOrNull(physicsClass, "getPositionY"); + physicsBreastSizeMethod = methodOrNull(physicsClass, "getBreastSize"); + physicsBounceRotationMethod = methodOrNull(physicsClass, "getBounceRotation"); + physicsPrePositionXField = declaredField(physicsClass, "prePositionX"); + physicsPositionXField = declaredField(physicsClass, "positionX"); + physicsPrePositionYField = declaredField(physicsClass, "prePositionY"); + physicsPositionYField = declaredField(physicsClass, "positionY"); + physicsPreBounceRotationField = declaredField(physicsClass, "preBounceRotation"); + physicsBounceRotationField = declaredField(physicsClass, "bounceRotation"); + Class entityConfigClass = Class.forName("com.wildfire.main.entitydata.EntityConfig"); + entityConfigGetEntityMethod = findStaticMethod(entityConfigClass, "getEntity", 1); + Class breastPhysicsClass = Class.forName("com.wildfire.physics.BreastPhysics"); + breastPhysicsConstructor = breastPhysicsClass.getConstructor(entityConfigClass); + breastPhysicsConstructor.setAccessible(true); + breastPhysicsFinishTickMethod = declaredMethod(breastPhysicsClass, "finishTick"); + breastPhysicsTargetBounceXField = declaredField(breastPhysicsClass, "targetBounceX"); + breastPhysicsTargetBounceYField = declaredField(breastPhysicsClass, "targetBounceY"); + breastPhysicsTargetRotVelField = declaredField(breastPhysicsClass, "targetRotVel"); + breastPhysicsPrePositionXField = declaredField(breastPhysicsClass, "prePositionX"); + breastPhysicsPositionXField = declaredField(breastPhysicsClass, "positionX"); + breastPhysicsPrePositionYField = declaredField(breastPhysicsClass, "prePositionY"); + breastPhysicsPositionYField = declaredField(breastPhysicsClass, "positionY"); + breastPhysicsPreBounceRotationField = declaredField(breastPhysicsClass, "wfg_preBounceRotation"); + breastPhysicsBounceRotationField = declaredField(breastPhysicsClass, "wfg_bounceRotation"); + Class uvLayoutClass = Class.forName("com.wildfire.main.uvs.UVLayout"); + uvGetAllSidesMethod = methodOrNull(uvLayoutClass, "getAllSides"); + Class uvQuadClass = Class.forName("com.wildfire.main.uvs.UVQuad"); + uvQuadX1Method = methodOrNull(uvQuadClass, "x1"); + uvQuadY1Method = methodOrNull(uvQuadClass, "y1"); + uvQuadX2Method = methodOrNull(uvQuadClass, "x2"); + uvQuadY2Method = methodOrNull(uvQuadClass, "y2"); + available = getRenderState != null + && genderLayerSubmitMethod != null + && entityConfigGetEntityMethod != null + && breastPhysicsConstructor != null + && breastPhysicsFinishTickMethod != null + && breastPhysicsTargetBounceXField != null + && breastPhysicsTargetBounceYField != null + && breastPhysicsTargetRotVelField != null + && breastPhysicsPreBounceRotationField != null + && breastPhysicsBounceRotationField != null; + } catch (ReflectiveOperationException | RuntimeException e) { + available = false; + logFailure("Female Gender compatibility disabled: unsupported API shape.", e); + } + + return available; + } + + private static Field field(Class owner, String name) throws NoSuchFieldException { + Field field = owner.getField(name); + field.setAccessible(true); + return field; + } + + private static Field declaredField(Class owner, String name) throws NoSuchFieldException { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + private static Method declaredMethod(Class owner, String name) throws NoSuchMethodException { + Method method = owner.getDeclaredMethod(name); + method.setAccessible(true); + return method; + } + + private static Field findField(Class owner, String... names) { + for (Class current = owner; current != null && current != Object.class; current = current.getSuperclass()) { + for (String name : names) { + try { + Field field = current.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (NoSuchFieldException var8) { + } + } + } + + return null; + } + + private static Method findStaticMethod(Class owner, String name, int parameterCount) { + for (Method method : owner.getMethods()) { + if (method.getName().equals(name) && method.getParameterCount() == parameterCount && Modifier.isStatic(method.getModifiers())) { + method.setAccessible(true); + return method; + } + } + + return null; + } + + private static Method findInstanceMethod(Class owner, String name, int parameterCount) { + for (Method method : owner.getMethods()) { + if (method.getName().equals(name) && method.getParameterCount() == parameterCount && !Modifier.isStatic(method.getModifiers())) { + method.setAccessible(true); + return method; + } + } + + return null; + } + + private static Method methodOrNull(Class owner, String name) { + try { + Method method = owner.getMethod(name); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + return null; + } + } + + private static Method findNoArgMethod(Class owner, String... names) { + for (Class current = owner; current != null && current != Object.class; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + if (method.getParameterCount() == 0 && !Modifier.isStatic(method.getModifiers())) { + for (String name : names) { + if (method.getName().equals(name)) { + method.setAccessible(true); + return method; + } + } + } + } + } + + return null; + } + + private static float invokeFloat(Method method, Object target, float fallback) throws ReflectiveOperationException { + if (method != null && target != null) { + return method.invoke(target) instanceof Number number ? number.floatValue() : fallback; + } else { + return fallback; + } + } + + private static float getFloat(Field field, Object target, float fallback) throws IllegalAccessException { + return field != null && target != null ? field.getFloat(target) : fallback; + } + + private static boolean getBoolean(Field field, Object target, boolean fallback) throws IllegalAccessException { + return field != null && target != null ? field.getBoolean(target) : fallback; + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + private static float shapedImpulse(float value, float reference, float min, float max, float exponent) { + if (Math.abs(value) < 1.0E-6F) { + return 0.0F; + } + + float magnitude = Math.min(Math.abs(value) / Math.max(reference, 1.0E-6F), 1.0F); + float shaped = (float)Math.pow(magnitude, exponent); + return Math.signum(value) * (value < 0.0F ? Math.abs(min) : max) * shaped; + } + + private static float signedYawDelta(Vec3d previousForward, Vec3d currentForward) { + if (previousForward != null && currentForward != null) { + Vec3d a = new Vec3d(previousForward.x, 0.0, previousForward.z); + Vec3d b = new Vec3d(currentForward.x, 0.0, currentForward.z); + if (!(a.lengthSquared() < 1.0E-8) && !(b.lengthSquared() < 1.0E-8)) { + a = a.normalize(); + b = b.normalize(); + double dot = MathHelper.clamp(a.dotProduct(b), -1.0, 1.0); + double crossY = a.z * b.x - a.x * b.z; + return (float)Math.toDegrees(Math.atan2(crossY, dot)); + } else { + return 0.0F; + } + } else { + return 0.0F; + } + } + + private static void logFailure(String message, Throwable error) { + if (!loggedFailure) { + loggedFailure = true; + AnimationEngine.LOGGER.warn("[NoN Animation] {}", message, error); + } + } + + private static final class AnimationPhysicsPair { + private final Object config; + private final Object left; + private final Object right; + private long lastTick = Long.MIN_VALUE; + private WildfireGenderCompat.BonePhysicsSample lastSample; + private WildfireGenderCompat.SideRenderBias previousLeftBias = WildfireGenderCompat.SideRenderBias.ZERO; + private WildfireGenderCompat.SideRenderBias leftBias = WildfireGenderCompat.SideRenderBias.ZERO; + private WildfireGenderCompat.SideRenderBias previousRightBias = WildfireGenderCompat.SideRenderBias.ZERO; + private WildfireGenderCompat.SideRenderBias rightBias = WildfireGenderCompat.SideRenderBias.ZERO; + private boolean hasSmoothedTarget; + private float smoothedTargetX; + private float smoothedTargetY; + private float smoothedTargetRotation; + + private AnimationPhysicsPair(Object config, Object left, Object right) { + this.config = config; + this.left = left; + this.right = right; + } + + private void stepRenderBias(WildfireGenderCompat.GravityBias gravity, boolean uniboob) { + this.previousLeftBias = this.leftBias; + this.previousRightBias = this.rightBias; + float x = WildfireGenderCompat.clamp(gravity.sidePull() * 0.45F, -0.8F, 0.8F); + float y = WildfireGenderCompat.clamp(gravity.forwardDown() * 0.85F, 0.0F, 1.2F); + float rotation = WildfireGenderCompat.clamp(gravity.sidePull() * 10.0F + gravity.forwardPull() * 2.0F, -10.0F, 10.0F); + this.leftBias = new WildfireGenderCompat.SideRenderBias(x, y, uniboob ? 0.0F : rotation); + this.rightBias = uniboob ? this.leftBias : this.leftBias.mirrored(); + } + + private WildfireGenderCompat.TargetImpulse smoothTarget(float x, float y, float rotation) { + if (!this.hasSmoothedTarget) { + this.hasSmoothedTarget = true; + this.smoothedTargetX = x; + this.smoothedTargetY = y; + this.smoothedTargetRotation = rotation; + return new WildfireGenderCompat.TargetImpulse(x, y, rotation); + } else { + this.smoothedTargetX = MathHelper.lerp(0.1F, this.smoothedTargetX, x); + this.smoothedTargetY = MathHelper.lerp(0.1F, this.smoothedTargetY, y); + this.smoothedTargetRotation = MathHelper.lerp(0.1F, this.smoothedTargetRotation, rotation); + return new WildfireGenderCompat.TargetImpulse(this.smoothedTargetX, this.smoothedTargetY, this.smoothedTargetRotation); + } + } + } + + private record BodyTransformSnapshot(Object body, float originX, float originY, float originZ, float pitch, float yaw, float roll) { + } + + private record BonePhysicsSample(float x, float y, float z, float chestX, float chestY, float chestZ, Vec3d forward, Vec3d right, Vec3d up) { + static WildfireGenderCompat.BonePhysicsSample from(MatrixStack rootPose, MatrixStack bonePose) { + Matrix4f relativePose = new Matrix4f(rootPose.peek().getPositionMatrix()).invert().mul(new Matrix4f(bonePose.peek().getPositionMatrix())); + Vector3f origin = relativePose.transformPosition(0.0F, 0.0F, 0.0F, new Vector3f()); + Vector3f forwardPoint = relativePose.transformPosition(0.0F, 0.0F, -1.0F, new Vector3f()); + Vector3f rightPoint = relativePose.transformPosition(1.0F, 0.0F, 0.0F, new Vector3f()); + Vector3f upPoint = relativePose.transformPosition(0.0F, 1.0F, 0.0F, new Vector3f()); + Vector3f chestPoint = relativePose.transformPosition(0.0F, 0.3F, -0.35F, new Vector3f()); + Vec3d forward = normalize(new Vec3d(forwardPoint.x() - origin.x(), forwardPoint.y() - origin.y(), forwardPoint.z() - origin.z())); + Vec3d right = normalize(new Vec3d(rightPoint.x() - origin.x(), rightPoint.y() - origin.y(), rightPoint.z() - origin.z())); + Vec3d up = normalize(new Vec3d(upPoint.x() - origin.x(), upPoint.y() - origin.y(), upPoint.z() - origin.z())); + return forward != null && right != null && up != null + ? new WildfireGenderCompat.BonePhysicsSample(origin.x(), origin.y(), origin.z(), chestPoint.x(), chestPoint.y(), chestPoint.z(), forward, right, up) + : null; + } + + private static Vec3d normalize(Vec3d vector) { + if (vector != null && !(vector.lengthSquared() < 1.0E-8)) { + Vec3d normalized = vector.normalize(); + return normalized.isFinite() ? normalized : null; + } else { + return null; + } + } + } + + private record GravityBias(float sidePull, float forwardPull, float forwardDown, float tiltInfluence) { + private static final Vec3d MODEL_DOWN = new Vec3d(0.0, -1.0, 0.0); + + static WildfireGenderCompat.GravityBias from(WildfireGenderCompat.BonePhysicsSample sample) { + if (sample == null) { + return new WildfireGenderCompat.GravityBias(0.0F, 0.0F, 0.0F, 0.0F); + } + + float sidePull = (float)sample.right().dotProduct(MODEL_DOWN); + float forwardPull = (float)sample.forward().dotProduct(MODEL_DOWN); + float forwardDown = Math.abs(forwardPull); + float upsideDown = Math.max(0.0F, (float)sample.up().dotProduct(MODEL_DOWN)); + float tiltInfluence = WildfireGenderCompat.clamp(Math.max(Math.abs(sidePull), Math.max(forwardDown, upsideDown)), 0.0F, 1.0F); + return new WildfireGenderCompat.GravityBias(sidePull, forwardPull, forwardDown, tiltInfluence); + } + } + + private record RenderPoseSnapshot(EntityPose pose, boolean sneaking) { + } + + private record SideRenderBias(float x, float y, float rotation) { + private static final WildfireGenderCompat.SideRenderBias ZERO = new WildfireGenderCompat.SideRenderBias(0.0F, 0.0F, 0.0F); + + private WildfireGenderCompat.SideRenderBias mirrored() { + return new WildfireGenderCompat.SideRenderBias(-this.x * 0.85F, this.y * 0.95F, -this.rotation * 0.85F); + } + } + + private record TargetImpulse(float x, float y, float rotation) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderState.java b/src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderState.java new file mode 100644 index 0000000..166a87a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/compat/wildfire/WildfireGenderState.java @@ -0,0 +1,24 @@ +package com.nonid.internal.animation.client.compat.wildfire; + +public record WildfireGenderState( + float bustSize, + float xOffset, + float yOffset, + float zOffset, + float cleavage, + boolean uniboob, + boolean hasJacketLayer, + WildfireGenderState.Side left, + WildfireGenderState.Side right +) { + public record Side( + float physicsX, float physicsY, float physicsSize, float bounceRotation, WildfireGenderState.UvRect baseUv, WildfireGenderState.UvRect overlayUv + ) { + } + + public record UvRect(float u1, float v1, float u2, float v2) { + public static WildfireGenderState.UvRect pixels(int x1, int y1, int x2, int y2) { + return new WildfireGenderState.UvRect(x1 / 64.0F, y1 / 64.0F, x2 / 64.0F, y2 / 64.0F); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/diagnostics/AnimationAssetDiagnostics.java b/src/main/java/com/nonid/internal/animation/client/diagnostics/AnimationAssetDiagnostics.java new file mode 100644 index 0000000..54f3f8e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/diagnostics/AnimationAssetDiagnostics.java @@ -0,0 +1,391 @@ +package com.nonid.internal.animation.client.diagnostics; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.nonid.NonDebugChatCategory; +import com.nonid.client.NonClientDiagnostics; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.client.render.gecko.GeckoResourceResolver; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AnimationStageInfo; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationAssetDiagnostics { + private static final Identifier RELOADER_ID = AnimationEngine.runtimeId("asset_diagnostics"); + private static final double LENGTH_WARNING_TOLERANCE_SECONDS = 0.05; + private static final Pattern CONJOINED_ACTOR_KEY = Pattern.compile("^(.+\\.p\\d+)_(.+)$"); + private static volatile Map> LENGTHS_BY_RESOURCE = Map.of(); + private static volatile List RESOURCE_WARNINGS = List.of(); + private static final Set WARNED_MISMATCHES = new LinkedHashSet<>(); + private static final Set WARNED_RESOURCE_CHAT = new LinkedHashSet<>(); + private static long reloadRevision = 0L; + private static String lastDefinitionValidationKey = ""; + + private AnimationAssetDiagnostics() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.CLIENT_RESOURCES).registerReloader(RELOADER_ID, new AnimationAssetDiagnostics.Reloader()); + } + + public static void validateLoadedDefinitionsOnce() { + sendResourceWarningsToChat(); + List definitions = AnimationDefinitions.getLoadedDefinitionsSnapshot(); + if (!definitions.isEmpty() && !LENGTHS_BY_RESOURCE.isEmpty()) { + String validationKey = buildDefinitionValidationKey(definitions); + if (!validationKey.equals(lastDefinitionValidationKey)) { + lastDefinitionValidationKey = validationKey; + + for (AnimationDefinitions.Definition definition : definitions) { + validateDefinition(definition); + } + } + } + } + + public static void validateStartPayload(Identifier animationId, List actorKeys, List stages) { + if (animationId != null && stages != null && !stages.isEmpty() && !LENGTHS_BY_RESOURCE.isEmpty()) { + List safeActorKeys = actorKeys == null ? List.of() : actorKeys; + + for (AnimationStageInfo stage : stages) { + if (stage != null) { + if (safeActorKeys.isEmpty()) { + validateStageResource(animationId, stage, null, List.of()); + } else { + for (String actorKey : safeActorKeys) { + validateStageResource(animationId, stage, actorKey, List.of()); + } + } + } + } + } + } + + private static void validateDefinition(AnimationDefinitions.Definition definition) { + if (definition != null && !definition.stages().isEmpty()) { + List actorKeys = AnimationDefinitions.resolveActorKeys(definition); + List actors = definition.actors(); + + for (AnimationStageInfo stage : definition.stages()) { + if (stage != null) { + int count = Math.max(actorKeys.size(), actors.size()); + if (count == 0) { + validateStageResource(definition.id(), stage, null, List.of()); + } else { + for (int i = 0; i < count; i++) { + String actorKey = i < actorKeys.size() ? actorKeys.get(i) : null; + AnimationDefinitions.ActorConstraint actor = i < actors.size() ? actors.get(i) : null; + List entityTypes = actor == null ? List.of() : sortedEntityTypes(actor.entityTypes()); + validateStageResource(definition.id(), stage, actorKey, entityTypes); + } + } + } + } + } + } + + private static void validateStageResource(Identifier definitionId, AnimationStageInfo stage, @Nullable String actorKey, List entityTypes) { + if (definitionId != null && stage != null && stage.lengthTicks() > 0L) { + Identifier playbackId = stage.effectiveAnimationId(); + if (playbackId != null) { + if (entityTypes != null && !entityTypes.isEmpty()) { + for (Identifier entityType : entityTypes) { + if (entityType != null) { + validateResolvedResource(definitionId, stage, actorKey, entityType); + } + } + } else { + validateResolvedResource(definitionId, stage, actorKey, null); + } + } + } + } + + private static void validateResolvedResource(Identifier definitionId, AnimationStageInfo stage, @Nullable String actorKey, @Nullable Identifier entityType) { + Identifier playbackId = stage.effectiveAnimationId(); + Identifier animationResource = GeckoResourceResolver.resolveAnimationResource(playbackId, actorKey, entityType, false); + if (animationResource != null) { + String animationKey = GeckoResourceResolver.resolveAnimationKey(animationResource, playbackId, actorKey, entityType, false); + if (animationKey != null) { + validateIfPresent(definitionId, stage, actorKey, animationResource, animationKey); + } + } + } + + private static boolean validateIfPresent( + Identifier definitionId, AnimationStageInfo stage, @Nullable String actorKey, @Nullable Identifier animationResource, @Nullable String animationKey + ) { + if (animationResource != null && animationKey != null) { + Map resourceAnimations = LENGTHS_BY_RESOURCE.get(animationResource); + AnimationAssetDiagnostics.AnimationAssetLength asset = resourceAnimations == null ? null : resourceAnimations.get(animationKey); + if (asset != null && !(asset.lengthSeconds() <= 0.0)) { + double stageSeconds = stageCycleSeconds(stage); + double delta = Math.abs(stageSeconds - asset.lengthSeconds()); + if (delta > 0.05) { + warnMismatch(definitionId, stage, actorKey, animationResource, asset, stageSeconds); + } + + return true; + } else { + return false; + } + } else { + return false; + } + } + + private static void warnMismatch( + Identifier definitionId, + AnimationStageInfo stage, + @Nullable String actorKey, + Identifier animationResource, + AnimationAssetDiagnostics.AnimationAssetLength asset, + double stageSeconds + ) { + String key = definitionId + "|" + stage.animationId() + "|" + actorKey + "|" + animationResource + "|" + asset.animationKey(); + if (WARNED_MISMATCHES.add(key)) { + String actorText = actorKey != null && !actorKey.isBlank() ? "actor '" + actorKey + "'" : "base"; + String playbackText = stage.animationId().equals(stage.effectiveAnimationId()) ? "" : " (plays " + stage.effectiveAnimationId() + ")"; + String message = String.format( + Locale.ROOT, + "%s stage %s%s declares cycle_seconds=%ss, but %s animation %s key '%s' has animation_length=%ss. cycle_seconds should match the authored GeckoLib clip length; use stage speed to slow/accelerate playback.", + definitionId, + stage.animationId(), + playbackText, + formatSeconds(stageSeconds), + actorText, + animationResource, + asset.animationKey(), + formatSeconds(asset.lengthSeconds()) + ); + AnimationEngine.LOGGER.warn("[NoN Animation] Setup warning: {}", message); + sendSetupChatWarning(message); + } + } + + private static void sendSetupChatWarning(String message) { + if (message != null && !message.isBlank()) { + NonClientDiagnostics.send(NonDebugChatCategory.SETUP, Text.literal("[NoN Animation] Setup warning: " + message), false); + } + } + + private static void sendResourceWarningsToChat() { + for (String warning : RESOURCE_WARNINGS) { + if (warning != null && WARNED_RESOURCE_CHAT.add(warning)) { + sendSetupChatWarning(warning); + } + } + } + + private static List sortedEntityTypes(Set entityTypes) { + if (entityTypes != null && !entityTypes.isEmpty()) { + List out = new ArrayList<>(entityTypes); + out.sort(Comparator.comparing(Identifier::toString)); + return List.copyOf(out); + } else { + return List.of(); + } + } + + private static String buildDefinitionValidationKey(List definitions) { + StringBuilder sb = new StringBuilder(); + sb.append(reloadRevision); + + for (AnimationDefinitions.Definition definition : definitions) { + if (definition != null) { + sb.append('|').append(definition.id()); + + for (AnimationStageInfo stage : definition.stages()) { + if (stage != null) { + sb.append(':') + .append(stage.animationId()) + .append("->") + .append(stage.effectiveAnimationId()) + .append('@') + .append(stage.lengthTicks()) + .append('~') + .append(stage.cycleTicks()) + .append('/') + .append(stage.cycleMidpointOffsetSeconds()); + } + } + } + } + + return sb.toString(); + } + + private static void reload(ResourceManager manager) { + Map resources = manager.findResources("geckolib/animations", id -> id.getPath().endsWith(".animation.json")); + List> entries = new ArrayList<>(resources.entrySet()); + entries.sort(Comparator.comparing(ex -> ((Identifier)ex.getKey()).toString())); + Map> loaded = new LinkedHashMap<>(); + List resourceWarnings = new ArrayList<>(GeckoModelCopyDiagnostics.scan(manager)); + + for (String warning : resourceWarnings) { + AnimationEngine.LOGGER.warn("[NoN Animation] Setup warning: {}", warning); + } + + for (Entry entry : entries) { + Identifier fileId = entry.getKey(); + Identifier animationResource = animationIdFromPath(fileId); + if (animationResource != null) { + try (InputStreamReader reader = new InputStreamReader(entry.getValue().getInputStream(), StandardCharsets.UTF_8)) { + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + JsonObject animationsObj = root.has("animations") && root.get("animations").isJsonObject() ? root.getAsJsonObject("animations") : null; + if (animationsObj != null && !animationsObj.isEmpty()) { + Map resourceLengths = new LinkedHashMap<>(); + Map> soundOwnersByStage = new LinkedHashMap<>(); + + for (Entry animationEntry : animationsObj.entrySet()) { + String animationKey = animationEntry.getKey(); + JsonElement animationEl = animationEntry.getValue(); + if (animationKey != null && !animationKey.isBlank() && animationEl != null && animationEl.isJsonObject()) { + JsonObject animationObj = animationEl.getAsJsonObject(); + double lengthSeconds = readAnimationLength(animationObj); + if (lengthSeconds > 0.0) { + resourceLengths.put(animationKey, new AnimationAssetDiagnostics.AnimationAssetLength(animationKey, lengthSeconds)); + } + + Matcher actorKeyMatcher = CONJOINED_ACTOR_KEY.matcher(animationKey); + if (actorKeyMatcher.matches() && hasNonEmptyObject(animationObj, "sound_effects")) { + soundOwnersByStage.computeIfAbsent(actorKeyMatcher.group(1), ignored -> new ArrayList<>()).add(animationKey); + } + } + } + + if (!resourceLengths.isEmpty()) { + loaded.put(animationResource, Map.copyOf(resourceLengths)); + } + + for (Entry> owners : soundOwnersByStage.entrySet()) { + if (owners.getValue().size() > 1) { + String warning = "Conjoined animation " + + animationResource + + " stage '" + + owners.getKey() + + "' contains sound_effects on multiple actor clips " + + owners.getValue() + + ". Keep the sound track on exactly one actor clip."; + resourceWarnings.add(warning); + AnimationEngine.LOGGER.warn("[NoN Animation] Setup warning: {}", warning); + } + } + } + } catch (IOException | RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to read animation length from {}", fileId, e); + } + } + } + + LENGTHS_BY_RESOURCE = Map.copyOf(loaded); + RESOURCE_WARNINGS = List.copyOf(resourceWarnings); + WARNED_MISMATCHES.clear(); + WARNED_RESOURCE_CHAT.clear(); + lastDefinitionValidationKey = ""; + reloadRevision++; + } + + private static double stageCycleSeconds(AnimationStageInfo stage) { + if (stage == null) { + return 0.0; + } else { + double exactTicks = stage.cycleTicks(); + if (Double.isFinite(exactTicks) && exactTicks > 0.0) { + return exactTicks / 20.0; + } else { + return stage.lengthTicks() > 0L ? stage.lengthTicks() / 20.0 : 0.0; + } + } + } + + private static double readAnimationLength(JsonObject animObj) { + Double animationLength = readFiniteDouble(animObj.get("animation_length")); + if (animationLength != null) { + return animationLength; + } + + Double length = readFiniteDouble(animObj.get("length")); + return length != null ? length : -1.0; + } + + private static boolean hasNonEmptyObject(JsonObject object, String key) { + return object != null && key != null && object.has(key) && object.get(key).isJsonObject() ? !object.getAsJsonObject(key).isEmpty() : false; + } + + private static Double readFiniteDouble(JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isNumber()) { + double value = primitive.getAsDouble(); + return Double.isFinite(value) ? value : null; + } + + if (!primitive.isString()) { + return null; + } + + String raw = primitive.getAsString(); + if (raw != null && !raw.isBlank()) { + try { + double value = Double.parseDouble(raw.trim()); + return Double.isFinite(value) ? value : null; + } catch (NumberFormatException ignored) { + return null; + } + } else { + return null; + } + } else { + return null; + } + } + + private static Identifier animationIdFromPath(Identifier fileId) { + String path = fileId.getPath(); + String prefix = "geckolib/animations/"; + String suffix = ".animation.json"; + if (path.startsWith(prefix) && path.endsWith(suffix)) { + String animPath = path.substring(prefix.length(), path.length() - suffix.length()); + return Identifier.of(fileId.getNamespace(), animPath); + } else { + return null; + } + } + + private static String formatSeconds(double seconds) { + return String.format(Locale.ROOT, "%.3f", seconds); + } + + private record AnimationAssetLength(String animationKey, double lengthSeconds) { + } + + public static final class Reloader implements SynchronousResourceReloader { + public void reload(ResourceManager manager) { + AnimationAssetDiagnostics.reload(manager); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/diagnostics/GeckoModelCopyDiagnostics.java b/src/main/java/com/nonid/internal/animation/client/diagnostics/GeckoModelCopyDiagnostics.java new file mode 100644 index 0000000..ef20b47 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/diagnostics/GeckoModelCopyDiagnostics.java @@ -0,0 +1,277 @@ +package com.nonid.internal.animation.client.diagnostics; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.nonid.internal.animation.AnimationEngine; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import net.minecraft.resource.InputSupplier; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +final class GeckoModelCopyDiagnostics { + private static final Identifier DEFAULT_PACK_ID = Identifier.of("needsofnature", "default_pack"); + private static final String MODEL_SCAN_PREFIX = "geckolib/models"; + private static final String SHARED_ENTITY_PREFIX = "geckolib/models/entity/"; + private static final String LOCAL_ENTITY_PREFIX = "geckolib/models/%s/entity/"; + private static final String GEO_SUFFIX = ".geo.json"; + private static final int MAX_LISTED_MODELS = 8; + + private GeckoModelCopyDiagnostics() { + } + + static List scan(ResourceManager manager) { + if (manager == null) { + return List.of(); + } + + List packs = readPackDescriptors(manager); + List defaultPacks = packs.stream().filter(packx -> DEFAULT_PACK_ID.equals(packx.logicalId())).toList(); + if (defaultPacks.isEmpty()) { + return List.of(); + } + + Map> defaultModels = new LinkedHashMap<>(); + + for (GeckoModelCopyDiagnostics.PackDescriptor defaultPack : defaultPacks) { + for (GeckoModelCopyDiagnostics.ModelFile model : readModels(defaultPack, null)) { + defaultModels.computeIfAbsent(model.location().key(), ignored -> new ArrayList<>()).add(model.json()); + } + } + + if (defaultModels.isEmpty()) { + return List.of(); + } + + Set defaultKeys = Set.copyOf(defaultModels.keySet()); + List warnings = new ArrayList<>(); + + for (GeckoModelCopyDiagnostics.PackDescriptor pack : packs) { + if (!DEFAULT_PACK_ID.equals(pack.logicalId())) { + LinkedHashSet duplicates = new LinkedHashSet<>(); + + for (GeckoModelCopyDiagnostics.ModelFile model : readModels(pack, defaultKeys)) { + List defaults = defaultModels.get(model.location().key()); + if (defaults != null && !defaults.stream().noneMatch(defaultModel -> modelsEqual(defaultModel, model.json()))) { + duplicates.add(model.location().displayName()); + } + } + + if (!duplicates.isEmpty()) { + warnings.add(buildWarning(pack, duplicates)); + } + } + } + + return warnings.isEmpty() ? List.of() : List.copyOf(warnings); + } + + static boolean modelJsonEquals(String left, String right) { + if (left != null && right != null) { + try { + JsonElement leftJson = JsonParser.parseString(left); + JsonElement rightJson = JsonParser.parseString(right); + return modelsEqual(leftJson, rightJson); + } catch (RuntimeException ignored) { + return false; + } + } else { + return false; + } + } + + private static boolean modelsEqual(JsonElement left, JsonElement right) { + return left != null && right != null && left.isJsonObject() && right.isJsonObject() && left.equals(right); + } + + @Nullable + static GeckoModelCopyDiagnostics.ModelLocation classifyModel(Identifier resourceId, @Nullable Identifier logicalPackId) { + if (resourceId != null && resourceId.getPath().endsWith(".geo.json")) { + String path = resourceId.getPath(); + if ("animationframework".equals(resourceId.getNamespace()) && path.startsWith("geckolib/models/entity/")) { + return location(path.substring("geckolib/models/entity/".length()), GeckoModelCopyDiagnostics.ModelScope.SHARED); + } else if (logicalPackId != null && logicalPackId.getNamespace().equals(resourceId.getNamespace())) { + String localPrefix = "geckolib/models/%s/entity/".formatted(logicalPackId.getPath()); + return path.startsWith(localPrefix) ? location(path.substring(localPrefix.length()), GeckoModelCopyDiagnostics.ModelScope.LOCAL) : null; + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static GeckoModelCopyDiagnostics.ModelLocation location(String relativePath, GeckoModelCopyDiagnostics.ModelScope scope) { + if (relativePath != null && relativePath.endsWith(".geo.json")) { + String key = relativePath.substring(0, relativePath.length() - ".geo.json".length()); + return key.isBlank() ? null : new GeckoModelCopyDiagnostics.ModelLocation(key, scope); + } else { + return null; + } + } + + private static List readPackDescriptors(ResourceManager manager) { + try (Stream stream = manager.streamResourcePacks()) { + return stream.filter(pack -> pack != null).map(GeckoModelCopyDiagnostics::describePack).toList(); + } catch (RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to inspect resource packs for duplicate GeckoLib models.", e); + return List.of(); + } + } + + private static GeckoModelCopyDiagnostics.PackDescriptor describePack(ResourcePack pack) { + String runtimeId = pack.getId() != null && !pack.getId().isBlank() ? pack.getId() : "unknown"; + Identifier logicalId = null; + String name = null; + + try { + InputSupplier supplier = pack.openRoot(new String[]{"pack.mcmeta"}); + if (supplier != null) { + try ( + InputStream input = (InputStream)supplier.get(); + InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8); + ) { + JsonElement parsed = JsonParser.parseReader(reader); + if (parsed.isJsonObject()) { + JsonObject root = parsed.getAsJsonObject(); + JsonElement metadataElement = root.get("animationframework"); + if (metadataElement != null && metadataElement.isJsonObject()) { + JsonObject metadata = metadataElement.getAsJsonObject(); + logicalId = readIdentifier(metadata, "id"); + name = readString(metadata, "name"); + } + } + } + } + } catch (IOException | RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to read pack identity for {}.", runtimeId, e); + } + + return new GeckoModelCopyDiagnostics.PackDescriptor(pack, runtimeId, logicalId, name); + } + + private static List readModels(GeckoModelCopyDiagnostics.PackDescriptor descriptor, @Nullable Set acceptedKeys) { + List models = new ArrayList<>(); + ResourcePack pack = descriptor.pack(); + + try { + List namespaces = new ArrayList<>(pack.getNamespaces(ResourceType.CLIENT_RESOURCES)); + namespaces.sort(String::compareTo); + + for (String namespace : namespaces) { + pack.findResources( + ResourceType.CLIENT_RESOURCES, + namespace, + "geckolib/models", + (resourceId, supplier) -> readModel(descriptor, resourceId, supplier, acceptedKeys, models) + ); + } + } catch (RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to scan GeckoLib models in {}.", descriptor.runtimeId(), e); + } + + models.sort(Comparator.comparing(model -> model.resourceId().toString())); + return List.copyOf(models); + } + + private static void readModel( + GeckoModelCopyDiagnostics.PackDescriptor descriptor, + Identifier resourceId, + InputSupplier supplier, + @Nullable Set acceptedKeys, + List output + ) { + GeckoModelCopyDiagnostics.ModelLocation location = classifyModel(resourceId, descriptor.logicalId()); + if (location != null && (acceptedKeys == null || acceptedKeys.contains(location.key()))) { + try ( + InputStream input = (InputStream)supplier.get(); + InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8); + ) { + JsonElement json = JsonParser.parseReader(reader); + if (json.isJsonObject()) { + output.add(new GeckoModelCopyDiagnostics.ModelFile(resourceId, location, json)); + } + } catch (IOException | RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to compare GeckoLib model {} from {}.", new Object[]{resourceId, descriptor.runtimeId(), e}); + } + } + } + + private static String buildWarning(GeckoModelCopyDiagnostics.PackDescriptor pack, Set duplicateModels) { + List sorted = duplicateModels.stream().sorted().toList(); + int shown = Math.min(8, sorted.size()); + String models = String.join(", ", sorted.subList(0, shown)); + if (shown < sorted.size()) { + models = models + ", +" + (sorted.size() - shown) + " more"; + } + + return "Pack '" + + pack.displayName() + + "' contains GeckoLib actor models identical to the NoN default pack: " + + models + + ". Remove these unchanged copies and let NoN use the maintained default models through fallback; copied defaults can become incompatible after updates."; + } + + @Nullable + private static Identifier readIdentifier(JsonObject object, String key) { + String raw = readString(object, key); + return raw != null && raw.indexOf(58) > 0 ? Identifier.tryParse(raw) : null; + } + + @Nullable + private static String readString(JsonObject object, String key) { + if (object != null && key != null) { + JsonElement element = object.get(key); + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String value = element.getAsString().trim(); + return value.isEmpty() ? null : value; + } else { + return null; + } + } else { + return null; + } + } + + private record ModelFile(Identifier resourceId, GeckoModelCopyDiagnostics.ModelLocation location, JsonElement json) { + } + + record ModelLocation(String key, GeckoModelCopyDiagnostics.ModelScope scope) { + String displayName() { + return this.key + " (" + this.scope.label + ")"; + } + } + + enum ModelScope { + LOCAL("local"), + SHARED("shared"); + + private final String label; + + ModelScope(String label) { + this.label = label; + } + } + + private record PackDescriptor(ResourcePack pack, String runtimeId, @Nullable Identifier logicalId, @Nullable String name) { + String displayName() { + String identity = this.logicalId == null ? this.runtimeId : this.logicalId.toString(); + return this.name != null && !this.name.equals(identity) ? this.name + " (" + identity + ")" : identity; + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/AnimationRenderStateAccess.java b/src/main/java/com/nonid/internal/animation/client/render/AnimationRenderStateAccess.java new file mode 100644 index 0000000..ea5a95b --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/AnimationRenderStateAccess.java @@ -0,0 +1,14 @@ +package com.nonid.internal.animation.client.render; + +import java.util.UUID; +import net.minecraft.util.Identifier; + +public interface AnimationRenderStateAccess { + UUID afw$getEntityUuid(); + + void afw$setEntityUuid(UUID var1); + + Identifier afw$getEntityTypeId(); + + void afw$setEntityTypeId(Identifier var1); +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorAnimatable.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorAnimatable.java new file mode 100644 index 0000000..014aa68 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorAnimatable.java @@ -0,0 +1,300 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.EnergyHolder; +import com.nonid.NonDebugChatCategory; +import com.nonid.api.client.NonAnimationParticleEvents; +import com.nonid.internal.animation.AnimationEngineClient; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.ParticleType; +import net.minecraft.particle.SimpleParticleType; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.manager.AnimatableManager.ControllerRegistrar; +import software.bernie.geckolib.animation.AnimationController; +import software.bernie.geckolib.animation.RawAnimation; +import software.bernie.geckolib.animation.object.LoopType; +import software.bernie.geckolib.animation.object.PlayState; +import software.bernie.geckolib.animation.state.AnimationTimeline; +import software.bernie.geckolib.animation.state.KeyFrameEvent; +import software.bernie.geckolib.cache.animation.keyframeevent.ParticleKeyframeData; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.util.GeckoLibUtil; +import org.jetbrains.annotations.Nullable; + +public final class AnimationActorAnimatable implements GeoAnimatable { + public static final AnimationActorAnimatable INSTANCE = new AnimationActorAnimatable(); + private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); + private static final Map LOOP_CACHE = new ConcurrentHashMap<>(); + private static final Map PLAY_ONCE_CACHE = new ConcurrentHashMap<>(); + private static final Set MISSING_ANIMATION_WARNED = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final double PARTICLE_Y_OFFSET = -0.0625; + private static final double LOOP_WRAP_EPSILON_SECONDS = 0.004166666666666667; + private static final LoopType LOOP_NO_ZERO_WRAP = LoopType.register( + "afw_loop_no_zero_wrap", (animatable, animationPoint, timelineStage, renderState, controller) -> { + double wrapped = timelineStage.startTime() + (controller.getCurrentTimelineTime() - timelineStage.endTime()); + double start = timelineStage.startTime(); + double end = timelineStage.endTime(); + if (wrapped <= start) { + wrapped = start + 0.004166666666666667; + } + + if (wrapped >= end) { + wrapped = Math.nextDown(end); + } + + controller.setTimelineTime(wrapped); + return true; + } + ); + + private AnimationActorAnimatable() { + } + + public void registerControllers(ControllerRegistrar controllers) { + AnimationController controller = new AnimationController( + "afw_main", + test -> { + GeoRenderState renderState = test.renderState(); + if (!renderState.hasGeckolibData(GeckoRenderTickets.ANIMATION_ID)) { + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_INSTANCE_ID, null); + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_RESOURCE_ID, null); + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_KEY, null); + return PlayState.STOP; + } + + Identifier animationId = (Identifier)renderState.getGeckolibData(GeckoRenderTickets.ANIMATION_ID); + if (animationId == null) { + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_INSTANCE_ID, null); + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_RESOURCE_ID, null); + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_KEY, null); + return PlayState.STOP; + } + + Identifier animationResource = (Identifier)renderState.getGeckolibData(GeckoRenderTickets.ANIMATION_RESOURCE_ID); + if (animationResource == null) { + animationResource = GeckoResourceResolver.toAnimationResource(animationId); + } + + if (!GeckoResourceResolver.hasBakedAnimation(animationResource)) { + warnMissingAnimation(animationResource); + return PlayState.STOP; + } + + boolean hasExplicitAnimationKey = renderState.hasGeckolibData(GeckoRenderTickets.ANIMATION_KEY); + String animationKey = hasExplicitAnimationKey ? (String)renderState.getGeckolibData(GeckoRenderTickets.ANIMATION_KEY) : null; + if (!hasExplicitAnimationKey) { + animationKey = GeckoResourceResolver.resolveAnimationKey(animationResource); + } + + if (animationKey == null || animationKey.isBlank()) { + return PlayState.STOP; + } + + if (!GeckoResourceResolver.hasBakedAnimationKey(animationResource, animationKey)) { + warnMissingAnimation(animationResource); + return PlayState.STOP; + } + + UUID instanceId = renderState.hasGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID) + ? (UUID)renderState.getGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID) + : null; + UUID lastInstanceId = (UUID)test.manager().getAnimatableData(GeckoRenderTickets.LAST_ANIMATION_INSTANCE_ID); + Identifier lastAnimationResource = (Identifier)test.manager().getAnimatableData(GeckoRenderTickets.LAST_ANIMATION_RESOURCE_ID); + String lastAnimationKey = (String)test.manager().getAnimatableData(GeckoRenderTickets.LAST_ANIMATION_KEY); + boolean reloadController = false; + if (instanceId != null && !instanceId.equals(lastInstanceId)) { + reloadController = true; + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_INSTANCE_ID, instanceId); + } + + if (lastAnimationResource == null || !lastAnimationResource.equals(animationResource)) { + reloadController = true; + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_RESOURCE_ID, animationResource); + } + + if (lastAnimationKey == null || !lastAnimationKey.equals(animationKey)) { + reloadController = true; + test.manager().setAnimatableData(GeckoRenderTickets.LAST_ANIMATION_KEY, animationKey); + } + + if (reloadController) { + test.controller().reset(); + } + + double speed = 1.0; + if (renderState.hasGeckolibData(GeckoRenderTickets.ANIMATION_SPEED)) { + Double s = (Double)renderState.getGeckolibData(GeckoRenderTickets.ANIMATION_SPEED); + if (s != null && Double.isFinite(s) && s > 0.0) { + speed = Math.max(0.1, Math.min(4.0, s)); + } + } + + boolean loop = ClientAnimationRuntime.isAnimationLooping(animationId); + RawAnimation toPlay = loop ? loopAnimation(animationKey) : playOnceAnimation(animationKey); + if (!loop && test.isCurrentAnimation(toPlay) && test.controller().hasAnimationFinished()) { + AnimationTimeline timeline = test.controller().getTimeline(); + if (timeline != null) { + double holdTime = Math.nextDown(timeline.lastAnimationEndTime()); + if (Double.isFinite(holdTime) && holdTime >= 0.0) { + test.controller().setTimelineTime(holdTime); + } + } + } + + PlayState playState = test.setAndContinue(toPlay); + test.setControllerSpeed((float)speed); + Double timelineSeconds = (Double)renderState.getGeckolibData(GeckoRenderTickets.ANIMATION_TIMELINE_SECONDS); + if (timelineSeconds != null && Double.isFinite(timelineSeconds) && timelineSeconds >= 0.0) { + test.controller().setTimelineTime(timelineSeconds); + } + + return playState; + } + ); + controller.setSoundKeyframeHandler(event -> {}); + controller.setParticleKeyframeHandler(AnimationActorAnimatable::handleParticleKeyframe); + controllers.add(controller); + } + + @NotNull + public AnimatableInstanceCache getAnimatableInstanceCache() { + return this.cache; + } + + private static RawAnimation loopAnimation(String key) { + return LOOP_CACHE.computeIfAbsent(key, k -> RawAnimation.begin().then(k, LOOP_NO_ZERO_WRAP)); + } + + private static RawAnimation playOnceAnimation(String key) { + return PLAY_ONCE_CACHE.computeIfAbsent(key, k -> RawAnimation.begin().thenPlay(k)); + } + + public static void clearResourceCaches() { + LOOP_CACHE.clear(); + PLAY_ONCE_CACHE.clear(); + MISSING_ANIMATION_WARNED.clear(); + } + + private static void handleParticleKeyframe(KeyFrameEvent event) { + if (event != null && event.keyframeData() != null && event.renderState() != null) { + String effect = ((ParticleKeyframeData)event.keyframeData()).getEffect(); + if (effect != null && !effect.isBlank()) { + Identifier particleId = Identifier.tryParse(effect.trim()); + if (particleId != null && Registries.PARTICLE_TYPE.containsId(particleId)) { + ParticleType type = (ParticleType)Registries.PARTICLE_TYPE.get(particleId); + ParticleEffect particle = type instanceof SimpleParticleType simpleParticle ? simpleParticle : null; + UUID actorUuid = (UUID)event.renderState().getGeckolibData(GeckoRenderTickets.ACTOR_UUID); + if (actorUuid != null) { + UUID instanceId = (UUID)event.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID); + Entity actor = resolveActor(actorUuid); + String locator = ((ParticleKeyframeData)event.keyframeData()).getLocator(); + MinecraftClient client = MinecraftClient.getInstance(); + long worldTick = client != null && client.world != null ? client.world.getTime() : Long.MIN_VALUE; + Vec3d spawnPosition = ParticleKeyframes.findParticleLocatorPosition(instanceId, actorUuid, worldTick, locator); + if (spawnPosition != null) { + spawnPosition = spawnPosition.add(0.0, -0.0625, 0.0); + NonAnimationParticleEvents.ParticleOverride override = ((NonAnimationParticleEvents.ParticleResolver)NonAnimationParticleEvents.RESOLVE + .invoker()) + .resolve( + new NonAnimationParticleEvents.ParticleContext( + actor, + actorUuid, + (Identifier)event.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_ID), + (Identifier)event.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_RESOURCE_ID), + (String)event.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_KEY), + particleId, + locator == null ? "" : locator, + ((ParticleKeyframeData)event.keyframeData()).script() == null ? "" : ((ParticleKeyframeData)event.keyframeData()).script(), + spawnPosition, + (UUID)event.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID) + ) + ); + if (override != null) { + if (override.cancel()) { + return; + } + + particle = override.particle(); + } + + if (particle != null) { + ClientWorld world = actor != null && actor.getEntityWorld() instanceof ClientWorld clientWorld ? clientWorld : null; + if (world == null) { + world = client == null ? null : client.world; + } + + Vec3d velocity = computeEjectVelocity(actor, instanceId, actorUuid, worldTick, locator); + spawnParticle(world, particle, spawnPosition, velocity); + } + } + } + } + } + } + } + + private static void spawnParticle(ClientWorld world, ParticleEffect particle, Vec3d pos, Vec3d velocity) { + if (world != null && particle != null && pos != null && pos.isFinite()) { + if (velocity != null && velocity.isFinite()) { + world.addParticleClient(particle, pos.x, pos.y, pos.z, velocity.x, velocity.y, velocity.z); + } else { + world.addParticleClient(particle, pos.x, pos.y, pos.z, 0.0, 0.0, 0.0); + } + } + } + + private static Vec3d computeEjectVelocity(@Nullable Entity actor, @Nullable UUID instanceId, UUID actorUuid, long worldTick, @Nullable String locator) { + if (actor == null || instanceId == null || actorUuid == null) { + return Vec3d.ZERO; + } + + Vec3d direction = ParticleKeyframes.findLocatorDirection(instanceId, actorUuid, worldTick, locator); + if (direction == null || direction.lengthSquared() <= 1.0E-4) { + return Vec3d.ZERO; + } + + double energyRatio = 0.0; + if (actor instanceof EnergyHolder holder) { + int max = holder.getMaxEnergy(); + if (max > 0) { + energyRatio = Math.max(0.0, Math.min(1.0, (double)holder.getEnergy() / (double)max)); + } + } + + double minSpeed = 0.06; + double maxSpeed = 0.3; + double speed = minSpeed + (maxSpeed - minSpeed) * energyRatio; + return direction.multiply(speed); + } + + private static Entity resolveActor(UUID actorUuid) { + if (actorUuid == null) { + return null; + } + + MinecraftClient client = MinecraftClient.getInstance(); + return client != null && client.world != null ? client.world.getEntity(actorUuid) : null; + } + + private static void warnMissingAnimation(Identifier animationResource) { + if (MISSING_ANIMATION_WARNED.add(animationResource)) { + AnimationEngineClient.sendClientDebugChat( + NonDebugChatCategory.SETUP, Text.literal("[NoN Animation] Missing animation file on client: " + animationResource + " (skipping)") + ); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorGeoModel.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorGeoModel.java new file mode 100644 index 0000000..6346507 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/AnimationActorGeoModel.java @@ -0,0 +1,38 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.internal.animation.AnimationEngine; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.model.GeoModel; +import software.bernie.geckolib.renderer.base.GeoRenderState; + +public final class AnimationActorGeoModel extends GeoModel { + private static final ThreadLocal CURRENT_ANIMATION_RESOURCE = new ThreadLocal<>(); + private static final Identifier DEFAULT_MODEL = AnimationEngine.packId("entity/missingmodel"); + private static final Identifier DEFAULT_TEXTURE = AnimationEngine.packId("textures/missingmodel.png"); + private static final Identifier PLACEHOLDER_ANIMATIONS = AnimationEngine.packId("afw/placeholder"); + + public static void setCurrentAnimationResource(Identifier resource) { + CURRENT_ANIMATION_RESOURCE.set(resource); + } + + public static void clearCurrentAnimationResource() { + CURRENT_ANIMATION_RESOURCE.remove(); + } + + @NotNull + public Identifier getModelResource(GeoRenderState renderState) { + return (Identifier)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.MODEL_ID, DEFAULT_MODEL); + } + + @NotNull + public Identifier getTextureResource(GeoRenderState renderState) { + return (Identifier)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.TEXTURE_ID, DEFAULT_TEXTURE); + } + + @NotNull + public Identifier getAnimationResource(AnimationActorAnimatable animatable) { + Identifier resource = CURRENT_ANIMATION_RESOURCE.get(); + return resource != null ? resource : PLACEHOLDER_ANIMATIONS; + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/BoneTextureOverrides.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/BoneTextureOverrides.java new file mode 100644 index 0000000..1ce66cb --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/BoneTextureOverrides.java @@ -0,0 +1,364 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.AnimationEngineClient; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; + +public final class BoneTextureOverrides { + private static volatile Map> BONE_TEXTURES_BY_MODEL = Map.of(); + private static volatile Map> EMISSIVE_TEXTURES_BY_MODEL = Map.of(); + private static volatile Map> EMISSIVE_BONE_TEXTURES_BY_MODEL = Map.of(); + private static volatile Map> LOCATORS_BY_MODEL = Map.of(); + private static volatile Map RENDER_SETTINGS_BY_MODEL = Map.of(); + private static final Identifier RELOADER_ID = AnimationEngine.runtimeId("bone_textures"); + + private BoneTextureOverrides() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.CLIENT_RESOURCES).registerReloader(RELOADER_ID, new BoneTextureOverrides.Reloader()); + } + + public static Map getBoneTextures(Identifier modelId) { + return modelId == null ? null : BONE_TEXTURES_BY_MODEL.get(modelId); + } + + public static List getEmissiveTextures(Identifier modelId) { + return modelId == null ? null : EMISSIVE_TEXTURES_BY_MODEL.get(modelId); + } + + public static Map getEmissiveBoneTextures(Identifier modelId) { + return modelId == null ? null : EMISSIVE_BONE_TEXTURES_BY_MODEL.get(modelId); + } + + public static Map getLocators(Identifier modelId) { + return modelId == null ? null : LOCATORS_BY_MODEL.get(modelId); + } + + public static boolean isTranslucent(Identifier modelId) { + if (modelId == null) { + return false; + } + + BoneTextureOverrides.RenderSettings settings = RENDER_SETTINGS_BY_MODEL.get(modelId); + return settings != null && settings.translucent(); + } + + public static Set getTranslucentBones(Identifier modelId) { + if (modelId == null) { + return Set.of(); + } + + BoneTextureOverrides.RenderSettings settings = RENDER_SETTINGS_BY_MODEL.get(modelId); + return settings == null ? Set.of() : settings.translucentBones(); + } + + private static void reload(ResourceManager manager) { + Map resources = manager.findResources("geckolib/models", id -> id.getPath().endsWith(".geo.json")); + List> entries = new ArrayList<>(resources.entrySet()); + entries.sort(Comparator.comparing(ex -> ((Identifier)ex.getKey()).toString())); + Map> loaded = new LinkedHashMap<>(); + Map> loadedEmissive = new LinkedHashMap<>(); + Map> loadedEmissiveBones = new LinkedHashMap<>(); + Map> loadedLocators = new LinkedHashMap<>(); + Map loadedRenderSettings = new LinkedHashMap<>(); + + for (Entry entry : entries) { + Identifier fileId = entry.getKey(); + Identifier modelId = modelIdFromGeoPath(fileId); + if (modelId != null) { + try (InputStreamReader reader = new InputStreamReader(entry.getValue().getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + Map boneTextures = readBoneTextures(obj, fileId); + if (boneTextures != null && !boneTextures.isEmpty()) { + loaded.put(modelId, Map.copyOf(boneTextures)); + } + + List emissiveTextures = readEmissiveTextures(obj, fileId); + if (emissiveTextures != null && !emissiveTextures.isEmpty()) { + loadedEmissive.put(modelId, List.copyOf(emissiveTextures)); + } + + Map emissiveBoneTextures = readTextureMap(obj, fileId, "afw_emissive_bone_textures"); + if (emissiveBoneTextures != null && !emissiveBoneTextures.isEmpty()) { + loadedEmissiveBones.put(modelId, Map.copyOf(emissiveBoneTextures)); + } + + Map locators = readLocators(obj); + if (locators != null && !locators.isEmpty()) { + loadedLocators.put(modelId, Map.copyOf(locators)); + } + + BoneTextureOverrides.RenderSettings renderSettings = readRenderSettings(obj, fileId); + if (renderSettings != null && !renderSettings.isDefault()) { + loadedRenderSettings.put(modelId, renderSettings); + } + } catch (IOException | RuntimeException e) { + setupError("[NoN Animation] Failed to load Gecko model render metadata {}", fileId, e); + } + } + } + + BONE_TEXTURES_BY_MODEL = Map.copyOf(loaded); + EMISSIVE_TEXTURES_BY_MODEL = Map.copyOf(loadedEmissive); + EMISSIVE_BONE_TEXTURES_BY_MODEL = Map.copyOf(loadedEmissiveBones); + LOCATORS_BY_MODEL = Map.copyOf(loadedLocators); + RENDER_SETTINGS_BY_MODEL = Map.copyOf(loadedRenderSettings); + } + + private static Map readBoneTextures(JsonObject obj, Identifier fileId) { + return readTextureMap(obj, fileId, "afw_bone_textures"); + } + + private static Map readTextureMap(JsonObject obj, Identifier fileId, String key) { + if (!obj.has(key)) { + return null; + } + + JsonElement el = obj.get(key); + if (el != null && el.isJsonObject()) { + JsonObject bonesObj = el.getAsJsonObject(); + Map out = new LinkedHashMap<>(); + + for (Entry boneEntry : bonesObj.entrySet()) { + String boneName = boneEntry.getKey(); + JsonElement textureEl = boneEntry.getValue(); + if (boneName != null && !boneName.isBlank()) { + if (textureEl != null && textureEl.isJsonPrimitive()) { + String textureRaw = textureEl.getAsString(); + Identifier textureId = Identifier.tryParse(textureRaw); + if (textureId == null) { + setupWarn("[NoN Animation] Skipping {}: invalid texture id '{}' for bone '{}' in '{}'", fileId, textureRaw, boneName, key); + } else { + out.put(boneName, textureId); + } + } else { + setupWarn("[NoN Animation] Skipping {}: invalid texture for bone '{}' in '{}'", fileId, boneName, key); + } + } + } + + return out; + } else { + setupWarn("[NoN Animation] Skipping {}: '{}' must be an object", fileId, key); + return null; + } + } + + private static List readEmissiveTextures(JsonObject obj, Identifier fileId) { + if (!obj.has("afw_emissive_textures")) { + return null; + } + + JsonElement el = obj.get("afw_emissive_textures"); + if (el != null && el.isJsonArray()) { + List out = new ArrayList<>(); + + for (JsonElement textureEl : el.getAsJsonArray()) { + if (textureEl != null && textureEl.isJsonPrimitive()) { + String textureRaw = textureEl.getAsString(); + Identifier textureId = Identifier.tryParse(textureRaw); + if (textureId == null) { + setupWarn("[NoN Animation] Skipping {}: invalid emissive texture id '{}'", fileId, textureRaw); + } else { + out.add(textureId); + } + } else { + setupWarn("[NoN Animation] Skipping {}: invalid emissive texture entry", fileId); + } + } + + return out; + } else { + setupWarn("[NoN Animation] Skipping {}: 'afw_emissive_textures' must be an array", fileId); + return null; + } + } + + private static BoneTextureOverrides.RenderSettings readRenderSettings(JsonObject obj, Identifier fileId) { + if (!obj.has("afw_render")) { + return null; + } else { + JsonElement el = obj.get("afw_render"); + if (el != null && el.isJsonObject()) { + JsonObject renderObj = el.getAsJsonObject(); + boolean translucent = readOptionalBoolean(renderObj, "translucent", fileId, "afw_render", false); + Set translucentBones = readStringSet(renderObj, "translucent_bones", fileId, "afw_render"); + return new BoneTextureOverrides.RenderSettings(translucent, translucentBones); + } else { + setupWarn("[NoN Animation] Skipping {}: 'afw_render' must be an object", fileId); + return null; + } + } + } + + private static Set readStringSet(JsonObject obj, String key, Identifier fileId, String context) { + if (obj != null && obj.has(key)) { + JsonElement el = obj.get(key); + if (el != null && el.isJsonArray()) { + Set out = new LinkedHashSet<>(); + + for (JsonElement entry : el.getAsJsonArray()) { + if (entry != null && entry.isJsonPrimitive() && entry.getAsJsonPrimitive().isString()) { + String boneName = entry.getAsString(); + if (boneName != null && !boneName.isBlank()) { + out.add(boneName); + } else { + setupWarn("[NoN Animation] Skipping blank bone name in '{}.{}' for {}", context, key, fileId); + } + } else { + setupWarn("[NoN Animation] Skipping invalid bone name in '{}.{}' for {}", context, key, fileId); + } + } + + return out.isEmpty() ? Set.of() : Collections.unmodifiableSet(new LinkedHashSet<>(out)); + } else { + setupWarn("[NoN Animation] Skipping {}: '{}.{}' must be an array", fileId, context, key); + return Set.of(); + } + } else { + return Set.of(); + } + } + + private static boolean readOptionalBoolean(JsonObject obj, String key, Identifier fileId, String context, boolean fallback) { + if (obj != null && obj.has(key)) { + JsonElement el = obj.get(key); + if (el != null && el.isJsonPrimitive() && el.getAsJsonPrimitive().isBoolean()) { + return el.getAsBoolean(); + } + + setupWarn("[NoN Animation] Skipping {}: '{}.{}' must be a boolean", fileId, context, key); + return fallback; + } else { + return fallback; + } + } + + private static Map readLocators(JsonObject obj) { + JsonElement geometryEl = obj.get("minecraft:geometry"); + if (geometryEl != null && geometryEl.isJsonArray()) { + Map out = new LinkedHashMap<>(); + + for (JsonElement geometryEntry : geometryEl.getAsJsonArray()) { + if (geometryEntry != null && geometryEntry.isJsonObject()) { + JsonElement bonesEl = geometryEntry.getAsJsonObject().get("bones"); + if (bonesEl != null && bonesEl.isJsonArray()) { + for (JsonElement boneEntry : bonesEl.getAsJsonArray()) { + if (boneEntry != null && boneEntry.isJsonObject()) { + JsonObject boneObj = boneEntry.getAsJsonObject(); + JsonElement nameEl = boneObj.get("name"); + if (nameEl != null && nameEl.isJsonPrimitive()) { + String boneName = nameEl.getAsString(); + if (boneName != null && !boneName.isBlank()) { + double[] pivot = readVec3(boneObj.get("pivot")); + if (pivot == null) { + pivot = new double[]{0.0, 0.0, 0.0}; + } + + JsonElement locatorsEl = boneObj.get("locators"); + if (locatorsEl != null && locatorsEl.isJsonObject()) { + for (Entry locatorEntry : locatorsEl.getAsJsonObject().entrySet()) { + String locatorName = locatorEntry.getKey(); + if (locatorName != null && !locatorName.isBlank()) { + double[] locator = readLocatorVec3(locatorEntry.getValue()); + if (locator != null) { + out.put( + locatorName, + new BoneTextureOverrides.ModelLocator(boneName, locator[0], locator[1], locator[2], pivot[0], pivot[1], pivot[2]) + ); + } + } + } + } + } + } + } + } + } + } + } + + return out; + } else { + return null; + } + } + + private static double[] readLocatorVec3(JsonElement el) { + if (el == null) { + return null; + } else if (el.isJsonArray()) { + return readVec3(el); + } else { + return el.isJsonObject() ? readVec3(el.getAsJsonObject().get("offset")) : null; + } + } + + private static double[] readVec3(JsonElement el) { + if (el != null && el.isJsonArray()) { + JsonArray array = el.getAsJsonArray(); + return array.size() < 3 ? null : new double[]{array.get(0).getAsDouble(), array.get(1).getAsDouble(), array.get(2).getAsDouble()}; + } else { + return null; + } + } + + private static Identifier modelIdFromGeoPath(Identifier fileId) { + String path = fileId.getPath(); + String prefix = "geckolib/models/"; + String suffix = ".geo.json"; + if (path.startsWith(prefix) && path.endsWith(suffix)) { + String modelPath = path.substring(prefix.length(), path.length() - suffix.length()); + return Identifier.of(fileId.getNamespace(), modelPath); + } else { + return null; + } + } + + private static void setupWarn(String template, Object... args) { + AnimationEngine.LOGGER.warn(template, args); + AnimationEngineClient.sendClientSetupWarning(AnimationEngine.formatLogTemplate(template, args)); + } + + private static void setupError(String template, Object... args) { + AnimationEngine.LOGGER.error(template, args); + AnimationEngineClient.sendClientSetupWarning(AnimationEngine.formatLogTemplate(template, args)); + } + + public record ModelLocator(String parentBone, double x, double y, double z, double parentPivotX, double parentPivotY, double parentPivotZ) { + } + + public static final class Reloader implements SynchronousResourceReloader { + public void reload(ResourceManager manager) { + BoneTextureOverrides.reload(manager); + } + } + + private record RenderSettings(boolean translucent, Set translucentBones) { + private boolean isDefault() { + return !this.translucent && this.translucentBones.isEmpty(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoRenderTickets.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoRenderTickets.java new file mode 100644 index 0000000..dbf72fa --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoRenderTickets.java @@ -0,0 +1,41 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.api.client.NonAnimationRenderEvents; +import com.nonid.internal.animation.client.compat.wildfire.WildfireGenderState; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.constant.dataticket.DataTicket; + +public final class GeckoRenderTickets { + public static final DataTicket<@NotNull Identifier> ANIMATION_ID = DataTicket.create("afw_animation_id", Identifier.class); + public static final DataTicket<@NotNull Identifier> ANIMATION_RESOURCE_ID = DataTicket.create("afw_animation_resource_id", Identifier.class); + public static final DataTicket<@NotNull String> ANIMATION_KEY = DataTicket.create("afw_animation_key", String.class); + public static final DataTicket<@NotNull UUID> ANIMATION_INSTANCE_ID = DataTicket.create("afw_instance_id", UUID.class); + public static final DataTicket<@NotNull UUID> ACTOR_UUID = DataTicket.create("afw_actor_uuid", UUID.class); + public static final DataTicket<@NotNull Identifier> MODEL_ID = DataTicket.create("afw_model_id", Identifier.class); + public static final DataTicket<@NotNull Identifier> TEXTURE_ID = DataTicket.create("afw_texture_id", Identifier.class); + public static final DataTicket<@NotNull List> LAYER_TEXTURES = DataTicket.create("afw_layer_textures", (Class)List.class); + public static final DataTicket<@NotNull List> EMISSIVE_TEXTURES = DataTicket.create("afw_emissive_textures", (Class)List.class); + public static final DataTicket<@NotNull Map> BONE_TEXTURES = DataTicket.create("afw_bone_textures", (Class)Map.class); + public static final DataTicket<@NotNull Map> EMISSIVE_BONE_TEXTURES = DataTicket.create("afw_emissive_bone_textures", (Class)Map.class); + public static final DataTicket<@NotNull Map> BONE_ITEMS = DataTicket.create("afw_bone_items", (Class)Map.class); + public static final DataTicket<@NotNull Map> BONE_VISIBILITY = DataTicket.create("afw_bone_visibility", (Class)Map.class); + public static final DataTicket<@NotNull Map>> HIDDEN_BONE_CUBE_INDICES = DataTicket.create("afw_hidden_bone_cube_indices", (Class)Map.class); + public static final DataTicket<@NotNull Boolean> TRANSLUCENT_RENDER = DataTicket.create("afw_translucent_render", Boolean.class); + public static final DataTicket<@NotNull Set> TRANSLUCENT_BONES = DataTicket.create("afw_translucent_bones", (Class)Set.class); + public static final DataTicket<@NotNull Identifier> LAST_ANIMATION_RESOURCE_ID = DataTicket.create("afw_last_animation_resource_id", Identifier.class); + public static final DataTicket<@NotNull String> LAST_ANIMATION_KEY = DataTicket.create("afw_last_animation_key", String.class); + public static final DataTicket<@NotNull UUID> LAST_ANIMATION_INSTANCE_ID = DataTicket.create("afw_last_instance_id", UUID.class); + public static final DataTicket<@NotNull Double> ANIMATION_SPEED = DataTicket.create("afw_animation_speed", Double.class); + public static final DataTicket<@NotNull Double> ANIMATION_TIMELINE_SECONDS = DataTicket.create("afw_animation_timeline_seconds", Double.class); + public static final DataTicket<@NotNull WildfireGenderState> WILDFIRE_GENDER_STATE = DataTicket.create( + "afw_wildfire_gender_state", WildfireGenderState.class + ); + + private GeckoRenderTickets() { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoReplacedRender.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoReplacedRender.java new file mode 100644 index 0000000..fe42b3e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoReplacedRender.java @@ -0,0 +1,616 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.GenderHolder; +import com.nonid.api.client.NonAnimationRenderEvents; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.client.compat.wildfire.WildfireGenderCompat; +import com.nonid.internal.animation.client.compat.wildfire.WildfireGenderState; +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import com.nonid.internal.animation.client.render.gecko.layer.BoneItemGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.BoneTextureGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.CameraPoseGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.EmissiveTextureGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.LayerTextureGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.ParticleKeyframeGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.TranslucentBoneGeoLayer; +import com.nonid.internal.animation.client.render.gecko.layer.WildfireGenderGeoLayer; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import com.nonid.internal.animation.mixin.client.EntityRenderManagerAccessor; +import com.nonid.internal.animation.util.EntityVariants; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.option.Perspective; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.EntityRenderManager; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.EntityRendererFactory.Context; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.render.state.CameraRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerSkinType; +import net.minecraft.entity.player.SkinTextures; +import net.minecraft.util.Identifier; +import software.bernie.geckolib.renderer.GeoReplacedEntityRenderer; +import software.bernie.geckolib.renderer.base.GeoRenderState; + +public final class GeckoReplacedRender { + private static final ConcurrentHashMap RENDERERS = new ConcurrentHashMap<>(); + private static final Identifier UNKNOWN_RENDERER_KEY = AnimationEngine.runtimeId("unknown_renderer"); + private static final Identifier PLAYER_TYPE_ID = Identifier.of("minecraft", "player"); + private static final String PLAYER_HEAD_BONE = "head"; + private static final float ACTIVE_MODEL_SCALE = 0.96F; + private static final ConcurrentHashMap, Method> TEXTURE_METHOD_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap, Field> TEXTURE_FIELD_CACHE = new ConcurrentHashMap<>(); + private static final Set LOGGED_TEXTURE_RESOLUTION_ERRORS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + private GeckoReplacedRender() { + } + + private static GeoReplacedEntityRenderer getOrCreateRenderer(Identifier entityTypeId, Identifier modelId) { + Identifier key = modelId != null ? modelId : (entityTypeId != null ? entityTypeId : UNKNOWN_RENDERER_KEY); + GeoReplacedEntityRenderer existing = RENDERERS.get(key); + if (existing != null) { + return existing; + } else { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return null; + } else { + EntityRenderManager renderManager = client.getEntityRenderDispatcher(); + if (renderManager instanceof EntityRenderManagerAccessor accessor) { + Context context = new Context( + renderManager, + client.getItemModelManager(), + client.getMapRenderer(), + client.getBlockRenderManager(), + client.getResourceManager(), + client.getLoadedEntityModels(), + accessor.afw$getEquipmentModelLoader(), + client.getAtlasManager(), + client.textRenderer, + client.getPlayerSkinCache() + ); + GeoReplacedEntityRenderer created = new ReplacedEntityRenderer<>(context, new AnimationActorGeoModel(), AnimationActorAnimatable.INSTANCE); + created.withRenderLayer(new LayerTextureGeoLayer(created)); + created.withRenderLayer(new BoneTextureGeoLayer(created)); + created.withRenderLayer(new TranslucentBoneGeoLayer(created)); + created.withRenderLayer(new EmissiveTextureGeoLayer(created)); + created.withRenderLayer(new BoneItemGeoLayer(created)); + created.withRenderLayer(new ParticleKeyframeGeoLayer(created)); + created.withRenderLayer(new WildfireGenderGeoLayer(created)); + created.withRenderLayer(new CameraPoseGeoLayer(created)); + created.fireCompileRenderLayersEvent(); + GeoReplacedEntityRenderer prior = RENDERERS.putIfAbsent(key, created); + return prior != null ? prior : created; + } else { + return null; + } + } + } + } + + public static void prepare(LivingEntity entity, LivingEntityRenderState state, float tickDelta) { + if (state instanceof GeoRenderState geoState) { + geoState.addGeckolibData(GeckoRenderTickets.ACTOR_UUID, entity.getUuid()); + Identifier entityTypeId = ((AnimationRenderStateAccess)state).afw$getEntityTypeId(); + Identifier animationId = ClientAnimationRuntime.findLatestActiveAnimationIdContaining(entity.getUuid()); + List modelRoots = ClientAnimationRuntime.findLatestModelRootsForActor(entity.getUuid()); + UUID animationInstanceId = ClientAnimationRuntime.findLatestActiveInstanceContaining(entity.getUuid()); + Identifier animationResource = null; + double animationSpeed = 1.0; + if (animationId != null) { + geoState.addGeckolibData(GeckoRenderTickets.ANIMATION_ID, animationId); + if (animationInstanceId != null) { + geoState.addGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID, animationInstanceId); + } + + String actorKey = ClientAnimationRuntime.findLatestActiveActorKey(entity.getUuid()); + animationSpeed = ClientAnimationRuntime.findLatestSpeedForActor(entity.getUuid()); + animationResource = GeckoResourceResolver.resolveAnimationResource(animationId, actorKey, entityTypeId); + if (animationResource != null) { + geoState.addGeckolibData(GeckoRenderTickets.ANIMATION_RESOURCE_ID, animationResource); + String animationKey = GeckoResourceResolver.resolveAnimationKey(animationResource, animationId, actorKey, entityTypeId); + geoState.addGeckolibData(GeckoRenderTickets.ANIMATION_KEY, animationKey == null ? "" : animationKey); + } + + Double timelineSeconds = ClientAnimationRuntime.findWarpedAuthoredTimelineSeconds(entity.getUuid(), tickDelta); + if (timelineSeconds != null && Double.isFinite(timelineSeconds)) { + geoState.addGeckolibData(GeckoRenderTickets.ANIMATION_TIMELINE_SECONDS, timelineSeconds); + } + } + + geoState.addGeckolibData(GeckoRenderTickets.ANIMATION_SPEED, animationSpeed); + List preferredModelPaths = preferredModelPaths(entity, entityTypeId); + GenderedModelResolver.ModelGender modelGender = entity instanceof GenderHolder holder + ? GenderedModelResolver.fromMask(holder.getGenderMask()) + : GenderedModelResolver.ModelGender.MALE; + GeckoResourceResolver.ModelAndTexture modelAndTex = GeckoResourceResolver.resolveModelAndTexture( + entityTypeId, modelRoots, preferredModelPaths, modelGender + ); + Identifier modelId = modelAndTex.model(); + Identifier fallbackTexture = modelAndTex.texture(); + boolean missingModel = modelAndTex.missingModel(); + boolean explicitTextureOverride = false; + NonAnimationRenderEvents.ModelOverride override = ((NonAnimationRenderEvents.ModelResolver)NonAnimationRenderEvents.RESOLVE.invoker()) + .resolve(entity, entityTypeId, modelId, fallbackTexture); + if (override != null) { + Identifier overrideModel = override.model(); + Identifier overrideTexture = override.texture(); + if (overrideModel != null && GeckoResourceResolver.hasGeoModel(overrideModel)) { + modelId = overrideModel; + missingModel = false; + } + + if (overrideTexture != null) { + fallbackTexture = overrideTexture; + explicitTextureOverride = true; + } + } + + Identifier vanillaTexture = resolveVanillaTexture(entity, state); + Identifier currentTexture = !missingModel && vanillaTexture != null && !explicitTextureOverride ? vanillaTexture : fallbackTexture; + NonAnimationRenderEvents.RenderOverride renderOverride = ((NonAnimationRenderEvents.RenderResolver)NonAnimationRenderEvents.RESOLVE_RENDER.invoker()) + .resolve(entity, entityTypeId, modelId, currentTexture); + List layerTextures = List.of(); + Map boneTextures = Map.of(); + Map runtimeBoneItems = ClientAnimationRuntime.findLatestBoneItemsForActor(entity.getUuid()); + Map boneItems = runtimeBoneItems; + Map boneVisibility = Map.of(); + Map> hiddenBoneCubeIndices = Map.of(); + if (renderOverride != null) { + Identifier overrideModel = renderOverride.model(); + Identifier overrideTexture = renderOverride.texture(); + if (overrideModel != null && GeckoResourceResolver.hasGeoModel(overrideModel)) { + modelId = overrideModel; + missingModel = false; + } + + if (overrideTexture != null) { + fallbackTexture = overrideTexture; + explicitTextureOverride = true; + } + + layerTextures = renderOverride.layerTextures(); + boneTextures = renderOverride.boneTextures(); + boneItems = mergeBoneItems(runtimeBoneItems, renderOverride.boneItems()); + boneVisibility = renderOverride.boneVisibility(); + hiddenBoneCubeIndices = renderOverride.hiddenBoneCubeIndices(); + } + + if (modelId == null || !GeckoResourceResolver.hasGeoModel(modelId)) { + GeckoResourceResolver.ModelAndTexture missing = GeckoResourceResolver.missingModelFallback(entityTypeId); + modelId = missing.model(); + fallbackTexture = missing.texture(); + missingModel = true; + } + + Map resourceBoneTextures = BoneTextureOverrides.getBoneTextures(modelId); + boneTextures = mergeBoneTextures(resourceBoneTextures, boneTextures); + List emissiveTextures = sanitizeLayerTextures(BoneTextureOverrides.getEmissiveTextures(modelId)); + Map emissiveBoneTextures = sanitizeBoneTextures(BoneTextureOverrides.getEmissiveBoneTextures(modelId)); + layerTextures = sanitizeLayerTextures(layerTextures); + boneTextures = sanitizeBoneTextures(boneTextures); + boneItems = sanitizeBoneItems(boneItems); + boneVisibility = sanitizeBoneVisibility(boneVisibility); + hiddenBoneCubeIndices = sanitizeHiddenBoneCubeIndices(hiddenBoneCubeIndices); + if (shouldHideLocalPlayerHeadInFirstPerson(entity)) { + boneVisibility = forceHiddenBone(boneVisibility, "head"); + } + + GeoReplacedEntityRenderer renderer = getOrCreateRenderer(entityTypeId, modelId); + if (renderer != null) { + geoState.addGeckolibData(GeckoRenderTickets.MODEL_ID, modelId); + Identifier finalTexture = explicitTextureOverride ? fallbackTexture : (!missingModel && vanillaTexture != null ? vanillaTexture : fallbackTexture); + finalTexture = VanillaTextureResolver.resolveTexture(finalTexture); + geoState.addGeckolibData(GeckoRenderTickets.TEXTURE_ID, finalTexture); + if (PLAYER_TYPE_ID.equals(entityTypeId)) { + WildfireGenderState wildfireGenderState = WildfireGenderCompat.resolve(entity, state); + if (wildfireGenderState != null) { + geoState.addGeckolibData(GeckoRenderTickets.WILDFIRE_GENDER_STATE, wildfireGenderState); + } + } + + geoState.addGeckolibData(GeckoRenderTickets.LAYER_TEXTURES, layerTextures); + geoState.addGeckolibData(GeckoRenderTickets.EMISSIVE_TEXTURES, emissiveTextures); + geoState.addGeckolibData(GeckoRenderTickets.BONE_TEXTURES, boneTextures); + geoState.addGeckolibData(GeckoRenderTickets.EMISSIVE_BONE_TEXTURES, emissiveBoneTextures); + geoState.addGeckolibData(GeckoRenderTickets.BONE_ITEMS, boneItems); + geoState.addGeckolibData(GeckoRenderTickets.BONE_VISIBILITY, boneVisibility); + geoState.addGeckolibData(GeckoRenderTickets.HIDDEN_BONE_CUBE_INDICES, hiddenBoneCubeIndices); + geoState.addGeckolibData(GeckoRenderTickets.TRANSLUCENT_RENDER, BoneTextureOverrides.isTranslucent(modelId)); + geoState.addGeckolibData(GeckoRenderTickets.TRANSLUCENT_BONES, BoneTextureOverrides.getTranslucentBones(modelId)); + + try { + if (animationResource != null) { + AnimationActorGeoModel.setCurrentAnimationResource(animationResource); + } + + renderer.fillRenderState(AnimationActorAnimatable.INSTANCE, entity, geoState, tickDelta); + } catch (RuntimeException e) { + AnimationEngine.LOGGER.error("[NoN Animation] GeckoLib prepare failed", e); + } finally { + AnimationActorGeoModel.clearCurrentAnimationResource(); + } + } + } + } + + private static List sanitizeLayerTextures(List textures) { + if (textures != null && !textures.isEmpty()) { + List resolved = new ArrayList<>(textures.size()); + + for (Identifier texture : textures) { + if (texture != null) { + resolved.add(VanillaTextureResolver.resolveTexture(texture)); + } + } + + return resolved.isEmpty() ? List.of() : List.copyOf(resolved); + } else { + return List.of(); + } + } + + private static Map sanitizeBoneTextures(Map textures) { + if (textures != null && !textures.isEmpty()) { + Map resolved = new LinkedHashMap<>(); + + for (Entry entry : textures.entrySet()) { + String bone = entry.getKey(); + Identifier texture = entry.getValue(); + if (bone != null && !bone.isBlank() && texture != null) { + resolved.put(bone, VanillaTextureResolver.resolveTexture(texture)); + } + } + + return resolved.isEmpty() ? Map.of() : Map.copyOf(resolved); + } else { + return Map.of(); + } + } + + private static Map sanitizeBoneItems(Map items) { + if (items != null && !items.isEmpty()) { + Map resolved = new LinkedHashMap<>(); + + for (Entry entry : items.entrySet()) { + String bone = entry.getKey(); + NonAnimationRenderEvents.BoneItemProp prop = entry.getValue(); + if (bone != null && !bone.isBlank() && prop != null && !prop.stack().isEmpty()) { + resolved.put(bone, new NonAnimationRenderEvents.BoneItemProp(prop.stack(), prop.displayContext())); + } + } + + return resolved.isEmpty() ? Map.of() : Map.copyOf(resolved); + } else { + return Map.of(); + } + } + + private static Map mergeBoneItems( + Map baseItems, Map overrideItems + ) { + boolean baseEmpty = baseItems == null || baseItems.isEmpty(); + boolean overrideEmpty = overrideItems == null || overrideItems.isEmpty(); + if (baseEmpty && overrideEmpty) { + return Map.of(); + } + + if (baseEmpty) { + return Map.copyOf(overrideItems); + } + + if (overrideEmpty) { + return Map.copyOf(baseItems); + } + + LinkedHashMap merged = new LinkedHashMap<>(baseItems); + merged.putAll(overrideItems); + return Map.copyOf(merged); + } + + private static Map mergeBoneTextures(Map baseTextures, Map overrideTextures) { + boolean baseEmpty = baseTextures == null || baseTextures.isEmpty(); + boolean overrideEmpty = overrideTextures == null || overrideTextures.isEmpty(); + if (baseEmpty && overrideEmpty) { + return Map.of(); + } + + if (baseEmpty) { + return Map.copyOf(overrideTextures); + } + + if (overrideEmpty) { + return Map.copyOf(baseTextures); + } + + LinkedHashMap merged = new LinkedHashMap<>(baseTextures); + merged.putAll(overrideTextures); + return Map.copyOf(merged); + } + + private static Map sanitizeBoneVisibility(Map visibility) { + if (visibility != null && !visibility.isEmpty()) { + Map resolved = new LinkedHashMap<>(); + + for (Entry entry : visibility.entrySet()) { + String bone = entry.getKey(); + Boolean visible = entry.getValue(); + if (bone != null && !bone.isBlank() && visible != null) { + resolved.put(bone, visible); + } + } + + return resolved.isEmpty() ? Map.of() : Map.copyOf(resolved); + } else { + return Map.of(); + } + } + + private static Map> sanitizeHiddenBoneCubeIndices(Map> hiddenIndices) { + if (hiddenIndices != null && !hiddenIndices.isEmpty()) { + Map> resolved = new LinkedHashMap<>(); + + for (Entry> entry : hiddenIndices.entrySet()) { + String bone = entry.getKey(); + Set indices = entry.getValue(); + if (bone != null && !bone.isBlank() && indices != null && !indices.isEmpty()) { + Set cleanIndices = new LinkedHashSet<>(); + + for (Integer index : indices) { + if (index != null && index >= 0) { + cleanIndices.add(index); + } + } + + if (!cleanIndices.isEmpty()) { + resolved.put(bone, Set.copyOf(cleanIndices)); + } + } + } + + return resolved.isEmpty() ? Map.of() : Map.copyOf(resolved); + } else { + return Map.of(); + } + } + + private static Map forceHiddenBone(Map visibility, String boneName) { + if (boneName == null || boneName.isBlank()) { + return visibility; + } else if (visibility != null && !visibility.isEmpty()) { + Map merged = new LinkedHashMap<>(visibility); + merged.put(boneName, Boolean.FALSE); + return Map.copyOf(merged); + } else { + return Map.of(boneName, Boolean.FALSE); + } + } + + private static List preferredModelPaths(LivingEntity entity, Identifier entityTypeId) { + if (entityTypeId == null) { + return List.of(); + } + + String basePath = entityTypeId.getPath(); + List paths = new ArrayList<>(); + if (PLAYER_TYPE_ID.equals(entityTypeId) && entity instanceof AbstractClientPlayerEntity player && isSlimPlayerModel(player)) { + paths.add("player_slim"); + } + + String variant = EntityVariants.resolveVariant(entity); + if (variant != null && !variant.isBlank()) { + paths.add(basePath + "_" + variant); + } + + paths.add(basePath); + return List.copyOf(new LinkedHashSet<>(paths)); + } + + private static boolean shouldHideLocalPlayerHeadInFirstPerson(LivingEntity entity) { + if (entity instanceof AbstractClientPlayerEntity playerEntity) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null && client.options != null) { + if (!playerEntity.getUuid().equals(client.player.getUuid())) { + return false; + } else { + return client.options.getPerspective() != Perspective.FIRST_PERSON + ? false + : ClientAnimationRuntime.isActorPendingOrActive(playerEntity.getUuid()); + } + } else { + return false; + } + } else { + return false; + } + } + + public static boolean render(LivingEntityRenderState state, MatrixStack matrices, OrderedRenderCommandQueue queue, CameraRenderState camera) { + if (state instanceof GeoRenderState geoState) { + Identifier entityTypeId = null; + if (state instanceof AnimationRenderStateAccess access) { + entityTypeId = access.afw$getEntityTypeId(); + } + + Identifier modelId = (Identifier)geoState.getGeckolibData(GeckoRenderTickets.MODEL_ID); + GeoReplacedEntityRenderer renderer = getOrCreateRenderer(entityTypeId, modelId); + if (renderer == null) { + return false; + } + + try { + matrices.push(); + + try { + matrices.scale(0.96F, 0.96F, 0.96F); + renderer.performRenderPass(geoState, matrices, queue, camera); + } finally { + matrices.pop(); + } + + return true; + } catch (RuntimeException e) { + AnimationEngine.LOGGER.error("[NoN Animation] GeckoLib render failed", e); + return false; + } + } else { + return false; + } + } + + private static Identifier resolveVanillaTexture(LivingEntity entity, LivingEntityRenderState renderState) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return null; + } + + if (entity instanceof AbstractClientPlayerEntity playerEntity) { + try { + SkinTextures skin = playerEntity.getSkin(); + if (skin != null && skin.body() != null) { + Identifier skinTexture = skin.body().texturePath(); + if (skinTexture != null) { + return skinTexture; + } + } + } catch (RuntimeException ex) { + String key = "player|" + playerEntity.getUuidAsString(); + if (LOGGED_TEXTURE_RESOLUTION_ERRORS.add(key)) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to resolve player skin texture for {}", key, ex); + } + } + } + + try { + EntityRenderManager renderManager = client.getEntityRenderDispatcher(); + EntityRenderer vanillaRenderer = null; + if (renderManager instanceof EntityRenderManagerAccessor accessor) { + vanillaRenderer = accessor.afw$getRenderer(renderState); + } + + if (vanillaRenderer == null) { + vanillaRenderer = renderManager.getRenderer(entity); + } + + if (vanillaRenderer != null) { + Method m = TEXTURE_METHOD_CACHE.computeIfAbsent(vanillaRenderer.getClass(), cls -> { + Method found = findTextureMethod(cls.getDeclaredMethods(), renderState, entity); + return found != null ? found : findTextureMethod(cls.getMethods(), renderState, entity); + }); + if (m != null) { + Object arg = null; + if (m.getParameterCount() == 1) { + Class param = m.getParameterTypes()[0]; + if (param.isAssignableFrom(renderState.getClass())) { + arg = renderState; + } else if (param.isAssignableFrom(entity.getClass())) { + arg = entity; + } + } + + if ((m.getParameterCount() == 0 ? m.invoke(vanillaRenderer) : m.invoke(vanillaRenderer, arg)) instanceof Identifier id) { + return id; + } + } + } + + Field f = TEXTURE_FIELD_CACHE.computeIfAbsent(renderState.getClass(), cls -> { + for (Class current = (Class)cls; current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (Identifier.class.isAssignableFrom(field.getType()) && !Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + return field; + } + } + } + + return null; + }); + if (f != null && f.get(renderState) instanceof Identifier id && isUsableTextureId(client, id)) { + return id; + } + } catch (ReflectiveOperationException | RuntimeException ex) { + String key = entity.getType().toString() + "|" + renderState.getClass().getName(); + if (LOGGED_TEXTURE_RESOLUTION_ERRORS.add(key)) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to reflect vanilla texture for {}", key, ex); + } + } + + return null; + } + + private static Method findTextureMethod(Method[] methods, LivingEntityRenderState renderState, LivingEntity entity) { + for (Method method : methods) { + if (!Modifier.isStatic(method.getModifiers()) && Identifier.class.isAssignableFrom(method.getReturnType())) { + int params = method.getParameterCount(); + switch (params) { + case 0: + method.setAccessible(true); + return method; + case 1: + Class param = method.getParameterTypes()[0]; + if (param.isAssignableFrom(renderState.getClass()) + || param.isAssignableFrom(entity.getClass()) + || LivingEntity.class.isAssignableFrom(param) + || Entity.class.isAssignableFrom(param)) { + method.setAccessible(true); + return method; + } + } + } + } + + return null; + } + + private static boolean isUsableTextureId(MinecraftClient client, Identifier id) { + if (id == null) { + return false; + } + + String path = id.getPath(); + if (path == null || !path.contains("textures/") && !path.endsWith(".png")) { + try { + return client.getResourceManager().getResource(id).isPresent(); + } catch (RuntimeException ex) { + return false; + } + } else { + return true; + } + } + + private static boolean isSlimPlayerModel(AbstractClientPlayerEntity playerEntity) { + if (playerEntity == null) { + return false; + } + + try { + SkinTextures skin = playerEntity.getSkin(); + return skin != null && skin.model() == PlayerSkinType.SLIM; + } catch (RuntimeException ex) { + String key = "skin_model|" + playerEntity.getUuidAsString(); + if (LOGGED_TEXTURE_RESOLUTION_ERRORS.add(key)) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to resolve skin model from client API for {}", key, ex); + } + + return false; + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoResourceResolver.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoResourceResolver.java new file mode 100644 index 0000000..842a464 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/GeckoResourceResolver.java @@ -0,0 +1,496 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.client.NonDebugSpinMode; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.AnimationEngineClient; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.GeckoLibConstants; +import software.bernie.geckolib.cache.GeckoLibResources; +import software.bernie.geckolib.loading.object.BakedAnimations; + +public final class GeckoResourceResolver { + private static final String SAFETY_FALLBACK_ANIMATION_KEY = "animation"; + private static final Identifier DEFAULT_MISSING_MODEL = AnimationEngine.packId("entity/missingmodel"); + private static final Identifier DEFAULT_MISSING_TEXTURE = AnimationEngine.packId("textures/missingmodel.png"); + private static final Set LOGGED_MISSING_TYPES = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Set LOGGED_MISSING_ANIMATIONS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Set LOGGED_MISSING_ANIMATION_KEYS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Set LOGGED_RESOURCE_LOOKUP_ERRORS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Map ANIMATION_RESOURCE_CACHE = new ConcurrentHashMap<>(); + private static final Map> ANIMATION_KEY_CACHE = new ConcurrentHashMap<>(); + private static final Map FIRST_ANIMATION_KEY_CACHE = new ConcurrentHashMap<>(); + private static final Map MODEL_AND_TEXTURE_CACHE = new ConcurrentHashMap<>(); + private static final Map GEO_MODEL_CACHE = new ConcurrentHashMap<>(); + private static final Map RESOURCE_EXISTS_CACHE = new ConcurrentHashMap<>(); + + private GeckoResourceResolver() { + } + + public static Identifier toAnimationResource(Identifier animationId) { + if (NonDebugSpinMode.isEnabled()) { + return NonDebugSpinMode.DEBUG_SPIN_ANIMATION_RESOURCE; + } else { + return animationId == null ? null : Identifier.of(animationId.getNamespace(), nestedAnimationPath(animationId.getPath(), null)); + } + } + + public static Identifier toAnimationResource(Identifier animationId, String actorKey) { + if (NonDebugSpinMode.isEnabled()) { + return NonDebugSpinMode.DEBUG_SPIN_ANIMATION_RESOURCE; + } else { + return animationId == null ? null : Identifier.of(animationId.getNamespace(), nestedAnimationPath(animationId.getPath(), actorKey)); + } + } + + public static Identifier toConjoinedAnimationResource(Identifier animationId) { + if (animationId == null) { + return null; + } + + String folder = animationFolderPath(animationId.getPath()); + return folder.equals(animationId.getPath()) ? null : Identifier.of(animationId.getNamespace(), "afw/" + folder); + } + + private static Identifier toNestedConjoinedAnimationResource(Identifier animationId) { + if (animationId == null) { + return null; + } + + String folder = animationFolderPath(animationId.getPath()); + return folder.equals(animationId.getPath()) ? null : Identifier.of(animationId.getNamespace(), "afw/" + folder + "/" + folder); + } + + public static Identifier resolveAnimationResource(Identifier animationId, String actorKey, Identifier entityTypeId) { + return resolveAnimationResource(animationId, actorKey, entityTypeId, true); + } + + public static Identifier resolveAnimationResource(Identifier animationId, String actorKey, Identifier entityTypeId, boolean warnMissing) { + if (NonDebugSpinMode.isEnabled()) { + return NonDebugSpinMode.DEBUG_SPIN_ANIMATION_RESOURCE; + } + + if (animationId == null) { + return null; + } + + GeckoResourceResolver.AnimationResourceKey key = new GeckoResourceResolver.AnimationResourceKey(animationId, actorKey, entityTypeId); + GeckoResourceResolver.AnimationResourceResolution resolution = ANIMATION_RESOURCE_CACHE.computeIfAbsent(key, GeckoResourceResolver::findAnimationResource); + if (warnMissing && resolution.warnableMissing()) { + String logKey = animationId + "|actor=" + actorKey + "|type=" + entityTypeId; + if (LOGGED_MISSING_ANIMATIONS.add(logKey)) { + String message = AnimationEngine.formatLogTemplate( + "[NoN Animation] Missing per-actor animation for {} (actorKey={}, typeId={}); no base animation found.", animationId, actorKey, entityTypeId + ); + AnimationEngine.LOGGER.warn("{}", message); + AnimationEngineClient.sendClientSetupWarning(message); + } + } + + return resolution.resource(); + } + + private static GeckoResourceResolver.AnimationResourceResolution findAnimationResource(GeckoResourceResolver.AnimationResourceKey key) { + Identifier animationId = key.animationId(); + String actorKey = key.actorKey(); + Identifier entityTypeId = key.entityTypeId(); + Identifier conjoined = toConjoinedAnimationResource(animationId); + if (hasBakedAnimation(conjoined)) { + return new GeckoResourceResolver.AnimationResourceResolution(conjoined, false); + } + + Identifier nestedConjoined = toNestedConjoinedAnimationResource(animationId); + if (hasBakedAnimation(nestedConjoined)) { + return new GeckoResourceResolver.AnimationResourceResolution(nestedConjoined, false); + } + + Identifier actorCandidate = null; + if (actorKey != null && !actorKey.isBlank()) { + actorCandidate = Identifier.of(animationId.getNamespace(), nestedAnimationPath(animationId.getPath(), actorKey)); + if (hasBakedAnimation(actorCandidate)) { + return new GeckoResourceResolver.AnimationResourceResolution(actorCandidate, false); + } + } + + Identifier typeCandidate = null; + if (entityTypeId != null) { + String typeKey = entityTypeId.getPath(); + typeCandidate = Identifier.of(animationId.getNamespace(), nestedAnimationPath(animationId.getPath(), typeKey)); + if (hasBakedAnimation(typeCandidate)) { + return new GeckoResourceResolver.AnimationResourceResolution(typeCandidate, false); + } + + String namespacedKey = entityTypeId.getNamespace() + "_" + entityTypeId.getPath(); + if (!namespacedKey.equals(typeKey)) { + Identifier namespacedCandidate = Identifier.of(animationId.getNamespace(), nestedAnimationPath(animationId.getPath(), namespacedKey)); + if (hasBakedAnimation(namespacedCandidate)) { + return new GeckoResourceResolver.AnimationResourceResolution(namespacedCandidate, false); + } + } + } + + Identifier base = Identifier.of(animationId.getNamespace(), nestedAnimationPath(animationId.getPath(), null)); + boolean baseBaked = hasBakedAnimation(base); + return new GeckoResourceResolver.AnimationResourceResolution(base, !baseBaked && (actorCandidate != null || typeCandidate != null)); + } + + public static boolean hasBakedAnimation(Identifier animationResource) { + return animationResource != null && GeckoLibResources.getBakedAnimations().cache().containsKey(animationResource); + } + + public static boolean hasBakedAnimationKey(Identifier animationResource, String animationKey) { + if (animationResource != null && animationKey != null && !animationKey.isBlank()) { + BakedAnimations baked = (BakedAnimations)GeckoLibResources.getBakedAnimations().cache().get(animationResource); + return baked != null && baked.animations().containsKey(animationKey); + } else { + return false; + } + } + + public static String resolveAnimationKey(Identifier animationResource) { + return animationResource == null ? "animation" : FIRST_ANIMATION_KEY_CACHE.computeIfAbsent(animationResource, resource -> { + BakedAnimations baked = (BakedAnimations)GeckoLibResources.getBakedAnimations().cache().get(resource); + return baked != null && !baked.animations().isEmpty() ? (String)baked.animations().keySet().iterator().next() : "animation"; + }); + } + + @Nullable + public static String resolveAnimationKey(Identifier animationResource, Identifier animationId, String actorKey, Identifier entityTypeId) { + return resolveAnimationKey(animationResource, animationId, actorKey, entityTypeId, true); + } + + @Nullable + public static String resolveAnimationKey(Identifier animationResource, Identifier animationId, String actorKey, Identifier entityTypeId, boolean warnMissing) { + if (animationResource != null && animationId != null) { + GeckoResourceResolver.AnimationKeyKey key = new GeckoResourceResolver.AnimationKeyKey(animationResource, animationId, actorKey, entityTypeId); + Optional resolved = ANIMATION_KEY_CACHE.computeIfAbsent(key, GeckoResourceResolver::findAnimationKey); + if (resolved.isPresent()) { + return resolved.get(); + } + + if (warnMissing) { + String logKey = animationResource + "|stage=" + animationId + "|actor=" + actorKey + "|type=" + entityTypeId; + if (LOGGED_MISSING_ANIMATION_KEYS.add(logKey)) { + String message = AnimationEngine.formatLogTemplate( + "[NoN Animation] Missing actor clip in conjoined animation {} for stage {} (actorKey={}, typeId={}).", + animationResource, + animationId, + actorKey, + entityTypeId + ); + AnimationEngine.LOGGER.warn("{}", message); + AnimationEngineClient.sendClientSetupWarning(message); + } + } + + return null; + } else { + return null; + } + } + + private static Optional findAnimationKey(GeckoResourceResolver.AnimationKeyKey key) { + Identifier animationResource = key.animationResource(); + Identifier animationId = key.animationId(); + Identifier conjoined = toConjoinedAnimationResource(animationId); + Identifier nestedConjoined = toNestedConjoinedAnimationResource(animationId); + boolean isConjoined = animationResource.equals(conjoined) || animationResource.equals(nestedConjoined); + if (!isConjoined) { + return hasBakedAnimation(animationResource) ? Optional.of(resolveAnimationKey(animationResource)) : Optional.empty(); + } + + for (String candidate : conjoinedAnimationKeyCandidates(animationId, key.actorKey(), key.entityTypeId())) { + if (hasBakedAnimationKey(animationResource, candidate)) { + return Optional.of(candidate); + } + } + + return Optional.empty(); + } + + private static Set conjoinedAnimationKeyCandidates(Identifier animationId, String actorKey, Identifier entityTypeId) { + Set candidates = new LinkedHashSet<>(); + String stageKey = conjoinedStageKey(animationId.getPath()); + if (stageKey == null) { + return candidates; + } + + if (actorKey != null && !actorKey.isBlank()) { + candidates.add(stageKey + "_" + actorKey); + } + + if (entityTypeId != null) { + candidates.add(stageKey + "_" + entityTypeId.getPath()); + candidates.add(stageKey + "_" + entityTypeId.getNamespace() + "_" + entityTypeId.getPath()); + } + + return candidates; + } + + @Nullable + private static String conjoinedStageKey(String animationPath) { + String folder = animationFolderPath(animationPath); + return folder != null && !folder.equals(animationPath) && folder.length() + 1 < animationPath.length() + ? animationPath.substring(folder.length() + 1) + : null; + } + + public static GeckoResourceResolver.ModelAndTexture resolveModelAndTexture(Identifier entityTypeId) { + return entityTypeId == null + ? missingWithLog(null) + : resolveModelAndTexture(entityTypeId, List.of(), List.of(entityTypeId.getPath()), GenderedModelResolver.ModelGender.MALE); + } + + public static GeckoResourceResolver.ModelAndTexture resolveModelAndTexture( + Identifier entityTypeId, List modelRoots, List preferredModelPaths, GenderedModelResolver.ModelGender gender + ) { + if (entityTypeId == null) { + return missingWithLog(null); + } + + List safeRoots = normalizeRoots(modelRoots); + List safePaths = normalizeModelPaths(preferredModelPaths, entityTypeId.getPath()); + GenderedModelResolver.ModelGender safeGender = gender == null ? GenderedModelResolver.ModelGender.MALE : gender; + return MODEL_AND_TEXTURE_CACHE.computeIfAbsent( + new GeckoResourceResolver.ModelKey(entityTypeId, safeRoots, safePaths, safeGender), GeckoResourceResolver::findModelAndTexture + ); + } + + private static GeckoResourceResolver.ModelAndTexture findModelAndTexture(GeckoResourceResolver.ModelKey key) { + Identifier entityTypeId = key.entityTypeId(); + GeckoResourceResolver.ResolvedModel resolved = findPreferredModel(key.modelRoots(), key.modelPaths(), key.gender()); + Identifier resolvedModel = resolved == null ? modelIdAtRoot(null, key.modelPaths().getFirst()) : resolved.model(); + String entityPath = entityTypeId.getPath(); + Identifier derivedTexture = Identifier.of(entityTypeId.getNamespace(), "textures/entity/" + entityPath + "/" + entityPath + ".png"); + Identifier resolvedTexture = resourceExists(derivedTexture) ? derivedTexture : DEFAULT_MISSING_TEXTURE; + return new GeckoResourceResolver.ModelAndTexture(resolvedModel, resolvedTexture, resolved == null, resolved == null ? null : resolved.modelRoot()); + } + + @Nullable + public static Identifier resolveGenderedModelAcrossRoots( + @Nullable Identifier currentModel, List modelRoots, GenderedModelResolver.ModelGender gender + ) { + String modelPath = logicalModelPath(currentModel, modelRoots); + if (modelPath == null) { + return null; + } + + GeckoResourceResolver.ResolvedModel resolved = findPreferredModel( + normalizeRoots(modelRoots), List.of(modelPath), gender == null ? GenderedModelResolver.ModelGender.MALE : gender + ); + return resolved == null ? null : resolved.model(); + } + + @Nullable + private static GeckoResourceResolver.ResolvedModel findPreferredModel( + List roots, List modelPaths, GenderedModelResolver.ModelGender gender + ) { + for (String candidatePath : ModelVariantCandidates.ordered(modelPaths, gender)) { + GeckoResourceResolver.ResolvedModel resolved = findAcrossProviders(roots, candidatePath); + if (resolved != null) { + return resolved; + } + } + + return null; + } + + @Nullable + private static GeckoResourceResolver.ResolvedModel findAcrossProviders(List roots, String modelPath) { + for (Identifier root : roots) { + Identifier candidate = modelIdAtRoot(root, modelPath); + if (hasGeoModel(candidate)) { + return new GeckoResourceResolver.ResolvedModel(candidate, root); + } + } + + Identifier shared = modelIdAtRoot(null, modelPath); + if (hasGeoModel(shared)) { + return new GeckoResourceResolver.ResolvedModel(shared, null); + } + + Identifier legacyShared = AnimationEngine.packId(modelPath); + return hasGeoModel(legacyShared) ? new GeckoResourceResolver.ResolvedModel(legacyShared, null) : null; + } + + private static List normalizeRoots(List roots) { + if (roots != null && !roots.isEmpty()) { + LinkedHashSet unique = new LinkedHashSet<>(); + + for (Identifier root : roots) { + if (root != null) { + unique.add(root); + } + } + + return unique.isEmpty() ? List.of() : List.copyOf(unique); + } else { + return List.of(); + } + } + + private static List normalizeModelPaths(List paths, String fallback) { + LinkedHashSet unique = new LinkedHashSet<>(); + if (paths != null) { + for (String path : paths) { + if (path != null) { + String clean = path.trim(); + if (!clean.isEmpty()) { + unique.add(clean); + } + } + } + } + + if (unique.isEmpty() && fallback != null && !fallback.isBlank()) { + unique.add(fallback); + } + + return List.copyOf(unique); + } + + @Nullable + private static String logicalModelPath(@Nullable Identifier model, List roots) { + if (model == null) { + return null; + } + + Identifier base = GenderedModelResolver.stripGenderSuffix(model); + + for (Identifier root : normalizeRoots(roots)) { + if (root.getNamespace().equals(base.getNamespace())) { + String prefix = root.getPath() + "/"; + if (base.getPath().startsWith(prefix)) { + return base.getPath().substring(prefix.length()); + } + } + } + + if ("animationframework".equals(base.getNamespace())) { + String path = base.getPath(); + return path.startsWith("entity/") ? path.substring("entity/".length()) : path; + } else { + return null; + } + } + + public static Identifier modelIdAtRoot(@Nullable Identifier modelRoot, String modelPath) { + String safePath = modelPath == null ? "" : modelPath; + return modelRoot == null ? AnimationEngine.packId("entity/" + safePath) : Identifier.of(modelRoot.getNamespace(), modelRoot.getPath() + "/" + safePath); + } + + public static GeckoResourceResolver.ModelAndTexture missingModelFallback(Identifier typeId) { + return missingWithLog(typeId); + } + + public static boolean hasGeoModel(Identifier modelId) { + return modelId == null ? false : GEO_MODEL_CACHE.computeIfAbsent(modelId, GeckoResourceResolver::findGeoModel); + } + + private static boolean findGeoModel(Identifier modelId) { + if (GeckoLibResources.getBakedModels().cache().containsKey(modelId)) { + return true; + } + + Identifier geoPath = geoModelPath(modelId); + return resourceExists(geoPath); + } + + private static String nestedAnimationPath(String animationPath, String suffix) { + String folder = animationFolderPath(animationPath); + String file = suffix != null && !suffix.isBlank() ? animationPath + "_" + suffix : animationPath; + return "afw/" + folder + "/" + file; + } + + private static String animationFolderPath(String animationPath) { + if (animationPath != null && !animationPath.isEmpty()) { + int stageIndex = animationPath.lastIndexOf(".p"); + if (stageIndex > 0 && stageIndex + 2 < animationPath.length()) { + for (int i = stageIndex + 2; i < animationPath.length(); i++) { + char c = animationPath.charAt(i); + if (c < '0' || c > '9') { + return animationPath; + } + } + + return animationPath.substring(0, stageIndex); + } else { + return animationPath; + } + } else { + return animationPath; + } + } + + private static Identifier geoModelPath(Identifier modelId) { + return Identifier.of(modelId.getNamespace(), "geckolib/models/" + modelId.getPath() + ".geo.json"); + } + + private static boolean resourceExists(Identifier resourceId) { + return resourceId == null ? false : RESOURCE_EXISTS_CACHE.computeIfAbsent(resourceId, GeckoResourceResolver::findResource); + } + + private static boolean findResource(Identifier resourceId) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return false; + } + + try { + return client.getResourceManager().getResource(resourceId).isPresent(); + } catch (RuntimeException ex) { + if (resourceId != null && LOGGED_RESOURCE_LOOKUP_ERRORS.add(resourceId.toString())) { + AnimationEngine.LOGGER.debug("[NoN Animation] Resource lookup failed for {}", resourceId, ex); + } + + return false; + } + } + + private static GeckoResourceResolver.ModelAndTexture missingWithLog(Identifier typeId) { + if (typeId != null && LOGGED_MISSING_TYPES.add(typeId.toString())) { + GeckoLibConstants.LOGGER.info("[NoN Animation] No specific gecko model for entity type {}, using missingmodel.", typeId); + } + + return new GeckoResourceResolver.ModelAndTexture(DEFAULT_MISSING_MODEL, DEFAULT_MISSING_TEXTURE, true, null); + } + + public static void clearResourceCaches() { + ANIMATION_RESOURCE_CACHE.clear(); + ANIMATION_KEY_CACHE.clear(); + FIRST_ANIMATION_KEY_CACHE.clear(); + MODEL_AND_TEXTURE_CACHE.clear(); + GEO_MODEL_CACHE.clear(); + RESOURCE_EXISTS_CACHE.clear(); + LOGGED_MISSING_TYPES.clear(); + LOGGED_MISSING_ANIMATIONS.clear(); + LOGGED_MISSING_ANIMATION_KEYS.clear(); + LOGGED_RESOURCE_LOOKUP_ERRORS.clear(); + } + + private record AnimationKeyKey(Identifier animationResource, Identifier animationId, String actorKey, Identifier entityTypeId) { + } + + private record AnimationResourceKey(Identifier animationId, String actorKey, Identifier entityTypeId) { + } + + private record AnimationResourceResolution(Identifier resource, boolean warnableMissing) { + } + + public record ModelAndTexture(Identifier model, Identifier texture, boolean missingModel, @Nullable Identifier modelRoot) { + } + + private record ModelKey(Identifier entityTypeId, List modelRoots, List modelPaths, GenderedModelResolver.ModelGender gender) { + } + + private record ResolvedModel(Identifier model, @Nullable Identifier modelRoot) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/GenderedModelResolver.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/GenderedModelResolver.java new file mode 100644 index 0000000..eb80b37 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/GenderedModelResolver.java @@ -0,0 +1,102 @@ +package com.nonid.internal.animation.client.render.gecko; + +import java.util.List; +import java.util.function.Predicate; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class GenderedModelResolver { + private static final String MALE_SUFFIX = ".m"; + private static final String FEMALE_SUFFIX = ".f"; + private static final String BOTH_SUFFIX = ".mf"; + private static final List ALL_SUFFIXES = List.of(".mf", ".m", ".f"); + + private GenderedModelResolver() { + } + + @Nullable + public static Identifier resolveExisting(@Nullable Identifier baseModel, GenderedModelResolver.ModelGender gender, Predicate exists) { + if (baseModel != null && gender != null && exists != null) { + if (hasGenderSuffix(baseModel)) { + return exists.test(baseModel) ? baseModel : null; + } + + List suffixes = preferredSuffixes(gender); + Identifier preferred = withSuffix(baseModel, suffixes.getFirst()); + if (exists.test(preferred)) { + return preferred; + } + + if (exists.test(baseModel)) { + return baseModel; + } + + for (int i = 1; i < suffixes.size(); i++) { + Identifier candidate = withSuffix(baseModel, suffixes.get(i)); + if (exists.test(candidate)) { + return candidate; + } + } + + return null; + } else { + return null; + } + } + + public static boolean hasGenderSuffix(@Nullable Identifier model) { + return model != null && hasGenderSuffix(model.getPath()); + } + + public static Identifier stripGenderSuffix(Identifier model) { + String path = model.getPath(); + + for (String suffix : ALL_SUFFIXES) { + if (path.endsWith(suffix)) { + return Identifier.of(model.getNamespace(), path.substring(0, path.length() - suffix.length())); + } + } + + return model; + } + + public static Identifier withGender(Identifier baseModel, GenderedModelResolver.ModelGender gender) { + return withSuffix(stripGenderSuffix(baseModel), preferredSuffixes(gender).getFirst()); + } + + public static GenderedModelResolver.ModelGender fromMask(int mask) { + return switch (mask & 3) { + case 2 -> GenderedModelResolver.ModelGender.FEMALE; + case 3 -> GenderedModelResolver.ModelGender.BOTH; + default -> GenderedModelResolver.ModelGender.MALE; + }; + } + + private static boolean hasGenderSuffix(String path) { + for (String suffix : ALL_SUFFIXES) { + if (path.endsWith(suffix)) { + return true; + } + } + + return false; + } + + private static List preferredSuffixes(GenderedModelResolver.ModelGender gender) { + return switch (gender) { + case MALE -> List.of(".m", ".f"); + case FEMALE -> List.of(".f", ".m"); + case BOTH -> List.of(".mf", ".m", ".f"); + }; + } + + private static Identifier withSuffix(Identifier model, String suffix) { + return Identifier.of(model.getNamespace(), model.getPath() + suffix); + } + + public enum ModelGender { + MALE, + FEMALE, + BOTH; + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/ModelVariantCandidates.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/ModelVariantCandidates.java new file mode 100644 index 0000000..67f6a29 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/ModelVariantCandidates.java @@ -0,0 +1,56 @@ +package com.nonid.internal.animation.client.render.gecko; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +final class ModelVariantCandidates { + private ModelVariantCandidates() { + } + + static List ordered(List modelPaths, GenderedModelResolver.ModelGender gender) { + LinkedHashSet uniquePaths = new LinkedHashSet<>(); + if (modelPaths != null) { + for (String path : modelPaths) { + if (path != null) { + String clean = path.trim(); + if (!clean.isEmpty()) { + uniquePaths.add(clean); + } + } + } + } + + if (uniquePaths.isEmpty()) { + return List.of(); + } + + GenderedModelResolver.ModelGender safeGender = gender == null ? GenderedModelResolver.ModelGender.MALE : gender; + + String preferredSuffix = switch (safeGender) { + case MALE -> ".m"; + case FEMALE -> ".f"; + case BOTH -> ".mf"; + }; + List paths = List.copyOf(uniquePaths); + List candidates = new ArrayList<>(); + + for (String path : paths) { + candidates.add(path + preferredSuffix); + } + + candidates.addAll(paths); + + for (String suffix : switch (safeGender) { + case MALE -> List.of(".f"); + case FEMALE -> List.of(".m"); + case BOTH -> List.of(".m", ".f"); + }) { + for (String path : paths) { + candidates.add(path + suffix); + } + } + + return List.copyOf(new LinkedHashSet<>(candidates)); + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/ParticleKeyframes.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/ParticleKeyframes.java new file mode 100644 index 0000000..34a6c9a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/ParticleKeyframes.java @@ -0,0 +1,158 @@ +package com.nonid.internal.animation.client.render.gecko; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.util.math.Vec3d; + +public final class ParticleKeyframes { + private static final long MAX_SAMPLE_AGE_TICKS = 2L; + private static final String V_LOCATOR = "v_locator"; + private static final String A_LOCATOR = "a_locator"; + private static final ConcurrentHashMap LOCATOR_POSITIONS = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap ACTOR_ORIGINS = new ConcurrentHashMap<>(); + + private ParticleKeyframes() { + } + + public static void updateLocatorPositions(UUID instanceId, UUID actorUuid, long worldTick, Map positions) { + updateLocatorData(instanceId, actorUuid, worldTick, positions, Map.of()); + } + + public static void updateLocatorData( + UUID instanceId, UUID actorUuid, long worldTick, Map positions, Map directions + ) { + if (instanceId != null && actorUuid != null) { + ParticleKeyframes.LocatorKey key = new ParticleKeyframes.LocatorKey(instanceId, actorUuid); + if (positions != null && !positions.isEmpty()) { + Map clean = new LinkedHashMap<>(); + Map cleanDirections = new LinkedHashMap<>(); + + for (Entry entry : positions.entrySet()) { + String locator = cleanLocator(entry.getKey()); + Vec3d position = entry.getValue(); + if (!locator.isEmpty() && position != null && position.isFinite()) { + clean.put(locator, position); + } + } + + if (directions != null) { + for (Entry entry : directions.entrySet()) { + String locator = cleanLocator(entry.getKey()); + Vec3d direction = entry.getValue(); + if (!locator.isEmpty() && direction != null && direction.isFinite()) { + cleanDirections.put(locator, direction); + } + } + } + + if (clean.isEmpty()) { + LOCATOR_POSITIONS.remove(key); + } else { + LOCATOR_POSITIONS.put(key, new ParticleKeyframes.LocatorSample(worldTick, Map.copyOf(clean), Map.copyOf(cleanDirections))); + } + } else { + LOCATOR_POSITIONS.remove(key); + } + } + } + + public static Vec3d findLocatorPosition(UUID instanceId, UUID actorUuid, long worldTick, String locator) { + if (instanceId != null && actorUuid != null) { + String cleanLocator = cleanLocator(locator); + if (cleanLocator.isEmpty()) { + return null; + } + + ParticleKeyframes.LocatorKey key = new ParticleKeyframes.LocatorKey(instanceId, actorUuid); + ParticleKeyframes.LocatorSample sample = LOCATOR_POSITIONS.get(key); + if (sample == null) { + return null; + } + + long age = worldTick - sample.worldTick(); + if (age >= 0L && age <= 2L) { + return sample.positions().get(cleanLocator); + } + + LOCATOR_POSITIONS.remove(key, sample); + return null; + } else { + return null; + } + } + + public static Vec3d findParticleLocatorPosition(UUID instanceId, UUID actorUuid, long worldTick, String locator) { + String cleanLocator = cleanLocator(locator); + Vec3d position = findLocatorPosition(instanceId, actorUuid, worldTick, cleanLocator); + return position == null && "v_locator".equals(cleanLocator) ? findLocatorPosition(instanceId, actorUuid, worldTick, "a_locator") : position; + } + + public static Vec3d findLocatorDirection(UUID instanceId, UUID actorUuid, long worldTick, String locator) { + if (instanceId != null && actorUuid != null) { + String cleanLocator = cleanLocator(locator); + if (cleanLocator.isEmpty()) { + return null; + } + + ParticleKeyframes.LocatorKey key = new ParticleKeyframes.LocatorKey(instanceId, actorUuid); + ParticleKeyframes.LocatorSample sample = LOCATOR_POSITIONS.get(key); + if (sample == null) { + return null; + } + + long age = worldTick - sample.worldTick(); + if (age >= 0L && age <= 2L) { + Vec3d direction = sample.directions().get(cleanLocator); + return direction == null && "v_locator".equals(cleanLocator) ? sample.directions().get("a_locator") : direction; + } + + LOCATOR_POSITIONS.remove(key, sample); + return null; + } else { + return null; + } + } + + public static void updateActorOrigin(UUID actorUuid, Vec3d position) { + if (actorUuid != null && position != null && position.isFinite()) { + ACTOR_ORIGINS.put(actorUuid, position); + } + } + + public static Vec3d findActorOrigin(UUID actorUuid) { + return actorUuid == null ? null : ACTOR_ORIGINS.get(actorUuid); + } + + public static void retainActorOrigins(Set activeActorUuids) { + if (activeActorUuids != null && !activeActorUuids.isEmpty()) { + ACTOR_ORIGINS.keySet().removeIf(actorUuid -> !activeActorUuids.contains(actorUuid)); + } else { + ACTOR_ORIGINS.clear(); + } + } + + public static void clearInstance(UUID instanceId) { + if (instanceId != null) { + LOCATOR_POSITIONS.keySet().removeIf(key -> instanceId.equals(key.instanceId())); + } + } + + public static void clearAll() { + LOCATOR_POSITIONS.clear(); + ACTOR_ORIGINS.clear(); + } + + private static String cleanLocator(String locator) { + return locator == null ? "" : locator.trim(); + } + + private record LocatorKey(UUID instanceId, UUID actorUuid) { + } + + private record LocatorSample(long worldTick, Map positions, Map directions) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/ReplacedEntityRenderer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/ReplacedEntityRenderer.java new file mode 100644 index 0000000..f41c38c --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/ReplacedEntityRenderer.java @@ -0,0 +1,76 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderLayers; +import net.minecraft.client.render.entity.EntityRendererFactory.Context; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.entity.Entity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.model.GeoModel; +import software.bernie.geckolib.renderer.GeoReplacedEntityRenderer; +import software.bernie.geckolib.renderer.base.BoneSnapshots; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.RenderPassInfo; + +public class ReplacedEntityRenderer + extends GeoReplacedEntityRenderer { + private static final Identifier PLAYER_TYPE_ID = Identifier.ofVanilla("player"); + + public ReplacedEntityRenderer(Context context, GeoModel<@NotNull T> model, T animatable) { + super(context, model, animatable); + } + + public RenderLayer getRenderType(R renderState, Identifier texture) { + boolean translucent = (Boolean)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.TRANSLUCENT_RENDER, false); + if (translucent) { + return RenderLayers.entityTranslucent(texture); + } else { + return renderState instanceof AnimationRenderStateAccess access && PLAYER_TYPE_ID.equals(access.afw$getEntityTypeId()) + ? RenderLayers.entityTranslucent(texture) + : super.getRenderType(renderState, texture); + } + } + + public void adjustModelBonesForRender(@NotNull RenderPassInfo renderPassInfo, @NotNull BoneSnapshots snapshots) { + super.adjustModelBonesForRender(renderPassInfo, snapshots); + R renderState = (R)renderPassInfo.renderState(); + Map boneTextures = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_TEXTURES, Map.of()); + Map boneVisibility = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_VISIBILITY, Map.of()); + Set translucentBones = (Set)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.TRANSLUCENT_BONES, Set.of()); + if (!boneTextures.isEmpty() || !boneVisibility.isEmpty() || !translucentBones.isEmpty()) { + for (String boneName : boneTextures.keySet()) { + if (boneName != null && !boneName.isBlank()) { + snapshots.ifPresent(boneName, snapshot -> { + snapshot.skipRender(true); + snapshot.skipChildrenRender(false); + }); + } + } + + for (Entry entry : boneVisibility.entrySet()) { + String boneName = entry.getKey(); + if (boneName != null && !boneName.isBlank() && Boolean.FALSE.equals(entry.getValue())) { + snapshots.ifPresent(boneName, snapshot -> { + snapshot.skipRender(true); + snapshot.skipChildrenRender(false); + }); + } + } + + for (String boneName : translucentBones) { + if (boneName != null && !boneName.isBlank()) { + snapshots.ifPresent(boneName, snapshot -> { + snapshot.skipRender(true); + snapshot.skipChildrenRender(false); + }); + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/VanillaTextureResolver.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/VanillaTextureResolver.java new file mode 100644 index 0000000..81dd623 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/VanillaTextureResolver.java @@ -0,0 +1,100 @@ +package com.nonid.internal.animation.client.render.gecko; + +import com.nonid.NeedsOfNature; +import com.nonid.internal.animation.AnimationEngine; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; + +public final class VanillaTextureResolver { + private static final Map REDIRECT_CACHE = new ConcurrentHashMap<>(); + private static final List UPLOADED_TEXTURES = new ArrayList<>(); + private static final Identifier RELOAD_LISTENER_ID = AnimationEngine.runtimeId("vanilla_textures"); + + private VanillaTextureResolver() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.CLIENT_RESOURCES).registerReloader(RELOAD_LISTENER_ID, (SynchronousResourceReloader)manager -> clearCache()); + } + + public static Identifier resolveTexture(Identifier textureId) { + if (textureId == null) { + return null; + } else { + return !NeedsOfNature.getConfig().forceVanillaEntityTextures() + ? textureId + : REDIRECT_CACHE.computeIfAbsent(textureId, VanillaTextureResolver::copyVanillaTexture); + } + } + + private static Identifier copyVanillaTexture(Identifier textureId) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return textureId; + } + + ResourceManager resourceManager = client.getResourceManager(); + + try { + List resources = resourceManager.getAllResources(textureId); + if (resources.isEmpty()) { + return textureId; + } + + Resource vanillaResource = null; + + for (Resource res : resources) { + String packId = res.getPackId(); + if ("vanilla".equals(packId) || "minecraft".equals(packId) || "default".equalsIgnoreCase(packId)) { + vanillaResource = res; + break; + } + } + + if (vanillaResource == null) { + vanillaResource = resources.getLast(); + } + + try (InputStream stream = vanillaResource.getInputStream()) { + NativeImage image = NativeImage.read(stream); + Identifier redirected = AnimationEngine.runtimeId("vanilla_textures/" + textureId.getNamespace() + "/" + textureId.getPath()); + client.getTextureManager().registerTexture(redirected, new NativeImageBackedTexture(() -> "non_animation_vanilla_texture", image)); + synchronized (UPLOADED_TEXTURES) { + UPLOADED_TEXTURES.add(redirected); + } + + return redirected; + } + } catch (IOException | RuntimeException e) { + AnimationEngine.LOGGER.warn("[NoN Animation] Failed to pull vanilla texture {}, using active resource stack", textureId, e); + return textureId; + } + } + + public static void clearCache() { + REDIRECT_CACHE.clear(); + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null) { + synchronized (UPLOADED_TEXTURES) { + for (Identifier id : UPLOADED_TEXTURES) { + client.getTextureManager().destroyTexture(id); + } + + UPLOADED_TEXTURES.clear(); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneItemGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneItemGeoLayer.java new file mode 100644 index 0000000..a30b7e9 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneItemGeoLayer.java @@ -0,0 +1,109 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.api.client.NonAnimationRenderEvents; +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemDisplayContext; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.animation.state.BoneSnapshot; +import software.bernie.geckolib.cache.model.BakedGeoModel; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.renderer.base.BoneSnapshots; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.builtin.BlockAndItemGeoLayer; +import software.bernie.geckolib.renderer.layer.builtin.BlockAndItemGeoLayer.RenderData; +import software.bernie.geckolib.util.RenderUtil; + +public final class BoneItemGeoLayer extends BlockAndItemGeoLayer { + public BoneItemGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + @NotNull + protected List> getRelevantBones(R renderState, @NotNull BakedGeoModel model) { + return List.of(); + } + + public void addRenderData(T animatable, O relatedObject, R renderState, float partialTick) { + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + Map boneItems = this.getBoneItems((R)renderPassInfo.renderState()); + if (!boneItems.isEmpty()) { + Map boneSnapshots = new HashMap<>(); + renderPassInfo.addBoneUpdater((passInfo, snapshots) -> this.captureBoneSnapshots(passInfo.model().topLevelBones(), snapshots, boneSnapshots)); + renderPassInfo.renderPosed(() -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(renderPassInfo.poseStack().peek()); + + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + this.renderBoneItems(renderPassInfo, modelStack, bone, boneItems, boneSnapshots, renderTasks); + } + }); + } + } + } + + private Map getBoneItems(R renderState) { + return (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_ITEMS, Map.of()); + } + + private void renderBoneItems( + RenderPassInfo<@NotNull R> renderPassInfo, + MatrixStack poseStack, + GeoBone bone, + Map boneItems, + Map boneSnapshots, + OrderedRenderCommandQueue renderTasks + ) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + NonAnimationRenderEvents.BoneItemProp prop = boneItems.get(bone.name()); + BoneSnapshot snapshot = boneSnapshots.get(bone.name()); + boolean hidden = snapshot != null && snapshot.isHidden(); + if (!hidden && prop != null && !prop.stack().isEmpty()) { + ItemStack stack = prop.stack().copy(); + ItemDisplayContext context = prop.displayContext(); + poseStack.push(); + bone.translateToPivotPoint(poseStack); + super.submitItemStackRender( + poseStack, + bone, + stack, + context, + renderPassInfo.renderState(), + renderTasks, + renderPassInfo.cameraState(), + renderPassInfo.packedLight(), + renderPassInfo.packedOverlay(), + renderPassInfo.renderColor() + ); + poseStack.pop(); + } + + boolean hideChildren = snapshot != null && snapshot.areChildrenHidden(); + if (!hideChildren) { + for (GeoBone child : bone.children()) { + this.renderBoneItems(renderPassInfo, poseStack, child, boneItems, boneSnapshots, renderTasks); + } + } + + poseStack.pop(); + } + + private void captureBoneSnapshots(GeoBone[] bones, BoneSnapshots snapshots, Map out) { + for (GeoBone bone : bones) { + snapshots.get(bone.name()).ifPresent(snapshot -> out.put(bone.name(), snapshot)); + this.captureBoneSnapshots(bone.children(), snapshots, out); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneTextureGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneTextureGeoLayer.java new file mode 100644 index 0000000..587675e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/BoneTextureGeoLayer.java @@ -0,0 +1,140 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.animation.state.BoneSnapshot; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.cache.model.cuboid.CuboidGeoBone; +import software.bernie.geckolib.cache.model.cuboid.GeoCube; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; +import software.bernie.geckolib.util.RenderUtil; + +public final class BoneTextureGeoLayer extends GeoRenderLayer { + public BoneTextureGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + Map boneTextures = this.getBoneTextures((R)renderPassInfo.renderState()); + if (!boneTextures.isEmpty()) { + Set translucentBones = (Set)renderPassInfo.renderState().getOrDefaultGeckolibData(GeckoRenderTickets.TRANSLUCENT_BONES, Set.of()); + Map> bonesByTexture = new LinkedHashMap<>(); + + for (Entry entry : boneTextures.entrySet()) { + String boneName = entry.getKey(); + Identifier texture = entry.getValue(); + if (boneName != null && !boneName.isBlank() && texture != null && !translucentBones.contains(boneName)) { + bonesByTexture.computeIfAbsent(texture, key -> new LinkedHashSet<>()).add(boneName); + } + } + + if (!bonesByTexture.isEmpty()) { + int packedOverlay = renderPassInfo.packedOverlay(); + int packedLight = renderPassInfo.packedLight(); + int renderColor = renderPassInfo.renderColor(); + Set explicitlyHiddenBones = this.getExplicitlyHiddenBones((R)renderPassInfo.renderState()); + + for (Entry> entry : bonesByTexture.entrySet()) { + Identifier texture = entry.getKey(); + Set boneNames = entry.getValue(); + if (boneNames != null && !boneNames.isEmpty()) { + RenderLayer renderLayer = this.renderer.getRenderType(renderPassInfo.renderState(), texture); + if (renderLayer != null) { + renderTasks.submitCustom( + renderPassInfo.poseStack(), + renderLayer, + (entryMatrices, vertexConsumer) -> renderPassInfo.renderPosed( + () -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(entryMatrices); + + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + this.renderBoneFiltered( + modelStack, bone, vertexConsumer, boneNames, explicitlyHiddenBones, packedOverlay, packedLight, renderColor + ); + } + } + ) + ); + } + } + } + } + } + } + } + + private Map getBoneTextures(R renderState) { + return (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_TEXTURES, Map.of()); + } + + private void renderBoneFiltered( + MatrixStack poseStack, + GeoBone bone, + VertexConsumer buffer, + Set allowedBones, + Set explicitlyHiddenBones, + int packedOverlay, + int packedLight, + int renderColor + ) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + BoneSnapshot snapshot = bone.frameSnapshot; + boolean overrideBone = allowedBones.contains(bone.name()); + boolean explicitlyHidden = explicitlyHiddenBones.contains(bone.name()); + if (overrideBone && !explicitlyHidden && bone instanceof CuboidGeoBone cuboidBone) { + for (GeoCube cube : cuboidBone.cubes) { + poseStack.push(); + this.renderCube(cube, poseStack, buffer, packedOverlay, packedLight, renderColor); + poseStack.pop(); + } + } + + boolean hideChildren = snapshot != null && snapshot.areChildrenHidden(); + if (!hideChildren) { + for (GeoBone child : bone.children()) { + this.renderBoneFiltered(poseStack, child, buffer, allowedBones, explicitlyHiddenBones, packedOverlay, packedLight, renderColor); + } + } + + poseStack.pop(); + } + + private Set getExplicitlyHiddenBones(R renderState) { + Map boneVisibility = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_VISIBILITY, Map.of()); + if (boneVisibility.isEmpty()) { + return Set.of(); + } + + Set out = new LinkedHashSet<>(); + + for (Entry entry : boneVisibility.entrySet()) { + String boneName = entry.getKey(); + if (boneName != null && !boneName.isBlank() && Boolean.FALSE.equals(entry.getValue())) { + out.add(boneName); + } + } + + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } + + private void renderCube(GeoCube cube, MatrixStack poseStack, VertexConsumer buffer, int packedOverlay, int packedLight, int renderColor) { + cube.render(poseStack, buffer, packedLight, packedOverlay, renderColor); + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/CameraPoseGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/CameraPoseGeoLayer.java new file mode 100644 index 0000000..21de5b8 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/CameraPoseGeoLayer.java @@ -0,0 +1,208 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.client.camera.AnimatedCameraPoseTracker; +import com.nonid.internal.animation.client.camera.CameraPositionAccess; +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.UUID; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3f; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; +import software.bernie.geckolib.util.RenderUtil; + +public final class CameraPoseGeoLayer extends GeoRenderLayer { + private static final Identifier PLAYER_TYPE_ID = Identifier.ofVanilla("player"); + private static final String HEAD_BONE = "head"; + private static final String BODY_BONE = "body"; + private static final float HEAD_CENTER_Y_OFFSET = 0.3F; + private static final float BODY_CENTER_Y_OFFSET = -0.375F; + + public CameraPoseGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + if (renderPassInfo.renderState() instanceof AnimationRenderStateAccess access) { + if (PLAYER_TYPE_ID.equals(access.afw$getEntityTypeId())) { + UUID actorUuid = access.afw$getEntityUuid(); + if (actorUuid != null) { + renderPassInfo.renderPosed( + () -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(renderPassInfo.poseStack().peek()); + CameraPoseGeoLayer.HeadSample headSample = this.findHeadSample(modelStack, renderPassInfo.model().topLevelBones()); + if (headSample != null) { + CameraPoseGeoLayer.BodySample bodySample = this.findBodySample(modelStack, renderPassInfo.model().topLevelBones()); + MinecraftClient client = MinecraftClient.getInstance(); + long nowTick = client != null && client.world != null ? client.world.getTime() : 0L; + UUID instanceId = (UUID)renderPassInfo.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID); + Identifier animationId = (Identifier)renderPassInfo.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_ID); + AnimatedCameraPoseTracker.capture( + actorUuid, + instanceId, + animationId, + headSample.worldPos(), + bodySample == null ? null : bodySample.worldPos(), + headSample.forward(), + headSample.up(), + headSample.right(), + bodySample == null ? null : bodySample.forward(), + bodySample == null ? null : bodySample.up(), + bodySample == null ? null : bodySample.right(), + nowTick + ); + } + } + ); + } + } + } + } + } + + private CameraPoseGeoLayer.HeadSample findHeadSample(MatrixStack poseStack, GeoBone[] bones) { + for (GeoBone bone : bones) { + CameraPoseGeoLayer.HeadSample headSample = this.findHeadSample(poseStack, bone); + if (headSample != null) { + return headSample; + } + } + + return null; + } + + private CameraPoseGeoLayer.HeadSample findHeadSample(MatrixStack poseStack, GeoBone bone) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + CameraPoseGeoLayer.HeadSample headSample = null; + if ("head".equalsIgnoreCase(bone.name())) { + poseStack.push(); + bone.translateToPivotPoint(poseStack); + Vec3d headPos = this.matrixToWorldPos(poseStack, 0.3F); + CameraPoseGeoLayer.Basis headBasis = this.matrixToBasis(poseStack); + if (headPos != null) { + headSample = new CameraPoseGeoLayer.HeadSample( + headPos, headBasis == null ? null : headBasis.forward(), headBasis == null ? null : headBasis.up(), headBasis == null ? null : headBasis.right() + ); + } + + poseStack.pop(); + } + + if (headSample == null) { + for (GeoBone child : bone.children()) { + headSample = this.findHeadSample(poseStack, child); + if (headSample != null) { + break; + } + } + } + + poseStack.pop(); + return headSample; + } + + private CameraPoseGeoLayer.BodySample findBodySample(MatrixStack poseStack, GeoBone[] bones) { + for (GeoBone bone : bones) { + CameraPoseGeoLayer.BodySample sample = this.findBodySample(poseStack, bone); + if (sample != null) { + return sample; + } + } + + return null; + } + + private CameraPoseGeoLayer.BodySample findBodySample(MatrixStack poseStack, GeoBone bone) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + CameraPoseGeoLayer.BodySample bodySample = null; + if ("body".equalsIgnoreCase(bone.name())) { + poseStack.push(); + bone.translateToPivotPoint(poseStack); + Vec3d bodyPos = this.matrixToWorldPos(poseStack, -0.375F); + CameraPoseGeoLayer.Basis bodyBasis = this.matrixToBasis(poseStack); + if (bodyPos != null) { + bodySample = new CameraPoseGeoLayer.BodySample( + bodyPos, bodyBasis == null ? null : bodyBasis.forward(), bodyBasis == null ? null : bodyBasis.up(), bodyBasis == null ? null : bodyBasis.right() + ); + } + + poseStack.pop(); + } + + if (bodySample == null) { + for (GeoBone child : bone.children()) { + bodySample = this.findBodySample(poseStack, child); + if (bodySample != null) { + break; + } + } + } + + poseStack.pop(); + return bodySample; + } + + private Vec3d matrixToWorldPos(MatrixStack poseStack, float localYOffset) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.gameRenderer != null && client.gameRenderer.getCamera() != null) { + Vector3f relative = poseStack.peek().getPositionMatrix().transformPosition(0.0F, localYOffset, 0.0F, new Vector3f()); + Vec3d cameraWorldPos = ((CameraPositionAccess)client.gameRenderer.getCamera()).afw$getPos(); + return new Vec3d(cameraWorldPos.x + relative.x(), cameraWorldPos.y + relative.y(), cameraWorldPos.z + relative.z()); + } else { + return null; + } + } + + private CameraPoseGeoLayer.Basis matrixToBasis(MatrixStack poseStack) { + Vector3f origin = poseStack.peek().getPositionMatrix().transformPosition(0.0F, 0.0F, 0.0F, new Vector3f()); + Vec3d forwardHint = this.vectorBetween(origin, poseStack.peek().getPositionMatrix().transformPosition(0.0F, 0.0F, -1.0F, new Vector3f())); + Vec3d upHint = this.vectorBetween(origin, poseStack.peek().getPositionMatrix().transformPosition(0.0F, 1.0F, 0.0F, new Vector3f())); + if (forwardHint != null && upHint != null) { + Vec3d forward = this.normalize(forwardHint); + Vec3d right = this.normalize(forward.crossProduct(upHint)); + if (forward != null && right != null) { + Vec3d up = this.normalize(right.crossProduct(forward)); + return up == null ? null : new CameraPoseGeoLayer.Basis(forward, up, right); + } else { + return null; + } + } else { + return null; + } + } + + private Vec3d vectorBetween(Vector3f origin, Vector3f target) { + return origin != null && target != null ? new Vec3d(target.x() - origin.x(), target.y() - origin.y(), target.z() - origin.z()) : null; + } + + private Vec3d normalize(Vec3d vector) { + if (vector != null && !(vector.lengthSquared() < 1.0E-8)) { + Vec3d normalized = vector.normalize(); + return normalized.isFinite() ? normalized : null; + } else { + return null; + } + } + + private record Basis(Vec3d forward, Vec3d up, Vec3d right) { + } + + private record BodySample(Vec3d worldPos, Vec3d forward, Vec3d up, Vec3d right) { + } + + private record HeadSample(Vec3d worldPos, Vec3d forward, Vec3d up, Vec3d right) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/EmissiveTextureGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/EmissiveTextureGeoLayer.java new file mode 100644 index 0000000..db7b7c5 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/EmissiveTextureGeoLayer.java @@ -0,0 +1,190 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderLayers; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.animation.state.BoneSnapshot; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.cache.model.cuboid.CuboidGeoBone; +import software.bernie.geckolib.cache.model.cuboid.GeoCube; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; +import software.bernie.geckolib.util.RenderUtil; + +public final class EmissiveTextureGeoLayer extends GeoRenderLayer { + public EmissiveTextureGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + List emissiveTextures = (List)renderPassInfo.renderState() + .getOrDefaultGeckolibData(GeckoRenderTickets.EMISSIVE_TEXTURES, List.of()); + Map emissiveBoneTextures = (Map)renderPassInfo.renderState() + .getOrDefaultGeckolibData(GeckoRenderTickets.EMISSIVE_BONE_TEXTURES, Map.of()); + if (!emissiveTextures.isEmpty() || !emissiveBoneTextures.isEmpty()) { + int packedOverlay = renderPassInfo.packedOverlay(); + int renderColor = -1; + Set customTextureBones = this.getCustomTextureBones((R)renderPassInfo.renderState()); + Set explicitlyHiddenBones = this.getExplicitlyHiddenBones((R)renderPassInfo.renderState()); + + for (Identifier texture : emissiveTextures) { + if (texture != null) { + this.submitEmissivePass( + renderPassInfo, renderTasks, texture, Set.of(), customTextureBones, false, explicitlyHiddenBones, packedOverlay, renderColor + ); + } + } + + for (Entry> entry : this.groupBonesByTexture(emissiveBoneTextures).entrySet()) { + this.submitEmissivePass( + renderPassInfo, renderTasks, entry.getKey(), entry.getValue(), Set.of(), true, explicitlyHiddenBones, packedOverlay, renderColor + ); + } + } + } + } + + private void submitEmissivePass( + RenderPassInfo<@NotNull R> renderPassInfo, + OrderedRenderCommandQueue renderTasks, + Identifier texture, + Set includedBones, + Set excludedBones, + boolean restrictToIncludedBones, + Set explicitlyHiddenBones, + int packedOverlay, + int renderColor + ) { + if (texture != null) { + RenderLayer renderLayer = RenderLayers.entityCutoutNoCullZOffset(texture); + renderTasks.submitCustom( + renderPassInfo.poseStack(), + renderLayer, + (pose, vertexConsumer) -> renderPassInfo.renderPosed( + () -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(pose); + + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + this.renderBone( + modelStack, + bone, + vertexConsumer, + includedBones, + excludedBones, + restrictToIncludedBones, + explicitlyHiddenBones, + packedOverlay, + renderColor + ); + } + } + ) + ); + } + } + + private void renderBone( + MatrixStack poseStack, + GeoBone bone, + VertexConsumer buffer, + Set includedBones, + Set excludedBones, + boolean restrictToIncludedBones, + Set explicitlyHiddenBones, + int packedOverlay, + int renderColor + ) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + BoneSnapshot snapshot = bone.frameSnapshot; + boolean selected = restrictToIncludedBones ? includedBones.contains(bone.name()) : !excludedBones.contains(bone.name()); + boolean explicitlyHidden = explicitlyHiddenBones.contains(bone.name()); + boolean hiddenBySnapshot = snapshot != null && snapshot.isHidden(); + if (selected && !explicitlyHidden && (!hiddenBySnapshot || restrictToIncludedBones) && bone instanceof CuboidGeoBone cuboidBone) { + for (GeoCube cube : cuboidBone.cubes) { + poseStack.push(); + cube.render(poseStack, buffer, 15728880, packedOverlay, renderColor); + poseStack.pop(); + } + } + + boolean hideChildren = snapshot != null && snapshot.areChildrenHidden(); + if (!hideChildren) { + for (GeoBone child : bone.children()) { + this.renderBone(poseStack, child, buffer, includedBones, excludedBones, restrictToIncludedBones, explicitlyHiddenBones, packedOverlay, renderColor); + } + } + + poseStack.pop(); + } + + private Set getCustomTextureBones(R renderState) { + Map boneTextures = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_TEXTURES, Map.of()); + if (boneTextures.isEmpty()) { + return Set.of(); + } + + Set out = new LinkedHashSet<>(); + + for (Entry entry : boneTextures.entrySet()) { + String boneName = entry.getKey(); + if (boneName != null && !boneName.isBlank() && entry.getValue() != null) { + out.add(boneName); + } + } + + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } + + private Map> groupBonesByTexture(Map boneTextures) { + if (boneTextures.isEmpty()) { + return Map.of(); + } + + Map> out = new LinkedHashMap<>(); + + for (Entry entry : boneTextures.entrySet()) { + String boneName = entry.getKey(); + Identifier texture = entry.getValue(); + if (boneName != null && !boneName.isBlank() && texture != null) { + out.computeIfAbsent(texture, ignored -> new LinkedHashSet<>()).add(boneName); + } + } + + return out; + } + + private Set getExplicitlyHiddenBones(R renderState) { + Map boneVisibility = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_VISIBILITY, Map.of()); + if (boneVisibility.isEmpty()) { + return Set.of(); + } + + Set out = new LinkedHashSet<>(); + + for (Entry entry : boneVisibility.entrySet()) { + String boneName = entry.getKey(); + if (boneName != null && !boneName.isBlank() && Boolean.FALSE.equals(entry.getValue())) { + out.add(boneName); + } + } + + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/LayerTextureGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/LayerTextureGeoLayer.java new file mode 100644 index 0000000..5ec4882 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/LayerTextureGeoLayer.java @@ -0,0 +1,51 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.List; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderLayers; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; + +public final class LayerTextureGeoLayer extends GeoRenderLayer { + public LayerTextureGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + List layerTextures = (List)renderPassInfo.renderState().getOrDefaultGeckolibData(GeckoRenderTickets.LAYER_TEXTURES, List.of()); + if (!layerTextures.isEmpty()) { + int packedLight = renderPassInfo.packedLight(); + int packedOverlay = renderPassInfo.packedOverlay(); + int renderColor = -1; + + for (Identifier texture : layerTextures) { + if (texture != null) { + RenderLayer renderLayer = RenderLayers.entityTranslucent(texture); + if (renderLayer != null) { + renderTasks.submitCustom( + renderPassInfo.poseStack(), + renderLayer, + (pose, vertexConsumer) -> { + renderPassInfo.poseStack().push(); + renderPassInfo.poseStack().peek().copy(pose); + renderPassInfo.renderPosed( + () -> renderPassInfo.model().render(renderPassInfo, vertexConsumer, packedLight, packedOverlay, renderColor) + ); + renderPassInfo.poseStack().pop(); + } + ); + } + } + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/ParticleKeyframeGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/ParticleKeyframeGeoLayer.java new file mode 100644 index 0000000..4e6fde1 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/ParticleKeyframeGeoLayer.java @@ -0,0 +1,182 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.client.camera.CameraPositionAccess; +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import com.nonid.internal.animation.client.render.gecko.BoneTextureOverrides; +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import com.nonid.internal.animation.client.render.gecko.ParticleKeyframes; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.Map.Entry; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector3f; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; +import software.bernie.geckolib.util.RenderUtil; + +public final class ParticleKeyframeGeoLayer extends GeoRenderLayer { + public ParticleKeyframeGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + UUID actorUuid = (UUID)renderPassInfo.renderState().getGeckolibData(GeckoRenderTickets.ACTOR_UUID); + UUID instanceId = (UUID)renderPassInfo.renderState().getGeckolibData(GeckoRenderTickets.ANIMATION_INSTANCE_ID); + MinecraftClient client = MinecraftClient.getInstance(); + if (actorUuid != null && instanceId != null && client != null && client.world != null) { + long worldTick = client.world.getTime(); + Identifier modelId = (Identifier)renderPassInfo.renderState().getGeckolibData(GeckoRenderTickets.MODEL_ID); + Map modelLocators = BoneTextureOverrides.getLocators(modelId); + if (modelLocators == null) { + modelLocators = Map.of(); + } + + String actorOriginBone = this.resolveActorOriginBone((R)renderPassInfo.renderState()); + if (modelLocators.isEmpty() && actorOriginBone == null) { + ParticleKeyframes.updateLocatorPositions(instanceId, actorUuid, worldTick, Map.of()); + } else { + ParticleKeyframeGeoLayer.PoseCapture capture = new ParticleKeyframeGeoLayer.PoseCapture(); + Map locators = modelLocators; + renderPassInfo.renderPosed( + () -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(renderPassInfo.poseStack().peek()); + if (locators.isEmpty()) { + capture.actorOrigin = renderPassInfo.model() + .getBone(actorOriginBone) + .map(bonex -> this.captureBonePosition(modelStack, bonex)) + .orElse(null); + } else { + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + this.capturePositions(modelStack, bone, locators, actorOriginBone, capture); + } + } + } + ); + ParticleKeyframes.updateLocatorData(instanceId, actorUuid, worldTick, capture.locators, capture.directions); + ParticleKeyframes.updateActorOrigin(actorUuid, capture.actorOrigin); + } + } + } + } + + private void capturePositions( + MatrixStack poseStack, + GeoBone bone, + Map modelLocators, + String actorOriginBone, + ParticleKeyframeGeoLayer.PoseCapture capture + ) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + if (capture.actorOrigin == null && bone.name().equals(actorOriginBone)) { + poseStack.push(); + bone.translateToPivotPoint(poseStack); + capture.actorOrigin = this.matrixToWorldPos(poseStack); + poseStack.pop(); + } + + for (Entry entry : modelLocators.entrySet()) { + BoneTextureOverrides.ModelLocator locator = entry.getValue(); + if (locator != null && bone.name().equals(locator.parentBone())) { + poseStack.push(); + bone.translateToPivotPoint(poseStack); + Vec3d pivotPos = this.matrixToWorldPos(poseStack); + poseStack.translate( + (float)((locator.x() - locator.parentPivotX()) / 16.0), + (float)((locator.y() - locator.parentPivotY()) / 16.0), + (float)((locator.z() - locator.parentPivotZ()) / 16.0) + ); + Vec3d worldPos = this.matrixToWorldPos(poseStack); + if (worldPos != null && worldPos.isFinite()) { + capture.locators.put(entry.getKey(), worldPos); + } + + if (worldPos != null && pivotPos != null && worldPos.isFinite() && pivotPos.isFinite()) { + Vec3d direction = worldPos.subtract(pivotPos); + if (direction.lengthSquared() > 1.0E-4) { + capture.directions.put(entry.getKey(), direction.normalize()); + } + } + + poseStack.pop(); + } + } + + for (GeoBone child : bone.children()) { + this.capturePositions(poseStack, child, modelLocators, actorOriginBone, capture); + } + + poseStack.pop(); + } + + private Vec3d captureBonePosition(MatrixStack poseStack, GeoBone target) { + Deque ancestry = new ArrayDeque<>(); + + for (GeoBone bone = target; bone != null; bone = bone.parent()) { + ancestry.addFirst(bone); + } + + int pushed = 0; + + try { + for (GeoBone bone : ancestry) { + poseStack.push(); + pushed++; + RenderUtil.prepMatrixForBone(poseStack, bone); + } + + poseStack.push(); + pushed++; + target.translateToPivotPoint(poseStack); + return this.matrixToWorldPos(poseStack); + } finally { + while (pushed-- > 0) { + poseStack.pop(); + } + } + } + + private String resolveActorOriginBone(R renderState) { + if (renderState instanceof AnimationRenderStateAccess access) { + Identifier entityTypeId = access.afw$getEntityTypeId(); + return entityTypeId != null && !entityTypeId.getPath().isBlank() ? entityTypeId.getPath() : null; + } else { + return null; + } + } + + private Vec3d matrixToWorldPos(MatrixStack poseStack) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.gameRenderer != null && client.gameRenderer.getCamera() != null) { + if (client.gameRenderer.getCamera() instanceof CameraPositionAccess access) { + Vector3f var6 = poseStack.peek().getPositionMatrix().transformPosition(0.0F, 0.0F, 0.0F, new Vector3f()); + Vec3d cameraWorldPos = access.afw$getPos(); + return new Vec3d(cameraWorldPos.x + var6.x(), cameraWorldPos.y + var6.y(), cameraWorldPos.z + var6.z()); + } else { + return null; + } + } else { + return null; + } + } + + private static final class PoseCapture { + private final Map locators = new LinkedHashMap<>(); + private final Map directions = new LinkedHashMap<>(); + private Vec3d actorOrigin; + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/TranslucentBoneGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/TranslucentBoneGeoLayer.java new file mode 100644 index 0000000..f9509ff --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/TranslucentBoneGeoLayer.java @@ -0,0 +1,149 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderLayers; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.command.RenderCommandQueue; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.animation.state.BoneSnapshot; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.cache.model.cuboid.CuboidGeoBone; +import software.bernie.geckolib.cache.model.cuboid.GeoCube; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; +import software.bernie.geckolib.util.RenderUtil; + +public final class TranslucentBoneGeoLayer extends GeoRenderLayer { + private static final int OVERLAY_QUEUE = 1; + + public TranslucentBoneGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + R renderState = (R)renderPassInfo.renderState(); + Set translucentBones = (Set)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.TRANSLUCENT_BONES, Set.of()); + if (!translucentBones.isEmpty()) { + Identifier baseTexture = (Identifier)renderState.getGeckolibData(GeckoRenderTickets.TEXTURE_ID); + Map boneTextures = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_TEXTURES, Map.of()); + Map> bonesByTexture = this.groupBonesByTexture(translucentBones, boneTextures, baseTexture); + if (!bonesByTexture.isEmpty()) { + int packedOverlay = renderPassInfo.packedOverlay(); + int packedLight = renderPassInfo.packedLight(); + int renderColor = renderPassInfo.renderColor(); + Set explicitlyHiddenBones = this.getExplicitlyHiddenBones(renderState); + Map> hiddenCubeIndices = (Map>)renderState.getOrDefaultGeckolibData( + GeckoRenderTickets.HIDDEN_BONE_CUBE_INDICES, Map.of() + ); + RenderCommandQueue overlayTasks = renderTasks.getBatchingQueue(1); + + for (Entry> entry : bonesByTexture.entrySet()) { + RenderLayer renderLayer = RenderLayers.entityTranslucent(entry.getKey()); + Set boneNames = entry.getValue(); + overlayTasks.submitCustom( + renderPassInfo.poseStack(), + renderLayer, + (entryMatrices, vertexConsumer) -> renderPassInfo.renderPosed( + () -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(entryMatrices); + + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + this.renderBoneFiltered( + modelStack, bone, vertexConsumer, boneNames, explicitlyHiddenBones, hiddenCubeIndices, packedOverlay, packedLight, renderColor + ); + } + } + ) + ); + } + } + } + } + } + + private Map> groupBonesByTexture(Set translucentBones, Map boneTextures, Identifier baseTexture) { + Map> out = new LinkedHashMap<>(); + + for (String boneName : translucentBones) { + if (boneName != null && !boneName.isBlank()) { + Identifier texture = boneTextures.getOrDefault(boneName, baseTexture); + if (texture != null) { + out.computeIfAbsent(texture, ignored -> new LinkedHashSet<>()).add(boneName); + } + } + } + + return out; + } + + private void renderBoneFiltered( + MatrixStack poseStack, + GeoBone bone, + VertexConsumer buffer, + Set allowedBones, + Set explicitlyHiddenBones, + Map> hiddenCubeIndices, + int packedOverlay, + int packedLight, + int renderColor + ) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + BoneSnapshot snapshot = bone.frameSnapshot; + boolean selected = allowedBones.contains(bone.name()); + boolean explicitlyHidden = explicitlyHiddenBones.contains(bone.name()); + if (selected && !explicitlyHidden && bone instanceof CuboidGeoBone cuboidBone) { + Set hiddenIndices = hiddenCubeIndices.getOrDefault(bone.name(), Set.of()); + + for (int index = 0; index < cuboidBone.cubes.length; index++) { + if (!hiddenIndices.contains(index)) { + GeoCube cube = cuboidBone.cubes[index]; + poseStack.push(); + cube.render(poseStack, buffer, packedLight, packedOverlay, renderColor); + poseStack.pop(); + } + } + } + + boolean hideChildren = snapshot != null && snapshot.areChildrenHidden(); + if (!hideChildren) { + for (GeoBone child : bone.children()) { + this.renderBoneFiltered(poseStack, child, buffer, allowedBones, explicitlyHiddenBones, hiddenCubeIndices, packedOverlay, packedLight, renderColor); + } + } + + poseStack.pop(); + } + + private Set getExplicitlyHiddenBones(R renderState) { + Map boneVisibility = (Map)renderState.getOrDefaultGeckolibData(GeckoRenderTickets.BONE_VISIBILITY, Map.of()); + if (boneVisibility.isEmpty()) { + return Set.of(); + } + + Set out = new LinkedHashSet<>(); + + for (Entry entry : boneVisibility.entrySet()) { + String boneName = entry.getKey(); + if (boneName != null && !boneName.isBlank() && Boolean.FALSE.equals(entry.getValue())) { + out.add(boneName); + } + } + + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/WildfireGenderGeoLayer.java b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/WildfireGenderGeoLayer.java new file mode 100644 index 0000000..b069571 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/render/gecko/layer/WildfireGenderGeoLayer.java @@ -0,0 +1,152 @@ +package com.nonid.internal.animation.client.render.gecko.layer; + +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.client.compat.wildfire.WildfireGenderCompat; +import com.nonid.internal.animation.client.compat.wildfire.WildfireGenderState; +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.UUID; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import org.jetbrains.annotations.NotNull; +import org.joml.Quaternionf; +import software.bernie.geckolib.animatable.GeoAnimatable; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.GeoRenderer; +import software.bernie.geckolib.renderer.base.RenderPassInfo; +import software.bernie.geckolib.renderer.layer.GeoRenderLayer; +import software.bernie.geckolib.util.RenderUtil; + +public final class WildfireGenderGeoLayer extends GeoRenderLayer { + private static final String BODY_BONE = "body"; + private static boolean loggedMissingBodyBone; + + public WildfireGenderGeoLayer(GeoRenderer<@NotNull T, @NotNull O, @NotNull R> renderer) { + super(renderer); + } + + public void submitRenderTask(RenderPassInfo<@NotNull R> renderPassInfo, @NotNull OrderedRenderCommandQueue renderTasks) { + if (renderPassInfo.willRender()) { + WildfireGenderState state = (WildfireGenderState)renderPassInfo.renderState().getGeckolibData(GeckoRenderTickets.WILDFIRE_GENDER_STATE); + if (state != null) { + if (renderPassInfo.renderState() instanceof LivingEntityRenderState livingState) { + if (renderPassInfo.renderState() instanceof AnimationRenderStateAccess access) { + UUID actorUuid = access.afw$getEntityUuid(); + if (actorUuid != null) { + renderPassInfo.renderPosed(() -> { + MatrixStack modelStack = new MatrixStack(); + modelStack.peek().copy(renderPassInfo.poseStack().peek()); + MatrixStack rootStack = new MatrixStack(); + rootStack.peek().copy(renderPassInfo.poseStack().peek()); + MatrixStack bodyStack = this.findBonePose(rootStack, renderPassInfo, "body"); + if (bodyStack == null) { + logMissingBodyBone(); + } else { + this.submitAtBone(rootStack, bodyStack, modelStack, renderPassInfo, renderTasks, livingState, actorUuid, "body"); + } + }); + } + } + } + } + } + } + + private boolean submitAtBone( + MatrixStack rootPose, + MatrixStack physicsPose, + MatrixStack poseStack, + RenderPassInfo<@NotNull R> renderPassInfo, + OrderedRenderCommandQueue renderTasks, + LivingEntityRenderState livingState, + UUID actorUuid, + String targetBone + ) { + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + if (this.submitAtBoneRecursive(rootPose, physicsPose, poseStack, bone, renderTasks, livingState, actorUuid, renderPassInfo.packedLight(), targetBone)) { + return true; + } + } + + return false; + } + + private boolean submitAtBoneRecursive( + MatrixStack rootPose, + MatrixStack physicsPose, + MatrixStack poseStack, + GeoBone bone, + OrderedRenderCommandQueue renderTasks, + LivingEntityRenderState livingState, + UUID actorUuid, + int packedLight, + String targetBone + ) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + boolean submitted = false; + if (targetBone.equals(bone.name())) { + WildfireGenderCompat.overrideRenderPhysics(actorUuid, livingState, rootPose, physicsPose); + applyVanillaLayerCoordinateBridge(poseStack); + submitted = WildfireGenderCompat.submitOriginalLayer(livingState, poseStack, renderTasks, packedLight); + } else { + for (GeoBone child : bone.children()) { + if (this.submitAtBoneRecursive(rootPose, physicsPose, poseStack, child, renderTasks, livingState, actorUuid, packedLight, targetBone)) { + submitted = true; + break; + } + } + } + + poseStack.pop(); + return submitted; + } + + private MatrixStack findBonePose(MatrixStack rootPose, RenderPassInfo<@NotNull R> renderPassInfo, String targetBone) { + MatrixStack searchStack = new MatrixStack(); + searchStack.peek().copy(rootPose.peek()); + + for (GeoBone bone : renderPassInfo.model().topLevelBones()) { + MatrixStack found = this.findBonePoseRecursive(searchStack, bone, targetBone); + if (found != null) { + return found; + } + } + + return null; + } + + private MatrixStack findBonePoseRecursive(MatrixStack poseStack, GeoBone bone, String targetBone) { + poseStack.push(); + RenderUtil.prepMatrixForBone(poseStack, bone); + MatrixStack found = null; + if (targetBone.equals(bone.name())) { + found = new MatrixStack(); + found.peek().copy(poseStack.peek()); + } else { + for (GeoBone child : bone.children()) { + found = this.findBonePoseRecursive(poseStack, child, targetBone); + if (found != null) { + break; + } + } + } + + poseStack.pop(); + return found; + } + + private static void logMissingBodyBone() { + if (!loggedMissingBodyBone) { + loggedMissingBodyBone = true; + AnimationEngine.LOGGER.warn("[NoN Animation] Female Gender compatibility requires Gecko player bone '{}'; skipping layer.", "body"); + } + } + + private static void applyVanillaLayerCoordinateBridge(MatrixStack poseStack) { + poseStack.translate(0.0F, 1.5F, 0.0F); + poseStack.multiply(new Quaternionf().rotationZ((float) Math.PI)); + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/runtime/ClientAnimationRuntime.java b/src/main/java/com/nonid/internal/animation/client/runtime/ClientAnimationRuntime.java new file mode 100644 index 0000000..f586bb7 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/runtime/ClientAnimationRuntime.java @@ -0,0 +1,1742 @@ +package com.nonid.internal.animation.client.runtime; + +import com.nonid.api.client.NonAnimationRenderEvents; +import com.nonid.api.client.NonAnimationSoundEvents; +import com.nonid.internal.animation.client.diagnostics.AnimationAssetDiagnostics; +import com.nonid.internal.animation.client.render.gecko.GeckoResourceResolver; +import com.nonid.internal.animation.client.render.gecko.ParticleKeyframes; +import com.nonid.internal.animation.client.sound.AnimationSoundAnticipation; +import com.nonid.internal.animation.client.sound.AnimationSoundEffects; +import com.nonid.internal.animation.data.AnimationCueEffects; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.util.StageTimeWarp; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemDisplayContext; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +public final class ClientAnimationRuntime { + private static final List PENDING_STARTS = new ArrayList<>(); + private static final List PENDING_STAGE_ADVANCES = new ArrayList<>(); + private static final Map INSTANCES = new HashMap<>(); + private static final Map PENDING_STOP_TICKS = new HashMap<>(); + private static final Map PENDING_MANUAL_ADVANCE = new HashMap<>(); + private static final Map PENDING_TARGET_STAGE_ADVANCE = new HashMap<>(); + private static final Map PENDING_SPEED_UPDATES = new HashMap<>(); + private static final ArrayDeque COMPLETED_INSTANCES = new ArrayDeque<>(); + private static final Map KNOWN_LOOP_BY_ANIMATION_ID = new HashMap<>(); + private static final Map ACTIVE_ACTOR_LATEST_ANIMATION_ID = new HashMap<>(); + private static final Map> ACTIVE_ACTOR_MODEL_ROOTS = new HashMap<>(); + private static final Map ACTIVE_ACTOR_LATEST_KEY = new HashMap<>(); + private static final Map ACTIVE_ACTOR_SPEED = new HashMap<>(); + private static final Map> ACTIVE_ACTOR_BONE_ITEMS = new HashMap<>(); + private static final Map ACTOR_RENDER_HOLDS = new HashMap<>(); + private static final Map LOCKED_ORIENTATION_BY_ACTOR = new HashMap<>(); + private static volatile boolean HAS_ACTIVE_INSTANCES = false; + private static final ArrayDeque START_ORDER = new ArrayDeque<>(); + private static volatile long CURRENT_TICK = 0L; + private static final long RENDER_HANDOFF_HOLD_TICKS = 1L; + private static final String PROP_LEFT_BONE = "propleft"; + private static final String PROP_RIGHT_BONE = "propright"; + private static final String PROP_FLOOR_BONE = "propfloor"; + private static Long STOP_ALL_AT_TICK = null; + + private ClientAnimationRuntime() { + } + + public static void queueStart( + Identifier animationId, + List> modelRootsByActor, + UUID instanceId, + List actorUuids, + List actorKeys, + List stages, + long startTick, + int stageIndex, + long stageStartTick, + long stageEndTick, + double speed, + boolean lockOrientation, + float lockedYaw, + float lockedHeadYaw, + float lockedPitch, + Vec3d cameraOrbitTarget + ) { + List safeKeys = actorKeys == null ? List.of() : List.copyOf(actorKeys); + List safeStages = stages == null ? List.of() : List.copyOf(stages); + rememberStageMeta(animationId, safeStages); + AnimationAssetDiagnostics.validateStartPayload(animationId, safeKeys, safeStages); + UUID anchorUuid = actorUuids.isEmpty() ? null : actorUuids.getFirst(); + ClientAnimationRuntime.LockedOrientation locked = lockOrientation + ? new ClientAnimationRuntime.LockedOrientation(lockedYaw, lockedHeadYaw, lockedPitch) + : null; + List> safeModelRoots = modelRootsByActor == null + ? Collections.nCopies(actorUuids.size(), List.of()) + : modelRootsByActor.stream().map(roots -> roots == null ? List.of() : List.copyOf((Collection)roots)).toList(); + PENDING_STARTS.add( + new ClientAnimationRuntime.PendingStart( + instanceId, + animationId, + safeModelRoots, + List.copyOf(actorUuids), + safeKeys, + safeStages, + startTick, + stageIndex, + stageStartTick, + stageEndTick, + sanitizeSpeed(speed), + anchorUuid, + lockOrientation, + locked, + sanitizeCameraOrbitTarget(cameraOrbitTarget) + ) + ); + } + + public static void queueStop(UUID instanceId, long stopTick) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + state.stopAtTick = state.stopAtTick == null ? stopTick : Math.min(state.stopAtTick, stopTick); + } else { + PENDING_STOP_TICKS.merge(instanceId, stopTick, Math::min); + if (stopTick <= CURRENT_TICK) { + PENDING_STARTS.removeIf(ps -> ps.instanceId().equals(instanceId)); + } + } + } + + public static void queueStopAll(long stopTick) { + if (STOP_ALL_AT_TICK == null) { + STOP_ALL_AT_TICK = stopTick; + } else { + STOP_ALL_AT_TICK = Math.min(STOP_ALL_AT_TICK, stopTick); + } + } + + public static void stopAllNow() { + STOP_ALL_AT_TICK = null; + PENDING_STARTS.clear(); + PENDING_STAGE_ADVANCES.clear(); + PENDING_STOP_TICKS.clear(); + PENDING_MANUAL_ADVANCE.clear(); + PENDING_TARGET_STAGE_ADVANCE.clear(); + PENDING_SPEED_UPDATES.clear(); + INSTANCES.clear(); + START_ORDER.clear(); + ACTIVE_ACTOR_LATEST_ANIMATION_ID.clear(); + ACTIVE_ACTOR_MODEL_ROOTS.clear(); + ACTIVE_ACTOR_LATEST_KEY.clear(); + ACTIVE_ACTOR_SPEED.clear(); + ACTIVE_ACTOR_BONE_ITEMS.clear(); + ACTOR_RENDER_HOLDS.clear(); + LOCKED_ORIENTATION_BY_ACTOR.clear(); + HAS_ACTIVE_INSTANCES = false; + COMPLETED_INSTANCES.clear(); + KNOWN_LOOP_BY_ANIMATION_ID.clear(); + ParticleKeyframes.clearAll(); + } + + public static void requestStageAdvance(UUID instanceId) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + state.manualAdvanceRequests++; + } else { + PENDING_MANUAL_ADVANCE.merge(instanceId, 1, Integer::sum); + } + } + + public static boolean advanceStage(UUID instanceId) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state == null) { + return false; + } + + state.manualAdvanceRequests++; + return true; + } + + public static void queueStageAdvance(UUID instanceId, long advanceTick, long stageEndTick, int stageIndex) { + if (advanceTick <= CURRENT_TICK) { + requestStageAdvanceTo(instanceId, stageIndex, advanceTick, stageEndTick); + } else { + PENDING_STAGE_ADVANCES.add(new ClientAnimationRuntime.PendingStageAdvance(instanceId, advanceTick, stageEndTick, stageIndex)); + } + } + + private static void requestStageAdvanceTo(UUID instanceId, int stageIndex, long advanceTick, long stageEndTick) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + state.setStageIndex(stageIndex, Math.max(0L, advanceTick), stageEndTick); + ClientAnimationRuntime.PendingSpeedUpdate queuedSpeed = PENDING_SPEED_UPDATES.remove(instanceId); + if (queuedSpeed != null) { + applySpeedToState(state, queuedSpeed); + } + } else { + PENDING_TARGET_STAGE_ADVANCE.put( + instanceId, new ClientAnimationRuntime.PendingStageAdvance(instanceId, Math.max(0L, advanceTick), stageEndTick, stageIndex) + ); + } + } + + public static void queueSpeedUpdate(UUID instanceId, double speed, long stageStartTick, long stageEndTick) { + ClientAnimationRuntime.PendingSpeedUpdate update = new ClientAnimationRuntime.PendingSpeedUpdate(sanitizeSpeed(speed), stageStartTick, stageEndTick); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + if (hasPendingStageAdvance(instanceId)) { + PENDING_SPEED_UPDATES.put(instanceId, update); + } else { + applySpeedToState(state, update); + } + } else { + PENDING_SPEED_UPDATES.put(instanceId, update); + } + } + + private static boolean hasPendingStageAdvance(UUID instanceId) { + if (instanceId == null) { + return false; + } + + if (PENDING_TARGET_STAGE_ADVANCE.containsKey(instanceId)) { + return true; + } + + for (ClientAnimationRuntime.PendingStageAdvance pending : PENDING_STAGE_ADVANCES) { + if (pending != null && instanceId.equals(pending.instanceId())) { + return true; + } + } + + return false; + } + + public static List drainCompletedInstances() { + if (COMPLETED_INSTANCES.isEmpty()) { + return List.of(); + } + + List out = new ArrayList<>(COMPLETED_INSTANCES); + COMPLETED_INSTANCES.clear(); + return out; + } + + public static void clientTick(MinecraftClient client) { + if (client.world != null) { + long now = client.world.getTime(); + CURRENT_TICK = now; + if (STOP_ALL_AT_TICK != null && now >= STOP_ALL_AT_TICK) { + stopAllNow(); + } else if (PENDING_STARTS.isEmpty() + && INSTANCES.isEmpty() + && ACTOR_RENDER_HOLDS.isEmpty() + && PENDING_TARGET_STAGE_ADVANCE.isEmpty() + && STOP_ALL_AT_TICK == null) { + if (!PENDING_STOP_TICKS.isEmpty()) { + PENDING_STOP_TICKS.entrySet().removeIf(e -> e.getValue() <= now); + } + + if (!PENDING_STAGE_ADVANCES.isEmpty()) { + PENDING_STAGE_ADVANCES.removeIf(p -> p.advanceTick() <= now); + } + + if (!ACTIVE_ACTOR_LATEST_ANIMATION_ID.isEmpty() + || !ACTIVE_ACTOR_MODEL_ROOTS.isEmpty() + || !ACTIVE_ACTOR_LATEST_KEY.isEmpty() + || !ACTIVE_ACTOR_SPEED.isEmpty() + || !ACTIVE_ACTOR_BONE_ITEMS.isEmpty()) { + ACTIVE_ACTOR_LATEST_ANIMATION_ID.clear(); + ACTIVE_ACTOR_MODEL_ROOTS.clear(); + ACTIVE_ACTOR_LATEST_KEY.clear(); + ACTIVE_ACTOR_SPEED.clear(); + ACTIVE_ACTOR_BONE_ITEMS.clear(); + HAS_ACTIVE_INSTANCES = false; + } + + START_ORDER.clear(); + ParticleKeyframes.retainActorOrigins(Set.of()); + } else { + if (!ACTOR_RENDER_HOLDS.isEmpty()) { + ACTOR_RENDER_HOLDS.entrySet().removeIf(e -> now > e.getValue().expiresAtTick()); + } + + int i = 0; + + while (i < PENDING_STARTS.size()) { + ClientAnimationRuntime.PendingStart ps = PENDING_STARTS.get(i); + if (now >= ps.startTick()) { + ClientAnimationRuntime.LockedOrientation anchorOrientation = ps.lockOrientation() + ? ps.lockedOrientation() + : resolveOrientation(client, ps.anchorUuid()); + double initialSpeed = sanitizeSpeed(ps.speed()); + long initialStageStartTick = ps.stageStartTick(); + long initialStageEndTick = ps.stageEndTick(); + ClientAnimationRuntime.PendingSpeedUpdate queuedSpeed = PENDING_SPEED_UPDATES.remove(ps.instanceId()); + if (queuedSpeed != null) { + initialSpeed = queuedSpeed.speed(); + initialStageStartTick = queuedSpeed.stageStartTick(); + initialStageEndTick = queuedSpeed.stageEndTick(); + } + + ClientAnimationRuntime.InstanceState state = new ClientAnimationRuntime.InstanceState( + ps.instanceId(), + ps.animationId(), + ps.modelRootsByActor(), + ps.actorUuids(), + ps.actorKeys(), + ps.stages(), + ps.startTick(), + ps.stageIndex(), + initialStageStartTick, + initialStageEndTick, + initialSpeed, + ps.anchorUuid(), + anchorOrientation, + ps.cameraOrbitTarget() + ); + ClientAnimationRuntime.PendingStageAdvance pendingTargetStage = PENDING_TARGET_STAGE_ADVANCE.remove(ps.instanceId()); + if (pendingTargetStage != null) { + state.setStageIndex(pendingTargetStage.stageIndex(), pendingTargetStage.advanceTick(), pendingTargetStage.stageEndTick()); + } + + Integer pendingAdvance = PENDING_MANUAL_ADVANCE.remove(ps.instanceId()); + if (pendingTargetStage == null && pendingAdvance != null) { + state.manualAdvanceRequests = pendingAdvance; + } + + Long pendingStop = PENDING_STOP_TICKS.remove(ps.instanceId()); + if (pendingStop != null) { + state.stopAtTick = pendingStop; + } + + INSTANCES.put(ps.instanceId(), state); + START_ORDER.addLast(ps.instanceId()); + + for (UUID actorUuid : ps.actorUuids()) { + ACTOR_RENDER_HOLDS.remove(actorUuid); + } + + PENDING_STARTS.remove(i); + } else { + i++; + } + } + + if (!PENDING_STAGE_ADVANCES.isEmpty()) { + i = 0; + + while (i < PENDING_STAGE_ADVANCES.size()) { + ClientAnimationRuntime.PendingStageAdvance pending = PENDING_STAGE_ADVANCES.get(i); + if (now >= pending.advanceTick()) { + requestStageAdvanceTo(pending.instanceId(), pending.stageIndex(), pending.advanceTick(), pending.stageEndTick()); + PENDING_STAGE_ADVANCES.remove(i); + } else { + i++; + } + } + } + + if (!INSTANCES.isEmpty()) { + Iterator> expiredIt = INSTANCES.entrySet().iterator(); + + while (expiredIt.hasNext()) { + Entry entry = expiredIt.next(); + ClientAnimationRuntime.InstanceState state = entry.getValue(); + if (now >= state.effectiveStopTick()) { + rememberRenderHolds(state, now); + ParticleKeyframes.clearInstance(entry.getKey()); + expiredIt.remove(); + } + } + } + + if (!INSTANCES.isEmpty()) { + List completed = new ArrayList<>(); + + for (ClientAnimationRuntime.InstanceState state : INSTANCES.values()) { + if (now >= state.startTick) { + while (state.manualAdvanceRequests > 0) { + state.manualAdvanceRequests--; + if (!state.advanceStage(now)) { + completed.add(state.instanceId); + break; + } + + ClientAnimationRuntime.PendingSpeedUpdate queued = PENDING_SPEED_UPDATES.remove(state.instanceId); + if (queued != null) { + applySpeedToState(state, queued); + } + } + } + } + + if (!completed.isEmpty()) { + for (UUID id : completed) { + ClientAnimationRuntime.InstanceState completedState = INSTANCES.remove(id); + if (completedState != null) { + rememberRenderHolds(completedState, now); + } + + ParticleKeyframes.clearInstance(id); + PENDING_STOP_TICKS.remove(id); + PENDING_MANUAL_ADVANCE.remove(id); + PENDING_TARGET_STAGE_ADVANCE.remove(id); + PENDING_SPEED_UPDATES.remove(id); + START_ORDER.removeIf(existing -> existing.equals(id)); + COMPLETED_INSTANCES.add(id); + } + } + } + + if (START_ORDER.size() > 256) { + ArrayDeque compact = new ArrayDeque<>(START_ORDER.size()); + + for (UUID id : START_ORDER) { + if (INSTANCES.containsKey(id)) { + compact.addLast(id); + } + } + + START_ORDER.clear(); + START_ORDER.addAll(compact); + } else { + START_ORDER.removeIf(idx -> !INSTANCES.containsKey(idx)); + } + + ACTIVE_ACTOR_LATEST_ANIMATION_ID.clear(); + ACTIVE_ACTOR_MODEL_ROOTS.clear(); + ACTIVE_ACTOR_LATEST_KEY.clear(); + ACTIVE_ACTOR_SPEED.clear(); + ACTIVE_ACTOR_BONE_ITEMS.clear(); + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID id = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(id); + if (state != null) { + for (int ix = 0; ix < state.actorUuids.size(); ix++) { + UUID actorUuid = state.actorUuids.get(ix); + ACTIVE_ACTOR_LATEST_ANIMATION_ID.putIfAbsent(actorUuid, state.currentAnimationId()); + ACTIVE_ACTOR_MODEL_ROOTS.putIfAbsent(actorUuid, state.modelRootsForActor(ix)); + ACTIVE_ACTOR_SPEED.putIfAbsent(actorUuid, state.speed); + ACTIVE_ACTOR_BONE_ITEMS.putIfAbsent(actorUuid, resolveActorBoneItems(state, ix)); + if (ix < state.actorKeys.size()) { + String actorKey = state.actorKeys.get(ix); + if (actorKey != null && !actorKey.isEmpty()) { + ACTIVE_ACTOR_LATEST_KEY.putIfAbsent(actorUuid, actorKey); + } + } + } + } + } + + if (!ACTOR_RENDER_HOLDS.isEmpty()) { + for (Entry entry : ACTOR_RENDER_HOLDS.entrySet()) { + UUID actorUuid = entry.getKey(); + ClientAnimationRuntime.RenderHold hold = entry.getValue(); + ACTIVE_ACTOR_LATEST_ANIMATION_ID.putIfAbsent(actorUuid, hold.animationId()); + ACTIVE_ACTOR_MODEL_ROOTS.putIfAbsent(actorUuid, hold.modelRoots()); + ACTIVE_ACTOR_SPEED.putIfAbsent(actorUuid, hold.speed()); + ACTIVE_ACTOR_BONE_ITEMS.putIfAbsent(actorUuid, hold.boneItems()); + if (hold.actorKey() != null && !hold.actorKey().isEmpty()) { + ACTIVE_ACTOR_LATEST_KEY.putIfAbsent(actorUuid, hold.actorKey()); + } + } + } + + ParticleKeyframes.retainActorOrigins(ACTIVE_ACTOR_LATEST_ANIMATION_ID.keySet()); + HAS_ACTIVE_INSTANCES = !ACTIVE_ACTOR_LATEST_ANIMATION_ID.isEmpty(); + syncLockedOrientations(client); + tickSoundCues(client, now); + } + } + } + + private static void rememberRenderHolds(ClientAnimationRuntime.InstanceState state, long nowTick) { + if (state != null && !state.actorUuids.isEmpty()) { + long expiresAt = nowTick + 1L; + + for (int i = 0; i < state.actorUuids.size(); i++) { + UUID actorUuid = state.actorUuids.get(i); + if (actorUuid != null) { + String actorKey = i < state.actorKeys.size() ? state.actorKeys.get(i) : null; + ACTOR_RENDER_HOLDS.put( + actorUuid, + new ClientAnimationRuntime.RenderHold( + state.instanceId, + state.currentAnimationId(), + state.modelRootsForActor(i), + actorKey, + state.speed, + resolveActorBoneItems(state, i), + state.anchorOrientation, + expiresAt + ) + ); + } + } + } + } + + public static boolean hasActiveInstances() { + return HAS_ACTIVE_INSTANCES; + } + + public static boolean isActorActive(UUID actorUuid) { + return ACTIVE_ACTOR_LATEST_ANIMATION_ID.containsKey(actorUuid); + } + + public static boolean isActorPendingOrActive(UUID actorUuid) { + if (ACTIVE_ACTOR_LATEST_ANIMATION_ID.containsKey(actorUuid)) { + return true; + } + + for (ClientAnimationRuntime.PendingStart ps : PENDING_STARTS) { + for (UUID uuid : ps.actorUuids()) { + if (uuid.equals(actorUuid)) { + return true; + } + } + } + + return false; + } + + @Nullable + public static ClientAnimationRuntime.LockedOrientation getLockedOrientation(UUID actorUuid) { + ClientAnimationRuntime.LockedOrientation locked = LOCKED_ORIENTATION_BY_ACTOR.get(actorUuid); + if (locked != null) { + return locked; + } + + ClientAnimationRuntime.RenderHold hold = ACTOR_RENDER_HOLDS.get(actorUuid); + return hold != null && CURRENT_TICK <= hold.expiresAtTick() ? hold.lockedOrientation() : null; + } + + @Nullable + public static Vec3d findCameraOrbitTargetForActor(UUID actorUuid) { + if (actorUuid == null) { + return null; + } + + long now = CURRENT_TICK; + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID id = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(id); + if (state != null && state.isActive(now) && state.containsActor(actorUuid)) { + return state.cameraOrbitTarget; + } + } + + return null; + } + + @Nullable + public static UUID findAnchorUuidForActor(UUID actorUuid) { + for (ClientAnimationRuntime.InstanceState state : INSTANCES.values()) { + if (state.containsActor(actorUuid)) { + return state.anchorUuid; + } + } + + for (ClientAnimationRuntime.PendingStart ps : PENDING_STARTS) { + for (UUID uuid : ps.actorUuids()) { + if (uuid.equals(actorUuid)) { + return ps.anchorUuid(); + } + } + } + + return null; + } + + @Nullable + public static Long findStartTickForActor(UUID actorUuid) { + for (ClientAnimationRuntime.InstanceState state : INSTANCES.values()) { + if (state.containsActor(actorUuid)) { + return state.startTick; + } + } + + for (ClientAnimationRuntime.PendingStart ps : PENDING_STARTS) { + for (UUID uuid : ps.actorUuids()) { + if (uuid.equals(actorUuid)) { + return ps.startTick(); + } + } + } + + return null; + } + + public static Identifier findLatestActiveAnimationIdContaining(UUID actorUuid) { + return ACTIVE_ACTOR_LATEST_ANIMATION_ID.get(actorUuid); + } + + public static List findLatestModelRootsForActor(UUID actorUuid) { + List roots = ACTIVE_ACTOR_MODEL_ROOTS.get(actorUuid); + return roots == null ? List.of() : roots; + } + + public static String findLatestActiveActorKey(UUID actorUuid) { + return ACTIVE_ACTOR_LATEST_KEY.get(actorUuid); + } + + public static double findLatestSpeedForActor(UUID actorUuid) { + return ACTIVE_ACTOR_SPEED.getOrDefault(actorUuid, 1.0); + } + + public static Map findLatestBoneItemsForActor(UUID actorUuid) { + Map props = ACTIVE_ACTOR_BONE_ITEMS.get(actorUuid); + return props != null && !props.isEmpty() ? props : Map.of(); + } + + public static void setPropOverride(UUID instanceId, UUID actorUuid, ClientAnimationRuntime.PropSlot slot, ItemStack stack) { + if (instanceId != null && actorUuid != null && slot != null) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + EnumMap actorOverrides = state.propOverridesByActor + .computeIfAbsent(actorUuid, unused -> new EnumMap<>(ClientAnimationRuntime.PropSlot.class)); + if (stack != null && !stack.isEmpty()) { + actorOverrides.put(slot, stack.copy()); + } else { + actorOverrides.remove(slot); + } + + if (actorOverrides.isEmpty()) { + state.propOverridesByActor.remove(actorUuid); + } + } + } + } + + public static void setLeftHandPropOverride(UUID instanceId, UUID actorUuid, ItemStack stack) { + setPropOverride(instanceId, actorUuid, ClientAnimationRuntime.PropSlot.LEFT, stack); + } + + public static void setRightHandPropOverride(UUID instanceId, UUID actorUuid, ItemStack stack) { + setPropOverride(instanceId, actorUuid, ClientAnimationRuntime.PropSlot.RIGHT, stack); + } + + public static void setFloorPropOverride(UUID instanceId, UUID actorUuid, ItemStack stack) { + setPropOverride(instanceId, actorUuid, ClientAnimationRuntime.PropSlot.FLOOR, stack); + } + + public static void clearPropOverride(UUID instanceId, UUID actorUuid, ClientAnimationRuntime.PropSlot slot) { + if (instanceId != null && actorUuid != null && slot != null) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + EnumMap actorOverrides = state.propOverridesByActor.get(actorUuid); + if (actorOverrides != null) { + actorOverrides.remove(slot); + if (actorOverrides.isEmpty()) { + state.propOverridesByActor.remove(actorUuid); + } + } + } + } + } + + public static void clearLeftHandPropOverride(UUID instanceId, UUID actorUuid) { + clearPropOverride(instanceId, actorUuid, ClientAnimationRuntime.PropSlot.LEFT); + } + + public static void clearRightHandPropOverride(UUID instanceId, UUID actorUuid) { + clearPropOverride(instanceId, actorUuid, ClientAnimationRuntime.PropSlot.RIGHT); + } + + public static void clearFloorPropOverride(UUID instanceId, UUID actorUuid) { + clearPropOverride(instanceId, actorUuid, ClientAnimationRuntime.PropSlot.FLOOR); + } + + public static void clearPropOverrides(UUID instanceId, UUID actorUuid) { + if (instanceId != null && actorUuid != null) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + state.propOverridesByActor.remove(actorUuid); + } + } + } + + public static UUID findLatestActiveInstanceContaining(UUID actorUuid) { + long now = CURRENT_TICK; + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID id = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(id); + if (state != null && state.isActive(now) && state.containsActor(actorUuid)) { + return id; + } + } + + ClientAnimationRuntime.RenderHold hold = ACTOR_RENDER_HOLDS.get(actorUuid); + return hold != null && now <= hold.expiresAtTick() ? hold.instanceId() : null; + } + + public static boolean shouldHideNameLabelForViewer(UUID viewerUuid, UUID renderedUuid) { + if (viewerUuid != null && renderedUuid != null && !viewerUuid.equals(renderedUuid)) { + UUID viewerInstance = findLatestActiveInstanceContaining(viewerUuid); + if (viewerInstance == null) { + return false; + } + + UUID renderedInstance = findLatestActiveInstanceContaining(renderedUuid); + return viewerInstance.equals(renderedInstance); + } else { + return false; + } + } + + public static double findOutsideViewerNameLabelYOffset(UUID viewerUuid, UUID renderedUuid) { + if (renderedUuid == null) { + return 0.0; + } + + ClientAnimationRuntime.InstanceState state = findLatestActiveStateContaining(renderedUuid); + if (state == null) { + return 0.0; + } + + if (viewerUuid != null && state.containsActor(viewerUuid)) { + return 0.0; + } + + int actorIndex = state.actorUuids.indexOf(renderedUuid); + return actorIndex <= 0 ? 0.0 : Math.min(1.5, actorIndex * 0.28); + } + + private static ClientAnimationRuntime.InstanceState findLatestActiveStateContaining(UUID actorUuid) { + if (actorUuid == null) { + return null; + } + + long now = CURRENT_TICK; + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID id = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(id); + if (state != null && state.isActive(now) && state.containsActor(actorUuid)) { + return state; + } + } + + return null; + } + + public static AnimationStageInfo findCurrentStage(UUID instanceId) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + return state == null ? null : state.currentStage(); + } + + public static AnimationStageInfo findStage(UUID instanceId, int stageIndex) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + return state != null && stageIndex >= 0 && stageIndex < state.stages.size() ? state.stages.get(stageIndex) : null; + } + + public static ClientAnimationRuntime.StageTimelineSnapshot findStageTimeline(UUID instanceId) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + return state == null + ? null + : new ClientAnimationRuntime.StageTimelineSnapshot(state.stages, state.stageIndex, state.stageStartTick, state.stageEndTick, state.speed); + } + + public static long findStageStartTick(UUID instanceId) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + return state == null ? -1L : state.stageStartTick; + } + + public static long findStageEndTick(UUID instanceId) { + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + return state == null ? -1L : state.stageEndTick; + } + + public static Double findWarpedAuthoredTimelineSeconds(UUID actorUuid, float tickDelta) { + if (actorUuid == null) { + return null; + } + + long now = CURRENT_TICK; + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID id = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(id); + if (state != null && state.isActive(now) && state.containsActor(actorUuid)) { + AnimationStageInfo stage = state.currentStage(); + double cycleTicks = exactCycleTicks(stage); + if (stage != null && stage.loop() && !(cycleTicks <= 0.0)) { + double offsetTicks = StageTimeWarp.offsetSecondsToTicks(stage.cycleMidpointOffsetSeconds()); + if (!StageTimeWarp.hasWarp(cycleTicks, offsetTicks)) { + return null; + } + + double warpedTicks = Math.max(0.0, ((float)now + Math.max(0.0F, tickDelta) - (float)state.stageStartTick) * state.speed); + double authoredTicks = StageTimeWarp.warpedToAuthoredCycleTicks(warpedTicks, cycleTicks, offsetTicks); + return authoredTicks / 20.0; + } + + return null; + } + } + + return null; + } + + public static UUID findLatestActiveInstance() { + long now = CURRENT_TICK; + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID id = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(id); + if (state != null && state.isActive(now)) { + return id; + } + } + + return null; + } + + public static boolean isAnimationLooping(Identifier animationId) { + if (animationId == null) { + return true; + } + + Boolean loop = KNOWN_LOOP_BY_ANIMATION_ID.get(animationId); + return loop != null ? loop : true; + } + + private static List normalizeStages(Identifier baseAnimationId, List stages) { + return stages != null && !stages.isEmpty() ? List.copyOf(stages) : List.of(new AnimationStageInfo(baseAnimationId, true, 0L, true, 1.0)); + } + + private static double sanitizeSpeed(double speed) { + return !Double.isNaN(speed) && !Double.isInfinite(speed) && !(speed <= 0.0) ? Math.max(0.1, Math.min(speed, 4.0)) : 1.0; + } + + private static double exactCycleTicks(AnimationStageInfo stage) { + if (stage == null) { + return 0.0; + } + + double exact = stage.cycleTicks(); + return Double.isFinite(exact) && exact > 0.0 ? exact : Math.max(0L, stage.lengthTicks()); + } + + private static Vec3d sanitizeCameraOrbitTarget(Vec3d target) { + return target != null && target.isFinite() ? target : null; + } + + private static void applySpeedToState(ClientAnimationRuntime.InstanceState state, ClientAnimationRuntime.PendingSpeedUpdate update) { + state.speed = update.speed(); + state.stageStartTick = Math.max(state.startTick, update.stageStartTick()); + state.stageEndTick = update.stageEndTick(); + } + + private static void rememberStageMeta(Identifier baseAnimationId, List stages) { + if (stages != null && !stages.isEmpty()) { + for (AnimationStageInfo stage : stages) { + if (stage != null && stage.animationId() != null) { + KNOWN_LOOP_BY_ANIMATION_ID.put(stage.animationId(), stage.loop()); + Identifier playbackId = stage.effectiveAnimationId(); + if (playbackId != null) { + KNOWN_LOOP_BY_ANIMATION_ID.put(playbackId, stage.loop()); + } + } + } + } else { + KNOWN_LOOP_BY_ANIMATION_ID.putIfAbsent(baseAnimationId, true); + } + } + + private static Map resolveActorBoneItems(ClientAnimationRuntime.InstanceState state, int actorIndex) { + if (state != null && actorIndex >= 0 && actorIndex < state.actorUuids.size()) { + String actorKey = actorIndex < state.actorKeys.size() ? state.actorKeys.get(actorIndex) : null; + UUID actorUuid = state.actorUuids.get(actorIndex); + LinkedHashMap out = new LinkedHashMap<>(); + AnimationStageInfo.ActorPropEntry stageProps = state.currentStage().actorPropsForKey(actorKey); + if (stageProps != null) { + NonAnimationRenderEvents.BoneItemProp left = stageProps.leftBoneProp(); + NonAnimationRenderEvents.BoneItemProp right = stageProps.rightBoneProp(); + NonAnimationRenderEvents.BoneItemProp floor = stageProps.floorBoneProp(); + if (left != null) { + out.put("propleft", left); + } + + if (right != null) { + out.put("propright", right); + } + + if (floor != null) { + out.put("propfloor", floor); + } + } + + EnumMap actorOverrides = state.propOverridesByActor.get(actorUuid); + if (actorOverrides != null && !actorOverrides.isEmpty()) { + for (Entry entry : actorOverrides.entrySet()) { + ClientAnimationRuntime.PropSlot slot = entry.getKey(); + ItemStack stack = entry.getValue(); + if (slot != null) { + if (stack != null && !stack.isEmpty()) { + out.put(slot.boneName(), new NonAnimationRenderEvents.BoneItemProp(stack, slot.displayContext())); + } else { + out.remove(slot.boneName()); + } + } + } + } + + return out.isEmpty() ? Map.of() : Map.copyOf(out); + } else { + return Map.of(); + } + } + + private static void syncLockedOrientations(MinecraftClient client) { + if (client != null && client.world != null) { + if (ACTIVE_ACTOR_LATEST_ANIMATION_ID.isEmpty()) { + LOCKED_ORIENTATION_BY_ACTOR.clear(); + } else { + Map rebuilt = new HashMap<>(); + Iterator it = START_ORDER.descendingIterator(); + + while (it.hasNext()) { + UUID instanceId = it.next(); + ClientAnimationRuntime.InstanceState state = INSTANCES.get(instanceId); + if (state != null) { + if (state.anchorOrientation == null) { + state.anchorOrientation = resolveOrientation(client, state.anchorUuid); + } + + ClientAnimationRuntime.LockedOrientation anchor = state.anchorOrientation; + + for (UUID actorUuid : state.actorUuids) { + if (ACTIVE_ACTOR_LATEST_ANIMATION_ID.containsKey(actorUuid) && !rebuilt.containsKey(actorUuid)) { + if (anchor != null) { + rebuilt.put(actorUuid, anchor); + } else if (client.world.getEntity(actorUuid) instanceof LivingEntity living) { + rebuilt.put(actorUuid, new ClientAnimationRuntime.LockedOrientation(living.getBodyYaw(), living.getHeadYaw(), living.getPitch())); + } + } + } + } + } + + LOCKED_ORIENTATION_BY_ACTOR.clear(); + LOCKED_ORIENTATION_BY_ACTOR.putAll(rebuilt); + } + } + } + + private static ClientAnimationRuntime.LockedOrientation resolveOrientation(MinecraftClient client, UUID uuid) { + if (uuid != null && client != null && client.world != null) { + return client.world.getEntity(uuid) instanceof LivingEntity living + ? new ClientAnimationRuntime.LockedOrientation(living.getBodyYaw(), living.getHeadYaw(), living.getPitch()) + : null; + } else { + return null; + } + } + + private static void tickSoundCues(MinecraftClient client, long now) { + if (client != null && client.world != null) { + if (!INSTANCES.isEmpty()) { + if (AnimationSoundEffects.hasAny()) { + ClientWorld world = client.world; + double lookaheadWorldTicks = AnimationSoundAnticipation.lookaheadMillis() / 50.0; + + for (ClientAnimationRuntime.InstanceState state : INSTANCES.values()) { + if (now >= state.startTick && now < state.effectiveStopTick() && !state.actorUuids.isEmpty()) { + if (state.soundStageIndex != state.stageIndex) { + state.soundStageIndex = state.stageIndex; + state.lastSoundTick = -1.0; + } + + if (state.anticipatedSoundStageIndex != state.stageIndex) { + state.anticipatedSoundStageIndex = state.stageIndex; + state.lastAnticipatedSoundTick = -1.0; + } + + AnimationStageInfo currentStage = state.currentStage(); + Identifier stageAnimationId = state.currentAnimationId(); + double animTicks = Math.max(0.0, (now - state.stageStartTick) * state.speed); + ClientAnimationRuntime.MergedSoundTrack soundTrack = resolveMergedSoundTrack(world, state, stageAnimationId, currentStage); + if (soundTrack != null && !soundTrack.cues().isEmpty()) { + double anticipatedWorldTick = now + lookaheadWorldTicks; + if (state.stageEndTick > state.stageStartTick && anticipatedWorldTick >= state.stageEndTick) { + anticipatedWorldTick = Math.nextDown(state.stageEndTick); + } + + double anticipatedTicks = Math.max(0.0, (anticipatedWorldTick - state.stageStartTick) * state.speed); + fireAnticipatedCues( + soundTrack.anchor(), + state, + soundTrack.cues(), + stageAnimationId, + state.stageIndex, + currentStage.loop(), + state.speed, + state.lastAnticipatedSoundTick, + anticipatedTicks, + soundTrack.cycleTicks() + ); + state.lastAnticipatedSoundTick = Math.max(state.lastAnticipatedSoundTick, anticipatedTicks); + fireSoundCues( + soundTrack.anchor(), + state, + soundTrack.cues(), + stageAnimationId, + currentStage.loop(), + state.lastSoundTick, + animTicks, + soundTrack.cycleTicks() + ); + state.lastSoundTick = animTicks; + } + + anticipateNextStageCues(world, state, now, lookaheadWorldTicks); + } + } + } + } + } + } + + private static ClientAnimationRuntime.MergedSoundTrack resolveMergedSoundTrack( + ClientWorld world, ClientAnimationRuntime.InstanceState state, Identifier stageAnimationId, AnimationStageInfo stage + ) { + if (world != null && state != null && stageAnimationId != null && stage != null) { + int count = state.actorUuids.size(); + if (count == 0) { + return null; + } + + LivingEntity anchor = resolveSoundAnchor(world, state); + if (anchor == null) { + return null; + } + + List rawCues = new ArrayList<>(); + double maxLengthTicks = 0.0; + + for (int index : soundActorOrder(state)) { + ClientAnimationRuntime.SoundTrackCandidate candidate = resolveSoundCandidateForIndex(world, state, stageAnimationId, index); + if (candidate != null) { + maxLengthTicks = Math.max(maxLengthTicks, candidate.soundAnimation().lengthTicks()); + + for (AnimationSoundEffects.SoundCue cue : candidate.soundAnimation().cues()) { + if (cue != null) { + rawCues.add(new ClientAnimationRuntime.MergedSoundCue(candidate.animationResource(), candidate.animationKey(), cue, cue.timeTicks())); + } + } + } + } + + if (rawCues.isEmpty()) { + return null; + } + + double exactStageCycleTicks = exactCycleTicks(stage); + double cycleTicks = exactStageCycleTicks > 0.0 ? exactStageCycleTicks : maxLengthTicks; + double offsetTicks = StageTimeWarp.offsetSecondsToTicks(stage.cycleMidpointOffsetSeconds()); + boolean warp = stage.loop() && StageTimeWarp.hasWarp(cycleTicks, offsetTicks); + Map byCue = new LinkedHashMap<>(); + + for (ClientAnimationRuntime.MergedSoundCue rawCue : rawCues) { + double cueTimeTicks = warp ? warpCueTick(rawCue.cue().timeTicks(), cycleTicks, offsetTicks) : rawCue.cue().timeTicks(); + long timeKey = soundCueTimeKey(cueTimeTicks); + ClientAnimationRuntime.SoundCueMergeKey mergeKey = new ClientAnimationRuntime.SoundCueMergeKey( + timeKey, AnimationCueEffects.timingFamily(rawCue.cue().effect()), rawCue.cue().randomSounds() + ); + byCue.putIfAbsent( + mergeKey, new ClientAnimationRuntime.MergedSoundCue(rawCue.animationResource(), rawCue.animationKey(), rawCue.cue(), cueTimeTicks) + ); + } + + if (byCue.isEmpty()) { + return null; + } + + List cues = new ArrayList<>(byCue.values()); + cues.sort(Comparator.comparingDouble(ClientAnimationRuntime.MergedSoundCue::timeTicks)); + return new ClientAnimationRuntime.MergedSoundTrack(anchor, List.copyOf(cues), cycleTicks); + } else { + return null; + } + } + + private static LivingEntity resolveSoundAnchor(ClientWorld world, ClientAnimationRuntime.InstanceState state) { + if (world != null && state != null) { + if (state.anchorUuid != null && world.getEntity(state.anchorUuid) instanceof LivingEntity living) { + return living; + } + + for (UUID actorId : state.actorUuids) { + if (world.getEntity(actorId) instanceof LivingEntity living) { + return living; + } + } + + return null; + } else { + return null; + } + } + + private static List soundActorOrder(ClientAnimationRuntime.InstanceState state) { + if (state != null && !state.actorUuids.isEmpty()) { + List order = new ArrayList<>(state.actorUuids.size()); + int anchorIndex = -1; + if (state.anchorUuid != null) { + for (int i = 0; i < state.actorUuids.size(); i++) { + if (state.actorUuids.get(i).equals(state.anchorUuid)) { + anchorIndex = i; + break; + } + } + } + + if (anchorIndex >= 0) { + order.add(anchorIndex); + } + + for (int i = 0; i < state.actorUuids.size(); i++) { + if (i != anchorIndex) { + order.add(i); + } + } + + return order; + } else { + return List.of(); + } + } + + private static long soundCueTimeKey(double timeTicks) { + return !Double.isFinite(timeTicks) ? Long.MIN_VALUE : Math.round(timeTicks * 1000.0); + } + + private static double warpCueTick(double cueTick, double cycleTicks, double offsetTicks) { + if (!StageTimeWarp.hasWarp(cycleTicks, offsetTicks)) { + return cueTick; + } + + double warped = StageTimeWarp.authoredToWarpedCycleTicks(cueTick, cycleTicks, offsetTicks); + return cueTick > 0.0 && Math.abs(cueTick - cycleTicks) < 1.0E-4 ? cycleTicks : warped; + } + + private static ClientAnimationRuntime.SoundTrackCandidate resolveSoundCandidateForIndex( + ClientWorld world, ClientAnimationRuntime.InstanceState state, Identifier stageAnimationId, int index + ) { + if (world == null || state == null || stageAnimationId == null) { + return null; + } + + if (index >= 0 && index < state.actorUuids.size()) { + UUID actorId = state.actorUuids.get(index); + if (world.getEntity(actorId) instanceof LivingEntity anchor) { + Identifier typeId = Registries.ENTITY_TYPE.getId(anchor.getType()); + String actorKey = index < state.actorKeys.size() ? state.actorKeys.get(index) : null; + ClientAnimationRuntime.SoundTrackCandidate candidate = resolveSoundCandidateForParams(stageAnimationId, actorKey, typeId, true); + if (candidate != null) { + return candidate; + } + + if (anchor instanceof PlayerEntity) { + Identifier playerType = Identifier.of("minecraft", "player"); + candidate = resolveSoundCandidateForParams(stageAnimationId, "player", playerType, false); + if (candidate != null) { + return candidate; + } + } + + return null; + } else { + return null; + } + } else { + return null; + } + } + + private static ClientAnimationRuntime.SoundTrackCandidate resolveSoundCandidateForParams( + Identifier stageAnimationId, String actorKey, Identifier typeId, boolean warnMissing + ) { + if (stageAnimationId == null) { + return null; + } + + Identifier animationResource = GeckoResourceResolver.resolveAnimationResource(stageAnimationId, actorKey, typeId, warnMissing); + if (animationResource == null) { + return null; + } + + String animationKey = GeckoResourceResolver.resolveAnimationKey(animationResource, stageAnimationId, actorKey, typeId, warnMissing); + if (animationKey == null) { + return null; + } + + AnimationSoundEffects.SoundAnimation soundAnimation = AnimationSoundEffects.getSoundAnimation(animationResource, animationKey); + return soundAnimation != null && !soundAnimation.cues().isEmpty() + ? new ClientAnimationRuntime.SoundTrackCandidate(animationResource, animationKey, soundAnimation) + : null; + } + + private static void anticipateNextStageCues(ClientWorld world, ClientAnimationRuntime.InstanceState state, long now, double lookaheadWorldTicks) { + if (!(lookaheadWorldTicks <= 0.0)) { + if (state.stageEndTick > state.stageStartTick) { + if (AnimationSoundAnticipation.allowNextStageLookahead(state.currentStage().animationId(), state.stageIndex)) { + double anticipatedWorldTick = now + lookaheadWorldTicks; + if (!(anticipatedWorldTick < state.stageEndTick)) { + int nextStageIndex = state.stageIndex + 1; + if (nextStageIndex < state.stages.size()) { + if (state.anticipatedNextSoundStageIndex != nextStageIndex) { + state.anticipatedNextSoundStageIndex = nextStageIndex; + state.lastAnticipatedNextSoundTick = -1.0; + } + + AnimationStageInfo nextStage = state.stages.get(nextStageIndex); + Identifier nextAnimationId = nextStage.effectiveAnimationId(); + ClientAnimationRuntime.MergedSoundTrack nextTrack = resolveMergedSoundTrack(world, state, nextAnimationId, nextStage); + if (nextTrack != null && !nextTrack.cues().isEmpty()) { + double nextSpeed = sanitizeSpeed(nextStage.speed()); + double anticipatedTicks = Math.max(0.0, (anticipatedWorldTick - state.stageEndTick) * nextSpeed); + if (!nextStage.loop() && nextTrack.cycleTicks() > 0.0) { + anticipatedTicks = Math.min(anticipatedTicks, nextTrack.cycleTicks()); + } + + fireAnticipatedCues( + nextTrack.anchor(), + state, + nextTrack.cues(), + nextAnimationId, + nextStageIndex, + nextStage.loop(), + nextSpeed, + state.lastAnticipatedNextSoundTick, + anticipatedTicks, + nextTrack.cycleTicks() + ); + state.lastAnticipatedNextSoundTick = Math.max(state.lastAnticipatedNextSoundTick, anticipatedTicks); + } + } + } + } + } + } + } + + private static void fireAnticipatedCues( + LivingEntity anchor, + ClientAnimationRuntime.InstanceState state, + List cues, + Identifier stageAnimationId, + int stageIndex, + boolean loop, + double speed, + double lastTicks, + double anticipatedTicks, + double cycleTicks + ) { + if (!(lastTicks >= 0.0) || !(anticipatedTicks <= lastTicks + 1.0E-4)) { + visitSoundCues( + cues, + loop, + lastTicks, + anticipatedTicks, + cycleTicks, + mergedCue -> anticipateSoundCue(anchor, state, mergedCue, stageAnimationId, cues, stageIndex, speed, loop, cycleTicks) + ); + } + } + + private static void anticipateSoundCue( + LivingEntity anchor, + ClientAnimationRuntime.InstanceState state, + ClientAnimationRuntime.MergedSoundCue mergedCue, + Identifier stageAnimationId, + List cues, + int stageIndex, + double speed, + boolean loop, + double cycleTicks + ) { + if (mergedCue != null && mergedCue.cue() != null) { + String effect = mergedCue.cue().effect(); + if (effect != null && !effect.isBlank()) { + AnimationSoundAnticipation.anticipate( + createSoundContext(anchor, state, mergedCue, stageAnimationId, cues, stageIndex, speed, effect, loop, cycleTicks) + ); + } + } + } + + private static void fireSoundCues( + LivingEntity anchor, + ClientAnimationRuntime.InstanceState state, + List cues, + Identifier stageAnimationId, + boolean loop, + double lastTicks, + double animTicks, + double cycleTicks + ) { + visitSoundCues( + cues, loop, lastTicks, animTicks, cycleTicks, mergedCue -> playSoundCue(anchor, state, mergedCue, stageAnimationId, cues, loop, cycleTicks) + ); + } + + private static void visitSoundCues( + List cues, + boolean loop, + double lastTicks, + double animTicks, + double cycleTicks, + ClientAnimationRuntime.SoundCueVisitor visitor + ) { + if (loop && !(cycleTicks <= 0.0)) { + long lastCycle = lastTicks < 0.0 ? -1L : (long)Math.floor(lastTicks / cycleTicks); + long currentCycle = (long)Math.floor(animTicks / cycleTicks); + double lastCyclePos = lastCycle < 0L ? -1.0 : lastTicks - lastCycle * cycleTicks; + double currentCyclePos = animTicks - currentCycle * cycleTicks; + if (lastCycle < 0L) { + lastCycle = currentCycle; + } + + if (currentCycle == lastCycle) { + visitSoundCueWindow(cues, lastCyclePos, currentCyclePos, visitor); + } else { + visitSoundCueWindow(cues, lastCyclePos, cycleTicks, visitor); + long maxExtraCycles = Math.min(2L, currentCycle - lastCycle - 1L); + + for (long i = 0L; i < maxExtraCycles; i++) { + visitSoundCueWindow(cues, -1.0, cycleTicks, visitor); + } + + visitSoundCueWindow(cues, -1.0, currentCyclePos, visitor); + } + } else { + visitSoundCueWindow(cues, lastTicks, animTicks, visitor); + } + } + + private static void visitSoundCueWindow( + List cues, double fromTick, double toTick, ClientAnimationRuntime.SoundCueVisitor visitor + ) { + for (ClientAnimationRuntime.MergedSoundCue mergedCue : cues) { + double cueTick = mergedCue.timeTicks(); + if (!(cueTick > toTick) && (!(fromTick >= 0.0) || !(cueTick <= fromTick))) { + visitor.visit(mergedCue); + } + } + } + + private static void playSoundCue( + LivingEntity anchor, + ClientAnimationRuntime.InstanceState state, + ClientAnimationRuntime.MergedSoundCue mergedCue, + Identifier stageAnimationId, + List cues, + boolean loop, + double cycleTicks + ) { + if (mergedCue != null && mergedCue.cue() != null) { + AnimationSoundEffects.SoundCue cue = mergedCue.cue(); + String effect = selectCueEffect(anchor, cue); + if (effect != null && !effect.isBlank()) { + NonAnimationSoundEvents.SoundContext context = createSoundContext( + anchor, state, mergedCue, stageAnimationId, cues, state.stageIndex, state.speed, effect, loop, cycleTicks + ); + NonAnimationSoundEvents.SoundOverride override = ((NonAnimationSoundEvents.SoundResolver)NonAnimationSoundEvents.RESOLVE.invoker()) + .resolve(context); + if (override != null) { + if (override.cancel()) { + return; + } + + Identifier overrideId = override.soundId(); + if (overrideId != null) { + playSound(anchor, overrideId, override.volume(), override.pitch()); + return; + } + } + + Identifier fallback = resolveEffectAsSoundId(effect, stageAnimationId, mergedCue.animationResource()); + if (fallback != null) { + playSound(anchor, fallback, cue.volume(), cue.pitch()); + } + } + } + } + + private static NonAnimationSoundEvents.SoundContext createSoundContext( + LivingEntity anchor, + ClientAnimationRuntime.InstanceState state, + ClientAnimationRuntime.MergedSoundCue mergedCue, + Identifier stageAnimationId, + List cues, + int stageIndex, + double speed, + String effect, + boolean loop, + double cycleTicks + ) { + AnimationSoundEffects.SoundCue cue = mergedCue.cue(); + double nextDelayTicks = nextSameEffectCueDelayTicks(cues, mergedCue, loop, cycleTicks); + double safeSpeed = Math.max(1.0E-4, sanitizeSpeed(speed)); + double nextDelayMs = nextDelayTicks > 0.0 ? nextDelayTicks / safeSpeed * 50.0 : 0.0; + return new NonAnimationSoundEvents.SoundContext( + anchor, + stageAnimationId, + mergedCue.animationResource(), + mergedCue.animationKey(), + effect, + mergedCue.timeTicks() / 20.0, + cue.volume(), + cue.pitch(), + state.instanceId, + stageIndex, + loop, + mergedCue.timeTicks(), + cycleTicks, + safeSpeed, + nextDelayTicks, + nextDelayMs + ); + } + + private static String selectCueEffect(LivingEntity anchor, AnimationSoundEffects.SoundCue cue) { + if (cue == null) { + return null; + } else { + List sounds = cue.randomSounds(); + if (sounds != null && !sounds.isEmpty()) { + int index = anchor.getEntityWorld().getRandom().nextInt(sounds.size()); + return sounds.get(index).toString(); + } else { + return cue.effect(); + } + } + } + + private static double nextSameEffectCueDelayTicks( + List cues, ClientAnimationRuntime.MergedSoundCue currentCue, boolean loop, double cycleTicks + ) { + if (cues != null && !cues.isEmpty() && currentCue != null) { + if (currentCue.cue() == null) { + return 0.0; + } + + String effectFamily = AnimationCueEffects.timingFamily(currentCue.cue().effect()); + if (effectFamily == null) { + return 0.0; + } + + double currentTick = currentCue.timeTicks(); + double epsilon = 1.0E-4; + ClientAnimationRuntime.MergedSoundCue firstSame = null; + + for (ClientAnimationRuntime.MergedSoundCue candidate : cues) { + if (candidate != null && candidate.cue() != null) { + AnimationSoundEffects.SoundCue cue = candidate.cue(); + if (effectFamily.equals(AnimationCueEffects.timingFamily(cue.effect()))) { + if (firstSame == null || candidate.timeTicks() < firstSame.timeTicks()) { + firstSame = candidate; + } + + if (candidate.timeTicks() > currentTick + 1.0E-4) { + return Math.max(0.0, candidate.timeTicks() - currentTick); + } + } + } + } + + if (loop && !(cycleTicks <= 0.0) && firstSame != null) { + double wrappedDelay = cycleTicks - currentTick + firstSame.timeTicks(); + if (wrappedDelay <= 1.0E-4) { + wrappedDelay = cycleTicks; + } + + return Math.max(0.0, wrappedDelay); + } else { + return 0.0; + } + } else { + return 0.0; + } + } + + private static Identifier resolveEffectAsSoundId(String effect, Identifier stageAnimationId, Identifier animationResource) { + if (effect != null && !effect.isBlank()) { + Identifier direct = Identifier.tryParse(effect); + if (direct != null) { + return direct; + } + + String namespace = null; + if (stageAnimationId != null) { + namespace = stageAnimationId.getNamespace(); + } else if (animationResource != null) { + namespace = animationResource.getNamespace(); + } + + return namespace != null && !namespace.isBlank() ? Identifier.tryParse(namespace + ":" + effect) : null; + } else { + return null; + } + } + + private static void playSound(LivingEntity anchor, Identifier soundId, float volume, float pitch) { + if (anchor != null && soundId != null) { + ClientWorld world = anchor.getEntityWorld() instanceof ClientWorld clientWorld ? clientWorld : null; + if (world != null) { + SoundEvent soundEvent = Registries.SOUND_EVENT.containsId(soundId) ? (SoundEvent)Registries.SOUND_EVENT.get(soundId) : SoundEvent.of(soundId); + SoundCategory category = anchor instanceof PlayerEntity ? SoundCategory.PLAYERS : SoundCategory.NEUTRAL; + world.playSoundFromEntityClient(anchor, soundEvent, category, volume, pitch); + } + } + } + + private static final class InstanceState { + final UUID instanceId; + final Identifier animationId; + final List> modelRootsByActor; + final List actorUuids; + final Set actorUuidSet; + final List actorKeys; + final UUID anchorUuid; + ClientAnimationRuntime.LockedOrientation anchorOrientation; + final Vec3d cameraOrbitTarget; + final long startTick; + final List stages; + int stageIndex = 0; + long stageStartTick; + long stageEndTick; + int manualAdvanceRequests = 0; + double speed; + int soundStageIndex = -1; + double lastSoundTick = -1.0; + int anticipatedSoundStageIndex = -1; + double lastAnticipatedSoundTick = -1.0; + int anticipatedNextSoundStageIndex = -1; + double lastAnticipatedNextSoundTick = -1.0; + final Map> propOverridesByActor = new HashMap<>(); + Long stopAtTick; + + InstanceState( + UUID instanceId, + Identifier animationId, + List> modelRootsByActor, + List actorUuids, + List actorKeys, + List stages, + long startTick, + int initialStageIndex, + long initialStageStartTick, + long initialStageEndTick, + double speed, + UUID anchorUuid, + ClientAnimationRuntime.LockedOrientation anchorOrientation, + Vec3d cameraOrbitTarget + ) { + this.instanceId = instanceId; + this.animationId = animationId; + this.actorUuids = List.copyOf(actorUuids); + this.modelRootsByActor = modelRootsByActor == null + ? Collections.nCopies(this.actorUuids.size(), List.of()) + : modelRootsByActor.stream().map(roots -> roots == null ? List.of() : List.copyOf((Collection)roots)).toList(); + if (this.modelRootsByActor.size() != this.actorUuids.size()) { + throw new IllegalArgumentException("modelRootsByActor size mismatch: " + this.modelRootsByActor.size()); + } + + this.actorUuidSet = Set.copyOf(this.actorUuids); + this.actorKeys = actorKeys == null ? List.of() : List.copyOf(actorKeys); + this.startTick = startTick; + this.stages = ClientAnimationRuntime.normalizeStages(animationId, stages); + this.stageIndex = this.stages.isEmpty() ? 0 : Math.max(0, Math.min(initialStageIndex, this.stages.size() - 1)); + this.stageStartTick = Math.max(startTick, initialStageStartTick); + this.stageEndTick = initialStageEndTick; + this.anchorUuid = anchorUuid; + this.anchorOrientation = anchorOrientation; + this.cameraOrbitTarget = cameraOrbitTarget; + this.speed = ClientAnimationRuntime.sanitizeSpeed(speed); + } + + long effectiveStopTick() { + return this.stopAtTick != null ? this.stopAtTick : Long.MAX_VALUE; + } + + boolean isActive(long nowTick) { + return nowTick < this.startTick ? false : nowTick < this.effectiveStopTick(); + } + + boolean containsActor(UUID actorUuid) { + return this.actorUuidSet.contains(actorUuid); + } + + List modelRootsForActor(int actorIndex) { + return actorIndex >= 0 && actorIndex < this.modelRootsByActor.size() ? this.modelRootsByActor.get(actorIndex) : List.of(); + } + + AnimationStageInfo currentStage() { + return this.stages.isEmpty() ? new AnimationStageInfo(this.animationId, true, 0L, true, 1.0) : this.stages.get(this.stageIndex); + } + + Identifier currentAnimationId() { + return this.currentStage().effectiveAnimationId(); + } + + boolean advanceStage(long nextStageStartTick) { + if (this.stageIndex + 1 >= this.stages.size()) { + return false; + } + + this.stageIndex++; + this.stageStartTick = nextStageStartTick; + this.stageEndTick = -1L; + this.speed = ClientAnimationRuntime.sanitizeSpeed(this.stages.get(this.stageIndex).speed()); + this.resetSoundTimelinesForStage(this.stageIndex); + return true; + } + + boolean setStageIndex(int targetStageIndex, long nextStageStartTick, long nextStageEndTick) { + if (targetStageIndex >= 0 && targetStageIndex < this.stages.size()) { + this.stageIndex = targetStageIndex; + this.stageStartTick = nextStageStartTick; + this.stageEndTick = nextStageEndTick; + this.speed = ClientAnimationRuntime.sanitizeSpeed(this.stages.get(this.stageIndex).speed()); + this.resetSoundTimelinesForStage(this.stageIndex); + return true; + } else { + return false; + } + } + + private void resetSoundTimelinesForStage(int nextStageIndex) { + this.soundStageIndex = nextStageIndex; + this.lastSoundTick = -1.0; + if (this.anticipatedNextSoundStageIndex == nextStageIndex) { + this.anticipatedSoundStageIndex = nextStageIndex; + this.lastAnticipatedSoundTick = this.lastAnticipatedNextSoundTick; + } else { + this.anticipatedSoundStageIndex = nextStageIndex; + this.lastAnticipatedSoundTick = -1.0; + } + + this.anticipatedNextSoundStageIndex = -1; + this.lastAnticipatedNextSoundTick = -1.0; + } + } + + public record LockedOrientation(float bodyYaw, float headYaw, float pitch) { + } + + private record MergedSoundCue(Identifier animationResource, String animationKey, AnimationSoundEffects.SoundCue cue, double timeTicks) { + } + + private record MergedSoundTrack(LivingEntity anchor, List cues, double cycleTicks) { + } + + private record PendingSpeedUpdate(double speed, long stageStartTick, long stageEndTick) { + } + + private record PendingStageAdvance(UUID instanceId, long advanceTick, long stageEndTick, int stageIndex) { + } + + private record PendingStart( + UUID instanceId, + Identifier animationId, + List> modelRootsByActor, + List actorUuids, + List actorKeys, + List stages, + long startTick, + int stageIndex, + long stageStartTick, + long stageEndTick, + double speed, + UUID anchorUuid, + boolean lockOrientation, + ClientAnimationRuntime.LockedOrientation lockedOrientation, + Vec3d cameraOrbitTarget + ) { + } + + public enum PropSlot { + LEFT("propleft", ItemDisplayContext.THIRD_PERSON_LEFT_HAND), + RIGHT("propright", ItemDisplayContext.THIRD_PERSON_RIGHT_HAND), + FLOOR("propfloor", ItemDisplayContext.GROUND); + + private final String boneName; + private final ItemDisplayContext displayContext; + + PropSlot(String boneName, ItemDisplayContext displayContext) { + this.boneName = boneName; + this.displayContext = displayContext; + } + + public String boneName() { + return this.boneName; + } + + public ItemDisplayContext displayContext() { + return this.displayContext; + } + } + + private record RenderHold( + UUID instanceId, + Identifier animationId, + List modelRoots, + String actorKey, + double speed, + Map boneItems, + ClientAnimationRuntime.LockedOrientation lockedOrientation, + long expiresAtTick + ) { + } + + private record SoundCueMergeKey(long timeKey, @Nullable String effectFamily, List randomSounds) { + private SoundCueMergeKey { + randomSounds = randomSounds == null ? List.of() : List.copyOf(randomSounds); + } + } + + @FunctionalInterface + private interface SoundCueVisitor { + void visit(ClientAnimationRuntime.MergedSoundCue var1); + } + + private record SoundTrackCandidate(Identifier animationResource, String animationKey, AnimationSoundEffects.SoundAnimation soundAnimation) { + } + + public record StageTimelineSnapshot(List stages, int stageIndex, long stageStartTick, long stageEndTick, double speed) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundAnticipation.java b/src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundAnticipation.java new file mode 100644 index 0000000..5ba45b1 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundAnticipation.java @@ -0,0 +1,56 @@ +package com.nonid.internal.animation.client.sound; + +import com.nonid.api.client.NonAnimationSoundEvents; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationSoundAnticipation { + private static final int MAX_LOOKAHEAD_MILLIS = 1000; + private static final AnimationSoundAnticipation.Listener NO_OP = new AnimationSoundAnticipation.Listener() { + @Override + public int lookaheadMillis() { + return 0; + } + + @Override + public void anticipate(NonAnimationSoundEvents.SoundContext context) { + } + + @Override + public boolean allowNextStageLookahead(Identifier currentStageId, int stageIndex) { + return true; + } + }; + private static volatile AnimationSoundAnticipation.Listener listener = NO_OP; + + private AnimationSoundAnticipation() { + } + + public static void setListener(@Nullable AnimationSoundAnticipation.Listener nextListener) { + listener = nextListener == null ? NO_OP : nextListener; + } + + public static int lookaheadMillis() { + return Math.max(0, Math.min(1000, listener.lookaheadMillis())); + } + + public static void anticipate(NonAnimationSoundEvents.SoundContext context) { + if (context != null) { + listener.anticipate(context); + } + } + + public static boolean allowNextStageLookahead(Identifier currentStageId, int stageIndex) { + return listener.allowNextStageLookahead(currentStageId, stageIndex); + } + + public interface Listener { + int lookaheadMillis(); + + void anticipate(NonAnimationSoundEvents.SoundContext var1); + + default boolean allowNextStageLookahead(Identifier currentStageId, int stageIndex) { + return true; + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundEffects.java b/src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundEffects.java new file mode 100644 index 0000000..bbdff22 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/client/sound/AnimationSoundEffects.java @@ -0,0 +1,145 @@ +package com.nonid.internal.animation.client.sound; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.AnimationEngineClient; +import com.nonid.internal.animation.data.AnimationSoundCueParser; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; + +public final class AnimationSoundEffects { + private static volatile Map> SOUND_BY_ANIMATION = Map.of(); + private static final Identifier RELOADER_ID = AnimationEngine.runtimeId("sound_effects"); + + private AnimationSoundEffects() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.CLIENT_RESOURCES).registerReloader(RELOADER_ID, new AnimationSoundEffects.Reloader()); + } + + public static boolean hasAny() { + return !SOUND_BY_ANIMATION.isEmpty(); + } + + public static AnimationSoundEffects.SoundAnimation getSoundAnimation(Identifier animationResource, String animationKey) { + if (animationResource != null && animationKey != null && !animationKey.isBlank()) { + Map byKey = SOUND_BY_ANIMATION.get(animationResource); + return byKey == null ? null : byKey.get(animationKey); + } else { + return null; + } + } + + private static void reload(ResourceManager manager) { + Map resources = manager.findResources("geckolib/animations", id -> id.getPath().endsWith(".animation.json")); + List> entries = new ArrayList<>(resources.entrySet()); + entries.sort(Comparator.comparing(ex -> ((Identifier)ex.getKey()).toString())); + Map> loaded = new LinkedHashMap<>(); + int cueCount = 0; + + for (Entry entry : entries) { + Identifier fileId = entry.getKey(); + Identifier animationId = animationIdFromPath(fileId); + if (animationId != null) { + try (InputStreamReader reader = new InputStreamReader(entry.getValue().getInputStream(), StandardCharsets.UTF_8)) { + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + JsonObject animationsObj = root.has("animations") && root.get("animations").isJsonObject() ? root.getAsJsonObject("animations") : null; + if (animationsObj != null) { + for (Entry animEntry : animationsObj.entrySet()) { + String animKey = animEntry.getKey(); + JsonElement animEl = animEntry.getValue(); + if (animKey != null && !animKey.isBlank() && animEl != null && animEl.isJsonObject()) { + JsonObject animObj = animEl.getAsJsonObject(); + AnimationSoundCueParser.ParsedAnimation parsed = AnimationSoundCueParser.parse( + animObj, fileId.toString(), AnimationSoundEffects::setupWarn + ); + List cues = parsed.cues() + .stream() + .map( + cue -> new AnimationSoundEffects.SoundCue( + cue.timeSeconds(), cue.timeTicks(), cue.effect(), cue.randomSounds(), cue.volume(), cue.pitch() + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + if (!cues.isEmpty()) { + cues.sort(Comparator.comparingDouble(AnimationSoundEffects.SoundCue::timeTicks)); + AnimationSoundEffects.SoundAnimation soundAnim = new AnimationSoundEffects.SoundAnimation(parsed.lengthSeconds(), List.copyOf(cues)); + loaded.computeIfAbsent(animationId, id -> new LinkedHashMap<>()).put(animKey, soundAnim); + cueCount += cues.size(); + } + } + } + } + } catch (IOException | RuntimeException e) { + setupError("[NoN Animation] Failed to load sound cues {}", fileId, e); + } + } + } + + Map> finalized = new LinkedHashMap<>(); + + for (Entry> entry : loaded.entrySet()) { + finalized.put(entry.getKey(), Map.copyOf(entry.getValue())); + } + + SOUND_BY_ANIMATION = Map.copyOf(finalized); + AnimationEngine.LOGGER.info("[NoN Animation] Loaded {} sound cue animation(s), {} cue(s)", SOUND_BY_ANIMATION.size(), cueCount); + } + + private static Identifier animationIdFromPath(Identifier fileId) { + String path = fileId.getPath(); + String prefix = "geckolib/animations/"; + String suffix = ".animation.json"; + if (path.startsWith(prefix) && path.endsWith(suffix)) { + String animPath = path.substring(prefix.length(), path.length() - suffix.length()); + return Identifier.of(fileId.getNamespace(), animPath); + } else { + return null; + } + } + + private static void setupWarn(String template, Object... args) { + AnimationEngine.LOGGER.warn(template, args); + AnimationEngineClient.sendClientSetupWarning(AnimationEngine.formatLogTemplate(template, args)); + } + + private static void setupError(String template, Object... args) { + AnimationEngine.LOGGER.error(template, args); + AnimationEngineClient.sendClientSetupWarning(AnimationEngine.formatLogTemplate(template, args)); + } + + public static final class Reloader implements SynchronousResourceReloader { + public void reload(ResourceManager manager) { + AnimationSoundEffects.reload(manager); + } + } + + public record SoundAnimation(double lengthSeconds, List cues) { + public double lengthTicks() { + return this.lengthSeconds <= 0.0 ? 0.0 : this.lengthSeconds * 20.0; + } + } + + public record SoundCue(double timeSeconds, double timeTicks, String effect, List randomSounds, float volume, float pitch) { + public SoundCue { + randomSounds = randomSounds == null ? List.of() : List.copyOf(randomSounds); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/data/AnimationCueEffects.java b/src/main/java/com/nonid/internal/animation/data/AnimationCueEffects.java new file mode 100644 index 0000000..6775f89 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/data/AnimationCueEffects.java @@ -0,0 +1,32 @@ +package com.nonid.internal.animation.data; + +import java.util.Locale; +import org.jetbrains.annotations.Nullable; + +public final class AnimationCueEffects { + public static final String REACTIVE_IMPACT = "reactiveimpact"; + public static final String REACTIVE_IMPACT_SILENT = "reactiveimpact_silent"; + + private AnimationCueEffects() { + } + + public static boolean isReactiveImpact(@Nullable String effect) { + String normalized = normalize(effect); + return "reactiveimpact".equals(normalized) || "reactiveimpact_silent".equals(normalized); + } + + public static boolean isSilentReactiveImpact(@Nullable String effect) { + return "reactiveimpact_silent".equals(normalize(effect)); + } + + @Nullable + public static String timingFamily(@Nullable String effect) { + String normalized = normalize(effect); + return "reactiveimpact_silent".equals(normalized) ? "reactiveimpact" : normalized; + } + + @Nullable + private static String normalize(@Nullable String effect) { + return effect != null && !effect.isBlank() ? effect.trim().toLowerCase(Locale.ROOT) : null; + } +} diff --git a/src/main/java/com/nonid/internal/animation/data/AnimationCueIndex.java b/src/main/java/com/nonid/internal/animation/data/AnimationCueIndex.java new file mode 100644 index 0000000..2dc1218 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/data/AnimationCueIndex.java @@ -0,0 +1,434 @@ +package com.nonid.internal.animation.data; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.util.AnimationIds; +import com.nonid.pack.NonExternalPackCatalog; +import com.nonid.pack.NonExternalPackProvider; +import com.nonid.pack.NonPackRootResolver; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.Map.Entry; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationCueIndex { + public static final String BIRTH_EFFECT = "birth"; + public static final String REACTIVE_IMPACT_EFFECT = "reactiveimpact"; + private static final String ANIMATION_SUFFIX = ".animation.json"; + private static final String ANIMATION_PATH_PREFIX = "geckolib/animations/afw/"; + private static volatile Map PROFILE_BY_CLIP = Map.of(); + + private AnimationCueIndex() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.SERVER_DATA).registerReloader(AnimationCueIndex.Reloader.ID, new AnimationCueIndex.Reloader()); + } + + @Nullable + public static AnimationCueIndex.CueProfile getProfile(@Nullable Identifier clipId) { + return clipId == null ? null : PROFILE_BY_CLIP.get(clipId); + } + + public static List getCueTicks(@Nullable Identifier clipId, @Nullable String effect) { + AnimationCueIndex.CueProfile profile = getProfile(clipId); + return profile == null ? List.of() : profile.cueTicks(effect); + } + + @Nullable + public static Integer getFirstCueTick(@Nullable Identifier clipId, @Nullable String effect) { + AnimationCueIndex.CueProfile profile = getProfile(clipId); + return profile == null ? null : profile.firstCueTick(effect); + } + + private static void reload() { + Map> selectedFiles = new LinkedHashMap<>(); + scanInstalledMods(selectedFiles); + scanExternalPacks(selectedFiles); + Map merged = new LinkedHashMap<>(); + + for (List clips : selectedFiles.values()) { + for (AnimationCueIndex.ParsedClip clip : clips) { + if (clip != null && clip.clipId() != null) { + AnimationCueIndex.MutableProfile profile = merged.computeIfAbsent(clip.clipId(), ignored -> new AnimationCueIndex.MutableProfile()); + + for (Entry> cues : clip.cueTicksByEffect().entrySet()) { + profile.cueTicksByEffect.computeIfAbsent(cues.getKey(), ignored -> new TreeSet<>()).addAll(cues.getValue()); + } + + Integer cycleTicks = clip.cycleTicks(); + if (cycleTicks != null && cycleTicks > 0 && (profile.cycleTicks == null || cycleTicks > profile.cycleTicks)) { + profile.cycleTicks = cycleTicks; + } + } + } + } + + Map finalized = new LinkedHashMap<>(); + + for (Entry entry : merged.entrySet()) { + Map> cuesByEffect = new LinkedHashMap<>(); + + for (Entry> cues : entry.getValue().cueTicksByEffect.entrySet()) { + if (!cues.getValue().isEmpty()) { + cuesByEffect.put(cues.getKey(), List.copyOf(cues.getValue())); + } + } + + Integer cycleTicks = entry.getValue().cycleTicks; + if (!cuesByEffect.isEmpty() || cycleTicks != null) { + finalized.put(entry.getKey(), new AnimationCueIndex.CueProfile(Map.copyOf(cuesByEffect), cycleTicks)); + } + } + + PROFILE_BY_CLIP = Map.copyOf(finalized); + } + + private static void scanInstalledMods(Map> selectedFiles) { + for (ModContainer mod : FabricLoader.getInstance().getAllMods()) { + for (Path root : mod.getRootPaths()) { + Path assetsRoot = root.resolve("assets"); + if (Files.isDirectory(assetsRoot)) { + loadAssetNamespaces(assetsRoot, selectedFiles); + } + } + } + } + + private static void scanExternalPacks(Map> selectedFiles) { + Path packsDir = NonExternalPackProvider.resolveDefaultPacksDir(); + if (Files.isDirectory(packsDir)) { + NonConfig config = NeedsOfNature.getConfig(); + List configuredOrder = config == null ? List.of() : config.getExternalNoNPackLoadOrder(); + + for (NonExternalPackCatalog.Entry entry : NonExternalPackCatalog.resolveLowToHigh(packsDir, configuredOrder)) { + scanExternalPack(entry.root(), selectedFiles); + } + } + } + + private static void scanExternalPack(NonPackRootResolver.ResolvedRoot root, Map> selectedFiles) { + if (!root.isZip()) { + Path assetsRoot = root.rootPath().resolve("assets"); + if (Files.isDirectory(assetsRoot)) { + loadAssetNamespaces(assetsRoot, selectedFiles); + } + } else { + try (ZipFile zip = new ZipFile(root.sourcePath().toFile())) { + Enumeration entries = zip.entries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry != null && !entry.isDirectory()) { + String entryPath = root.stripZipPrefix(entry.getName()); + if (entryPath != null && !entryPath.isBlank()) { + AnimationCueIndex.ParsedPath parsedPath = parseZipAssetPath(entryPath.replace('\\', '/')); + if (parsedPath != null && isAnimationAssetPath(parsedPath.namespaceRelativePath())) { + try (InputStream input = zip.getInputStream(entry)) { + selectedFiles.put( + parsedPath.resourceKey(), + parseAnimationFile(input, parsedPath.namespace(), parsedPath.namespaceRelativePath(), root.sourcePath() + "!" + entryPath) + ); + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to parse animation cues in {} entry {}", new Object[]{root.sourcePath(), entryPath, e}); + selectedFiles.put(parsedPath.resourceKey(), List.of()); + } + } + } + } + } + } catch (IOException e) { + NeedsOfNature.logSetupWarning("[NoN] Failed to scan external NoN pack for animation cues {}", root.sourcePath(), e); + } + } + } + + private static void loadAssetNamespaces(Path assetsRoot, Map> selectedFiles) { + try (Stream namespaces = Files.list(assetsRoot)) { + namespaces.filter(x$0 -> Files.isDirectory(x$0)).forEach(namespaceDir -> loadNamespaceFiles(namespaceDir, selectedFiles)); + } catch (IOException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to enumerate namespaces under {}", assetsRoot, e); + } + } + + private static void loadNamespaceFiles(Path namespaceDir, Map> selectedFiles) { + String namespace = namespaceDir.getFileName().toString(); + Path animationRoot = namespaceDir.resolve("geckolib").resolve("animations").resolve("afw"); + if (Files.isDirectory(animationRoot)) { + try (Stream files = Files.walk(animationRoot)) { + files.filter(x$0 -> Files.isRegularFile(x$0)).filter(path -> path.getFileName().toString().endsWith(".animation.json")).forEach(path -> { + String relativePath = namespaceDir.relativize(path).toString().replace('\\', '/'); + String resourceKey = namespace + ":" + relativePath; + + try (InputStream input = Files.newInputStream(path)) { + selectedFiles.put(resourceKey, parseAnimationFile(input, namespace, relativePath, path.toString())); + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to parse animation cue file {}", path, e); + selectedFiles.put(resourceKey, List.of()); + } + }); + } catch (IOException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to walk animation assets under {}", animationRoot, e); + } + } + } + + private static List parseAnimationFile(InputStream input, String namespace, String namespaceRelativePath, String sourceName) { + if (input != null && namespace != null && !namespace.isBlank() && isAnimationAssetPath(namespaceRelativePath)) { + String normalizedPath = namespaceRelativePath.replace('\\', '/'); + String relativeAnimationPath = normalizedPath.substring("geckolib/animations/afw/".length()); + Identifier pathClipId = stageClipIdFromAnimationPath(namespace, relativeAnimationPath); + Identifier conjoinedAnimationId = conjoinedAnimationIdFromPath(namespace, relativeAnimationPath); + + try (InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) { + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + JsonObject animations = asObject(root.get("animations")); + if (animations == null || animations.isEmpty()) { + return List.of(); + } + + Map parsedByClip = new LinkedHashMap<>(); + + for (Entry animationEntry : animations.entrySet()) { + JsonObject animation = asObject(animationEntry.getValue()); + if (animation != null) { + Identifier clipId = pathClipId != null ? pathClipId : stageClipId(namespace, animationEntry.getKey()); + if (clipId == null) { + clipId = conjoinedStageClipId(conjoinedAnimationId, animationEntry.getKey()); + } + + if (clipId != null) { + AnimationCueIndex.MutableProfile profile = parsedByClip.computeIfAbsent(clipId, ignored -> new AnimationCueIndex.MutableProfile()); + AnimationSoundCueParser.ParsedAnimation parsedAnimation = AnimationSoundCueParser.parse( + animation, sourceName, NeedsOfNature::logSetupWarning + ); + Integer cycleTicks = parsedAnimation.lengthSeconds() > 0.0 ? Math.max(1, (int)Math.round(parsedAnimation.lengthSeconds() * 20.0)) : null; + if (cycleTicks != null && cycleTicks > 0 && (profile.cycleTicks == null || cycleTicks > profile.cycleTicks)) { + profile.cycleTicks = cycleTicks; + } + + for (AnimationSoundCueParser.ParsedCue cue : parsedAnimation.cues()) { + if (cue.effect() != null && !cue.effect().isBlank()) { + String effect = AnimationCueEffects.timingFamily(cue.effect()); + if (effect != null) { + int cueTick = Math.max(0, (int)Math.round(cue.timeTicks())); + profile.cueTicksByEffect.computeIfAbsent(effect, ignored -> new TreeSet<>()).add(cueTick); + } + } + } + } + } + } + + List parsed = new ArrayList<>(); + + for (Entry entry : parsedByClip.entrySet()) { + Map> cuesByEffect = new LinkedHashMap<>(); + + for (Entry> cues : entry.getValue().cueTicksByEffect.entrySet()) { + if (!cues.getValue().isEmpty()) { + cuesByEffect.put(cues.getKey(), List.copyOf(cues.getValue())); + } + } + + parsed.add(new AnimationCueIndex.ParsedClip(entry.getKey(), Map.copyOf(cuesByEffect), entry.getValue().cycleTicks)); + } + + return List.copyOf(parsed); + } catch (IOException | RuntimeException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to parse animation cues in {}", sourceName, e); + return List.of(); + } + } else { + return List.of(); + } + } + + private static boolean isAnimationAssetPath(@Nullable String namespaceRelativePath) { + if (namespaceRelativePath != null && !namespaceRelativePath.isBlank()) { + String normalized = namespaceRelativePath.replace('\\', '/'); + return normalized.startsWith("geckolib/animations/afw/") && normalized.endsWith(".animation.json"); + } else { + return false; + } + } + + @Nullable + private static Identifier stageClipIdFromAnimationPath(String namespace, String relativeAnimationPath) { + if (namespace != null && !namespace.isBlank() && relativeAnimationPath != null && !relativeAnimationPath.isBlank()) { + String normalized = relativeAnimationPath.replace('\\', '/'); + int slash = normalized.lastIndexOf(47); + String fileName = slash >= 0 ? normalized.substring(slash + 1) : normalized; + return !fileName.endsWith(".animation.json") ? null : stageClipId(namespace, fileName.substring(0, fileName.length() - ".animation.json".length())); + } else { + return null; + } + } + + @Nullable + private static Identifier conjoinedAnimationIdFromPath(String namespace, String relativeAnimationPath) { + if (namespace != null && !namespace.isBlank() && relativeAnimationPath != null && !relativeAnimationPath.isBlank()) { + String normalized = relativeAnimationPath.replace('\\', '/'); + if (!normalized.endsWith(".animation.json")) { + return null; + } + + String animationPath = normalized.substring(0, normalized.length() - ".animation.json".length()); + if (animationPath.isBlank()) { + return null; + } + + int slash = animationPath.lastIndexOf(47); + if (slash > 0) { + String fileName = animationPath.substring(slash + 1); + String parentPath = animationPath.substring(0, slash); + int parentSlash = parentPath.lastIndexOf(47); + String parentName = parentSlash >= 0 ? parentPath.substring(parentSlash + 1) : parentPath; + if (fileName.equals(parentName)) { + animationPath = parentPath; + } + } + + return Identifier.tryParse(namespace + ":" + animationPath); + } else { + return null; + } + } + + @Nullable + private static Identifier stageClipId(String namespace, String rawClipPath) { + if (namespace != null && !namespace.isBlank() && rawClipPath != null && !rawClipPath.isBlank()) { + int stageMarker = rawClipPath.lastIndexOf(".p"); + if (stageMarker <= 0) { + return null; + } + + int end = stageMarker + 2; + + while (end < rawClipPath.length() && Character.isDigit(rawClipPath.charAt(end))) { + end++; + } + + if (end == stageMarker + 2) { + return null; + } else { + return end < rawClipPath.length() && rawClipPath.charAt(end) != '_' ? null : Identifier.tryParse(namespace + ":" + rawClipPath.substring(0, end)); + } + } else { + return null; + } + } + + @Nullable + private static Identifier conjoinedStageClipId(@Nullable Identifier animationId, String animationKey) { + if (animationId != null && animationKey != null && animationKey.length() >= 4 && animationKey.charAt(0) == 'p') { + int separator = 1; + + while (separator < animationKey.length() && Character.isDigit(animationKey.charAt(separator))) { + separator++; + } + + if (separator != 1 && separator < animationKey.length() - 1 && animationKey.charAt(separator) == '_') { + int stageNumber; + try { + stageNumber = Integer.parseInt(animationKey.substring(1, separator)); + } catch (NumberFormatException e) { + return null; + } + + return stageNumber <= 0 ? null : AnimationIds.stageId(animationId, stageNumber); + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static JsonObject asObject(@Nullable JsonElement element) { + return element != null && element.isJsonObject() ? element.getAsJsonObject() : null; + } + + @Nullable + private static AnimationCueIndex.ParsedPath parseZipAssetPath(String entryPath) { + if (entryPath != null && entryPath.startsWith("assets/")) { + int namespaceStart = "assets/".length(); + int namespaceEnd = entryPath.indexOf(47, namespaceStart); + if (namespaceEnd <= namespaceStart) { + return null; + } + + String namespace = entryPath.substring(namespaceStart, namespaceEnd); + String namespaceRelativePath = entryPath.substring(namespaceEnd + 1); + return !namespace.isBlank() && !namespaceRelativePath.isBlank() ? new AnimationCueIndex.ParsedPath(namespace, namespaceRelativePath) : null; + } else { + return null; + } + } + + public record CueProfile(Map> cueTicksByEffect, @Nullable Integer cycleTicks) { + public List cueTicks(@Nullable String effect) { + String normalized = normalizeEffect(effect); + return normalized == null ? List.of() : this.cueTicksByEffect.getOrDefault(normalized, List.of()); + } + + @Nullable + public Integer firstCueTick(@Nullable String effect) { + List ticks = this.cueTicks(effect); + return ticks.isEmpty() ? null : ticks.getFirst(); + } + + @Nullable + private static String normalizeEffect(@Nullable String effect) { + return AnimationCueEffects.timingFamily(effect); + } + } + + private static final class MutableProfile { + private final Map> cueTicksByEffect = new LinkedHashMap<>(); + @Nullable + private Integer cycleTicks; + } + + private record ParsedClip(Identifier clipId, Map> cueTicksByEffect, @Nullable Integer cycleTicks) { + } + + private record ParsedPath(String namespace, String namespaceRelativePath) { + private String resourceKey() { + return this.namespace + ":" + this.namespaceRelativePath; + } + } + + private static final class Reloader implements SynchronousResourceReloader { + private static final Identifier ID = AnimationEngine.runtimeId("cue_index"); + + public void reload(ResourceManager manager) { + AnimationCueIndex.reload(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/data/AnimationDefinitions.java b/src/main/java/com/nonid/internal/animation/data/AnimationDefinitions.java new file mode 100644 index 0000000..4a3408f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/data/AnimationDefinitions.java @@ -0,0 +1,2931 @@ +package com.nonid.internal.animation.data; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.nonid.api.animation.NonActorEvents; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.util.AnimationIds; +import com.nonid.internal.animation.util.EntityVariants; +import com.nonid.internal.animation.util.StageTimeWarp; +import com.nonid.internal.animation.util.WeightedPicker; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.stream.Stream; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.registry.Registries; +import net.minecraft.resource.InputSupplier; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.SynchronousResourceReloader; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Direction.Axis; +import org.jetbrains.annotations.Nullable; + +public final class AnimationDefinitions { + private static final int MAX_MODEL_PROVIDER_ROOTS = 32; + private static final String NORMAL_MATCH_TAG = "normal_match"; + private static final Identifier PLAYER_ID = Identifier.of("minecraft", "player"); + private static final Identifier PLAYER_SLIM_ID = Identifier.of("minecraft", "player_slim"); + private static final String INVALID_ENTITY_VARIANT = "\u0000"; + private static final Set KNOWN_DEFINITION_KEYS = Set.of( + "actors", + "display_name", + "animation_tags", + "content_tags", + "join_after", + "required_union_tags", + "weight", + "attack_eligible", + "speed", + "block_requirements", + "water", + "position_anchor_actor", + "stages", + "manual_peak", + "liquid_gain_multiplier", + "stage_seconds", + "escapable" + ); + private static final Set KNOWN_ACTOR_KEYS = Set.of( + "label", + "entity_types", + "entity_variant", + "actor_tags", + "actor_tags_any", + "age", + "activity", + "prop_left", + "prop_right", + "prop_floor", + "injector", + "receiver" + ); + private static final Set KNOWN_STAGE_KEYS = Set.of( + "stage", + "use_stage", + "loop", + "cycle_seconds", + "speed", + "allow_join", + "cycle_midpoint_offset_seconds", + "props", + "stage_seconds", + "stage_duration_multiplier", + "escapable", + "non_peak" + ); + private static final Set KNOWN_BLOCK_REQUIREMENT_KEYS = Set.of( + "type", "height", "width", "facing", "clearance", "support", "placement", "surface_radius", "surface_footprint", "blocks" + ); + private static final Set KNOWN_WALL_HEIGHT_KEYS = Set.of("min", "max"); + private static final Set KNOWN_WALL_WIDTH_KEYS = Set.of("min", "max"); + private static final Set KNOWN_CLEARANCE_KEYS = Set.of("width", "height", "depth"); + private static final Set KNOWN_SURFACE_FOOTPRINT_KEYS = Set.of("width", "height", "depth", "margin"); + private static final Set KNOWN_STAGE_ACTOR_PROP_KEYS = Set.of("prop_left", "prop_right", "prop_floor"); + private static volatile List DEFINITIONS = List.of(); + private static volatile Map DEFINITIONS_BY_ID = Map.of(); + private static volatile Map> DEFINITIONS_BY_ACTOR_COUNT = Map.of(); + private static volatile Map> MATCH_ORDER_BY_DEFINITION = Map.of(); + private static volatile Map> JOIN_FOLLOWERS_BY_PREDECESSOR = Map.of(); + private static volatile Set GENERIC_ACTOR_TYPES = Set.of(); + private static volatile Set GENERIC_MOB_PAIR_TYPES = Set.of(); + private static volatile Map> GENERIC_MOB_PAIR_COMPATIBILITY = Map.of(); + private static volatile Set DISABLED_ANIMATION_IDS = Set.of(); + private static volatile Set DISABLED_ANIMATION_PACK_IDS = Set.of(); + + private AnimationDefinitions() { + } + + public static void registerReloadListener() { + ResourceLoader.get(ResourceType.SERVER_DATA).registerReloader(AnimationDefinitions.Reloader.RELOADER_ID, new AnimationDefinitions.Reloader()); + } + + public static AnimationDefinitions.MatchResult match(List selectedActorsSortedById) { + return match(selectedActorsSortedById, Set.of()); + } + + public static AnimationDefinitions.MatchResult match(List selectedActorsSortedById, Set requiredAnimationTags) { + List candidates = findMatchingCandidates(selectedActorsSortedById, requiredAnimationTags); + AnimationDefinitions.Definition chosen = pickWeighted(candidates, new Random()); + return new AnimationDefinitions.MatchResult(candidates, chosen); + } + + public static List findMatchingCandidates(List selectedActorsSortedById, Set requiredAnimationTags) { + List actorMatches = findActorMatchingCandidates(selectedActorsSortedById); + if (actorMatches.isEmpty()) { + return List.of(); + } + + Set normalizedRequiredTags = normalizeTags(requiredAnimationTags); + List candidates = new ArrayList<>(); + + for (AnimationDefinitions.Definition definition : actorMatches) { + if (definition.matchesAnimationTags(normalizedRequiredTags)) { + candidates.add(definition); + } + } + + return candidates.isEmpty() ? List.of() : List.copyOf(candidates); + } + + public static List findActorMatchingCandidates(List selectedActorsSortedById) { + if (selectedActorsSortedById != null && !selectedActorsSortedById.isEmpty()) { + AnimationDefinitions.MatchContext context = AnimationDefinitions.MatchContext.create(selectedActorsSortedById); + if (context.actors().size() != selectedActorsSortedById.size()) { + return List.of(); + } + + List candidates = new ArrayList<>(); + + for (AnimationDefinitions.Definition d : DEFINITIONS_BY_ACTOR_COUNT.getOrDefault(context.actors().size(), List.of())) { + if (isDefinitionEnabled(d) && d.matchesActors(context)) { + candidates.add(d); + } + } + + return candidates.isEmpty() ? List.of() : List.copyOf(candidates); + } else { + return List.of(); + } + } + + private static AnimationDefinitions.Definition pickWeighted(List definitions, Random random) { + return random == null ? null : WeightedPicker.pick(definitions, AnimationDefinitions.Definition::weight, random.nextDouble()); + } + + public static AnimationDefinitions.Definition getDefinition(Identifier id) { + return id == null ? null : DEFINITIONS_BY_ID.get(id); + } + + public static boolean isDefinitionEnabled(@Nullable AnimationDefinitions.Definition definition) { + if (definition == null) { + return false; + } + + if (DISABLED_ANIMATION_IDS.contains(definition.id())) { + return false; + } + + AnimationDefinitions.AnimationPackInfo packInfo = definition.packInfo(); + return packInfo == null || !DISABLED_ANIMATION_PACK_IDS.contains(packInfo.id()); + } + + public static boolean isAnimationEnabled(Identifier animationId) { + AnimationDefinitions.Definition definition = getDefinition(animationId); + return definition != null && isDefinitionEnabled(definition); + } + + public static boolean isJoinCandidateAllowed(@Nullable Identifier predecessorAnimationId, @Nullable AnimationDefinitions.Definition candidate) { + if (candidate == null || !isDefinitionEnabled(candidate)) { + return false; + } + + if (predecessorAnimationId == null) { + return true; + } + + boolean hasEnabledFollower = false; + + for (AnimationDefinitions.Definition follower : JOIN_FOLLOWERS_BY_PREDECESSOR.getOrDefault(predecessorAnimationId, List.of())) { + if (isDefinitionEnabled(follower)) { + hasEnabledFollower = true; + if (follower.id().equals(candidate.id())) { + return true; + } + } + } + + return !hasEnabledFollower && candidate.joinAfter().isEmpty(); + } + + public static boolean hasEnabledGenericAnimationForEntityType(@Nullable Identifier entityTypeId, boolean baby) { + return entityTypeId != null && GENERIC_ACTOR_TYPES.contains(new AnimationDefinitions.ActorKey(entityTypeId, baby)); + } + + public static boolean hasEnabledGenericMobPairAnimationForEntityType(@Nullable Identifier entityTypeId, boolean baby) { + return entityTypeId != null && GENERIC_MOB_PAIR_TYPES.contains(new AnimationDefinitions.ActorKey(entityTypeId, baby)); + } + + public static boolean areEnabledGenericMobPairTypesCompatible( + @Nullable Identifier firstType, boolean firstBaby, @Nullable Identifier secondType, boolean secondBaby + ) { + if (firstType != null && secondType != null) { + Set partners = GENERIC_MOB_PAIR_COMPATIBILITY.get(new AnimationDefinitions.ActorKey(firstType, firstBaby)); + return partners != null && partners.contains(new AnimationDefinitions.ActorKey(secondType, secondBaby)); + } else { + return false; + } + } + + private static boolean isGenericMatchDefinition(AnimationDefinitions.Definition definition) { + if (definition == null) { + return false; + } + + Set tags = definition.animationTags(); + return tags == null || tags.isEmpty() || tags.contains("normal_match"); + } + + public static void setDisabledAnimationFilters(@Nullable Collection animationIds, @Nullable Collection packIds) { + LinkedHashSet cleanedAnimations = new LinkedHashSet<>(); + if (animationIds != null) { + for (Identifier id : animationIds) { + if (id != null) { + cleanedAnimations.add(id); + } + } + } + + LinkedHashSet cleanedPacks = new LinkedHashSet<>(); + if (packIds != null) { + for (String raw : packIds) { + String id = sanitizeNullable(raw); + if (id != null) { + cleanedPacks.add(id); + } + } + } + + DISABLED_ANIMATION_IDS = Set.copyOf(cleanedAnimations); + DISABLED_ANIMATION_PACK_IDS = Set.copyOf(cleanedPacks); + rebuildEligibilityIndexes(); + } + + public static List getLoadedDefinitionsSnapshot() { + return List.copyOf(DEFINITIONS); + } + + public static List resolveActorKeys(AnimationDefinitions.Definition definition) { + return definition != null && !definition.actors().isEmpty() ? List.of(buildUniqueActorKeys(definition.actors())) : List.of(); + } + + @Nullable + public static AnimationDefinitions.ActorConstraint resolveActorConstraintForKey(AnimationDefinitions.Definition definition, String actorKey) { + if (definition != null && actorKey != null && !actorKey.isBlank()) { + String[] actorKeys = buildUniqueActorKeys(definition.actors()); + + for (int i = 0; i < actorKeys.length; i++) { + if (actorKey.equals(actorKeys[i])) { + return definition.actors().get(i); + } + } + + return null; + } else { + return null; + } + } + + public static List resolveActorKeys(AnimationDefinitions.Definition definition, List selectedActorsSortedById) { + return resolveActorKeys(definition, selectedActorsSortedById, net.minecraft.util.math.random.Random.create()); + } + + @Nullable + public static AnimationDefinitions.ActorConstraint resolveActorConstraintForEntity( + AnimationDefinitions.Definition definition, List selectedActorsSortedById, Entity actor + ) { + return resolveActorConstraintForEntity(definition, selectedActorsSortedById, actor, net.minecraft.util.math.random.Random.create()); + } + + @Nullable + public static AnimationDefinitions.ActorConstraint resolveActorConstraintForEntity( + AnimationDefinitions.Definition definition, List selectedActorsSortedById, Entity actor, net.minecraft.util.math.random.Random rng + ) { + if (definition != null && selectedActorsSortedById != null && actor != null) { + if (selectedActorsSortedById.size() != definition.actors().size()) { + return null; + } + + int actorIndex = -1; + + for (int i = 0; i < selectedActorsSortedById.size(); i++) { + Entity candidate = selectedActorsSortedById.get(i); + if (candidate == actor || candidate.getUuid().equals(actor.getUuid())) { + actorIndex = i; + break; + } + } + + if (actorIndex < 0) { + return null; + } + + int[] assignment = matchUnorderedWithAssignment(selectedActorsSortedById, definition.actors(), rng); + if (assignment == null) { + return null; + } + + for (int ci = 0; ci < assignment.length; ci++) { + if (assignment[ci] == actorIndex) { + return definition.actors().get(ci); + } + } + + return null; + } else { + return null; + } + } + + public static boolean canResolveActorAsActivity( + AnimationDefinitions.Definition definition, List selectedActorsSortedById, Entity actor, AnimationDefinitions.ActorActivity activity + ) { + if (definition != null && selectedActorsSortedById != null && actor != null && activity != null) { + if (selectedActorsSortedById.size() != definition.actors().size()) { + return false; + } + + int actorIndex = -1; + + for (int i = 0; i < selectedActorsSortedById.size(); i++) { + Entity candidate = selectedActorsSortedById.get(i); + if (candidate == actor || candidate.getUuid().equals(actor.getUuid())) { + actorIndex = i; + break; + } + } + + if (actorIndex < 0) { + return false; + } + + List constraints = definition.actors(); + Entity actorEntity = selectedActorsSortedById.get(actorIndex); + + for (int ci = 0; ci < constraints.size(); ci++) { + AnimationDefinitions.ActorConstraint forced = constraints.get(ci); + AnimationDefinitions.ActorActivity forcedActivity = forced.activity() == null ? AnimationDefinitions.ActorActivity.ACTIVE : forced.activity(); + if (forcedActivity == activity && forced.matches(actorEntity)) { + boolean[] used = new boolean[selectedActorsSortedById.size()]; + used[actorIndex] = true; + List order = new ArrayList<>(constraints.size() - 1); + + for (int i = 0; i < constraints.size(); i++) { + if (i != ci) { + order.add(i); + } + } + + order.sort((a, b) -> { + AnimationDefinitions.ActorConstraint ca = constraints.get(a); + AnimationDefinitions.ActorConstraint cb = constraints.get(b); + int cmp = Integer.compare(cb.assignmentPriority(), ca.assignmentPriority()); + return cmp != 0 ? cmp : ca.stableKey().compareTo(cb.stableKey()); + }); + if (backtrackAssign(0, order, constraints, selectedActorsSortedById, buildEntityOrder(selectedActorsSortedById.size(), null), used, null)) { + return true; + } + } + } + + return false; + } else { + return false; + } + } + + public static List resolveActorKeys( + AnimationDefinitions.Definition definition, List selectedActorsSortedById, net.minecraft.util.math.random.Random rng + ) { + if (definition == null) { + return List.of(); + } + + if (selectedActorsSortedById.size() != definition.actors().size()) { + return fallbackActorKeys(selectedActorsSortedById); + } + + int[] assignment = matchUnorderedWithAssignment(selectedActorsSortedById, definition.actors(), rng); + if (assignment == null) { + return fallbackActorKeys(selectedActorsSortedById); + } + + String[] constraintKeys = buildUniqueActorKeys(definition.actors()); + String[] keysByEntityIndex = new String[selectedActorsSortedById.size()]; + + for (int ci = 0; ci < assignment.length; ci++) { + int entityIndex = assignment[ci]; + if (entityIndex >= 0 && entityIndex < keysByEntityIndex.length) { + keysByEntityIndex[entityIndex] = constraintKeys[ci]; + } + } + + for (int i = 0; i < keysByEntityIndex.length; i++) { + if (keysByEntityIndex[i] == null) { + keysByEntityIndex[i] = fallbackActorKey(selectedActorsSortedById.get(i)); + } + } + + return List.of(keysByEntityIndex); + } + + public static List> resolveAllActorKeyAssignments(AnimationDefinitions.Definition definition, List selectedActorsSortedById) { + if (definition != null && selectedActorsSortedById != null) { + if (selectedActorsSortedById.size() != definition.actors().size()) { + return List.of(); + } + + List constraints = definition.actors(); + List constraintOrder = new ArrayList<>(constraints.size()); + + for (int i = 0; i < constraints.size(); i++) { + constraintOrder.add(i); + } + + constraintOrder.sort((a, b) -> { + AnimationDefinitions.ActorConstraint ca = constraints.get(a); + AnimationDefinitions.ActorConstraint cb = constraints.get(b); + int cmp = Integer.compare(cb.assignmentPriority(), ca.assignmentPriority()); + return cmp != 0 ? cmp : ca.stableKey().compareTo(cb.stableKey()); + }); + int[] assignment = new int[constraints.size()]; + Arrays.fill(assignment, -1); + List assignments = new ArrayList<>(); + collectAssignments(0, constraintOrder, constraints, selectedActorsSortedById, new boolean[selectedActorsSortedById.size()], assignment, assignments); + String[] constraintKeys = buildUniqueActorKeys(constraints); + LinkedHashSet> unique = new LinkedHashSet<>(); + + for (int[] resolved : assignments) { + String[] keysByEntityIndex = new String[selectedActorsSortedById.size()]; + + for (int constraintIndex = 0; constraintIndex < resolved.length; constraintIndex++) { + int entityIndex = resolved[constraintIndex]; + if (entityIndex >= 0 && entityIndex < keysByEntityIndex.length) { + keysByEntityIndex[entityIndex] = constraintKeys[constraintIndex]; + } + } + + if (Arrays.stream(keysByEntityIndex).noneMatch(Objects::isNull)) { + unique.add(List.of(keysByEntityIndex)); + } + } + + List> result = new ArrayList<>(unique); + result.sort(Comparator.comparing(keys -> String.join("\u0000", keys))); + return List.copyOf(result); + } else { + return List.of(); + } + } + + private static void collectAssignments( + int pos, + List constraintOrder, + List constraints, + List entitiesSortedById, + boolean[] used, + int[] assignment, + List output + ) { + if (pos >= constraintOrder.size()) { + output.add(Arrays.copyOf(assignment, assignment.length)); + } else { + int constraintIndex = constraintOrder.get(pos); + AnimationDefinitions.ActorConstraint constraint = constraints.get(constraintIndex); + + for (int entityIndex = 0; entityIndex < entitiesSortedById.size(); entityIndex++) { + if (!used[entityIndex] && constraint.matches(entitiesSortedById.get(entityIndex))) { + used[entityIndex] = true; + assignment[constraintIndex] = entityIndex; + collectAssignments(pos + 1, constraintOrder, constraints, entitiesSortedById, used, assignment, output); + assignment[constraintIndex] = -1; + used[entityIndex] = false; + } + } + } + } + + private static boolean matchesUnordered( + List actors, List constraints, @Nullable List cachedOrder + ) { + int n = actors.size(); + boolean[] used = new boolean[n]; + List order = cachedOrder == null ? buildConstraintOrder(constraints) : cachedOrder; + return backtrackMatchActors(0, order, constraints, actors, used); + } + + private static boolean backtrackMatchActors( + int pos, + List constraintOrder, + List constraints, + List actors, + boolean[] used + ) { + if (pos >= constraintOrder.size()) { + return true; + } + + AnimationDefinitions.ActorConstraint constraint = constraints.get(constraintOrder.get(pos)); + + for (int actorIndex = 0; actorIndex < actors.size(); actorIndex++) { + if (!used[actorIndex] && constraint.matches(actors.get(actorIndex))) { + used[actorIndex] = true; + if (backtrackMatchActors(pos + 1, constraintOrder, constraints, actors, used)) { + return true; + } + + used[actorIndex] = false; + } + } + + return false; + } + + private static List buildConstraintOrder(List constraints) { + List order = new ArrayList<>(constraints.size()); + + for (int i = 0; i < constraints.size(); i++) { + order.add(i); + } + + order.sort((a, b) -> { + AnimationDefinitions.ActorConstraint left = constraints.get(a); + AnimationDefinitions.ActorConstraint right = constraints.get(b); + int priority = Integer.compare(right.assignmentPriority(), left.assignmentPriority()); + return priority != 0 ? priority : left.stableKey().compareTo(right.stableKey()); + }); + return List.copyOf(order); + } + + private static int[] matchUnorderedWithAssignment( + List entitiesSortedById, List constraints, net.minecraft.util.math.random.Random rng + ) { + int n = entitiesSortedById.size(); + boolean[] used = new boolean[n]; + int[] assignment = new int[constraints.size()]; + Arrays.fill(assignment, -1); + int[] entityOrder = buildEntityOrder(n, rng); + List order = new ArrayList<>(constraints.size()); + + for (int i = 0; i < constraints.size(); i++) { + order.add(i); + } + + order.sort((a, b) -> { + AnimationDefinitions.ActorConstraint ca = constraints.get(a); + AnimationDefinitions.ActorConstraint cb = constraints.get(b); + int cmp = Integer.compare(cb.assignmentPriority(), ca.assignmentPriority()); + return cmp != 0 ? cmp : ca.stableKey().compareTo(cb.stableKey()); + }); + return backtrackAssign(0, order, constraints, entitiesSortedById, entityOrder, used, assignment) ? assignment : null; + } + + private static boolean backtrackAssign( + int pos, + List constraintOrder, + List constraints, + List entitiesSortedById, + int[] entityOrder, + boolean[] used, + int[] assignment + ) { + if (pos >= constraintOrder.size()) { + return true; + } + + int constraintIndex = constraintOrder.get(pos); + AnimationDefinitions.ActorConstraint c = constraints.get(constraintIndex); + + for (int ei : entityOrder) { + if (!used[ei]) { + Entity e = entitiesSortedById.get(ei); + if (c.matches(e)) { + used[ei] = true; + if (assignment != null) { + assignment[constraintIndex] = ei; + } + + if (backtrackAssign(pos + 1, constraintOrder, constraints, entitiesSortedById, entityOrder, used, assignment)) { + return true; + } + + used[ei] = false; + if (assignment != null) { + assignment[constraintIndex] = -1; + } + } + } + } + + return false; + } + + private static int[] buildEntityOrder(int size, net.minecraft.util.math.random.Random rng) { + int[] order = new int[size]; + int i = 0; + + while (i < size) { + order[i] = i++; + } + + if (rng != null && size > 1) { + for (int ix = size - 1; ix > 0; ix--) { + int j = rng.nextInt(ix + 1); + int tmp = order[ix]; + order[ix] = order[j]; + order[j] = tmp; + } + + return order; + } else { + return order; + } + } + + private static List fallbackActorKeys(List selectedActorsSortedById) { + if (selectedActorsSortedById.isEmpty()) { + return List.of(); + } + + String[] out = new String[selectedActorsSortedById.size()]; + + for (int i = 0; i < selectedActorsSortedById.size(); i++) { + out[i] = fallbackActorKey(selectedActorsSortedById.get(i)); + } + + return List.of(out); + } + + private static String fallbackActorKey(Entity entity) { + Identifier typeId = Registries.ENTITY_TYPE.getId(entity.getType()); + return typeId.getPath(); + } + + private static String[] buildUniqueActorKeys(List constraints) { + String[] out = new String[constraints.size()]; + Map counts = new HashMap<>(); + + for (int i = 0; i < constraints.size(); i++) { + String base = constraints.get(i).derivedKey(); + int count = counts.merge(base, 1, Integer::sum); + out[i] = count == 1 ? base : base + "_" + count; + } + + return out; + } + + private static String sanitizeKeyPart(String value) { + if (value != null && !value.isEmpty()) { + String lower = value.toLowerCase(Locale.ROOT); + StringBuilder sb = new StringBuilder(lower.length()); + + for (int i = 0; i < lower.length(); i++) { + char c = lower.charAt(i); + if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_') { + sb.append('_'); + } else { + sb.append(c); + } + } + + return sb.toString(); + } else { + return "actor"; + } + } + + private static void reload(ResourceManager resourceManager) { + Map packInfoByResourcePackId = readAnimationPackInfos(resourceManager); + Map resources = resourceManager.findResources("afw_animdefs", id -> id.getPath().endsWith(".json")); + List loaded = new ArrayList<>(); + + for (Entry entry : resources.entrySet()) { + Identifier fileId = entry.getKey(); + String resourcePackId = entry.getValue().getPackId(); + AnimationDefinitions.AnimationPackInfo packInfo = packInfoByResourcePackId.getOrDefault(resourcePackId, fallbackPackInfo(resourcePackId)); + + try (InputStreamReader reader = new InputStreamReader(entry.getValue().getInputStream(), StandardCharsets.UTF_8)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + warnUnknownKeys(obj, fileId, "animdef", KNOWN_DEFINITION_KEYS); + if (!obj.has("actors")) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: missing 'actors' array", fileId); + } else { + JsonArray actorsArr = obj.getAsJsonArray("actors"); + if (actorsArr.isEmpty()) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: 'actors' is empty", fileId); + } else { + List actorConstraints = new ArrayList<>(); + + for (int i = 0; i < actorsArr.size(); i++) { + JsonObject a = actorsArr.get(i).getAsJsonObject(); + warnUnknownKeys(a, fileId, "actor[" + i + "]", KNOWN_ACTOR_KEYS); + Set types = readEntityTypes(a, fileId); + if (types == null) { + actorConstraints = null; + break; + } + + String entityVariant = readEntityVariant(a, fileId, "actor[" + i + "]"); + if ("\u0000".equals(entityVariant)) { + actorConstraints = null; + break; + } + + Set reqTags = readStringArray(a, "actor_tags"); + Set reqTagsAny = readStringArray(a, "actor_tags_any"); + String label = a.has("label") ? a.get("label").getAsString() : null; + AnimationDefinitions.AgeRequirement ageRequirement = readAgeRequirement(a, fileId); + if (ageRequirement == null) { + actorConstraints = null; + break; + } + + AnimationDefinitions.ActorActivity activity = readActorActivity(a, fileId); + Identifier propLeftItemId = readOptionalItemId(a, "prop_left", fileId, "actor[" + i + "]"); + Identifier propRightItemId = readOptionalItemId(a, "prop_right", fileId, "actor[" + i + "]"); + Identifier propFloorItemId = readOptionalItemId(a, "prop_floor", fileId, "actor[" + i + "]"); + actorConstraints.add( + new AnimationDefinitions.ActorConstraint( + Set.copyOf(types), + entityVariant, + Set.copyOf(reqTags), + Set.copyOf(reqTagsAny), + label, + ageRequirement, + activity, + propLeftItemId, + propRightItemId, + propFloorItemId + ) + ); + } + + if (actorConstraints != null) { + Set animationTags = normalizeTags(readStringArray(obj, "animation_tags")); + List contentTags = normalizeTagList(readStringArray(obj, "content_tags")); + Set requiredUnionTags = readStringArray(obj, "required_union_tags"); + double weight = readWeightOrDefault(obj, fileId); + String displayName = readOptionalDisplayName(obj, fileId); + Identifier defId = definitionIdFromFile(fileId); + Set joinAfter = readJoinAfter(obj, fileId, defId); + AnimationDefinitions.BlockRequirements blockRequirements = readBlockRequirements(obj, fileId); + AnimationDefinitions.WaterRequirement waterRequirement = readWaterRequirement(obj, fileId); + String positionAnchorActor = readOptionalAnchorActorKey(obj, "position_anchor_actor"); + List stages = readStageInfos(obj, defId, fileId, actorConstraints); + if (stages.isEmpty()) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: missing 'stages' array", fileId); + } else { + AnimationDefinitions.Definition definition = new AnimationDefinitions.Definition( + defId, + List.copyOf(actorConstraints), + Set.copyOf(requiredUnionTags), + Set.copyOf(animationTags), + contentTags, + stages, + weight, + blockRequirements, + waterRequirement, + positionAnchorActor, + packInfo, + displayName, + joinAfter + ); + definition = definition.withGameplayMetadata( + AnimationGameplayMetadata.parse(fileId, definition, obj, List.of(buildUniqueActorKeys(actorConstraints))) + ); + loaded.add(definition); + } + } + } + } + } catch (IOException | RuntimeException e) { + AnimationEngine.logSetupError("[NoN Animation] Failed to load {}", fileId, e); + } + } + + loaded.sort(Comparator.comparing(d -> d.id().toString())); + validateJoinAfterReferences(loaded); + DEFINITIONS = List.copyOf(loaded); + Map definitionsById = new LinkedHashMap<>(); + DEFINITIONS.forEach(definitionx -> definitionsById.put(definitionx.id(), definitionx)); + DEFINITIONS_BY_ID = Map.copyOf(definitionsById); + rebuildMatchingIndexes(); + JOIN_FOLLOWERS_BY_PREDECESSOR = buildJoinFollowerIndex(DEFINITIONS); + rebuildEligibilityIndexes(); + AnimationEngine.LOGGER.info("[NoN Animation] Loaded {} animation definition(s) from datapacks", DEFINITIONS.size()); + } + + private static void rebuildMatchingIndexes() { + Map> byActorCount = new LinkedHashMap<>(); + Map> matchOrders = new LinkedHashMap<>(); + + for (AnimationDefinitions.Definition definition : DEFINITIONS) { + byActorCount.computeIfAbsent(definition.actors().size(), ignored -> new ArrayList<>()).add(definition); + matchOrders.put(definition.id(), buildConstraintOrder(definition.actors())); + } + + Comparator order = Comparator.comparing(definition -> definition.id().toString()); + Map> finalized = new LinkedHashMap<>(); + byActorCount.forEach((actorCount, definitions) -> { + definitions.sort(order); + finalized.put(actorCount, List.copyOf((Collection)definitions)); + }); + DEFINITIONS_BY_ACTOR_COUNT = Map.copyOf(finalized); + MATCH_ORDER_BY_DEFINITION = Map.copyOf(matchOrders); + } + + private static void rebuildEligibilityIndexes() { + Set actorTypes = new LinkedHashSet<>(); + Set pairTypes = new LinkedHashSet<>(); + Map> compatibility = new LinkedHashMap<>(); + + for (AnimationDefinitions.Definition definition : DEFINITIONS) { + if (isDefinitionEnabled(definition) && isGenericMatchDefinition(definition)) { + List actors = definition.actors(); + if (actors != null && actors.size() >= 2) { + List> keysByActor = actors.stream().map(actor -> { + boolean baby = actor.ageRequirement() == AnimationDefinitions.AgeRequirement.BABY; + Set keys = new LinkedHashSet<>(); + + for (Identifier entityType : actor.entityTypes()) { + AnimationDefinitions.ActorKey key = new AnimationDefinitions.ActorKey(entityType, baby); + keys.add(key); + actorTypes.add(key); + } + + return Set.copyOf(keys); + }).toList(); + if (keysByActor.size() == 2 && !keysByActor.get(0).isEmpty() && !keysByActor.get(1).isEmpty()) { + for (AnimationDefinitions.ActorKey left : keysByActor.get(0)) { + for (AnimationDefinitions.ActorKey right : keysByActor.get(1)) { + pairTypes.add(left); + pairTypes.add(right); + compatibility.computeIfAbsent(left, ignored -> new LinkedHashSet<>()).add(right); + compatibility.computeIfAbsent(right, ignored -> new LinkedHashSet<>()).add(left); + } + } + } + } + } + } + + Map> immutableCompatibility = new LinkedHashMap<>(); + compatibility.forEach((key, value) -> immutableCompatibility.put(key, Set.copyOf((Collection)value))); + GENERIC_ACTOR_TYPES = Set.copyOf(actorTypes); + GENERIC_MOB_PAIR_TYPES = Set.copyOf(pairTypes); + GENERIC_MOB_PAIR_COMPATIBILITY = Map.copyOf(immutableCompatibility); + } + + private static Map readAnimationPackInfos(ResourceManager resourceManager) { + LinkedHashMap out = new LinkedHashMap<>(); + if (resourceManager == null) { + return out; + } + + try (Stream stream = resourceManager.streamResourcePacks()) { + stream.forEach(pack -> { + if (pack != null) { + String resourcePackId = pack.getId(); + if (resourcePackId != null && !resourcePackId.isBlank()) { + AnimationDefinitions.AnimationPackInfo info = readAnimationPackInfo(pack, resourcePackId); + out.put(resourcePackId, info == null ? fallbackPackInfo(resourcePackId) : info); + } + } + }); + } catch (RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to read animation pack metadata from pack.mcmeta.", e); + } + + validateModelProviders(out.values()); + return out; + } + + private static void validateModelProviders(Collection packInfos) { + if (packInfos != null && !packInfos.isEmpty()) { + Map ownerByRoot = new LinkedHashMap<>(); + Set availablePackIds = new LinkedHashSet<>(); + + for (AnimationDefinitions.AnimationPackInfo info : packInfos) { + if (info != null && info.localModelRoot() != null) { + availablePackIds.add(info.id()); + String previousOwner = ownerByRoot.putIfAbsent(info.localModelRoot(), info.resourcePackId()); + if (previousOwner != null && !previousOwner.equals(info.resourcePackId())) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Local model root {} is shared by resource packs '{}' and '{}'. Give each pack a unique animationframework.id.", + info.localModelRoot(), + previousOwner, + info.resourcePackId() + ); + } + } + } + + for (AnimationDefinitions.AnimationPackInfo info : packInfos) { + if (info != null) { + for (AnimationDefinitions.ModelImport modelImport : info.modelImports()) { + if (!availablePackIds.contains(modelImport.packId())) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Pack '{}' imports models from missing pack '{}'. Missing models will fall back to shared resources.", + info.id(), + modelImport.packId() + ); + } + } + } + } + } + } + + @Nullable + private static AnimationDefinitions.AnimationPackInfo readAnimationPackInfo(ResourcePack pack, String resourcePackId) { + try (InputStream stream = openRootResource(pack, "pack.mcmeta")) { + if (stream == null) { + return null; + } + + InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); + + JsonObject root; + try { + root = JsonParser.parseReader(reader).getAsJsonObject(); + } catch (Throwable var17) { + try { + reader.close(); + } catch (Throwable var16) { + var17.addSuppressed(var16); + } + + throw var17; + } + + reader.close(); + JsonObject var21 = null; + if (root.has("animationframework") && root.get("animationframework").isJsonObject()) { + var21 = root.getAsJsonObject("animationframework"); + } + + String packDescription = readPackDescription(root); + if (var21 == null) { + AnimationDefinitions.AnimationPackInfo fallback = fallbackPackInfo(resourcePackId); + return new AnimationDefinitions.AnimationPackInfo( + fallback.id(), resourcePackId, fallback.name(), fallback.author(), fallback.version(), packDescription, null + ); + } + + String rawId = readOptionalString(var21, "id"); + String id = sanitizePackMetadataId(rawId, resourcePackId); + String name = readOptionalString(var21, "name"); + String author = readOptionalString(var21, "author"); + String version = readOptionalString(var21, "version"); + String description = readOptionalString(var21, "description"); + if (description == null) { + description = packDescription; + } + + Identifier localModelRoot = localModelRoot(rawId); + warnRemovedModelScope(var21, resourcePackId); + List modelImports = readModelImports(var21, resourcePackId, rawId, true); + return new AnimationDefinitions.AnimationPackInfo(id, resourcePackId, name, author, version, description, localModelRoot, modelImports); + } catch (IOException | RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN Animation] Failed to parse animation pack metadata for {}", resourcePackId, e); + return null; + } + } + + @Nullable + private static InputStream openRootResource(ResourcePack pack, String fileName) { + if (pack != null && fileName != null) { + InputSupplier supplier = pack.openRoot(new String[]{fileName}); + if (supplier == null) { + return null; + } + + try { + return (InputStream)supplier.get(); + } catch (IOException | RuntimeException e) { + return null; + } + } else { + return null; + } + } + + private static AnimationDefinitions.AnimationPackInfo fallbackPackInfo(String resourcePackId) { + String safePackId = sanitizeNonBlank(resourcePackId, "unknown"); + return new AnimationDefinitions.AnimationPackInfo(safePackId, safePackId, displayNameFromPackId(safePackId), "Unknown", null, null, null); + } + + @Nullable + private static Identifier localModelRoot(@Nullable String rawPackId) { + String cleaned = sanitizeNullable(rawPackId); + Identifier packId = cleaned != null && cleaned.indexOf(58) > 0 ? Identifier.tryParse(cleaned) : null; + return packId == null ? null : Identifier.of(packId.getNamespace(), packId.getPath() + "/entity"); + } + + private static void warnRemovedModelScope(JsonObject animationMetadata, String resourcePackId) { + if (animationMetadata != null && animationMetadata.has("model_scope")) { + AnimationEngine.logSetupWarning( + "[NoN Animation] animationframework.model_scope in pack.mcmeta for {} is no longer needed and has been ignored. All packs now use local models automatically.", + resourcePackId + ); + } + } + + static List readModelImports( + JsonObject animationMetadata, String resourcePackId, @Nullable String ownPackId, boolean logWarnings + ) { + if (animationMetadata != null && animationMetadata.has("model_imports")) { + JsonElement importsElement = animationMetadata.get("model_imports"); + if (importsElement != null && importsElement.isJsonArray()) { + List imports = new ArrayList<>(); + Set seenMappings = new LinkedHashSet<>(); + Identifier ownId = ownPackId != null && ownPackId.indexOf(58) > 0 ? Identifier.tryParse(ownPackId) : null; + + for (JsonElement importElement : importsElement.getAsJsonArray()) { + if (importElement != null && importElement.isJsonObject()) { + JsonObject importObject = importElement.getAsJsonObject(); + String importedPackId = readOptionalString(importObject, "pack"); + Identifier importedId = importedPackId != null && importedPackId.indexOf(58) > 0 ? Identifier.tryParse(importedPackId) : null; + if (importedId == null) { + logPackMetadataWarning( + logWarnings, + "[NoN Animation] Ignoring invalid model import pack id '{}' in pack.mcmeta for {}. Use a namespaced id like creator:model_pack.", + importedPackId, + resourcePackId + ); + } else if (importedId.equals(ownId)) { + logPackMetadataWarning( + logWarnings, "[NoN Animation] Ignoring self model import '{}' in pack.mcmeta for {}.", importedPackId, resourcePackId + ); + } else { + JsonElement entitiesElement = importObject.get("entities"); + if (entitiesElement != null && entitiesElement.isJsonArray()) { + LinkedHashSet entityTypes = new LinkedHashSet<>(); + + for (JsonElement entityElement : entitiesElement.getAsJsonArray()) { + String rawEntity = entityElement != null && entityElement.isJsonPrimitive() && entityElement.getAsJsonPrimitive().isString() + ? entityElement.getAsString().trim() + : null; + Identifier entityId = rawEntity != null && rawEntity.indexOf(58) > 0 ? Identifier.tryParse(rawEntity) : null; + if (entityId == null) { + logPackMetadataWarning( + logWarnings, + "[NoN Animation] Ignoring invalid entity '{}' in model import '{}' for {}.", + rawEntity, + importedPackId, + resourcePackId + ); + } else { + String mappingKey = importedId + "|" + entityId; + if (seenMappings.add(mappingKey)) { + entityTypes.add(entityId); + } + } + } + + if (entityTypes.isEmpty()) { + logPackMetadataWarning( + logWarnings, + "[NoN Animation] Ignoring model import '{}' in pack.mcmeta for {} because it contains no valid entities.", + importedPackId, + resourcePackId + ); + } else { + imports.add( + new AnimationDefinitions.ModelImport( + importedId.toString(), Identifier.of(importedId.getNamespace(), importedId.getPath() + "/entity"), entityTypes + ) + ); + } + } else { + logPackMetadataWarning( + logWarnings, + "[NoN Animation] Ignoring model import '{}' in pack.mcmeta for {}: 'entities' must be a non-empty array.", + importedPackId, + resourcePackId + ); + } + } + } else { + logPackMetadataWarning( + logWarnings, + "[NoN Animation] Ignoring invalid model_imports entry in pack.mcmeta for {}: expected an object with 'pack' and 'entities'.", + resourcePackId + ); + } + } + + return imports.isEmpty() ? List.of() : List.copyOf(imports); + } else { + logPackMetadataWarning( + logWarnings, "[NoN Animation] Ignoring invalid animationframework.model_imports in pack.mcmeta for {}: expected an array.", resourcePackId + ); + return List.of(); + } + } else { + return List.of(); + } + } + + private static void logPackMetadataWarning(boolean enabled, String template, Object... args) { + if (enabled) { + AnimationEngine.logSetupWarning(template, args); + } + } + + private static String sanitizePackMetadataId(@Nullable String rawId, String resourcePackId) { + String fallback = sanitizeNonBlank(resourcePackId, "unknown"); + String cleaned = sanitizeNullable(rawId); + if (cleaned == null) { + return fallback; + } else if (Identifier.tryParse(cleaned) == null) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Ignoring invalid animationframework.id '{}' in pack.mcmeta for {}. Use a namespaced id like creator:pack_name.", + cleaned, + resourcePackId + ); + return fallback; + } else { + return cleaned; + } + } + + @Nullable + private static String readPackDescription(JsonObject root) { + if (root != null && root.has("pack") && root.get("pack").isJsonObject()) { + JsonObject pack = root.getAsJsonObject("pack"); + return readOptionalString(pack, "description"); + } else { + return null; + } + } + + @Nullable + private static String readOptionalString(JsonObject object, String key) { + if (object != null && key != null && object.has(key)) { + JsonElement element = object.get(key); + if (element != null && element.isJsonPrimitive()) { + try { + return sanitizeNullable(element.getAsString()); + } catch (RuntimeException e) { + return null; + } + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static String readOptionalDisplayName(JsonObject object, Identifier fileId) { + if (object != null && object.has("display_name")) { + JsonElement element = object.get("display_name"); + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String value = element.getAsString().trim(); + if (value.isEmpty()) { + AnimationEngine.logSetupWarning("[NoN Animation] Ignoring blank display_name in {}. The animation id will be shown instead.", fileId); + return null; + } else { + return value; + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Ignoring invalid display_name in {}: expected a non-empty string.", fileId); + return null; + } + } else { + return null; + } + } + + private static String displayNameFromPackId(String rawPackId) { + String id = sanitizeNonBlank(rawPackId, "Unknown Pack"); + int slash = id.lastIndexOf(47); + if (slash >= 0 && slash < id.length() - 1) { + id = id.substring(slash + 1); + } + + int colon = id.indexOf(58); + if (colon >= 0 && colon < id.length() - 1) { + id = id.substring(colon + 1); + } + + int underscore = id.indexOf(95); + if (underscore > 0 && isNumericPrefix(id, underscore)) { + id = id.substring(underscore + 1); + } + + id = id.replace('_', ' ').replace('-', ' ').trim(); + return id.isBlank() ? "Unknown Pack" : id; + } + + private static boolean isNumericPrefix(String value, int endExclusive) { + if (value != null && endExclusive > 0 && endExclusive <= value.length()) { + for (int i = 0; i < endExclusive; i++) { + char c = value.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + + return true; + } else { + return false; + } + } + + private static String sanitizeNonBlank(@Nullable String value, String fallback) { + String cleaned = sanitizeNullable(value); + return cleaned == null ? fallback : cleaned; + } + + @Nullable + private static String sanitizeNullable(@Nullable String value) { + if (value == null) { + return null; + } + + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private static void warnUnknownKeys(JsonObject obj, Identifier fileId, String context, Set knownKeys) { + if (obj != null && knownKeys != null && !knownKeys.isEmpty()) { + String safeContext = context != null && !context.isBlank() ? context : "object"; + + for (Entry entry : obj.entrySet()) { + String key = entry.getKey(); + if (key != null && !knownKeys.contains(key)) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Unknown field '{}' in {} {}. It will be ignored; check for typos.", key, fileId == null ? "" : fileId, safeContext + ); + } + } + } + } + + private static Set readStringArray(JsonObject obj, String key) { + if (!obj.has(key)) { + return new LinkedHashSet<>(); + } + + JsonArray arr = obj.getAsJsonArray(key); + Set out = new LinkedHashSet<>(); + + for (int i = 0; i < arr.size(); i++) { + out.add(arr.get(i).getAsString()); + } + + return out; + } + + private static double readWeightOrDefault(JsonObject obj, Identifier fileId) { + if (obj != null && obj.has("weight")) { + JsonElement element = obj.get("weight"); + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) { + double value; + try { + value = element.getAsDouble(); + } catch (RuntimeException ignored) { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid weight in {} (must be a positive number). Using default 1.0.", fileId); + return 1.0; + } + + if (Double.isFinite(value) && !(value <= 0.0)) { + return value; + } + + AnimationEngine.logSetupWarning("[NoN Animation] Invalid weight '{}' in {} (must be > 0). Using default 1.0.", value, fileId); + return 1.0; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid weight in {} (must be a positive number). Using default 1.0.", fileId); + return 1.0; + } + } else { + return 1.0; + } + } + + @Nullable + private static String readOptionalAnchorActorKey(JsonObject obj, String key) { + if (obj == null || key == null || key.isBlank()) { + return null; + } + + if (!obj.has(key)) { + return null; + } + + JsonElement element = obj.get(key); + if (element != null && element.isJsonPrimitive()) { + String raw = element.getAsString(); + if (raw == null) { + return null; + } + + String trimmed = raw.trim(); + return trimmed.isEmpty() ? null : trimmed; + } else { + return null; + } + } + + private static Set normalizeTags(Set tags) { + if (tags != null && !tags.isEmpty()) { + Set out = new LinkedHashSet<>(); + + for (String raw : tags) { + if (raw != null) { + String value = raw.trim().toLowerCase(Locale.ROOT); + if (!value.isEmpty()) { + out.add(value); + } + } + } + + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } else { + return Set.of(); + } + } + + private static AnimationDefinitions.AgeRequirement readAgeRequirement(JsonObject obj, Identifier fileId) { + if (!obj.has("age")) { + return AnimationDefinitions.AgeRequirement.ADULT; + } + + String raw = obj.get("age").getAsString(); + if (raw != null && !raw.isBlank()) { + String value = raw.toLowerCase(Locale.ROOT).trim(); + + return switch (value) { + case "adult" -> AnimationDefinitions.AgeRequirement.ADULT; + case "baby" -> AnimationDefinitions.AgeRequirement.BABY; + default -> { + AnimationEngine.logSetupWarning("[NoN Animation] Unknown age '{}' in {} (expected adult|baby). Skipping definition.", raw, fileId); + yield null; + } + }; + } else { + return AnimationDefinitions.AgeRequirement.ADULT; + } + } + + private static AnimationDefinitions.ActorActivity readActorActivity(JsonObject obj, Identifier fileId) { + if (!obj.has("activity")) { + return AnimationDefinitions.ActorActivity.ACTIVE; + } + + String raw = obj.get("activity").getAsString(); + if (raw != null && !raw.isBlank()) { + String value = raw.toLowerCase(Locale.ROOT).trim(); + + return switch (value) { + case "active" -> AnimationDefinitions.ActorActivity.ACTIVE; + case "passive" -> AnimationDefinitions.ActorActivity.PASSIVE; + default -> { + AnimationEngine.logSetupWarning("[NoN Animation] Unknown actor activity '{}' in {} (expected active|passive). Using active.", raw, fileId); + yield AnimationDefinitions.ActorActivity.ACTIVE; + } + }; + } else { + return AnimationDefinitions.ActorActivity.ACTIVE; + } + } + + static AnimationDefinitions.BlockRequirements readBlockRequirements(JsonObject obj, Identifier fileId) { + if (!obj.has("block_requirements")) { + return null; + } + + JsonObject req = obj.getAsJsonObject("block_requirements"); + if (req == null) { + return null; + } + + warnUnknownKeys(req, fileId, "block_requirements", KNOWN_BLOCK_REQUIREMENT_KEYS); + String type = req.has("type") ? req.get("type").getAsString() : "wall"; + String normalizedType = type == null ? "wall" : type.trim().toLowerCase(Locale.ROOT); + AnimationDefinitions.BlockPredicate blocks = readBlockPredicate(req, fileId); + if (req.has("blocks") && blocks == null) { + return null; + } + + if ("wall".equals(normalizedType)) { + AnimationDefinitions.WallHeight height = readWallHeight(req, fileId); + if (height == null) { + return null; + } + + AnimationDefinitions.WallWidth width = readWallWidth(req, fileId); + if (width == null) { + return null; + } + + AnimationDefinitions.WallFacing facing = readWallFacing(req, fileId, blocks); + if (req.has("facing") && facing == null) { + return null; + } + + AnimationDefinitions.Clearance clearance = readClearance(req, fileId); + return new AnimationDefinitions.BlockRequirements("wall", height, width, facing, clearance, null, null, 0, null, blocks); + } else if ("center_support".equals(normalizedType)) { + if (req.has("facing")) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements facing is only supported for walls.", fileId); + return null; + } + + if (req.has("width")) { + AnimationEngine.logSetupWarning("[NoN Animation] {}: center_support ignores wall 'width'.", fileId); + } + + AnimationDefinitions.CenterSupport support = readCenterSupport(req, fileId); + if (support == null) { + return null; + } + + if (support != AnimationDefinitions.CenterSupport.SURFACE || blocks != null && !blocks.isEmpty()) { + AnimationDefinitions.CenterPlacement placement = readCenterPlacement(req, fileId); + if (placement == null) { + return null; + } + + AnimationDefinitions.Clearance clearance = null; + int surfaceRadius = 0; + AnimationDefinitions.SurfaceFootprint surfaceFootprint = null; + if (placement == AnimationDefinitions.CenterPlacement.SURFACE_FOOTPRINT) { + surfaceFootprint = readSurfaceFootprint(req, fileId); + if (surfaceFootprint == null) { + return null; + } + + if (req.has("clearance")) { + AnimationEngine.logSetupWarning("[NoN Animation] {}: center_support surface_footprint ignores 'clearance'.", fileId); + } + + if (req.has("surface_radius")) { + AnimationEngine.logSetupWarning("[NoN Animation] {}: center_support surface_footprint ignores 'surface_radius'.", fileId); + } + } else { + clearance = readClearance(req, fileId); + surfaceRadius = readNonNegativeInt(req, "surface_radius", 0); + if (req.has("surface_footprint")) { + AnimationEngine.logSetupWarning("[NoN Animation] {}: center_support directional_clearance ignores 'surface_footprint'.", fileId); + } + } + + return new AnimationDefinitions.BlockRequirements( + "center_support", null, null, null, clearance, support, placement, surfaceRadius, surfaceFootprint, blocks + ); + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: center_support surface requires 'blocks'.", fileId); + return null; + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: unsupported block_requirements type '{}'", fileId, type); + return null; + } + } + + @Nullable + private static AnimationDefinitions.WallFacing readWallFacing(JsonObject req, Identifier fileId, @Nullable AnimationDefinitions.BlockPredicate blocks) { + if (!req.has("facing")) { + return null; + } + + if (blocks != null && !blocks.isEmpty()) { + JsonElement element = req.get("facing"); + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String raw = element.getAsString(); + String value = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT); + + for (AnimationDefinitions.WallFacing facing : AnimationDefinitions.WallFacing.values()) { + if (facing.id().equals(value)) { + return facing; + } + } + + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: unsupported wall facing '{}' (expected front|back|left|right).", fileId, raw); + return null; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: wall block_requirements facing must be a string.", fileId); + return null; + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: wall block_requirements facing requires 'blocks'.", fileId); + return null; + } + } + + private static AnimationDefinitions.CenterPlacement readCenterPlacement(JsonObject req, Identifier fileId) { + if (!req.has("placement")) { + return AnimationDefinitions.CenterPlacement.DIRECTIONAL_CLEARANCE; + } + + String raw = req.get("placement").getAsString(); + if (raw != null && !raw.isBlank()) { + String value = raw.trim().toLowerCase(Locale.ROOT); + + for (AnimationDefinitions.CenterPlacement placement : AnimationDefinitions.CenterPlacement.values()) { + if (placement.id().equals(value)) { + return placement; + } + } + + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: unsupported center_support placement '{}'", fileId, raw); + return null; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: center_support block_requirements has empty placement", fileId); + return null; + } + } + + private static AnimationDefinitions.CenterSupport readCenterSupport(JsonObject req, Identifier fileId) { + if (!req.has("support")) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: center_support block_requirements missing support", fileId); + return null; + } + + String raw = req.get("support").getAsString(); + if (raw != null && !raw.isBlank()) { + String value = raw.trim().toLowerCase(Locale.ROOT); + + for (AnimationDefinitions.CenterSupport support : AnimationDefinitions.CenterSupport.values()) { + if (support.id().equals(value)) { + return support; + } + } + + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: unsupported center_support support '{}'", fileId, raw); + return null; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: center_support block_requirements has empty support", fileId); + return null; + } + } + + @Nullable + private static AnimationDefinitions.SurfaceFootprint readSurfaceFootprint(JsonObject req, Identifier fileId) { + if (!req.has("surface_footprint")) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: center_support surface_footprint placement requires 'surface_footprint'.", fileId); + return null; + } else { + JsonElement element = req.get("surface_footprint"); + if (element != null && element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + warnUnknownKeys(obj, fileId, "block_requirements.surface_footprint", KNOWN_SURFACE_FOOTPRINT_KEYS); + int width = readPositiveInt(obj, "width", 1); + int depth = readPositiveInt(obj, "depth", 1); + int height = readPositiveInt(obj, "height", 2); + int margin = readNonNegativeInt(obj, "margin", 0); + return new AnimationDefinitions.SurfaceFootprint(width, depth, height, margin); + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: center_support surface_footprint must be an object.", fileId); + return null; + } + } + } + + @Nullable + private static AnimationDefinitions.BlockPredicate readBlockPredicate(JsonObject req, Identifier fileId) { + if (!req.has("blocks")) { + return null; + } + + JsonElement blocksElement = req.get("blocks"); + if (blocksElement != null && !blocksElement.isJsonNull()) { + Set blockIds = new LinkedHashSet<>(); + Set tagIds = new LinkedHashSet<>(); + if (blocksElement.isJsonArray()) { + JsonArray arr = blocksElement.getAsJsonArray(); + if (arr.isEmpty()) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements blocks array is empty.", fileId); + return null; + } + + for (JsonElement element : arr) { + if (!readBlockPredicateEntry(element, fileId, blockIds, tagIds)) { + return null; + } + } + } else if (!readBlockPredicateEntry(blocksElement, fileId, blockIds, tagIds)) { + return null; + } + + if (blockIds.isEmpty() && tagIds.isEmpty()) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements blocks did not contain any valid entries.", fileId); + return null; + } else { + return new AnimationDefinitions.BlockPredicate(Set.copyOf(blockIds), Set.copyOf(tagIds)); + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements blocks must not be null.", fileId); + return null; + } + } + + private static boolean readBlockPredicateEntry(JsonElement element, Identifier fileId, Set blockIds, Set tagIds) { + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String raw = element.getAsString(); + if (raw != null && !raw.isBlank()) { + String trimmed = raw.trim(); + boolean tag = trimmed.startsWith("#"); + String idText = tag ? trimmed.substring(1) : trimmed; + if (!idText.contains(":")) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Skipping {}: block_requirements blocks entry '{}' must use a namespaced id like 'minecraft:chest' or '#minecraft:beds'.", + fileId, + raw + ); + return false; + } + + Identifier id = Identifier.tryParse(idText); + if (id == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements blocks identifier '{}'.", fileId, raw); + return false; + } + + if (tag) { + tagIds.add(id); + } else { + if (!Registries.BLOCK.containsId(id)) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: unknown block id '{}' in block_requirements blocks.", fileId, raw); + return false; + } + + blockIds.add(id); + } + + return true; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements blocks contains an empty entry.", fileId); + return false; + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements blocks entries must be strings.", fileId); + return false; + } + } + + private static AnimationDefinitions.WaterRequirement readWaterRequirement(JsonObject obj, Identifier fileId) { + if (!obj.has("water")) { + return AnimationDefinitions.WaterRequirement.NONE; + } + + JsonElement el = obj.get("water"); + if (el != null && el.isJsonPrimitive()) { + String raw = el.getAsString(); + if (raw != null && !raw.isBlank()) { + String value = raw.toLowerCase(Locale.ROOT).trim(); + + return switch (value) { + case "surface" -> AnimationDefinitions.WaterRequirement.SURFACE; + case "underwater" -> AnimationDefinitions.WaterRequirement.UNDERWATER; + case "none" -> AnimationDefinitions.WaterRequirement.NONE; + default -> { + AnimationEngine.logSetupWarning("[NoN Animation] Unknown water requirement '{}' in {}; expected none|surface|underwater.", raw, fileId); + yield AnimationDefinitions.WaterRequirement.NONE; + } + }; + } else { + return AnimationDefinitions.WaterRequirement.NONE; + } + } else { + return AnimationDefinitions.WaterRequirement.NONE; + } + } + + private static AnimationDefinitions.WallHeight readWallHeight(JsonObject req, Identifier fileId) { + int min = 2; + Integer max = null; + boolean minSet = false; + if (req.has("height")) { + JsonElement heightEl = req.get("height"); + if (heightEl.isJsonObject()) { + JsonObject heightObj = heightEl.getAsJsonObject(); + warnUnknownKeys(heightObj, fileId, "block_requirements.height", KNOWN_WALL_HEIGHT_KEYS); + Integer minVal = readOptionalInt(heightObj, "min"); + Integer maxVal = readOptionalInt(heightObj, "max"); + if (minVal != null) { + min = minVal; + minSet = true; + } + + if (maxVal != null) { + max = maxVal; + if (!minSet) { + min = 1; + } + } + } else { + Integer exact = readOptionalInt(heightEl); + if (exact != null) { + min = exact; + max = exact; + } + } + } + + if (min < 1) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements height min '{}'", fileId, min); + return null; + } + + if (max != null) { + if (max < 1) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements height max '{}'", fileId, max); + return null; + } + + if (max < min) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements height max '{}' < min '{}'", fileId, max, min); + return null; + } + } + + return new AnimationDefinitions.WallHeight(min, max); + } + + static AnimationDefinitions.WallWidth readWallWidth(JsonObject req, Identifier fileId) { + if (!req.has("width")) { + return new AnimationDefinitions.WallWidth(1, null); + } + + int min = 1; + Integer max = null; + JsonElement widthElement = req.get("width"); + if (widthElement.isJsonObject()) { + JsonObject widthObject = widthElement.getAsJsonObject(); + warnUnknownKeys(widthObject, fileId, "block_requirements.width", KNOWN_WALL_WIDTH_KEYS); + if (!widthObject.has("min") && !widthObject.has("max")) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements width must define 'min' or 'max'.", fileId); + return null; + } + + Integer minValue = readOptionalInt(widthObject, "min"); + Integer maxValue = readOptionalInt(widthObject, "max"); + if (widthObject.has("min") && minValue == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements width min.", fileId); + return null; + } + + if (widthObject.has("max") && maxValue == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements width max.", fileId); + return null; + } + + if (minValue != null) { + min = minValue; + } + + max = maxValue; + } else { + Integer minimum = readOptionalInt(widthElement); + if (minimum == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements width.", fileId); + return null; + } + + min = minimum; + } + + if (min < 1) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid block_requirements width min '{}'.", fileId, min); + return null; + } else if (max != null && max < min) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: block_requirements width max '{}' < min '{}'.", fileId, max, min); + return null; + } else { + return new AnimationDefinitions.WallWidth(min, max); + } + } + + static AnimationDefinitions.Clearance readClearance(JsonObject req, Identifier fileId) { + int width = 1; + int height = 2; + int depth = 1; + if (req.has("clearance")) { + JsonObject clearance = req.getAsJsonObject("clearance"); + if (clearance != null) { + warnUnknownKeys(clearance, fileId, "block_requirements.clearance", KNOWN_CLEARANCE_KEYS); + width = readPositiveInt(clearance, "width", width); + height = readPositiveInt(clearance, "height", height); + depth = readPositiveInt(clearance, "depth", depth); + } + } + + return new AnimationDefinitions.Clearance(width, height, depth); + } + + private static int readPositiveInt(JsonObject obj, String key, int fallback) { + if (!obj.has(key)) { + return fallback; + } else { + Integer value = readOptionalInt(obj, key); + if (value == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid integer value for {}", key); + return fallback; + } else { + return Math.max(1, value); + } + } + } + + private static int readNonNegativeInt(JsonObject obj, String key, int fallback) { + if (!obj.has(key)) { + return fallback; + } else { + Integer value = readOptionalInt(obj, key); + if (value == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid integer value for {}", key); + return fallback; + } else { + return Math.max(0, value); + } + } + } + + private static Integer readOptionalInt(JsonObject obj, String key) { + return !obj.has(key) ? null : readOptionalInt(obj.get(key)); + } + + private static Set readEntityTypes(JsonObject obj, Identifier fileId) { + String key = "entity_types"; + if (!obj.has(key)) { + return new LinkedHashSet<>(); + } + + JsonArray arr = obj.getAsJsonArray(key); + Set out = new LinkedHashSet<>(); + + for (int i = 0; i < arr.size(); i++) { + String s = arr.get(i).getAsString(); + Identifier id = Identifier.tryParse(s); + if (id == null) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: invalid identifier in {}: '{}'", fileId, key, s); + return null; + } + + if (PLAYER_SLIM_ID.equals(id)) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Skipping {}: '{}' is no longer supported in {}. Use '{}' only.", fileId, PLAYER_SLIM_ID, key, PLAYER_ID + ); + return null; + } + + out.add(id); + } + + return out; + } + + private static Set readJoinAfter(JsonObject obj, Identifier fileId, Identifier definitionId) { + if (!obj.has("join_after")) { + return Set.of(); + } + + JsonElement element = obj.get("join_after"); + if (element != null && element.isJsonArray()) { + LinkedHashSet out = new LinkedHashSet<>(); + JsonArray values = element.getAsJsonArray(); + + for (int i = 0; i < values.size(); i++) { + JsonElement value = values.get(i); + if (value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) { + String raw = value.getAsString().trim(); + Identifier id = raw.indexOf(58) > 0 ? Identifier.tryParse(raw) : null; + if (id == null) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Invalid join_after entry '{}' in {}: use an exact namespaced id such as creator:wolfmplayer.", raw, fileId + ); + } else if (id.equals(definitionId)) { + AnimationEngine.logSetupWarning("[NoN Animation] Ignoring self-referencing join_after entry '{}' in {}.", id, fileId); + } else { + out.add(id); + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid join_after entry {} in {}: expected an exact namespaced animation id.", i, fileId); + } + } + + return Set.copyOf(out); + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid join_after in {}: expected an array of exact namespaced animation ids.", fileId); + return Set.of(); + } + } + + private static void validateJoinAfterReferences(List definitions) { + Set loadedIds = new HashSet<>(); + + for (AnimationDefinitions.Definition definition : definitions) { + loadedIds.add(definition.id()); + } + + for (AnimationDefinitions.Definition definition : definitions) { + for (Identifier predecessorId : definition.joinAfter()) { + if (!loadedIds.contains(predecessorId)) { + AnimationEngine.logSetupWarning("[NoN Animation] Animation {} references missing join_after predecessor {}.", definition.id(), predecessorId); + } + } + } + } + + private static Map> buildJoinFollowerIndex(List definitions) { + LinkedHashMap> mutable = new LinkedHashMap<>(); + + for (AnimationDefinitions.Definition definition : definitions) { + for (Identifier predecessorId : definition.joinAfter()) { + mutable.computeIfAbsent(predecessorId, ignored -> new ArrayList<>()).add(definition); + } + } + + LinkedHashMap> immutable = new LinkedHashMap<>(); + mutable.forEach((id, followers) -> immutable.put(id, List.copyOf((Collection)followers))); + return Map.copyOf(immutable); + } + + @Nullable + private static String readEntityVariant(JsonObject obj, Identifier fileId, String contextLabel) { + String key = "entity_variant"; + if (obj != null && obj.has(key)) { + JsonElement el = obj.get(key); + if (el == null || el.isJsonNull()) { + return null; + } + + if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()) { + String value = el.getAsString(); + if (value != null && !value.isBlank()) { + value = value.trim().toLowerCase(Locale.ROOT); + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + boolean valid = c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_'; + if (!valid) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Skipping {}: invalid {} '{}' in {}. Use lowercase letters, numbers, and underscores.", + fileId, + key, + value, + contextLabel + ); + return "\u0000"; + } + } + + return value; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: {} in {} must not be blank.", fileId, key, contextLabel); + return "\u0000"; + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: {} in {} must be a string.", fileId, key, contextLabel); + return "\u0000"; + } + } else { + return null; + } + } + + @Nullable + private static Identifier readOptionalItemId(JsonObject obj, String key, Identifier fileId, String contextLabel) { + if (obj != null && obj.has(key)) { + JsonElement el = obj.get(key); + if (el == null || el.isJsonNull()) { + return null; + } + + if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()) { + String raw = el.getAsString(); + if (raw != null && !raw.isBlank()) { + Identifier id = Identifier.tryParse(raw.trim()); + if (id != null && Registries.ITEM.containsId(id)) { + return id; + } + + AnimationEngine.logSetupWarning("[NoN Animation] Invalid item id '{}' for {} in {} {}.", raw, key, fileId, contextLabel); + return null; + } else { + return null; + } + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid {} in {} {}: expected item id string.", key, fileId, contextLabel); + return null; + } + } else { + return null; + } + } + + private static List readStageInfos( + JsonObject obj, Identifier defId, Identifier fileId, List actorConstraints + ) { + List stages = new ArrayList<>(); + double baseSpeed = readSpeed(obj, 1.0); + Map defaultActorProps = buildDefaultActorPropMap(actorConstraints); + Set validActorKeys = buildActorKeySet(actorConstraints); + if (obj.has("stages")) { + JsonArray stagesArr = obj.getAsJsonArray("stages"); + Map stageSpecs = new TreeMap<>(); + Map> stagePropsByNumber = new TreeMap<>(); + + for (int i = 0; i < stagesArr.size(); i++) { + JsonObject stageObj = stagesArr.get(i).getAsJsonObject(); + warnUnknownKeys(stageObj, fileId, "stage[" + i + "]", KNOWN_STAGE_KEYS); + Integer stageNumber = parseStageNumber(stageObj, fileId); + if (stageNumber != null && stageNumber > 0) { + if (stageSpecs.containsKey(stageNumber)) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: duplicate stage p{}", fileId, stageNumber); + } else { + boolean loop = readLoopFlag(stageObj); + Double cycleSeconds = readCycleSeconds(stageObj); + long lengthTicks = secondsToTicks(cycleSeconds == null ? 0.0 : cycleSeconds); + double cycleTicks = secondsToExactTicks(cycleSeconds == null ? 0.0 : cycleSeconds); + boolean allowJoin = readAllowJoin(stageObj); + double speed = readSpeed(stageObj, baseSpeed); + Integer useStage = readOptionalStageReference(stageObj, "use_stage", fileId); + double cycleMidpointOffsetSeconds = readCycleMidpointOffsetSeconds(stageObj, fileId, stageNumber, loop, cycleTicks); + if (useStage != null && useStage <= 0) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Invalid use_stage '{}' in {} stage p{}; falling back to stage itself.", useStage, fileId, stageNumber + ); + useStage = null; + } + + if (!loop && lengthTicks <= 0L) { + AnimationEngine.logSetupWarning("[NoN Animation] Stage p{} in {} is non-looping but has no cycle_seconds.", stageNumber, fileId); + } + + stageSpecs.put( + stageNumber, new AnimationDefinitions.StageSpec(loop, lengthTicks, cycleTicks, allowJoin, speed, useStage, cycleMidpointOffsetSeconds) + ); + stagePropsByNumber.put(stageNumber, resolveStageActorProps(stageObj, fileId, "p" + stageNumber, validActorKeys, defaultActorProps)); + } + } + } + + Map stageMap = new TreeMap<>(); + + for (Entry entry : stageSpecs.entrySet()) { + int stageNumber = entry.getKey(); + AnimationDefinitions.StageSpec spec = entry.getValue(); + Identifier stageId = AnimationIds.stageId(defId, stageNumber); + List stageProps = stagePropsByNumber.getOrDefault(stageNumber, List.of()); + Identifier playbackId = stageId; + if (spec.useStage != null && spec.useStage != stageNumber) { + if (stageSpecs.containsKey(spec.useStage)) { + playbackId = AnimationIds.stageId(defId, spec.useStage); + } else { + AnimationEngine.logSetupWarning( + "[NoN Animation] use_stage '{}' in {} stage p{} points to missing stage; falling back to p{}.", + spec.useStage, + fileId, + stageNumber, + stageNumber + ); + } + } + + stageMap.put( + stageNumber, + new AnimationStageInfo( + stageId, spec.loop, spec.lengthTicks, spec.allowJoin, spec.speed, playbackId, stageProps, spec.cycleMidpointOffsetSeconds, spec.cycleTicks + ) + ); + } + + stages.addAll(stageMap.values()); + } + + return List.copyOf(stages); + } + + private static Map buildDefaultActorPropMap(List actorConstraints) { + if (actorConstraints != null && !actorConstraints.isEmpty()) { + String[] actorKeys = buildUniqueActorKeys(actorConstraints); + Map defaults = new LinkedHashMap<>(); + int count = Math.min(actorConstraints.size(), actorKeys.length); + + for (int i = 0; i < count; i++) { + AnimationDefinitions.ActorConstraint constraint = actorConstraints.get(i); + Identifier left = constraint.propLeftItemId(); + Identifier right = constraint.propRightItemId(); + Identifier floor = constraint.propFloorItemId(); + if (left != null || right != null || floor != null) { + String actorKey = actorKeys[i]; + defaults.put(actorKey, new AnimationStageInfo.ActorPropEntry(actorKey, left, right, floor)); + } + } + + return defaults.isEmpty() ? Map.of() : Map.copyOf(defaults); + } else { + return Map.of(); + } + } + + private static Set buildActorKeySet(List actorConstraints) { + if (actorConstraints != null && !actorConstraints.isEmpty()) { + String[] actorKeys = buildUniqueActorKeys(actorConstraints); + LinkedHashSet out = new LinkedHashSet<>(); + Collections.addAll(out, actorKeys); + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } else { + return Set.of(); + } + } + + private static List normalizeTagList(Set tags) { + if (tags != null && !tags.isEmpty()) { + List out = new ArrayList<>(); + Set seen = new LinkedHashSet<>(); + + for (String raw : tags) { + if (raw != null) { + String value = raw.trim().toLowerCase(Locale.ROOT); + if (!value.isEmpty() && seen.add(value)) { + out.add(value); + } + } + } + + return out.isEmpty() ? List.of() : List.copyOf(out); + } else { + return List.of(); + } + } + + private static List resolveStageActorProps( + JsonObject stageObj, Identifier fileId, String stageLabel, Set validActorKeys, Map defaultActorProps + ) { + if (stageObj == null) { + return List.of(); + } + + Map effective = new LinkedHashMap<>(); + if (defaultActorProps != null && !defaultActorProps.isEmpty()) { + effective.putAll(defaultActorProps); + } + + if (stageObj.has("props")) { + JsonElement propsEl = stageObj.get("props"); + if (propsEl == null || propsEl.isJsonNull()) { + effective.clear(); + } else if (!propsEl.isJsonObject()) { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid props in {} {}: expected object.", fileId, stageLabel); + } else { + JsonObject propsObj = propsEl.getAsJsonObject(); + + for (Entry entry : propsObj.entrySet()) { + String actorKey = entry.getKey() == null ? "" : entry.getKey().trim(); + if (actorKey.isBlank()) { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid props actor key in {} {}: blank key.", fileId, stageLabel); + } else if (validActorKeys != null && !validActorKeys.isEmpty() && !validActorKeys.contains(actorKey)) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Unknown props actor key '{}' in {} {}. Valid keys: {}", actorKey, fileId, stageLabel, validActorKeys + ); + } else { + JsonElement actorPropsEl = entry.getValue(); + if (actorPropsEl == null || actorPropsEl.isJsonNull()) { + effective.remove(actorKey); + } else if (!actorPropsEl.isJsonObject()) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Invalid props value for actor '{}' in {} {}: expected object.", actorKey, fileId, stageLabel + ); + } else { + JsonObject actorProps = actorPropsEl.getAsJsonObject(); + warnUnknownKeys(actorProps, fileId, stageLabel + ".props." + actorKey, KNOWN_STAGE_ACTOR_PROP_KEYS); + AnimationStageInfo.ActorPropEntry current = effective.get(actorKey); + Identifier left = current == null ? null : current.propLeftItemId(); + Identifier right = current == null ? null : current.propRightItemId(); + Identifier floor = current == null ? null : current.propFloorItemId(); + boolean touched = false; + if (actorProps.has("prop_left")) { + touched = true; + left = parseStagePropItemId(actorProps.get("prop_left"), fileId, stageLabel, actorKey, "prop_left", left); + } + + if (actorProps.has("prop_right")) { + touched = true; + right = parseStagePropItemId(actorProps.get("prop_right"), fileId, stageLabel, actorKey, "prop_right", right); + } + + if (actorProps.has("prop_floor")) { + touched = true; + floor = parseStagePropItemId(actorProps.get("prop_floor"), fileId, stageLabel, actorKey, "prop_floor", floor); + } + + if (touched) { + if (left == null && right == null && floor == null) { + effective.remove(actorKey); + } else { + effective.put(actorKey, new AnimationStageInfo.ActorPropEntry(actorKey, left, right, floor)); + } + } + } + } + } + } + } + + if (effective.isEmpty()) { + return List.of(); + } + + List out = new ArrayList<>(effective.values()); + out.sort(Comparator.comparing(AnimationStageInfo.ActorPropEntry::actorKey)); + return List.copyOf(out); + } + + @Nullable + private static Identifier parseStagePropItemId( + JsonElement element, Identifier fileId, String stageLabel, String actorKey, String field, @Nullable Identifier fallback + ) { + if (element == null || element.isJsonNull()) { + return null; + } + + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String raw = element.getAsString(); + if (raw != null && !raw.isBlank()) { + Identifier id = Identifier.tryParse(raw.trim()); + if (id != null && Registries.ITEM.containsId(id)) { + return id; + } + + AnimationEngine.logSetupWarning("[NoN Animation] Invalid item id '{}' for {} actor '{}' in {} {}.", raw, field, actorKey, fileId, stageLabel); + return fallback; + } else { + return null; + } + } else { + AnimationEngine.logSetupWarning( + "[NoN Animation] Invalid {} for actor '{}' in {} {}: expected item id string or null.", field, actorKey, fileId, stageLabel + ); + return fallback; + } + } + + private static Integer parseStageNumber(JsonObject obj, Identifier fileId) { + if (!obj.has("stage")) { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping stage in {}: missing 'stage' number", fileId); + return null; + } + + JsonElement el = obj.get("stage"); + if (el != null && el.isJsonPrimitive()) { + JsonPrimitive prim = el.getAsJsonPrimitive(); + if (prim.isNumber()) { + return prim.getAsInt(); + } + + AnimationEngine.logSetupWarning("[NoN Animation] Skipping {}: 'stage' must be a number", fileId); + return null; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Skipping stage in {}: invalid 'stage' value", fileId); + return null; + } + } + + private static Integer readOptionalStageReference(JsonObject obj, String key, Identifier fileId) { + if (!obj.has(key)) { + return null; + } + + JsonElement el = obj.get(key); + if (el != null && el.isJsonPrimitive()) { + JsonPrimitive prim = el.getAsJsonPrimitive(); + if (prim.isNumber()) { + return prim.getAsInt(); + } + + AnimationEngine.logSetupWarning("[NoN Animation] Invalid {} in {}: value must be a number", key, fileId); + return null; + } else { + AnimationEngine.logSetupWarning("[NoN Animation] Invalid {} in {}: value is not a primitive", key, fileId); + return null; + } + } + + private static boolean readLoopFlag(JsonObject obj) { + if (!obj.has("loop")) { + return true; + } else { + JsonElement el = obj.get("loop"); + if (el != null && el.isJsonPrimitive()) { + JsonPrimitive prim = el.getAsJsonPrimitive(); + return prim.isBoolean() ? prim.getAsBoolean() : true; + } else { + return true; + } + } + } + + private static boolean readAllowJoin(JsonObject obj) { + if (!obj.has("allow_join")) { + return true; + } else { + JsonElement el = obj.get("allow_join"); + if (el != null && el.isJsonPrimitive()) { + JsonPrimitive prim = el.getAsJsonPrimitive(); + return prim.isBoolean() ? prim.getAsBoolean() : true; + } else { + return true; + } + } + } + + private static Double readCycleSeconds(JsonObject obj) { + Double seconds = readDouble(obj, "cycle_seconds"); + return seconds == null ? null : seconds; + } + + private static Double readDouble(JsonObject obj, String key) { + return !obj.has(key) ? null : readFiniteDouble(obj.get(key)); + } + + private static double readSpeed(JsonObject obj, double fallback) { + Double value = readDouble(obj, "speed"); + return value != null && Double.isFinite(value) && !(value <= 0.0) ? value : fallback; + } + + private static double readCycleMidpointOffsetSeconds(JsonObject obj, Identifier fileId, int stageNumber, boolean loop, double cycleTicks) { + if (!obj.has("cycle_midpoint_offset_seconds")) { + return 0.0; + } + + Double value = readDouble(obj, "cycle_midpoint_offset_seconds"); + if (value == null || Math.abs(value) < 1.0E-6) { + return 0.0; + } + + if (!loop) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Ignoring cycle_midpoint_offset_seconds in {} stage p{}: timing warp only applies to looping stages.", fileId, stageNumber + ); + return 0.0; + } + + if (cycleTicks <= 0.0) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Ignoring cycle_midpoint_offset_seconds in {} stage p{}: stage has no positive cycle_seconds.", fileId, stageNumber + ); + return 0.0; + } + + double clamped = StageTimeWarp.clampOffsetSeconds(cycleTicks, value); + if (Math.abs(clamped - value) > 1.0E-6) { + AnimationEngine.logSetupWarning( + "[NoN Animation] Clamped cycle_midpoint_offset_seconds in {} stage p{} from {} to {}.", fileId, stageNumber, value, clamped + ); + } + + return clamped; + } + + private static long secondsToTicks(double lengthSeconds) { + return lengthSeconds <= 0.0 ? 0L : Math.max(1L, (long)Math.ceil(lengthSeconds * 20.0)); + } + + private static double secondsToExactTicks(double lengthSeconds) { + return Double.isFinite(lengthSeconds) && !(lengthSeconds <= 0.0) ? lengthSeconds * 20.0 : 0.0; + } + + private static Integer readOptionalInt(JsonElement element) { + Double value = readFiniteDouble(element); + if (value == null) { + return null; + } else if (value < -2.1474836E9F || value > 2.147483647E9) { + return null; + } else { + return value != Math.rint(value) ? null : value.intValue(); + } + } + + private static Double readFiniteDouble(JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isNumber()) { + double value = primitive.getAsDouble(); + return Double.isFinite(value) ? value : null; + } else { + return null; + } + } else { + return null; + } + } + + private static Identifier definitionIdFromFile(Identifier fileId) { + String path = fileId.getPath(); + String prefix = "afw_animdefs/"; + if (path.startsWith(prefix)) { + path = path.substring(prefix.length()); + } + + if (path.endsWith(".json")) { + path = path.substring(0, path.length() - 5); + } + + return Identifier.of(fileId.getNamespace(), path); + } + + public enum ActorActivity { + ACTIVE, + PASSIVE; + } + + public record ActorConstraint( + Set entityTypes, + @Nullable String entityVariant, + Set requiredTags, + Set requiredTagsAny, + String label, + AnimationDefinitions.AgeRequirement ageRequirement, + AnimationDefinitions.ActorActivity activity, + @Nullable Identifier propLeftItemId, + @Nullable Identifier propRightItemId, + @Nullable Identifier propFloorItemId + ) { + public boolean matches(Entity e) { + return this.matches(AnimationDefinitions.MatchActor.from(e)); + } + + private boolean matches(AnimationDefinitions.MatchActor actor) { + if (!this.entityTypes.isEmpty() && !this.entityTypes.contains(actor.entityType())) { + return false; + } else if (this.entityVariant != null && !this.entityVariant.equals(actor.variant())) { + return false; + } else { + Set tags = actor.tags(); + if (!this.requiredTags.isEmpty() && !tags.containsAll(this.requiredTags)) { + return false; + } else { + return !this.requiredTagsAny.isEmpty() && this.requiredTagsAny.stream().noneMatch(tags::contains) ? false : this.matchesAge(actor.baby()); + } + } + } + + public int assignmentPriority() { + return this.entityTypes.size() + this.requiredTags.size() + this.requiredTagsAny.size() + (this.entityVariant == null ? 0 : 1); + } + + public String stableKey() { + List types = this.entityTypes.stream().map(Identifier::toString).sorted().toList(); + List tagsAll = this.requiredTags.stream().sorted().toList(); + List tagsAny = this.requiredTagsAny.stream().sorted().toList(); + return "types=" + + types + + "|variant=" + + this.entityVariant + + "|tagsAll=" + + tagsAll + + "|tagsAny=" + + tagsAny + + "|age=" + + this.ageRequirement + + "|activity=" + + this.activity; + } + + public String derivedKey() { + if (this.label != null && !this.label.isBlank()) { + return AnimationDefinitions.sanitizeKeyPart(this.label); + } + + String base = "actor"; + if (!this.entityTypes.isEmpty()) { + if (this.entityTypes.size() == 1) { + base = this.entityTypes.iterator().next().getPath(); + } else { + List paths = this.entityTypes.stream().map(Identifier::getPath).sorted().toList(); + base = String.join("_", paths); + } + } + + if (!this.requiredTags.isEmpty()) { + List tags = this.requiredTags.stream().sorted().map(AnimationDefinitions::sanitizeKeyPart).toList(); + base = base + "_" + String.join("_", tags); + } + + if (!this.requiredTagsAny.isEmpty()) { + List tagsAny = this.requiredTagsAny.stream().sorted().map(AnimationDefinitions::sanitizeKeyPart).toList(); + base = base + "_any_" + String.join("_", tagsAny); + } + + if (this.entityVariant != null && !this.entityVariant.isBlank()) { + base = base + "_" + this.entityVariant; + } + + if (this.ageRequirement == AnimationDefinitions.AgeRequirement.BABY) { + base = base + "_baby"; + } + + return AnimationDefinitions.sanitizeKeyPart(base); + } + + private boolean matchesAge(Entity e) { + boolean baby = false; + if (e instanceof LivingEntity living) { + baby = living.isBaby(); + } + + return this.matchesAge(baby); + } + + private boolean matchesAge(boolean baby) { + AnimationDefinitions.AgeRequirement req = this.ageRequirement == null ? AnimationDefinitions.AgeRequirement.ADULT : this.ageRequirement; + return req == AnimationDefinitions.AgeRequirement.BABY == baby; + } + } + + private record ActorKey(Identifier entityType, boolean baby) { + } + + public enum AgeRequirement { + ADULT, + BABY; + } + + public record AnimationPackInfo( + String id, + String resourcePackId, + String name, + String author, + String version, + String description, + @Nullable Identifier localModelRoot, + List modelImports + ) { + public AnimationPackInfo { + id = AnimationDefinitions.sanitizeNonBlank(id, "unknown"); + resourcePackId = AnimationDefinitions.sanitizeNonBlank(resourcePackId, id); + name = AnimationDefinitions.sanitizeNonBlank(name, AnimationDefinitions.displayNameFromPackId(id)); + author = AnimationDefinitions.sanitizeNonBlank(author, "Unknown"); + version = AnimationDefinitions.sanitizeNullable(version); + description = AnimationDefinitions.sanitizeNullable(description); + modelImports = modelImports == null ? List.of() : List.copyOf(modelImports); + } + + public AnimationPackInfo( + String id, String resourcePackId, String name, String author, String version, String description, @Nullable Identifier localModelRoot + ) { + this(id, resourcePackId, name, author, version, description, localModelRoot, List.of()); + } + + public List modelRootsFor(@Nullable Identifier entityTypeId) { + LinkedHashSet roots = new LinkedHashSet<>(); + if (this.localModelRoot != null) { + roots.add(this.localModelRoot); + } + + if (entityTypeId != null) { + for (AnimationDefinitions.ModelImport modelImport : this.modelImports) { + if (modelImport.entityTypes().contains(entityTypeId)) { + roots.add(modelImport.modelRoot()); + if (roots.size() >= 32) { + break; + } + } + } + } + + return roots.isEmpty() ? List.of() : List.copyOf(roots); + } + } + + public record BlockPredicate(Set blockIds, Set tagIds) { + public boolean isEmpty() { + return this.blockIds.isEmpty() && this.tagIds.isEmpty(); + } + } + + public record BlockRequirements( + String type, + AnimationDefinitions.WallHeight height, + AnimationDefinitions.WallWidth width, + @Nullable AnimationDefinitions.WallFacing facing, + AnimationDefinitions.Clearance clearance, + AnimationDefinitions.CenterSupport support, + AnimationDefinitions.CenterPlacement placement, + int surfaceRadius, + AnimationDefinitions.SurfaceFootprint surfaceFootprint, + @Nullable AnimationDefinitions.BlockPredicate blocks + ) { + public boolean isWall() { + return "wall".equalsIgnoreCase(this.type); + } + + public boolean isCenterSupport() { + return "center_support".equalsIgnoreCase(this.type); + } + } + + public enum CenterPlacement { + DIRECTIONAL_CLEARANCE("directional_clearance"), + SURFACE_FOOTPRINT("surface_footprint"); + + private final String id; + + CenterPlacement(String id) { + this.id = id; + } + + public String id() { + return this.id; + } + } + + public enum CenterSupport { + FULL_BLOCK("full_block"), + SLAB("slab"), + HALF_HEIGHT("half_height"), + SURFACE("surface"); + + private final String id; + + CenterSupport(String id) { + this.id = id; + } + + public String id() { + return this.id; + } + } + + public record Clearance(int width, int height, int depth) { + } + + public record Definition( + Identifier id, + List actors, + Set requiredUnionTags, + Set animationTags, + List contentTags, + List stages, + double weight, + AnimationDefinitions.BlockRequirements blockRequirements, + AnimationDefinitions.WaterRequirement waterRequirement, + @Nullable String positionAnchorActor, + AnimationDefinitions.AnimationPackInfo packInfo, + @Nullable String displayName, + Set joinAfter, + AnimationGameplayMetadata.Data gameplayMetadata + ) { + public Definition { + joinAfter = joinAfter == null ? Set.of() : Set.copyOf(joinAfter); + gameplayMetadata = gameplayMetadata == null ? AnimationGameplayMetadata.Data.empty() : gameplayMetadata; + } + + public Definition( + Identifier id, + List actors, + Set requiredUnionTags, + Set animationTags, + List contentTags, + List stages, + double weight, + AnimationDefinitions.BlockRequirements blockRequirements, + AnimationDefinitions.WaterRequirement waterRequirement, + @Nullable String positionAnchorActor, + AnimationDefinitions.AnimationPackInfo packInfo, + @Nullable String displayName, + Set joinAfter + ) { + this( + id, + actors, + requiredUnionTags, + animationTags, + contentTags, + stages, + weight, + blockRequirements, + waterRequirement, + positionAnchorActor, + packInfo, + displayName, + joinAfter, + AnimationGameplayMetadata.Data.empty() + ); + } + + public Definition( + Identifier id, + List actors, + Set requiredUnionTags, + Set animationTags, + List contentTags, + List stages, + double weight, + AnimationDefinitions.BlockRequirements blockRequirements, + AnimationDefinitions.WaterRequirement waterRequirement, + @Nullable String positionAnchorActor, + AnimationDefinitions.AnimationPackInfo packInfo, + @Nullable String displayName + ) { + this( + id, + actors, + requiredUnionTags, + animationTags, + contentTags, + stages, + weight, + blockRequirements, + waterRequirement, + positionAnchorActor, + packInfo, + displayName, + Set.of() + ); + } + + public Definition( + Identifier id, + List actors, + Set requiredUnionTags, + Set animationTags, + List contentTags, + List stages, + double weight, + AnimationDefinitions.BlockRequirements blockRequirements, + AnimationDefinitions.WaterRequirement waterRequirement, + @Nullable String positionAnchorActor, + AnimationDefinitions.AnimationPackInfo packInfo + ) { + this( + id, actors, requiredUnionTags, animationTags, contentTags, stages, weight, blockRequirements, waterRequirement, positionAnchorActor, packInfo, null + ); + } + + public boolean matches(List selectedActorsSortedById, Set requiredAnimationTags) { + return this.matches(AnimationDefinitions.MatchContext.create(selectedActorsSortedById), requiredAnimationTags); + } + + private boolean matches(AnimationDefinitions.MatchContext context, Set requiredAnimationTags) { + return !this.matchesAnimationTags(requiredAnimationTags) ? false : this.matchesActors(context); + } + + private boolean matchesActors(AnimationDefinitions.MatchContext context) { + if (context.actors().size() != this.actors.size()) { + return false; + } else { + return !context.unionTags().containsAll(this.requiredUnionTags) + ? false + : AnimationDefinitions.matchesUnordered(context.actors(), this.actors, AnimationDefinitions.MATCH_ORDER_BY_DEFINITION.get(this.id)); + } + } + + private boolean matchesAnimationTags(Set requiredAnimationTags) { + Set required = requiredAnimationTags == null ? Set.of() : requiredAnimationTags; + if (!required.isEmpty()) { + return this.animationTags != null && !this.animationTags.isEmpty() ? this.animationTags.containsAll(required) : false; + } else { + return this.animationTags == null || this.animationTags.isEmpty() || this.animationTags.contains("normal_match"); + } + } + + public AnimationDefinitions.Definition withGameplayMetadata(AnimationGameplayMetadata.Data metadata) { + return new AnimationDefinitions.Definition( + this.id, + this.actors, + this.requiredUnionTags, + this.animationTags, + this.contentTags, + this.stages, + this.weight, + this.blockRequirements, + this.waterRequirement, + this.positionAnchorActor, + this.packInfo, + this.displayName, + this.joinAfter, + metadata + ); + } + + public boolean attackEligible() { + return this.gameplayMetadata.attackEligible(); + } + } + + private record MatchActor(Entity entity, Identifier entityType, @Nullable String variant, Set tags, boolean baby) { + private static AnimationDefinitions.MatchActor from(Entity entity) { + boolean baby = entity instanceof LivingEntity living && living.isBaby(); + return new AnimationDefinitions.MatchActor( + entity, Registries.ENTITY_TYPE.getId(entity.getType()), EntityVariants.resolveVariant(entity), Set.copyOf(NonActorEvents.resolve(entity)), baby + ); + } + } + + private record MatchContext(List actors, Set unionTags) { + private static AnimationDefinitions.MatchContext create(List entities) { + if (entities != null && !entities.isEmpty()) { + List actors = new ArrayList<>(entities.size()); + Set unionTags = new LinkedHashSet<>(); + + for (Entity entity : entities) { + if (entity != null) { + AnimationDefinitions.MatchActor actor = AnimationDefinitions.MatchActor.from(entity); + actors.add(actor); + unionTags.addAll(actor.tags()); + } + } + + return new AnimationDefinitions.MatchContext(List.copyOf(actors), Set.copyOf(unionTags)); + } else { + return new AnimationDefinitions.MatchContext(List.of(), Set.of()); + } + } + } + + public record MatchResult(List candidatesSorted, AnimationDefinitions.Definition chosen) { + } + + public record ModelImport(String packId, Identifier modelRoot, Set entityTypes) { + public ModelImport { + packId = AnimationDefinitions.sanitizeNonBlank(packId, "unknown"); + entityTypes = entityTypes == null ? Set.of() : Set.copyOf(entityTypes); + } + } + + public static final class Reloader implements SynchronousResourceReloader { + static final Identifier RELOADER_ID = AnimationEngine.runtimeId("definitions"); + + public void reload(ResourceManager manager) { + AnimationDefinitions.reload(manager); + } + } + + private record StageSpec( + boolean loop, long lengthTicks, double cycleTicks, boolean allowJoin, double speed, Integer useStage, double cycleMidpointOffsetSeconds + ) { + } + + public record SurfaceFootprint(int width, int depth, int height, int margin) { + } + + public enum WallFacing { + FRONT("front"), + BACK("back"), + LEFT("left"), + RIGHT("right"); + + private final String id; + + WallFacing(String id) { + this.id = id; + } + + public String id() { + return this.id; + } + + public boolean matches(Direction blockFacing, Direction wallSurfaceFacing) { + if (blockFacing != null && wallSurfaceFacing != null && blockFacing.getAxis() != Axis.Y && wallSurfaceFacing.getAxis() != Axis.Y) { + Direction requiredSurface = switch (this) { + case FRONT -> blockFacing; + case BACK -> blockFacing.getOpposite(); + case LEFT -> blockFacing.rotateYCounterclockwise(); + case RIGHT -> blockFacing.rotateYClockwise(); + }; + return requiredSurface == wallSurfaceFacing; + } else { + return false; + } + } + } + + public record WallHeight(int min, Integer max) { + } + + public record WallWidth(int min, Integer max) { + } + + public enum WaterRequirement { + NONE, + SURFACE, + UNDERWATER; + } +} diff --git a/src/main/java/com/nonid/internal/animation/data/AnimationGameplayMetadata.java b/src/main/java/com/nonid/internal/animation/data/AnimationGameplayMetadata.java new file mode 100644 index 0000000..5c0ef3c --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/data/AnimationGameplayMetadata.java @@ -0,0 +1,484 @@ +package com.nonid.internal.animation.data; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.nonid.NeedsOfNature; +import com.nonid.data.NonLiquidRoles; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.util.AnimationIds; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationGameplayMetadata { + public static final int INFINITE_STAGE_SECONDS = -1; + private static final double MIN_STAGE_DURATION_MULTIPLIER = 0.05; + private static final double MAX_STAGE_DURATION_MULTIPLIER = 20.0; + + private AnimationGameplayMetadata() { + } + + public static AnimationGameplayMetadata.Data parse(Identifier fileId, AnimationDefinitions.Definition definition, JsonObject root, List actorKeys) { + AnimationGameplayMetadata.Builder builder = new AnimationGameplayMetadata.Builder(); + builder.accept(fileId, definition, root, actorKeys); + return builder.build(); + } + + @Nullable + public static Integer peakStage(Identifier animationId) { + AnimationGameplayMetadata.Data data = data(animationId); + return data == null ? null : data.peakStage(); + } + + public static Map peakStages() { + Map out = new LinkedHashMap<>(); + + for (AnimationDefinitions.Definition definition : AnimationDefinitions.getLoadedDefinitionsSnapshot()) { + Integer peakStage = definition.gameplayMetadata().peakStage(); + if (peakStage != null) { + out.put(definition.id(), peakStage); + } + } + + return Map.copyOf(out); + } + + @Nullable + public static Integer stageSeconds(Identifier animationId) { + AnimationGameplayMetadata.Data data = data(animationId); + return data != null && animationId != null ? data.stageSeconds().get(animationId) : null; + } + + public static boolean isInfiniteStageSeconds(@Nullable Integer seconds) { + return seconds != null && seconds < 0; + } + + public static double cycleTicks(Identifier animationId) { + if (animationId == null) { + return 0.0; + } + + AnimationGameplayMetadata.Data data = data(animationId); + Double value = data == null ? null : data.cycleTicks().get(animationId); + return value != null && Double.isFinite(value) && !(value <= 0.0) ? value : 0.0; + } + + public static double stageDurationMultiplier(Identifier animationId) { + if (animationId == null) { + return 1.0; + } + + AnimationGameplayMetadata.Data data = data(animationId); + Double value = data == null ? null : data.stageDurationMultipliers().get(animationId); + return value != null && Double.isFinite(value) && !(value <= 0.0) ? value : 1.0; + } + + public static boolean escapable(Identifier animationId) { + if (animationId == null) { + return true; + } + + AnimationGameplayMetadata.Data data = data(animationId); + if (data == null) { + return true; + } + + Boolean direct = data.escapable().get(animationId); + if (direct != null) { + return direct; + } + + Boolean base = data.escapable().get(AnimationIds.baseId(animationId)); + return base == null || base; + } + + @Nullable + public static NonLiquidRoles.LiquidRoles liquidRoles(Identifier animationId) { + AnimationGameplayMetadata.Data data = data(animationId); + return data == null ? null : data.liquidRoles(); + } + + @Nullable + public static Double liquidGainMultiplier(Identifier animationId) { + AnimationGameplayMetadata.Data data = data(animationId); + return data == null ? null : data.liquidGainMultiplier(); + } + + @Nullable + public static AnimationGameplayMetadata.ManualPeakRule manualPeakRule(Identifier animationId) { + AnimationGameplayMetadata.Data data = data(animationId); + return data == null ? null : data.manualPeakRule(); + } + + @Nullable + private static AnimationGameplayMetadata.Data data(@Nullable Identifier animationId) { + Identifier baseId = AnimationIds.baseId(animationId); + AnimationDefinitions.Definition definition = AnimationDefinitions.getDefinition(baseId); + return definition == null ? null : definition.gameplayMetadata(); + } + + private static int clampStageSeconds(int value) { + return value < 0 ? -1 : Math.max(1, Math.min(300, value)); + } + + @Nullable + private static Integer readInteger(JsonElement element) { + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) { + double value = element.getAsDouble(); + return Double.isFinite(value) && value == Math.rint(value) && !(value < -2.1474836E9F) && !(value > 2.147483647E9) ? (int)value : null; + } else { + return null; + } + } + + @Nullable + private static Double readDouble(JsonElement element) { + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) { + double value = element.getAsDouble(); + return Double.isFinite(value) ? value : null; + } else { + return null; + } + } + + @Nullable + static Boolean readBoolean(JsonElement element) { + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean() ? element.getAsBoolean() : null; + } + + private static final class Builder { + private Identifier definitionId; + private final Map peakStages = new HashMap<>(); + private final Map stageSeconds = new HashMap<>(); + private final Map cycleTicks = new HashMap<>(); + private final Map stageDurationMultipliers = new HashMap<>(); + private final Map escapable = new HashMap<>(); + private final Map liquidRoles = new HashMap<>(); + private final Map liquidGainMultipliers = new HashMap<>(); + private final Map manualPeakRules = new HashMap<>(); + private boolean attackEligible = true; + + private void accept(Identifier fileId, AnimationDefinitions.Definition definition, JsonObject root, List actorKeys) { + if (definition != null && root != null) { + this.definitionId = definition.id(); + this.readAttackEligibility(fileId, root); + + try { + this.readStageMetadata(fileId, definition.id(), root); + } catch (RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN] Failed to read stage gameplay metadata from {}", fileId, e); + } + + try { + this.readLiquidRoles(fileId, definition, root, actorKeys); + } catch (RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN] Failed to read liquid roles from {}", fileId, e); + } + + try { + this.readManualPeak(fileId, definition.id(), root); + } catch (RuntimeException e) { + AnimationEngine.LOGGER.debug("[NoN] Failed to read manual peak metadata from {}", fileId, e); + } + } + } + + private void readAttackEligibility(Identifier fileId, JsonObject root) { + if (root.has("attack_eligible")) { + Boolean parsed = AnimationGameplayMetadata.readBoolean(root.get("attack_eligible")); + if (parsed == null) { + AnimationEngine.logSetupWarning("[NoN] Invalid attack_eligible in {}. Expected true or false; using default true.", fileId); + } else { + this.attackEligible = parsed; + } + } + } + + private void readStageMetadata(Identifier fileId, Identifier definitionId, JsonObject root) { + Integer baseSeconds = AnimationGameplayMetadata.readInteger(root.get("stage_seconds")); + if (baseSeconds != null) { + this.stageSeconds.put(definitionId, AnimationGameplayMetadata.clampStageSeconds(baseSeconds)); + } + + Boolean defaultEscapable = AnimationGameplayMetadata.readBoolean(root.get("escapable")); + boolean defaultEscapableValue = defaultEscapable == null || defaultEscapable; + this.escapable.put(definitionId, defaultEscapableValue); + Double liquidMultiplier = AnimationGameplayMetadata.readDouble(root.get("liquid_gain_multiplier")); + if (liquidMultiplier != null && liquidMultiplier >= 0.0) { + this.liquidGainMultipliers.put(definitionId, liquidMultiplier); + } + + if (root.has("stages") && root.get("stages").isJsonArray()) { + JsonArray stages = root.getAsJsonArray("stages"); + Integer firstPeak = null; + + for (int i = 0; i < stages.size(); i++) { + if (stages.get(i).isJsonObject()) { + JsonObject stage = stages.get(i).getAsJsonObject(); + Integer stageNumber = AnimationGameplayMetadata.readInteger(stage.get("stage")); + if (stageNumber != null && stageNumber > 0) { + Identifier stageId = AnimationIds.stageId(definitionId, stageNumber); + if (Boolean.TRUE.equals(AnimationGameplayMetadata.readBoolean(stage.get("non_peak"))) && (firstPeak == null || stageNumber < firstPeak)) { + firstPeak = stageNumber; + } + + Integer seconds = AnimationGameplayMetadata.readInteger(stage.get("stage_seconds")); + if (seconds != null) { + this.stageSeconds.put(stageId, AnimationGameplayMetadata.clampStageSeconds(seconds)); + } + + Double secondsPerCycle = AnimationGameplayMetadata.readDouble(stage.get("cycle_seconds")); + if (secondsPerCycle != null && secondsPerCycle > 0.0) { + this.cycleTicks.put(stageId, secondsPerCycle * 20.0); + } + + Boolean stageEscapable = AnimationGameplayMetadata.readBoolean(stage.get("escapable")); + this.escapable.put(stageId, stageEscapable == null ? defaultEscapableValue : stageEscapable); + if (stage.has("stage_duration_multiplier")) { + Double multiplier = AnimationGameplayMetadata.readDouble(stage.get("stage_duration_multiplier")); + Boolean loop = AnimationGameplayMetadata.readBoolean(stage.get("loop")); + if (Boolean.FALSE.equals(loop)) { + AnimationEngine.logSetupWarning( + "[NoN] Ignoring stage_duration_multiplier in {} stage {} because the stage is not looping.", definitionId, stageNumber + ); + } else if (multiplier != null && !(multiplier < 0.05) && !(multiplier > 20.0)) { + this.stageDurationMultipliers.put(stageId, multiplier); + } else { + AnimationEngine.logSetupWarning( + "[NoN] Ignoring invalid stage_duration_multiplier in {} stage {}. Expected a number from {} to {}.", + definitionId, + stageNumber, + 0.05, + 20.0 + ); + } + } + } + } + } + + if (firstPeak != null) { + this.peakStages.put(definitionId, firstPeak); + } + } + } + + private void readLiquidRoles(Identifier fileId, AnimationDefinitions.Definition definition, JsonObject root, List actorKeys) { + if (root.has("actors") && root.get("actors").isJsonArray()) { + JsonArray actors = root.getAsJsonArray("actors"); + if (!actors.isEmpty() && actorKeys != null && actorKeys.size() == actors.size()) { + Set injectorKeys = new LinkedHashSet<>(); + Set receiverKeys = new LinkedHashSet<>(); + Map injectorRoles = new LinkedHashMap<>(); + boolean explicitReceiver = false; + boolean[] injectors = new boolean[actors.size()]; + + for (int i = 0; i < actors.size(); i++) { + if (actors.get(i).isJsonObject()) { + JsonObject actor = actors.get(i).getAsJsonObject(); + String actorKey = actorKeys.get(i); + NonLiquidRoles.InjectorRole role = this.readInjectorRole(fileId, actor, i); + injectors[i] = role != NonLiquidRoles.InjectorRole.NONE; + if (role == NonLiquidRoles.InjectorRole.V || role == NonLiquidRoles.InjectorRole.A) { + injectorKeys.add(actorKey); + } + + if (injectors[i]) { + injectorRoles.put(actorKey, role); + } + + if (Boolean.TRUE.equals(AnimationGameplayMetadata.readBoolean(actor.get("receiver")))) { + receiverKeys.add(actorKey); + explicitReceiver = true; + } + } + } + + if (!explicitReceiver) { + for (int i = 0; i < actors.size(); i++) { + if (!injectors[i]) { + receiverKeys.add(actorKeys.get(i)); + } + } + } + + if (!explicitReceiver && receiverKeys.isEmpty()) { + for (int i = 0; i < definition.actors().size(); i++) { + if (definition.actors().get(i).entityTypes().contains(Identifier.of("minecraft", "player"))) { + receiverKeys.add(actorKeys.get(i)); + } + } + } + + this.liquidRoles + .put(definition.id(), new NonLiquidRoles.LiquidRoles(Set.copyOf(injectorKeys), Set.copyOf(receiverKeys), Map.copyOf(injectorRoles))); + } + } + } + + private NonLiquidRoles.InjectorRole readInjectorRole(Identifier fileId, JsonObject actor, int actorIndex) { + JsonElement element = actor.get("injector"); + if (element == null || element.isJsonNull()) { + return NonLiquidRoles.InjectorRole.NONE; + } + + if (!element.isJsonPrimitive()) { + this.warnInvalidInjector(fileId, actorIndex, "value must be true/false or V/A/M"); + return NonLiquidRoles.InjectorRole.NONE; + } + + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isBoolean()) { + return primitive.getAsBoolean() ? NonLiquidRoles.InjectorRole.V : NonLiquidRoles.InjectorRole.NONE; + } + + if (!primitive.isString()) { + this.warnInvalidInjector(fileId, actorIndex, "value must be true/false or V/A/M"); + return NonLiquidRoles.InjectorRole.NONE; + } + + String raw = primitive.getAsString(); + if (raw != null && !raw.isBlank()) { + return switch (raw.trim().toUpperCase(Locale.ROOT)) { + case "V" -> NonLiquidRoles.InjectorRole.V; + case "A" -> NonLiquidRoles.InjectorRole.A; + case "M" -> NonLiquidRoles.InjectorRole.M; + default -> { + this.warnInvalidInjector(fileId, actorIndex, "unknown injector role '" + raw + "'"); + yield NonLiquidRoles.InjectorRole.NONE; + } + }; + } else { + return NonLiquidRoles.InjectorRole.NONE; + } + } + + private void warnInvalidInjector(Identifier fileId, int actorIndex, String reason) { + AnimationEngine.logSetupWarning("[NoN] Ignoring invalid injector in {} actor {}: {}. Expected true/false or V/A/M.", fileId, actorIndex, reason); + } + + private void readManualPeak(Identifier fileId, Identifier definitionId, JsonObject root) { + if (root.has("manual_peak") && root.get("manual_peak").isJsonObject()) { + JsonObject manualPeak = root.getAsJsonObject("manual_peak"); + Set items = new LinkedHashSet<>(); + this.addHeldItem(fileId, items, manualPeak.get("held_item")); + if (manualPeak.has("held_items") && manualPeak.get("held_items").isJsonArray()) { + for (JsonElement heldItem : manualPeak.getAsJsonArray("held_items")) { + this.addHeldItem(fileId, items, heldItem); + } + } + + Integer priority = AnimationGameplayMetadata.readInteger(manualPeak.get("priority")); + boolean propFromHeldItem = Boolean.TRUE.equals(AnimationGameplayMetadata.readBoolean(manualPeak.get("prop_from_held_item"))); + AnimationStageInfo.PropSlot propSlot = this.readManualPeakPropSlot(fileId, manualPeak.get("held_item_prop_slot")); + if (propSlot == null) { + propFromHeldItem = false; + propSlot = AnimationStageInfo.PropSlot.RIGHT; + } + + this.manualPeakRules + .put(definitionId, new AnimationGameplayMetadata.ManualPeakRule(items, priority == null ? 0 : priority, propFromHeldItem, propSlot)); + } + } + + @Nullable + private AnimationStageInfo.PropSlot readManualPeakPropSlot(Identifier fileId, @Nullable JsonElement element) { + if (element == null || element.isJsonNull()) { + return AnimationStageInfo.PropSlot.RIGHT; + } + + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String value = element.getAsString().trim().toLowerCase(Locale.ROOT); + + return switch (value) { + case "left" -> AnimationStageInfo.PropSlot.LEFT; + case "right" -> AnimationStageInfo.PropSlot.RIGHT; + case "floor" -> AnimationStageInfo.PropSlot.FLOOR; + default -> { + AnimationEngine.logSetupWarning( + "[NoN] Invalid manual_peak held_item_prop_slot '{}' in {}. Expected left, right, or floor; dynamic held-item prop disabled.", + value, + fileId + ); + yield null; + } + }; + } else { + AnimationEngine.logSetupWarning( + "[NoN] Invalid manual_peak held_item_prop_slot in {}. Expected left, right, or floor; dynamic held-item prop disabled.", fileId + ); + return null; + } + } + + private void addHeldItem(Identifier fileId, Set items, JsonElement element) { + if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + String raw = element.getAsString(); + if (raw != null && !raw.isBlank()) { + Identifier itemId = Identifier.tryParse(raw.trim()); + if (itemId != null && Registries.ITEM.containsId(itemId)) { + items.add(itemId); + } else { + NeedsOfNature.logSetupWarning("[NoN] Ignoring invalid manual peak held item '{}' in {}.", raw, fileId); + } + } + } + } + + private AnimationGameplayMetadata.Data build() { + return this.definitionId == null + ? AnimationGameplayMetadata.Data.empty() + : new AnimationGameplayMetadata.Data( + this.peakStages.get(this.definitionId), + Map.copyOf(this.stageSeconds), + Map.copyOf(this.cycleTicks), + Map.copyOf(this.stageDurationMultipliers), + Map.copyOf(this.escapable), + this.liquidRoles.get(this.definitionId), + this.liquidGainMultipliers.get(this.definitionId), + this.manualPeakRules.get(this.definitionId), + this.attackEligible + ); + } + } + + public record Data( + @Nullable Integer peakStage, + Map stageSeconds, + Map cycleTicks, + Map stageDurationMultipliers, + Map escapable, + @Nullable NonLiquidRoles.LiquidRoles liquidRoles, + @Nullable Double liquidGainMultiplier, + @Nullable AnimationGameplayMetadata.ManualPeakRule manualPeakRule, + boolean attackEligible + ) { + public Data { + stageSeconds = stageSeconds == null ? Map.of() : Map.copyOf(stageSeconds); + cycleTicks = cycleTicks == null ? Map.of() : Map.copyOf(cycleTicks); + stageDurationMultipliers = stageDurationMultipliers == null ? Map.of() : Map.copyOf(stageDurationMultipliers); + escapable = escapable == null ? Map.of() : Map.copyOf(escapable); + } + + public static AnimationGameplayMetadata.Data empty() { + return new AnimationGameplayMetadata.Data(null, Map.of(), Map.of(), Map.of(), Map.of(), null, null, null, true); + } + } + + public record ManualPeakRule(Set heldItems, int priority, boolean propFromHeldItem, AnimationStageInfo.PropSlot heldItemPropSlot) { + public ManualPeakRule { + heldItems = heldItems == null ? Set.of() : Set.copyOf(heldItems); + heldItemPropSlot = heldItemPropSlot == null ? AnimationStageInfo.PropSlot.RIGHT : heldItemPropSlot; + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/data/AnimationSoundCueParser.java b/src/main/java/com/nonid/internal/animation/data/AnimationSoundCueParser.java new file mode 100644 index 0000000..3c9f373 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/data/AnimationSoundCueParser.java @@ -0,0 +1,191 @@ +package com.nonid.internal.animation.data; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationSoundCueParser { + private AnimationSoundCueParser() { + } + + public static AnimationSoundCueParser.ParsedAnimation parse(JsonObject animation, String sourceName, AnimationSoundCueParser.WarningSink warnings) { + if (animation == null) { + return new AnimationSoundCueParser.ParsedAnimation(-1.0, List.of()); + } + + double lengthSeconds = readAnimationLength(animation); + JsonElement soundElement = animation.get("sound_effects"); + if (soundElement == null) { + return new AnimationSoundCueParser.ParsedAnimation(lengthSeconds, List.of()); + } + + if (!soundElement.isJsonObject()) { + warnings.warn("[NoN Animation] Skipping {}: 'sound_effects' must be an object", sourceName); + return new AnimationSoundCueParser.ParsedAnimation(lengthSeconds, List.of()); + } + + List cues = new ArrayList<>(); + + for (Entry entry : soundElement.getAsJsonObject().entrySet()) { + String timeKey = entry.getKey(); + Double timeSeconds = parseTime(timeKey); + if (timeSeconds != null && !(timeSeconds < 0.0)) { + collectCues(entry.getValue(), timeSeconds, sourceName, warnings, cues); + } else { + warnings.warn("[NoN Animation] Skipping {}: invalid sound time '{}'", sourceName, timeKey); + } + } + + return new AnimationSoundCueParser.ParsedAnimation(lengthSeconds, List.copyOf(cues)); + } + + private static void collectCues( + JsonElement element, double timeSeconds, String sourceName, AnimationSoundCueParser.WarningSink warnings, List output + ) { + if (element != null && !element.isJsonNull() && element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + String effect = readString(object, "effect"); + if (effect != null && !effect.isBlank()) { + float volume = readFloat(object, "volume", 1.0F); + float pitch = readFloat(object, "pitch", 1.0F); + collectCue(effect, timeSeconds, sourceName, warnings, output, volume, pitch); + String sound = readString(object, "sound"); + if (sound != null && !sound.isBlank() && !sound.trim().equals(effect.trim())) { + collectCue(sound, timeSeconds, sourceName, warnings, output, volume, pitch); + } + } else { + warnMissingEffect(sourceName, timeSeconds, warnings); + } + } else { + warnings.warn("[NoN Animation] Skipping {}: sound cue at {} must be an object", sourceName, String.format(Locale.ROOT, "%.3f", timeSeconds)); + } + } + + private static void collectCue( + String effect, + double timeSeconds, + String sourceName, + AnimationSoundCueParser.WarningSink warnings, + List output, + float volume, + float pitch + ) { + String normalizedEffect = effect.trim(); + if (normalizedEffect.startsWith("random(")) { + List sounds = readInlineSoundPool(normalizedEffect, sourceName, timeSeconds, warnings); + if (sounds != null) { + output.add(new AnimationSoundCueParser.ParsedCue(timeSeconds, null, sounds, volume, pitch)); + } + } else { + output.add(new AnimationSoundCueParser.ParsedCue(timeSeconds, normalizedEffect, List.of(), volume, pitch)); + } + } + + private static void warnMissingEffect(String sourceName, double timeSeconds, AnimationSoundCueParser.WarningSink warnings) { + warnings.warn("[NoN Animation] Skipping {}: sound cue at {} missing 'effect'", sourceName, String.format(Locale.ROOT, "%.3f", timeSeconds)); + } + + @Nullable + private static List readInlineSoundPool(String effect, String sourceName, double timeSeconds, AnimationSoundCueParser.WarningSink warnings) { + String formattedTime = String.format(Locale.ROOT, "%.3f", timeSeconds); + if (!effect.endsWith(")")) { + warnings.warn("[NoN Animation] Skipping {}: malformed random sound pool at {}. Expected random(namespace:sound, ...)", sourceName, formattedTime); + return null; + } + + String contents = effect.substring("random(".length(), effect.length() - 1).trim(); + if (contents.isEmpty()) { + warnings.warn("[NoN Animation] Skipping {}: random sound pool at {} cannot be empty", sourceName, formattedTime); + return null; + } + + List sounds = new ArrayList<>(); + + for (String candidate : contents.split(",", -1)) { + String raw = candidate.trim(); + Identifier soundId = raw.indexOf(58) > 0 ? Identifier.tryParse(raw) : null; + if (soundId == null) { + warnings.warn("[NoN Animation] Skipping {}: invalid or unqualified sound id '{}' in random sound pool at {}", sourceName, raw, formattedTime); + return null; + } + + sounds.add(soundId); + } + + return List.copyOf(sounds); + } + + private static double readAnimationLength(JsonObject animation) { + Double animationLength = readFiniteDouble(animation.get("animation_length")); + if (animationLength != null) { + return animationLength; + } + + Double length = readFiniteDouble(animation.get("length")); + return length == null ? -1.0 : length; + } + + @Nullable + private static Double parseTime(String raw) { + if (raw != null && !raw.isBlank()) { + try { + double value = Double.parseDouble(raw.trim()); + return Double.isFinite(value) ? value : null; + } catch (NumberFormatException e) { + return null; + } + } else { + return null; + } + } + + @Nullable + private static String readString(JsonObject object, String key) { + JsonElement value = object.get(key); + return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isString() ? value.getAsString() : null; + } + + private static float readFloat(JsonObject object, String key, float fallback) { + Double value = readFiniteDouble(object.get(key)); + return value == null ? fallback : value.floatValue(); + } + + @Nullable + private static Double readFiniteDouble(JsonElement element) { + if (element != null && element.isJsonPrimitive()) { + JsonPrimitive primitive = element.getAsJsonPrimitive(); + if (primitive.isNumber()) { + double value = primitive.getAsDouble(); + return Double.isFinite(value) ? value : null; + } else { + return !primitive.isString() ? null : parseTime(primitive.getAsString()); + } + } else { + return null; + } + } + + public record ParsedAnimation(double lengthSeconds, List cues) { + } + + public record ParsedCue(double timeSeconds, @Nullable String effect, List randomSounds, float volume, float pitch) { + public ParsedCue { + randomSounds = randomSounds == null ? List.of() : List.copyOf(randomSounds); + } + + public double timeTicks() { + return this.timeSeconds * 20.0; + } + } + + @FunctionalInterface + public interface WarningSink { + void warn(String var1, Object... var2); + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/EntityPushAwayMixin.java b/src/main/java/com/nonid/internal/animation/mixin/EntityPushAwayMixin.java new file mode 100644 index 0000000..cd6a5ad --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/EntityPushAwayMixin.java @@ -0,0 +1,25 @@ +package com.nonid.internal.animation.mixin; + +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.entity.Entity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityPushAwayMixin { + @Inject(method = "pushAwayFrom", at = @At("HEAD"), cancellable = true) + private void afw$disablePushAwayDuringAnimation(Entity other, CallbackInfo ci) { + if (other != null) { + Entity self = (Entity)(Object)this; + if (self.getEntityWorld() instanceof ServerWorld world) { + if (ServerAnimationController.isActorPendingOrActive(world, self.getUuid()) + || ServerAnimationController.isActorPendingOrActive(world, other.getUuid())) { + ci.cancel(); + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/LivingEntityDamageMixin.java b/src/main/java/com/nonid/internal/animation/mixin/LivingEntityDamageMixin.java new file mode 100644 index 0000000..4c1d357 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/LivingEntityDamageMixin.java @@ -0,0 +1,28 @@ +package com.nonid.internal.animation.mixin; + +import com.nonid.NonAnimationRescueSystem; +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityDamageMixin { + @Inject(method = "damage", at = @At("HEAD"), cancellable = true) + private void afw$handleDamage(ServerWorld world, DamageSource source, float amount, CallbackInfoReturnable cir) { + LivingEntity actor = (LivingEntity)(Object)this; + MobEntity redirectTarget = NonAnimationRescueSystem.findDirectDamageRedirectTarget(world, actor, source); + if (redirectTarget != null) { + cir.setReturnValue(redirectTarget.damage(world, source, amount)); + } else { + if (!ServerAnimationController.shouldAllowActorDamage(world, actor, source)) { + cir.setReturnValue(false); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/LivingEntityPushableMixin.java b/src/main/java/com/nonid/internal/animation/mixin/LivingEntityPushableMixin.java new file mode 100644 index 0000000..2540e6f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/LivingEntityPushableMixin.java @@ -0,0 +1,22 @@ +package com.nonid.internal.animation.mixin; + +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.entity.LivingEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityPushableMixin { + @Inject(method = "isPushable", at = @At("HEAD"), cancellable = true) + private void afw$disablePushDuringAnimation(CallbackInfoReturnable cir) { + LivingEntity self = (LivingEntity)(Object)this; + if (self.getEntityWorld() instanceof ServerWorld world) { + if (ServerAnimationController.isActorPendingOrActive(world, self.getUuid())) { + cir.setReturnValue(false); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/MobEntityIgnoreAttackersMixin.java b/src/main/java/com/nonid/internal/animation/mixin/MobEntityIgnoreAttackersMixin.java new file mode 100644 index 0000000..75a315a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/MobEntityIgnoreAttackersMixin.java @@ -0,0 +1,23 @@ +package com.nonid.internal.animation.mixin; + +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MobEntity.class) +public abstract class MobEntityIgnoreAttackersMixin { + @Inject(method = "tryAttack", at = @At("HEAD"), cancellable = true) + private void afw$blockDirectAttackOnIgnoredActors(ServerWorld world, Entity target, CallbackInfoReturnable cir) { + if (target instanceof LivingEntity livingTarget) { + if (ServerAnimationController.shouldIgnoreAttackers(world, livingTarget)) { + cir.setReturnValue(false); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/ServerPlayNetworkHandlerMixin.java b/src/main/java/com/nonid/internal/animation/mixin/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..88e2d28 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,115 @@ +package com.nonid.internal.animation.mixin; + +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket; +import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket; +import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; +import net.minecraft.network.packet.c2s.play.PickItemFromBlockC2SPacket; +import net.minecraft.network.packet.c2s.play.PickItemFromEntityC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerInteractItemC2SPacket; +import net.minecraft.network.packet.c2s.play.SlotChangedStateC2SPacket; +import net.minecraft.network.packet.c2s.play.UpdateSelectedSlotC2SPacket; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerPlayNetworkHandlerMixin { + @Shadow + public ServerPlayerEntity player; + + @Unique + private boolean afw$shouldBlock() { + if (this.player == null) { + return false; + } else { + return this.player.getEntityWorld() instanceof ServerWorld world ? ServerAnimationController.isActorActive(world, this.player.getUuid()) : false; + } + } + + @Inject(method = "onPlayerAction", at = @At("HEAD"), cancellable = true) + private void afw$blockPlayerAction(PlayerActionC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onPlayerInteractBlock", at = @At("HEAD"), cancellable = true) + private void afw$blockInteractBlock(PlayerInteractBlockC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onPlayerInteractItem", at = @At("HEAD"), cancellable = true) + private void afw$blockInteractItem(PlayerInteractItemC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onPlayerInteractEntity", at = @At("HEAD"), cancellable = true) + private void afw$blockInteractEntity(PlayerInteractEntityC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onClickSlot", at = @At("HEAD"), cancellable = true) + private void afw$blockClickSlot(ClickSlotC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onButtonClick", at = @At("HEAD"), cancellable = true) + private void afw$blockButtonClick(ButtonClickC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onCreativeInventoryAction", at = @At("HEAD"), cancellable = true) + private void afw$blockCreative(CreativeInventoryActionC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onSlotChangedState", at = @At("HEAD"), cancellable = true) + private void afw$blockSlotState(SlotChangedStateC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onUpdateSelectedSlot", at = @At("HEAD"), cancellable = true) + private void afw$blockSelectedSlot(UpdateSelectedSlotC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onPickItemFromBlock", at = @At("HEAD"), cancellable = true) + private void afw$blockPickFromBlock(PickItemFromBlockC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "onPickItemFromEntity", at = @At("HEAD"), cancellable = true) + private void afw$blockPickFromEntity(PickItemFromEntityC2SPacket packet, CallbackInfo ci) { + if (this.afw$shouldBlock()) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/TargetPredicateMixin.java b/src/main/java/com/nonid/internal/animation/mixin/TargetPredicateMixin.java new file mode 100644 index 0000000..b7c2a21 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/TargetPredicateMixin.java @@ -0,0 +1,20 @@ +package com.nonid.internal.animation.mixin; + +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.TargetPredicate; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(TargetPredicate.class) +public abstract class TargetPredicateMixin { + @Inject(method = "test", at = @At("HEAD"), cancellable = true) + private void afw$ignoreAnimationActors(ServerWorld world, LivingEntity tester, LivingEntity target, CallbackInfoReturnable cir) { + if (ServerAnimationController.shouldIgnoreAttackers(world, target)) { + cir.setReturnValue(false); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/CameraAnimationMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/CameraAnimationMixin.java new file mode 100644 index 0000000..a9271e2 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/CameraAnimationMixin.java @@ -0,0 +1,461 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.camera.AnimatedCameraPoseTracker; +import com.nonid.internal.animation.client.camera.AnimationCameraZoom; +import com.nonid.internal.animation.client.camera.FirstPersonCameraCollision; +import com.nonid.internal.animation.client.camera.FirstPersonLookConstraint; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.Objects; +import java.util.UUID; +import net.minecraft.block.BlockState; +import net.minecraft.block.ShapeContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Camera; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Camera.class) +public abstract class CameraAnimationMixin { + private static final double CAMERA_FORWARD_OFFSET = 0.2; + private static final double THIRD_PERSON_ORBIT_EPSILON_SQ = 1.0E-8; + private static final long FALLBACK_POSE_HOLD_TICKS = 20L; + private static final long START_ALIGNMENT_TICKS = 8L; + private static final long THIRD_PERSON_ORBIT_SMOOTH_TICKS = 6L; + private static final long NEW_INSTANCE_ORBIT_SAMPLE_DELAY_TICKS = 4L; + private static final long BODY_ORBIT_VALIDATION_INTERVAL_TICKS = 20L; + private static final double BODY_ORBIT_MISMATCH_DISTANCE_SQ = 0.25; + private static final int BODY_ORBIT_MISMATCHES_BEFORE_CORRECTION = 2; + private static final double THIRD_PERSON_FOCUS_BOX_SIZE = 0.25; + private static final double THIRD_PERSON_FOCUS_UP_STEP = 0.1; + private static final int THIRD_PERSON_FOCUS_UP_STEPS = 16; + private static final double THIRD_PERSON_FOCUS_HORIZONTAL_STEP = 0.25; + private static final int THIRD_PERSON_FOCUS_HORIZONTAL_RINGS = 4; + private static AnimatedCameraPoseTracker.PoseSample afw$lastValidPosedPose = null; + private static Vec3d afw$lastConstrainedLook = null; + private static UUID afw$lastActiveInstanceId = null; + private static UUID afw$startAlignInstanceId = null; + private static UUID afw$bodyOrbitInstanceId = null; + private static Identifier afw$bodyOrbitAnimationId = null; + private static Vec3d afw$bodyOrbitTarget = null; + private static boolean afw$delayBodyOrbitForCurrentInstance = false; + private static long afw$nextBodyOrbitValidationTick = 0L; + private static int afw$bodyOrbitMismatchCount = 0; + private static UUID afw$smoothedOrbitInstanceId = null; + private static Identifier afw$smoothedOrbitAnimationId = null; + private static Vec3d afw$orbitSmoothStart = null; + private static Vec3d afw$orbitSmoothTarget = null; + private static Vec3d afw$currentSmoothedOrbitTarget = null; + private static long afw$orbitSmoothStartTick = 0L; + private static boolean afw$wasFirstPersonLastFrame = false; + private static boolean afw$startAlignPendingTarget = false; + private static boolean afw$startAlignActive = false; + private static float afw$startAlignFromYaw = 0.0F; + private static float afw$startAlignFromPitch = 0.0F; + private static float afw$startAlignTargetYaw = 0.0F; + private static float afw$startAlignTargetPitch = 0.0F; + private static long afw$startAlignTick = 0L; + + @Shadow + protected abstract void setPos(Vec3d var1); + + @Shadow + protected abstract void setRotation(float var1, float var2); + + @Shadow + protected abstract void moveBy(float var1, float var2, float var3); + + @Shadow + private float clipToSpace(float startingDistance) { + return startingDistance; + } + + @Inject(method = "update", at = @At("TAIL")) + private void afw$lockFirstPersonCameraToAnimatedPose( + World area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickDelta, CallbackInfo ci + ) { + if (!(focusedEntity instanceof PlayerEntity focusedPlayer)) { + afw$resetAnimationCameraState(false); + } else { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null && client.player != null) { + if (!focusedPlayer.getUuid().equals(client.player.getUuid())) { + afw$resetAnimationCameraState(!thirdPerson); + } else { + UUID actorUuid = focusedPlayer.getUuid(); + boolean pendingOrActive = ClientAnimationRuntime.isActorPendingOrActive(actorUuid); + UUID activeInstanceId = ClientAnimationRuntime.findLatestActiveInstanceContaining(actorUuid); + if (!pendingOrActive) { + afw$resetAnimationCameraState(!thirdPerson); + } else if (thirdPerson) { + afw$lastActiveInstanceId = activeInstanceId; + afw$wasFirstPersonLastFrame = false; + Identifier activeAnimationId = ClientAnimationRuntime.findLatestActiveAnimationIdContaining(actorUuid); + this.afw$applyThirdPersonCameraAdjustments(actorUuid, activeInstanceId, activeAnimationId, focusedPlayer, tickDelta, client.world.getTime()); + } else { + boolean enteredFirstPerson = !afw$wasFirstPersonLastFrame; + boolean instanceChanged = !Objects.equals(activeInstanceId, afw$lastActiveInstanceId); + boolean replacingActiveInstance = instanceChanged && afw$lastActiveInstanceId != null; + if (activeInstanceId == null || !enteredFirstPerson && (!instanceChanged || replacingActiveInstance)) { + if (replacingActiveInstance) { + afw$clearStartAlignment(); + } + } else { + afw$beginStartAlignment(activeInstanceId, focusedPlayer.getYaw(tickDelta), focusedPlayer.getPitch(tickDelta)); + } + + afw$lastActiveInstanceId = activeInstanceId; + afw$wasFirstPersonLastFrame = true; + long nowTick = client.world.getTime(); + AnimatedCameraPoseTracker.PoseSample posedPose = AnimatedCameraPoseTracker.resolve(actorUuid, nowTick); + if (posedPose != null) { + afw$lastValidPosedPose = posedPose; + } else { + if (afw$lastValidPosedPose == null || nowTick - afw$lastValidPosedPose.tick() > 20L) { + return; + } + + posedPose = afw$lastValidPosedPose; + } + + if (afw$startAlignPendingTarget && Objects.equals(activeInstanceId, afw$startAlignInstanceId)) { + Vec3d targetLook = afw$resolveStartAlignmentTargetLook(posedPose); + if (targetLook != null) { + afw$startAlignTargetYaw = FirstPersonLookConstraint.toMinecraftYaw(targetLook, afw$startAlignFromYaw); + afw$startAlignTargetPitch = FirstPersonLookConstraint.toMinecraftPitch(targetLook); + afw$startAlignTick = nowTick; + afw$startAlignPendingTarget = false; + afw$startAlignActive = true; + } + } + + float desiredYaw = focusedPlayer.getYaw(tickDelta); + float desiredPitch = focusedPlayer.getPitch(tickDelta); + if (afw$startAlignActive && Objects.equals(activeInstanceId, afw$startAlignInstanceId)) { + float progress = MathHelper.clamp(((float)(nowTick - afw$startAlignTick) + tickDelta) / 8.0F, 0.0F, 1.0F); + float eased = afw$smootherStep(progress); + desiredYaw = afw$lerpAngleDegrees(eased, afw$startAlignFromYaw, afw$startAlignTargetYaw); + desiredPitch = MathHelper.lerp(eased, afw$startAlignFromPitch, afw$startAlignTargetPitch); + if (progress >= 1.0F) { + afw$startAlignActive = false; + } + } + + Vec3d desiredLook = Vec3d.fromPolar(desiredPitch, desiredYaw); + Vec3d constrainedLook = FirstPersonLookConstraint.constrain(desiredLook, posedPose, afw$lastConstrainedLook); + float constrainedYaw = FirstPersonLookConstraint.toMinecraftYaw(constrainedLook, desiredYaw); + float constrainedPitch = FirstPersonLookConstraint.toMinecraftPitch(constrainedLook); + focusedPlayer.lastYaw = constrainedYaw; + focusedPlayer.lastPitch = constrainedPitch; + focusedPlayer.setYaw(constrainedYaw); + focusedPlayer.setPitch(constrainedPitch); + focusedPlayer.lastHeadYaw = constrainedYaw; + focusedPlayer.headYaw = constrainedYaw; + focusedPlayer.setHeadYaw(constrainedYaw); + focusedPlayer.lastBodyYaw = constrainedYaw; + focusedPlayer.bodyYaw = constrainedYaw; + focusedPlayer.setBodyYaw(constrainedYaw); + this.setRotation(constrainedYaw, constrainedPitch); + Vec3d desiredCameraPos = posedPose.worldPos().add(constrainedLook.multiply(0.2)); + this.setPos(FirstPersonCameraCollision.resolve(area, desiredCameraPos)); + afw$lastConstrainedLook = constrainedLook; + } + } + } else { + afw$resetAnimationCameraState(!thirdPerson); + } + } + } + + private static void afw$beginStartAlignment(UUID instanceId, float startYaw, float startPitch) { + afw$startAlignInstanceId = instanceId; + afw$startAlignPendingTarget = true; + afw$startAlignActive = false; + afw$startAlignFromYaw = startYaw; + afw$startAlignFromPitch = startPitch; + afw$startAlignTargetYaw = startYaw; + afw$startAlignTargetPitch = startPitch; + afw$startAlignTick = 0L; + } + + private static void afw$clearStartAlignment() { + afw$startAlignInstanceId = null; + afw$startAlignPendingTarget = false; + afw$startAlignActive = false; + } + + private static Vec3d afw$resolveStartAlignmentTargetLook(AnimatedCameraPoseTracker.PoseSample pose) { + Vec3d targetLook = pose.headForward(); + if (targetLook == null || targetLook.lengthSquared() < 1.0E-8 || !targetLook.isFinite()) { + targetLook = pose.bodyForward(); + } + + return targetLook != null && !(targetLook.lengthSquared() < 1.0E-8) && targetLook.isFinite() ? targetLook.normalize() : null; + } + + private void afw$applyThirdPersonCameraAdjustments( + UUID actorUuid, UUID activeInstanceId, Identifier activeAnimationId, PlayerEntity focusedPlayer, float tickDelta, long nowTick + ) { + Vec3d focusPos = focusedPlayer.getCameraPosVec(tickDelta); + if (focusPos != null) { + Vec3d adjustedFocus = focusPos; + Vec3d orbitTarget = ClientAnimationRuntime.findCameraOrbitTargetForActor(actorUuid); + if (orbitTarget != null) { + afw$clearBodyOrbitTarget(); + Vec3d horizontalDelta = new Vec3d(orbitTarget.x - focusPos.x, 0.0, orbitTarget.z - focusPos.z); + if (horizontalDelta.isFinite() && horizontalDelta.lengthSquared() >= 1.0E-8) { + adjustedFocus = focusPos.add(horizontalDelta); + } + } else { + Vec3d bodyTarget = afw$resolveBodyOrbitTarget(actorUuid, activeInstanceId, activeAnimationId, nowTick); + if (bodyTarget != null) { + adjustedFocus = afw$resolveSmoothedOrbitTarget(activeInstanceId, activeAnimationId, focusPos, bodyTarget, nowTick, tickDelta); + } else if (afw$currentSmoothedOrbitTarget != null && afw$currentSmoothedOrbitTarget.isFinite()) { + adjustedFocus = afw$currentSmoothedOrbitTarget; + } else if (activeInstanceId == null) { + afw$clearOrbitSmoothing(); + } + } + + adjustedFocus = afw$resolveClearThirdPersonFocus(focusedPlayer.getEntityWorld(), adjustedFocus); + float zoomDistance = AnimationCameraZoom.distance(tickDelta); + this.setPos(adjustedFocus); + this.moveBy(-this.clipToSpace(zoomDistance), 0.0F, 0.0F); + } + } + + private static Vec3d afw$resolveClearThirdPersonFocus(World world, Vec3d desiredFocus) { + if (world != null && desiredFocus != null && desiredFocus.isFinite()) { + if (afw$isThirdPersonFocusClear(world, desiredFocus)) { + return desiredFocus; + } + + for (int step = 1; step <= 16; step++) { + Vec3d candidate = desiredFocus.add(0.0, step * 0.1, 0.0); + if (afw$isThirdPersonFocusClear(world, candidate)) { + return candidate; + } + } + + for (int ring = 1; ring <= 4; ring++) { + double distance = ring * 0.25; + + for (int i = 0; i < 8; i++) { + double angle = (Math.PI * 2) * i / 8.0; + Vec3d candidate = desiredFocus.add(Math.cos(angle) * distance, 0.0, Math.sin(angle) * distance); + if (afw$isThirdPersonFocusClear(world, candidate)) { + return candidate; + } + } + } + + return desiredFocus; + } else { + return desiredFocus; + } + } + + private static boolean afw$isThirdPersonFocusClear(World world, Vec3d focus) { + if (world != null && focus != null && focus.isFinite()) { + Box focusBox = Box.of(focus, 0.25, 0.25, 0.25); + return world.isSpaceEmpty(focusBox) && !afw$intersectsCameraCollisionShape(world, focusBox); + } else { + return true; + } + } + + private static boolean afw$intersectsCameraCollisionShape(World world, Box focusBox) { + int minX = MathHelper.floor(focusBox.minX); + int maxX = MathHelper.floor(focusBox.maxX); + int minY = MathHelper.floor(focusBox.minY); + int maxY = MathHelper.floor(focusBox.maxY); + int minZ = MathHelper.floor(focusBox.minZ); + int maxZ = MathHelper.floor(focusBox.maxZ); + ShapeContext shapeContext = ShapeContext.absent(); + + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = world.getBlockState(pos); + if (!state.isAir()) { + VoxelShape shape = state.getCameraCollisionShape(world, pos, shapeContext); + if (!shape.isEmpty()) { + for (Box shapeBox : shape.getBoundingBoxes()) { + if (focusBox.intersects(shapeBox.offset(pos))) { + return true; + } + } + } + } + } + } + } + + return false; + } + + private static Vec3d afw$resolveBodyOrbitTarget(UUID actorUuid, UUID activeInstanceId, Identifier activeAnimationId, long nowTick) { + if (actorUuid != null && activeInstanceId != null && activeAnimationId != null) { + boolean instanceChanged = !Objects.equals(activeInstanceId, afw$bodyOrbitInstanceId); + boolean animationChanged = !Objects.equals(activeAnimationId, afw$bodyOrbitAnimationId); + if (instanceChanged || animationChanged) { + if (instanceChanged) { + afw$delayBodyOrbitForCurrentInstance = afw$currentSmoothedOrbitTarget == null; + } + + afw$bodyOrbitInstanceId = activeInstanceId; + afw$bodyOrbitAnimationId = activeAnimationId; + afw$bodyOrbitTarget = null; + afw$resetBodyOrbitValidation(); + } + + if (afw$bodyOrbitTarget != null) { + afw$validateBodyOrbitTarget(actorUuid, activeInstanceId, activeAnimationId, nowTick); + return afw$bodyOrbitTarget; + } else { + Long startTick = ClientAnimationRuntime.findStartTickForActor(actorUuid); + if (afw$delayBodyOrbitForCurrentInstance && startTick != null && nowTick - startTick < 4L) { + return null; + } else { + AnimatedCameraPoseTracker.PoseSample sample = afw$resolveMatchingBodyPose(actorUuid, activeInstanceId, activeAnimationId, nowTick); + if (sample == null) { + return null; + } else if (afw$delayBodyOrbitForCurrentInstance && startTick != null && sample.tick() - startTick < 4L) { + return null; + } else { + Vec3d bodyPos = sample.bodyWorldPos(); + if (bodyPos != null && bodyPos.isFinite()) { + afw$bodyOrbitTarget = bodyPos; + afw$nextBodyOrbitValidationTick = nowTick + 20L; + afw$bodyOrbitMismatchCount = 0; + return afw$bodyOrbitTarget; + } else { + return null; + } + } + } + } + } else { + afw$clearBodyOrbitTarget(); + return null; + } + } + + private static void afw$validateBodyOrbitTarget(UUID actorUuid, UUID activeInstanceId, Identifier activeAnimationId, long nowTick) { + if (nowTick >= afw$nextBodyOrbitValidationTick) { + afw$nextBodyOrbitValidationTick = nowTick + 20L; + AnimatedCameraPoseTracker.PoseSample sample = afw$resolveMatchingBodyPose(actorUuid, activeInstanceId, activeAnimationId, nowTick); + if (sample != null && sample.bodyWorldPos() != null && sample.bodyWorldPos().isFinite()) { + Vec3d latestBodyPos = sample.bodyWorldPos(); + if (afw$bodyOrbitTarget.squaredDistanceTo(latestBodyPos) <= 0.25) { + afw$bodyOrbitMismatchCount = 0; + } else { + afw$bodyOrbitMismatchCount++; + if (afw$bodyOrbitMismatchCount >= 2) { + afw$bodyOrbitTarget = latestBodyPos; + afw$bodyOrbitMismatchCount = 0; + } + } + } + } + } + + private static AnimatedCameraPoseTracker.PoseSample afw$resolveMatchingBodyPose( + UUID actorUuid, UUID activeInstanceId, Identifier activeAnimationId, long nowTick + ) { + AnimatedCameraPoseTracker.PoseSample sample = AnimatedCameraPoseTracker.resolve(actorUuid, nowTick); + return sample != null && Objects.equals(sample.instanceId(), activeInstanceId) && Objects.equals(sample.animationId(), activeAnimationId) ? sample : null; + } + + private static Vec3d afw$resolveSmoothedOrbitTarget( + UUID activeInstanceId, Identifier activeAnimationId, Vec3d fallbackStart, Vec3d desiredTarget, long nowTick, float tickDelta + ) { + if (desiredTarget != null && desiredTarget.isFinite()) { + boolean targetChanged = afw$orbitSmoothTarget == null || afw$orbitSmoothTarget.squaredDistanceTo(desiredTarget) >= 1.0E-6; + if (!Objects.equals(activeInstanceId, afw$smoothedOrbitInstanceId) + || !Objects.equals(activeAnimationId, afw$smoothedOrbitAnimationId) + || targetChanged) { + afw$smoothedOrbitInstanceId = activeInstanceId; + afw$smoothedOrbitAnimationId = activeAnimationId; + afw$orbitSmoothStart = afw$currentSmoothedOrbitTarget != null ? afw$currentSmoothedOrbitTarget : fallbackStart; + afw$orbitSmoothTarget = desiredTarget; + afw$orbitSmoothStartTick = nowTick; + } + + if (afw$orbitSmoothStart != null && afw$orbitSmoothTarget != null) { + float progress = MathHelper.clamp(((float)(nowTick - afw$orbitSmoothStartTick) + tickDelta) / 6.0F, 0.0F, 1.0F); + float eased = afw$smootherStep(progress); + afw$currentSmoothedOrbitTarget = new Vec3d( + MathHelper.lerp(eased, afw$orbitSmoothStart.x, afw$orbitSmoothTarget.x), + MathHelper.lerp(eased, afw$orbitSmoothStart.y, afw$orbitSmoothTarget.y), + MathHelper.lerp(eased, afw$orbitSmoothStart.z, afw$orbitSmoothTarget.z) + ); + if (progress >= 1.0F) { + afw$orbitSmoothStart = afw$orbitSmoothTarget; + } + + return afw$currentSmoothedOrbitTarget; + } else { + afw$currentSmoothedOrbitTarget = desiredTarget; + return desiredTarget; + } + } else { + return fallbackStart; + } + } + + private static void afw$clearBodyOrbitTarget() { + afw$bodyOrbitInstanceId = null; + afw$bodyOrbitAnimationId = null; + afw$bodyOrbitTarget = null; + afw$delayBodyOrbitForCurrentInstance = false; + afw$resetBodyOrbitValidation(); + } + + private static void afw$resetBodyOrbitValidation() { + afw$nextBodyOrbitValidationTick = 0L; + afw$bodyOrbitMismatchCount = 0; + } + + private static void afw$clearOrbitSmoothing() { + afw$smoothedOrbitInstanceId = null; + afw$smoothedOrbitAnimationId = null; + afw$orbitSmoothStart = null; + afw$orbitSmoothTarget = null; + afw$currentSmoothedOrbitTarget = null; + afw$orbitSmoothStartTick = 0L; + } + + private static float afw$lerpAngleDegrees(float delta, float start, float end) { + return start + MathHelper.wrapDegrees(end - start) * delta; + } + + private static float afw$smootherStep(float value) { + float clamped = MathHelper.clamp(value, 0.0F, 1.0F); + return clamped * clamped * clamped * (clamped * (clamped * 6.0F - 15.0F) + 10.0F); + } + + private static void afw$resetAnimationCameraState(boolean firstPerson) { + afw$lastValidPosedPose = null; + afw$lastConstrainedLook = null; + afw$lastActiveInstanceId = null; + afw$startAlignInstanceId = null; + afw$startAlignPendingTarget = false; + afw$startAlignActive = false; + afw$clearBodyOrbitTarget(); + afw$clearOrbitSmoothing(); + afw$wasFirstPersonLastFrame = firstPerson; + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/CameraPositionAccessMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/CameraPositionAccessMixin.java new file mode 100644 index 0000000..83b0398 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/CameraPositionAccessMixin.java @@ -0,0 +1,20 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.camera.CameraPositionAccess; +import net.minecraft.client.render.Camera; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Camera.class) +public abstract class CameraPositionAccessMixin implements CameraPositionAccess { + @Shadow + private Vec3d pos; + + @Unique + @Override + public Vec3d afw$getPos() { + return this.pos; + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerInteractionManagerMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerInteractionManagerMixin.java new file mode 100644 index 0000000..f044866 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerInteractionManagerMixin.java @@ -0,0 +1,152 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.NetworkRecipeId; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientPlayerInteractionManager.class) +public abstract class ClientPlayerInteractionManagerMixin { + @Unique + private static boolean shouldBlock() { + MinecraftClient client = MinecraftClient.getInstance(); + ClientPlayerEntity player = client == null ? null : client.player; + return player == null ? false : ClientAnimationRuntime.isActorActive(player.getUuid()); + } + + @Inject(method = "breakBlock", at = @At("HEAD"), cancellable = true) + private void afw$blockBreak(BlockPos pos, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(false); + } + } + + @Inject(method = "attackBlock", at = @At("HEAD"), cancellable = true) + private void afw$blockAttack(BlockPos pos, Direction direction, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(false); + } + } + + @Inject(method = "updateBlockBreakingProgress", at = @At("HEAD"), cancellable = true) + private void afw$blockBreakProgress(BlockPos pos, Direction direction, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(false); + } + } + + @Inject(method = "interactBlock", at = @At("HEAD"), cancellable = true) + private void afw$interactBlock(ClientPlayerEntity player, Hand hand, BlockHitResult hit, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(ActionResult.FAIL); + } + } + + @Inject(method = "interactItem", at = @At("HEAD"), cancellable = true) + private void afw$interactItem(PlayerEntity player, Hand hand, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(ActionResult.FAIL); + } + } + + @Inject(method = "interactEntity", at = @At("HEAD"), cancellable = true) + private void afw$interactEntity(PlayerEntity player, Entity entity, Hand hand, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(ActionResult.FAIL); + } + } + + @Inject(method = "interactEntityAtLocation", at = @At("HEAD"), cancellable = true) + private void afw$interactEntityAt(PlayerEntity player, Entity entity, EntityHitResult hit, Hand hand, CallbackInfoReturnable cir) { + if (shouldBlock()) { + cir.setReturnValue(ActionResult.FAIL); + } + } + + @Inject(method = "attackEntity", at = @At("HEAD"), cancellable = true) + private void afw$attackEntity(PlayerEntity player, Entity target, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "clickSlot", at = @At("HEAD"), cancellable = true) + private void afw$clickSlot(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "clickRecipe", at = @At("HEAD"), cancellable = true) + private void afw$clickRecipe(int syncId, NetworkRecipeId recipeId, boolean craftAll, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "clickButton", at = @At("HEAD"), cancellable = true) + private void afw$clickButton(int syncId, int buttonId, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "clickCreativeStack", at = @At("HEAD"), cancellable = true) + private void afw$clickCreative(ItemStack stack, int slotId, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "dropCreativeStack", at = @At("HEAD"), cancellable = true) + private void afw$dropCreative(ItemStack stack, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "stopUsingItem", at = @At("HEAD"), cancellable = true) + private void afw$stopUsingItem(PlayerEntity player, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "pickItemFromBlock", at = @At("HEAD"), cancellable = true) + private void afw$pickItemFromBlock(BlockPos pos, boolean includeData, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "pickItemFromEntity", at = @At("HEAD"), cancellable = true) + private void afw$pickItemFromEntity(Entity entity, boolean includeData, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } + + @Inject(method = "slotChangedState", at = @At("HEAD"), cancellable = true) + private void afw$slotChangedState(int syncId, int revision, boolean newState, CallbackInfo ci) { + if (shouldBlock()) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerMovementMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerMovementMixin.java new file mode 100644 index 0000000..3e92c4b --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/ClientPlayerMovementMixin.java @@ -0,0 +1,34 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.input.Input; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.util.PlayerInput; +import net.minecraft.util.math.Vec2f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayerEntity.class) +public abstract class ClientPlayerMovementMixin { + @Inject(method = "tickMovementInput", at = @At("HEAD"), cancellable = true) + private void afw$lockMovementWhileAnimating(CallbackInfo ci) { + ClientPlayerEntity self = (ClientPlayerEntity)(Object)this; + if (ClientAnimationRuntime.isActorPendingOrActive(self.getUuid())) { + self.sidewaysSpeed = 0.0F; + self.forwardSpeed = 0.0F; + self.setJumping(false); + self.setSprinting(false); + Input input = self.input; + if (input != null) { + input.playerInput = new PlayerInput(false, false, false, false, false, false, false); + if (input instanceof InputAccessor accessor) { + accessor.afw$setMovementVector(Vec2f.ZERO); + } + } + + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/ClientWorldAnimatedPlayerParticleMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/ClientWorldAnimatedPlayerParticleMixin.java new file mode 100644 index 0000000..d1aca39 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/ClientWorldAnimatedPlayerParticleMixin.java @@ -0,0 +1,53 @@ +package com.nonid.internal.animation.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.particle.ParticleEffect; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientWorld.class) +public abstract class ClientWorldAnimatedPlayerParticleMixin { + @Unique + private int afw$animatedPlayerParticleSuppressionDepth; + + @WrapOperation(method = "tickEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;tick()V")) + private void afw$trackAnimatedPlayerTick(Entity entity, Operation original) { + if (entity instanceof PlayerEntity && ClientAnimationRuntime.isActorActive(entity.getUuid())) { + this.afw$animatedPlayerParticleSuppressionDepth++; + + try { + original.call(new Object[]{entity}); + } finally { + this.afw$animatedPlayerParticleSuppressionDepth--; + } + } else { + original.call(new Object[]{entity}); + } + } + + @Inject(method = "addParticle(Lnet/minecraft/particle/ParticleEffect;ZZDDDDDD)V", at = @At("HEAD"), cancellable = true) + private void afw$suppressAnimatedPlayerParticles( + ParticleEffect particle, + boolean force, + boolean important, + double x, + double y, + double z, + double velocityX, + double velocityY, + double velocityZ, + CallbackInfo ci + ) { + if (this.afw$animatedPlayerParticleSuppressionDepth > 0) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/CuboidGeoBoneHiddenCubeMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/CuboidGeoBoneHiddenCubeMixin.java new file mode 100644 index 0000000..94b0b23 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/CuboidGeoBoneHiddenCubeMixin.java @@ -0,0 +1,59 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.render.gecko.GeckoRenderTickets; +import java.util.Map; +import java.util.Set; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import software.bernie.geckolib.cache.model.GeoBone; +import software.bernie.geckolib.cache.model.cuboid.CuboidGeoBone; +import software.bernie.geckolib.cache.model.cuboid.GeoCube; +import software.bernie.geckolib.renderer.base.GeoRenderState; +import software.bernie.geckolib.renderer.base.RenderPassInfo; + +@Mixin(value = CuboidGeoBone.class, remap = false) +public abstract class CuboidGeoBoneHiddenCubeMixin { + @Shadow + @Final + public GeoCube[] cubes; + + @Inject(method = "render", at = @At("HEAD"), cancellable = true) + private void afw$skipHiddenCubeIndices( + RenderPassInfo renderPassInfo, + MatrixStack poseStack, + VertexConsumer vertexConsumer, + int packedLight, + int packedOverlay, + int renderColor, + CallbackInfo ci + ) { + if (renderPassInfo != null && renderPassInfo.renderState() != null) { + Map> hiddenIndicesByBone = (Map>)renderPassInfo.renderState() + .getOrDefaultGeckolibData(GeckoRenderTickets.HIDDEN_BONE_CUBE_INDICES, Map.of()); + if (!hiddenIndicesByBone.isEmpty()) { + GeoBone bone = (GeoBone)(Object)this; + Set hiddenIndices = hiddenIndicesByBone.get(bone.name()); + if (hiddenIndices != null && !hiddenIndices.isEmpty()) { + if (bone.frameSnapshot == null || !bone.frameSnapshot.isHidden()) { + for (int i = 0; i < this.cubes.length; i++) { + GeoCube cube = this.cubes[i]; + if (cube != null && !hiddenIndices.contains(i)) { + poseStack.push(); + cube.render(poseStack, vertexConsumer, packedLight, packedOverlay, renderColor); + poseStack.pop(); + } + } + } + + ci.cancel(); + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityAnimationParticlePositionMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityAnimationParticlePositionMixin.java new file mode 100644 index 0000000..0afa71a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityAnimationParticlePositionMixin.java @@ -0,0 +1,49 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.render.gecko.ParticleKeyframes; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.UUID; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public abstract class EntityAnimationParticlePositionMixin { + @Inject(method = "getBodyX", at = @At("RETURN"), cancellable = true) + private void afw$moveBodyParticleX(double widthScale, CallbackInfoReturnable cir) { + Vec3d offset = this.afw$getAnimatedParticleOffset(); + if (offset != null) { + cir.setReturnValue((Double)cir.getReturnValue() + offset.x); + } + } + + @Inject(method = "getBodyY", at = @At("RETURN"), cancellable = true) + private void afw$moveBodyParticleY(double heightScale, CallbackInfoReturnable cir) { + Vec3d offset = this.afw$getAnimatedParticleOffset(); + if (offset != null) { + cir.setReturnValue((Double)cir.getReturnValue() + offset.y); + } + } + + @Inject(method = "getBodyZ", at = @At("RETURN"), cancellable = true) + private void afw$moveBodyParticleZ(double widthScale, CallbackInfoReturnable cir) { + Vec3d offset = this.afw$getAnimatedParticleOffset(); + if (offset != null) { + cir.setReturnValue((Double)cir.getReturnValue() + offset.z); + } + } + + private Vec3d afw$getAnimatedParticleOffset() { + Entity entity = (Entity)(Object)this; + UUID actorUuid = entity.getUuid(); + if (!ClientAnimationRuntime.isActorActive(actorUuid)) { + return null; + } + + Vec3d animatedOrigin = ParticleKeyframes.findActorOrigin(actorUuid); + return animatedOrigin == null ? null : animatedOrigin.subtract(entity.getEntityPos()); + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityPushAwayMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityPushAwayMixin.java new file mode 100644 index 0000000..1ed5301 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityPushAwayMixin.java @@ -0,0 +1,23 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityPushAwayMixin { + @Inject(method = "pushAwayFrom", at = @At("HEAD"), cancellable = true) + private void afw$disablePushAwayDuringAnimation(Entity other, CallbackInfo ci) { + if (other != null) { + Entity self = (Entity)(Object)this; + if (self.getEntityWorld().isClient()) { + if (ClientAnimationRuntime.isActorPendingOrActive(self.getUuid()) || ClientAnimationRuntime.isActorPendingOrActive(other.getUuid())) { + ci.cancel(); + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerAccessor.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerAccessor.java new file mode 100644 index 0000000..132a22a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerAccessor.java @@ -0,0 +1,18 @@ +package com.nonid.internal.animation.mixin.client; + +import net.minecraft.client.render.entity.EntityRenderManager; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; +import net.minecraft.client.render.entity.state.EntityRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(EntityRenderManager.class) +public interface EntityRenderManagerAccessor { + @Accessor("equipmentModelLoader") + EquipmentModelLoader afw$getEquipmentModelLoader(); + + @Invoker("getRenderer") + EntityRenderer afw$getRenderer(EntityRenderState var1); +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerMixin.java new file mode 100644 index 0000000..a9daf27 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderManagerMixin.java @@ -0,0 +1,73 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import com.nonid.internal.animation.client.render.gecko.GeckoReplacedRender; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.UUID; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.EntityRenderManager; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.render.state.CameraRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.registry.Registries; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(EntityRenderManager.class) +public class EntityRenderManagerMixin { + @Inject(method = "getAndUpdateRenderState", at = @At("RETURN")) + private void afw$attachIdentity(Entity entity, float tickDelta, CallbackInfoReturnable cir) { + EntityRenderState state = (EntityRenderState)cir.getReturnValue(); + if (state instanceof AnimationRenderStateAccess access) { + access.afw$setEntityUuid(entity.getUuid()); + access.afw$setEntityTypeId(Registries.ENTITY_TYPE.getId(entity.getType())); + } + + if (state instanceof LivingEntityRenderState livingState) { + if (entity instanceof LivingEntity living) { + if (ClientAnimationRuntime.hasActiveInstances()) { + if (ClientAnimationRuntime.isActorActive(entity.getUuid())) { + UUID actorUuid = entity.getUuid(); + UUID anchorUuid = ClientAnimationRuntime.findAnchorUuidForActor(actorUuid); + boolean keepShadow = anchorUuid == null || anchorUuid.equals(actorUuid); + if (!keepShadow) { + state.shadowRadius = 0.0F; + state.shadowPieces.clear(); + } + + GeckoReplacedRender.prepare(living, livingState, tickDelta); + } + } + } + } + } + + @Redirect( + method = "render", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/render/entity/EntityRenderer;render(Lnet/minecraft/client/render/entity/state/EntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/command/OrderedRenderCommandQueue;Lnet/minecraft/client/render/state/CameraRenderState;)V" + ) + ) + private void afw$swapRenderer( + EntityRenderer vanillaRenderer, EntityRenderState state, MatrixStack matrices, OrderedRenderCommandQueue queue, CameraRenderState camera + ) { + if (state instanceof LivingEntityRenderState livingState && ClientAnimationRuntime.hasActiveInstances()) { + AnimationRenderStateAccess access = (AnimationRenderStateAccess)state; + UUID uuid = access.afw$getEntityUuid(); + if (uuid != null && ClientAnimationRuntime.isActorActive(uuid) && GeckoReplacedRender.render(livingState, matrices, queue, camera)) { + state.onFire = false; + return; + } + } + + ((EntityRendererInvoker)vanillaRenderer).afw$render(state, matrices, queue, camera); + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderStateMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderStateMixin.java new file mode 100644 index 0000000..c5ee8cf --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRenderStateMixin.java @@ -0,0 +1,36 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import java.util.UUID; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(EntityRenderState.class) +public class EntityRenderStateMixin implements AnimationRenderStateAccess { + @Unique + private UUID afw$entityUuid; + @Unique + private Identifier afw$entityTypeId; + + @Override + public UUID afw$getEntityUuid() { + return this.afw$entityUuid; + } + + @Override + public void afw$setEntityUuid(UUID uuid) { + this.afw$entityUuid = uuid; + } + + @Override + public Identifier afw$getEntityTypeId() { + return this.afw$entityTypeId; + } + + @Override + public void afw$setEntityTypeId(Identifier id) { + this.afw$entityTypeId = id; + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererAnimationNameTagMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererAnimationNameTagMixin.java new file mode 100644 index 0000000..3738828 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererAnimationNameTagMixin.java @@ -0,0 +1,42 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.UUID; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = EntityRenderer.class, priority = 1500) +public abstract class EntityRendererAnimationNameTagMixin { + @Inject(method = "hasLabel", at = @At("HEAD"), cancellable = true) + private void afw$hideSameAnimationNameLabel(Entity entity, double distance, CallbackInfoReturnable cir) { + if (entity != null) { + UUID viewerUuid = afw$getLocalPlayerUuid(); + if (ClientAnimationRuntime.shouldHideNameLabelForViewer(viewerUuid, entity.getUuid())) { + cir.setReturnValue(false); + } + } + } + + @Inject(method = "updateRenderState", at = @At("TAIL")) + private void afw$offsetOutsideViewerAnimationNameLabel(Entity entity, EntityRenderState state, float tickDelta, CallbackInfo ci) { + if (entity != null && state != null && state.nameLabelPos != null) { + UUID viewerUuid = afw$getLocalPlayerUuid(); + double yOffset = ClientAnimationRuntime.findOutsideViewerNameLabelYOffset(viewerUuid, entity.getUuid()); + if (!(yOffset <= 0.0)) { + state.nameLabelPos = state.nameLabelPos.add(0.0, yOffset, 0.0); + } + } + } + + private static UUID afw$getLocalPlayerUuid() { + MinecraftClient client = MinecraftClient.getInstance(); + return client != null && client.player != null ? client.player.getUuid() : null; + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererInvoker.java b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererInvoker.java new file mode 100644 index 0000000..166c2fa --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/EntityRendererInvoker.java @@ -0,0 +1,15 @@ +package com.nonid.internal.animation.mixin.client; + +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.client.render.state.CameraRenderState; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(EntityRenderer.class) +public interface EntityRendererInvoker { + @Invoker("render") + void afw$render(EntityRenderState var1, MatrixStack var2, OrderedRenderCommandQueue var3, CameraRenderState var4); +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/HeldItemRendererMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/HeldItemRendererMixin.java new file mode 100644 index 0000000..5986e82 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/HeldItemRendererMixin.java @@ -0,0 +1,27 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.item.HeldItemRenderer; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(HeldItemRenderer.class) +public abstract class HeldItemRendererMixin { + @Inject( + method = "renderItem(FLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/command/OrderedRenderCommandQueue;Lnet/minecraft/client/network/ClientPlayerEntity;I)V", + at = @At("HEAD"), + cancellable = true + ) + private void afw$hideFirstPersonHandsWhileAnimating( + float tickProgress, MatrixStack matrices, OrderedRenderCommandQueue commandQueue, ClientPlayerEntity player, int light, CallbackInfo ci + ) { + if (player != null && ClientAnimationRuntime.isActorPendingOrActive(player.getUuid())) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/InputAccessor.java b/src/main/java/com/nonid/internal/animation/mixin/client/InputAccessor.java new file mode 100644 index 0000000..2a88c55 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/InputAccessor.java @@ -0,0 +1,12 @@ +package com.nonid.internal.animation.mixin.client; + +import net.minecraft.client.input.Input; +import net.minecraft.util.math.Vec2f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Input.class) +public interface InputAccessor { + @Accessor("movementVector") + void afw$setMovementVector(Vec2f var1); +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/KeyboardInputMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/KeyboardInputMixin.java new file mode 100644 index 0000000..361005c --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/KeyboardInputMixin.java @@ -0,0 +1,31 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.input.Input; +import net.minecraft.client.input.KeyboardInput; +import net.minecraft.util.PlayerInput; +import net.minecraft.util.math.Vec2f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(KeyboardInput.class) +public abstract class KeyboardInputMixin { + @Inject(method = "tick", at = @At("HEAD"), cancellable = true) + private void afw$lockKeyboardInput(CallbackInfo ci) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + if (ClientAnimationRuntime.isActorPendingOrActive(client.player.getUuid())) { + Input self = (Input)(Object)this; + self.playerInput = new PlayerInput(false, false, false, false, false, false, false); + if (self instanceof InputAccessor accessor) { + accessor.afw$setMovementVector(Vec2f.ZERO); + } + + ci.cancel(); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityJumpMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityJumpMixin.java new file mode 100644 index 0000000..8bed2b7 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityJumpMixin.java @@ -0,0 +1,20 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityJumpMixin { + @Inject(method = "jump", at = @At("HEAD"), cancellable = true) + private void afw$blockJumpWhileAnimating(CallbackInfo ci) { + LivingEntity self = (LivingEntity)(Object)this; + if (self instanceof ClientPlayerEntity player && ClientAnimationRuntime.isActorPendingOrActive(player.getUuid())) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityPushableMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityPushableMixin.java new file mode 100644 index 0000000..e11b37f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityPushableMixin.java @@ -0,0 +1,21 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityPushableMixin { + @Inject(method = "isPushable", at = @At("HEAD"), cancellable = true) + private void afw$disablePushDuringAnimation(CallbackInfoReturnable cir) { + LivingEntity self = (LivingEntity)(Object)this; + if (self.getEntityWorld().isClient()) { + if (ClientAnimationRuntime.isActorPendingOrActive(self.getUuid())) { + cir.setReturnValue(false); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererAnimationNameTagMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererAnimationNameTagMixin.java new file mode 100644 index 0000000..e376b93 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererAnimationNameTagMixin.java @@ -0,0 +1,29 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import java.util.UUID; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = LivingEntityRenderer.class, priority = 1500) +public abstract class LivingEntityRendererAnimationNameTagMixin { + @Inject(method = "hasLabel(Lnet/minecraft/entity/LivingEntity;D)Z", at = @At("HEAD"), cancellable = true) + private void afw$hideSameAnimationNameLabel(LivingEntity entity, double distance, CallbackInfoReturnable cir) { + if (entity != null) { + UUID viewerUuid = afw$getLocalPlayerUuid(); + if (ClientAnimationRuntime.shouldHideNameLabelForViewer(viewerUuid, entity.getUuid())) { + cir.setReturnValue(false); + } + } + } + + private static UUID afw$getLocalPlayerUuid() { + MinecraftClient client = MinecraftClient.getInstance(); + return client != null && client.player != null ? client.player.getUuid() : null; + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererMixin.java new file mode 100644 index 0000000..0ae8c33 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/LivingEntityRendererMixin.java @@ -0,0 +1,34 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.render.AnimationRenderStateAccess; +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.registry.Registries; +import net.minecraft.util.math.MathHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntityRenderer.class) +public abstract class LivingEntityRendererMixin { + @Inject( + method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V", + at = @At("TAIL") + ) + private void afw$attachUuidAndType(T entity, S state, float tickDelta, CallbackInfo ci) { + AnimationRenderStateAccess access = (AnimationRenderStateAccess)state; + access.afw$setEntityUuid(entity.getUuid()); + access.afw$setEntityTypeId(Registries.ENTITY_TYPE.getId(entity.getType())); + if (ClientAnimationRuntime.hasActiveInstances()) { + ClientAnimationRuntime.LockedOrientation locked = ClientAnimationRuntime.getLockedOrientation(entity.getUuid()); + if (locked != null) { + state.bodyYaw = locked.bodyYaw(); + state.relativeHeadYaw = MathHelper.wrapDegrees(locked.headYaw() - locked.bodyYaw()); + state.pitch = locked.pitch(); + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/MinecraftClientMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/MinecraftClientMixin.java new file mode 100644 index 0000000..eee3c03 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/MinecraftClientMixin.java @@ -0,0 +1,25 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public abstract class MinecraftClientMixin { + @Inject(method = "setScreen", at = @At("HEAD"), cancellable = true) + private void afw$blockHandledScreens(Screen screen, CallbackInfo ci) { + if (screen instanceof HandledScreen) { + MinecraftClient client = (MinecraftClient)(Object)this; + if (client.player != null) { + if (ClientAnimationRuntime.isActorPendingOrActive(client.player.getUuid())) { + ci.cancel(); + } + } + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/MouseScrollAnimationZoomMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/MouseScrollAnimationZoomMixin.java new file mode 100644 index 0000000..c453577 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/MouseScrollAnimationZoomMixin.java @@ -0,0 +1,25 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.camera.AnimationCameraZoom; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.Mouse; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Mouse.class) +public abstract class MouseScrollAnimationZoomMixin { + @Shadow + @Final + private MinecraftClient client; + + @Inject(method = "onMouseScroll", at = @At("HEAD"), cancellable = true) + private void afw$zoomAnimationCamera(long window, double horizontal, double vertical, CallbackInfo ci) { + if (AnimationCameraZoom.handleScroll(this.client, vertical)) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/mixin/client/WorldRendererFirstPersonActorMixin.java b/src/main/java/com/nonid/internal/animation/mixin/client/WorldRendererFirstPersonActorMixin.java new file mode 100644 index 0000000..1f0baa2 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/mixin/client/WorldRendererFirstPersonActorMixin.java @@ -0,0 +1,32 @@ +package com.nonid.internal.animation.mixin.client; + +import com.nonid.internal.animation.client.runtime.ClientAnimationRuntime; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(WorldRenderer.class) +public class WorldRendererFirstPersonActorMixin { + @Redirect(method = "fillEntityRenderStates", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;isThirdPerson()Z")) + private boolean afw$keepFocusedActorRenderableInFirstPerson(Camera camera) { + boolean thirdPerson = camera.isThirdPerson(); + if (thirdPerson) { + return true; + } + + if (camera.getFocusedEntity() instanceof PlayerEntity player) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + return !player.getUuid().equals(client.player.getUuid()) ? false : ClientAnimationRuntime.isActorPendingOrActive(player.getUuid()); + } else { + return false; + } + } else { + return false; + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/AdjustAnimationSpeedC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/AdjustAnimationSpeedC2SPayload.java new file mode 100644 index 0000000..8dcf6fd --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/AdjustAnimationSpeedC2SPayload.java @@ -0,0 +1,27 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record AdjustAnimationSpeedC2SPayload(UUID instanceId, double multiplier) implements CustomPayload { + public static final Identifier ID_RAW = AnimationEngine.runtimeId("adjust_animation_speed"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + AdjustAnimationSpeedC2SPayload::instanceId, + PacketCodecs.DOUBLE, + AdjustAnimationSpeedC2SPayload::multiplier, + AdjustAnimationSpeedC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/AdvanceAnimationStageS2CPayload.java b/src/main/java/com/nonid/internal/animation/network/AdvanceAnimationStageS2CPayload.java new file mode 100644 index 0000000..257d780 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/AdvanceAnimationStageS2CPayload.java @@ -0,0 +1,31 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record AdvanceAnimationStageS2CPayload(UUID instanceId, long advanceTick, long stageEndTick, int stageIndex) implements CustomPayload { + public static final Identifier ADVANCE_STAGE_ID = AnimationEngine.runtimeId("advance_stage"); + public static final Id ID = new Id(ADVANCE_STAGE_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + AdvanceAnimationStageS2CPayload::instanceId, + PacketCodecs.LONG, + AdvanceAnimationStageS2CPayload::advanceTick, + PacketCodecs.LONG, + AdvanceAnimationStageS2CPayload::stageEndTick, + PacketCodecs.INTEGER, + AdvanceAnimationStageS2CPayload::stageIndex, + AdvanceAnimationStageS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/AnimationSpeedUpdateS2CPayload.java b/src/main/java/com/nonid/internal/animation/network/AnimationSpeedUpdateS2CPayload.java new file mode 100644 index 0000000..55f3b5a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/AnimationSpeedUpdateS2CPayload.java @@ -0,0 +1,31 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record AnimationSpeedUpdateS2CPayload(UUID instanceId, double speed, long stageStartTick, long stageEndTick) implements CustomPayload { + public static final Identifier ID_RAW = AnimationEngine.runtimeId("animation_speed_update"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + AnimationSpeedUpdateS2CPayload::instanceId, + PacketCodecs.DOUBLE, + AnimationSpeedUpdateS2CPayload::speed, + PacketCodecs.LONG, + AnimationSpeedUpdateS2CPayload::stageStartTick, + PacketCodecs.LONG, + AnimationSpeedUpdateS2CPayload::stageEndTick, + AnimationSpeedUpdateS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/AnimationStageInfo.java b/src/main/java/com/nonid/internal/animation/network/AnimationStageInfo.java new file mode 100644 index 0000000..b93d995 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/AnimationStageInfo.java @@ -0,0 +1,318 @@ +package com.nonid.internal.animation.network; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.nonid.api.client.NonAnimationRenderEvents; +import com.nonid.internal.animation.util.StageTimeWarp; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import net.minecraft.item.ItemConvertible; +import net.minecraft.item.ItemDisplayContext; +import net.minecraft.item.ItemStack; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record AnimationStageInfo( + Identifier animationId, + boolean loop, + long lengthTicks, + boolean allowJoin, + double speed, + Identifier playbackAnimationId, + List actorProps, + double cycleMidpointOffsetSeconds, + double cycleTicks +) { + public static final Codec DATA_CODEC = RecordCodecBuilder.create( + instance -> instance.group( + Identifier.CODEC.fieldOf("id").forGetter(AnimationStageInfo::animationId), + Codec.BOOL.optionalFieldOf("loop", true).forGetter(AnimationStageInfo::loop), + Codec.DOUBLE.optionalFieldOf("cycle_seconds", 0.0).forGetter(stage -> exactTicksToSeconds(stage.cycleTicks())), + Codec.BOOL.optionalFieldOf("allow_join", true).forGetter(AnimationStageInfo::allowJoin), + Codec.DOUBLE.optionalFieldOf("speed", 1.0).forGetter(AnimationStageInfo::speed), + Codec.DOUBLE.optionalFieldOf("cycle_midpoint_offset_seconds", 0.0).forGetter(AnimationStageInfo::cycleMidpointOffsetSeconds) + ) + .apply( + instance, + (animationId, loop, cycleSeconds, allowJoin, speed, offset) -> new AnimationStageInfo( + animationId, loop, secondsToTicks(cycleSeconds), allowJoin, speed, animationId, List.of(), offset, secondsToExactTicks(cycleSeconds) + ) + ) + ); + public static final PacketCodec CODEC = PacketCodec.tuple( + Identifier.PACKET_CODEC.cast(), + AnimationStageInfo::animationId, + PacketCodecs.BOOLEAN, + AnimationStageInfo::loop, + PacketCodecs.LONG, + AnimationStageInfo::lengthTicks, + PacketCodecs.BOOLEAN, + AnimationStageInfo::allowJoin, + PacketCodecs.DOUBLE, + AnimationStageInfo::speed, + Identifier.PACKET_CODEC.cast(), + AnimationStageInfo::playbackAnimationId, + AnimationStageInfo.ActorPropEntry.CODEC.collect(PacketCodecs.toList()).xmap(List::copyOf, list -> list).cast(), + AnimationStageInfo::actorProps, + PacketCodecs.DOUBLE, + AnimationStageInfo::cycleMidpointOffsetSeconds, + PacketCodecs.DOUBLE, + AnimationStageInfo::cycleTicks, + AnimationStageInfo::new + ); + + public AnimationStageInfo { + if (animationId == null) { + throw new IllegalArgumentException("animationId must not be null"); + } + + if (playbackAnimationId == null) { + playbackAnimationId = animationId; + } + + if (lengthTicks < 0L) { + lengthTicks = 0L; + } + + if (!Double.isFinite(cycleTicks) || cycleTicks <= 0.0) { + cycleTicks = lengthTicks > 0L ? lengthTicks : 0.0; + } + + actorProps = sanitizeActorProps(actorProps); + if (loop && !(cycleTicks <= 0.0) && Double.isFinite(cycleMidpointOffsetSeconds)) { + cycleMidpointOffsetSeconds = StageTimeWarp.clampOffsetSeconds(cycleTicks, cycleMidpointOffsetSeconds); + } else { + cycleMidpointOffsetSeconds = 0.0; + } + } + + public AnimationStageInfo(Identifier animationId, boolean loop, long lengthTicks, boolean allowJoin, double speed) { + this(animationId, loop, lengthTicks, allowJoin, speed, animationId, List.of(), 0.0, lengthTicks); + } + + public AnimationStageInfo(Identifier animationId, boolean loop, long lengthTicks, boolean allowJoin, double speed, Identifier playbackAnimationId) { + this(animationId, loop, lengthTicks, allowJoin, speed, playbackAnimationId, List.of(), 0.0, lengthTicks); + } + + public AnimationStageInfo( + Identifier animationId, + boolean loop, + long lengthTicks, + boolean allowJoin, + double speed, + Identifier playbackAnimationId, + List actorProps + ) { + this(animationId, loop, lengthTicks, allowJoin, speed, playbackAnimationId, actorProps, 0.0, lengthTicks); + } + + public AnimationStageInfo( + Identifier animationId, + boolean loop, + long lengthTicks, + boolean allowJoin, + double speed, + Identifier playbackAnimationId, + List actorProps, + double cycleMidpointOffsetSeconds + ) { + this(animationId, loop, lengthTicks, allowJoin, speed, playbackAnimationId, actorProps, cycleMidpointOffsetSeconds, lengthTicks); + } + + private static long secondsToTicks(double seconds) { + return Double.isFinite(seconds) && !(seconds <= 0.0) ? Math.max(1L, (long)Math.ceil(seconds * 20.0)) : 0L; + } + + private static double ticksToSeconds(long ticks) { + return ticks <= 0L ? 0.0 : ticks / 20.0; + } + + private static double secondsToExactTicks(double seconds) { + return Double.isFinite(seconds) && !(seconds <= 0.0) ? seconds * 20.0 : 0.0; + } + + private static double exactTicksToSeconds(double ticks) { + return Double.isFinite(ticks) && !(ticks <= 0.0) ? ticks / 20.0 : 0.0; + } + + @Nullable + public AnimationStageInfo.ActorPropEntry actorPropsForKey(@Nullable String actorKey) { + if (actorKey != null && !actorKey.isBlank() && !this.actorProps.isEmpty()) { + for (AnimationStageInfo.ActorPropEntry entry : this.actorProps) { + if (entry != null && actorKey.equals(entry.actorKey())) { + return entry; + } + } + + return null; + } else { + return null; + } + } + + public AnimationStageInfo withActorProp(String actorKey, AnimationStageInfo.PropSlot slot, Identifier itemId) { + if (actorKey != null && !actorKey.isBlank() && slot != null && itemId != null) { + List updated = new ArrayList<>(this.actorProps); + boolean replaced = false; + + for (int i = 0; i < updated.size(); i++) { + AnimationStageInfo.ActorPropEntry entry = updated.get(i); + if (entry != null && actorKey.equals(entry.actorKey())) { + updated.set(i, withProp(entry, slot, itemId)); + replaced = true; + break; + } + } + + if (!replaced) { + updated.add(withProp(new AnimationStageInfo.ActorPropEntry(actorKey, null, null, null), slot, itemId)); + } + + return new AnimationStageInfo( + this.animationId, + this.loop, + this.lengthTicks, + this.allowJoin, + this.speed, + this.playbackAnimationId, + updated, + this.cycleMidpointOffsetSeconds, + this.cycleTicks + ); + } else { + return this; + } + } + + private static AnimationStageInfo.ActorPropEntry withProp(AnimationStageInfo.ActorPropEntry entry, AnimationStageInfo.PropSlot slot, Identifier itemId) { + return switch (slot) { + case LEFT -> new AnimationStageInfo.ActorPropEntry(entry.actorKey(), itemId, entry.propRightItemId(), entry.propFloorItemId()); + case RIGHT -> new AnimationStageInfo.ActorPropEntry(entry.actorKey(), entry.propLeftItemId(), itemId, entry.propFloorItemId()); + case FLOOR -> new AnimationStageInfo.ActorPropEntry(entry.actorKey(), entry.propLeftItemId(), entry.propRightItemId(), itemId); + }; + } + + public Identifier effectiveAnimationId() { + return this.playbackAnimationId; + } + + private static List sanitizeActorProps(List props) { + if (props != null && !props.isEmpty()) { + Map dedup = new LinkedHashMap<>(); + + for (AnimationStageInfo.ActorPropEntry entry : props) { + if (entry != null) { + String key = entry.actorKey() == null ? "" : entry.actorKey().trim(); + if (!key.isBlank()) { + Identifier left = entry.propLeftItemId(); + Identifier right = entry.propRightItemId(); + Identifier floor = entry.propFloorItemId(); + if (left != null || right != null || floor != null) { + dedup.put(key, new AnimationStageInfo.ActorPropEntry(key, left, right, floor)); + } + } + } + } + + if (dedup.isEmpty()) { + return List.of(); + } + + List out = new ArrayList<>(dedup.values()); + return List.copyOf(out); + } else { + return List.of(); + } + } + + public record ActorPropEntry( + String actorKey, @Nullable Identifier propLeftItemId, @Nullable Identifier propRightItemId, @Nullable Identifier propFloorItemId + ) { + private static final PacketCodec NULLABLE_IDENTIFIER_STRING_CODEC = PacketCodecs.STRING.cast(); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.STRING, + AnimationStageInfo.ActorPropEntry::actorKey, + NULLABLE_IDENTIFIER_STRING_CODEC, + entry -> identifierToString(entry.propLeftItemId()), + NULLABLE_IDENTIFIER_STRING_CODEC, + entry -> identifierToString(entry.propRightItemId()), + NULLABLE_IDENTIFIER_STRING_CODEC, + entry -> identifierToString(entry.propFloorItemId()), + (actorKey, leftId, rightId, floorId) -> new AnimationStageInfo.ActorPropEntry( + actorKey, parseIdentifier(leftId), parseIdentifier(rightId), parseIdentifier(floorId) + ) + ); + + public ActorPropEntry { + actorKey = actorKey == null ? "" : actorKey.trim(); + } + + public boolean isEmpty() { + return this.propLeftItemId == null && this.propRightItemId == null && this.propFloorItemId == null; + } + + @Nullable + public ItemStack leftStack() { + return createStack(this.propLeftItemId); + } + + @Nullable + public ItemStack rightStack() { + return createStack(this.propRightItemId); + } + + @Nullable + public ItemStack floorStack() { + return createStack(this.propFloorItemId); + } + + @Nullable + public NonAnimationRenderEvents.BoneItemProp leftBoneProp() { + ItemStack stack = this.leftStack(); + return stack != null && !stack.isEmpty() ? new NonAnimationRenderEvents.BoneItemProp(stack, ItemDisplayContext.THIRD_PERSON_LEFT_HAND) : null; + } + + @Nullable + public NonAnimationRenderEvents.BoneItemProp rightBoneProp() { + ItemStack stack = this.rightStack(); + return stack != null && !stack.isEmpty() ? new NonAnimationRenderEvents.BoneItemProp(stack, ItemDisplayContext.THIRD_PERSON_RIGHT_HAND) : null; + } + + @Nullable + public NonAnimationRenderEvents.BoneItemProp floorBoneProp() { + ItemStack stack = this.floorStack(); + return stack != null && !stack.isEmpty() ? new NonAnimationRenderEvents.BoneItemProp(stack, ItemDisplayContext.GROUND) : null; + } + + @Nullable + private static ItemStack createStack(@Nullable Identifier itemId) { + return itemId != null && Registries.ITEM.containsId(itemId) ? new ItemStack((ItemConvertible)Registries.ITEM.get(itemId)) : null; + } + + @Nullable + private static Identifier parseIdentifier(String raw) { + if (raw != null && !raw.isBlank()) { + Identifier id = Identifier.tryParse(raw); + return id != null && Registries.ITEM.containsId(id) ? id : null; + } else { + return null; + } + } + + private static String identifierToString(@Nullable Identifier id) { + return id == null ? "" : id.toString(); + } + } + + public enum PropSlot { + LEFT, + RIGHT, + FLOOR; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/DebugAdvanceStageC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/DebugAdvanceStageC2SPayload.java new file mode 100644 index 0000000..687c30b --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/DebugAdvanceStageC2SPayload.java @@ -0,0 +1,22 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record DebugAdvanceStageC2SPayload(UUID instanceId) implements CustomPayload { + public static final Identifier DEBUG_ADVANCE_STAGE_ID = AnimationEngine.runtimeId("debug_advance_stage"); + public static final Id ID = new Id(DEBUG_ADVANCE_STAGE_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, DebugAdvanceStageC2SPayload::instanceId, DebugAdvanceStageC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/DebugChatPreferenceC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/DebugChatPreferenceC2SPayload.java new file mode 100644 index 0000000..1ec067a --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/DebugChatPreferenceC2SPayload.java @@ -0,0 +1,28 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DebugChatPreferenceC2SPayload(String modeId) implements CustomPayload { + public static final Identifier DEBUG_CHAT_PREF_ID = AnimationEngine.runtimeId("debug_chat_pref"); + public static final Id ID = new Id(DEBUG_CHAT_PREF_ID); + public static final PacketCodec CODEC = PacketCodec.of( + DebugChatPreferenceC2SPayload::encode, DebugChatPreferenceC2SPayload::decode + ); + + public Id getId() { + return ID; + } + + private static void encode(DebugChatPreferenceC2SPayload payload, RegistryByteBuf buf) { + buf.writeString(payload.modeId()); + } + + private static DebugChatPreferenceC2SPayload decode(RegistryByteBuf buf) { + return new DebugChatPreferenceC2SPayload(buf.readString(64)); + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/DebugJoinAnimationC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/DebugJoinAnimationC2SPayload.java new file mode 100644 index 0000000..c7ea61e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/DebugJoinAnimationC2SPayload.java @@ -0,0 +1,28 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.List; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record DebugJoinAnimationC2SPayload(UUID instanceId, List actorEntityIds) implements CustomPayload { + public static final Identifier DEBUG_JOIN_ANIMATION_ID = AnimationEngine.runtimeId("debug_join_animation"); + public static final Id ID = new Id(DEBUG_JOIN_ANIMATION_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + DebugJoinAnimationC2SPayload::instanceId, + PacketCodecs.INTEGER.collect(PacketCodecs.toList(32)), + DebugJoinAnimationC2SPayload::actorEntityIds, + DebugJoinAnimationC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/DebugStartAnimationC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/DebugStartAnimationC2SPayload.java new file mode 100644 index 0000000..ef3704f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/DebugStartAnimationC2SPayload.java @@ -0,0 +1,31 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.List; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DebugStartAnimationC2SPayload(List actorEntityIds, String damageBehaviorId, boolean ignoreAttackers, int anchorEntityId) + implements CustomPayload { + public static final Identifier DEBUG_START_ANIMATION_ID = AnimationEngine.runtimeId("debug_start_animation"); + public static final Id ID = new Id(DEBUG_START_ANIMATION_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.INTEGER.collect(PacketCodecs.toList(32)), + DebugStartAnimationC2SPayload::actorEntityIds, + PacketCodecs.STRING, + DebugStartAnimationC2SPayload::damageBehaviorId, + PacketCodecs.BOOLEAN, + DebugStartAnimationC2SPayload::ignoreAttackers, + PacketCodecs.INTEGER, + DebugStartAnimationC2SPayload::anchorEntityId, + DebugStartAnimationC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/DebugStopAllAnimationsC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/DebugStopAllAnimationsC2SPayload.java new file mode 100644 index 0000000..a7435c3 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/DebugStopAllAnimationsC2SPayload.java @@ -0,0 +1,18 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DebugStopAllAnimationsC2SPayload() implements CustomPayload { + public static final Identifier DEBUG_STOP_ALL_ID = AnimationEngine.runtimeId("debug_stop_all"); + public static final Id ID = new Id(DEBUG_STOP_ALL_ID); + public static final PacketCodec CODEC = PacketCodec.unit(new DebugStopAllAnimationsC2SPayload()); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/DebugStopAnimationC2SPayload.java b/src/main/java/com/nonid/internal/animation/network/DebugStopAnimationC2SPayload.java new file mode 100644 index 0000000..25ce541 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/DebugStopAnimationC2SPayload.java @@ -0,0 +1,22 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record DebugStopAnimationC2SPayload(UUID instanceId) implements CustomPayload { + public static final Identifier DEBUG_STOP_ANIMATION_ID = AnimationEngine.runtimeId("debug_stop_animation"); + public static final Id ID = new Id(DEBUG_STOP_ANIMATION_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, DebugStopAnimationC2SPayload::instanceId, DebugStopAnimationC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/StartAnimationS2CPayload.java b/src/main/java/com/nonid/internal/animation/network/StartAnimationS2CPayload.java new file mode 100644 index 0000000..7b1b388 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/StartAnimationS2CPayload.java @@ -0,0 +1,227 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +public record StartAnimationS2CPayload( + Identifier animationId, + List> modelRootsByActor, + UUID instanceId, + List actorUuids, + List actorKeys, + List stages, + long startTick, + int stageIndex, + long stageStartTick, + long stageEndTick, + double speed, + boolean lockOrientation, + float lockedYaw, + float lockedHeadYaw, + float lockedPitch, + @Nullable Vec3d cameraOrbitTarget +) implements CustomPayload { + public static final Identifier START_ANIMATION_ID = AnimationEngine.runtimeId("start_animation"); + public static final Id ID = new Id(START_ANIMATION_ID); + private static final int MAX_ACTORS = 16; + private static final int MAX_STAGES = 16; + private static final int MAX_MODEL_ROOTS_PER_ACTOR = 32; + public static final PacketCodec CODEC = PacketCodec.of( + StartAnimationS2CPayload::encode, StartAnimationS2CPayload::decode + ); + + public StartAnimationS2CPayload { + actorUuids = List.copyOf(actorUuids); + modelRootsByActor = modelRootsByActor != null && !modelRootsByActor.isEmpty() + ? modelRootsByActor.stream().map(rootsx -> rootsx == null ? List.of() : List.copyOf(rootsx)).toList() + : Collections.nCopies(actorUuids.size(), List.of()); + actorKeys = actorKeys == null ? List.of() : List.copyOf(actorKeys); + stages = stages == null ? List.of() : List.copyOf(stages); + if (actorUuids.size() > 16) { + throw new IllegalArgumentException("Too many actor UUIDs: " + actorUuids.size()); + } + + if (modelRootsByActor.size() != actorUuids.size()) { + throw new IllegalArgumentException("modelRootsByActor size mismatch: " + modelRootsByActor.size()); + } + + for (List roots : modelRootsByActor) { + if (roots.size() > 32) { + throw new IllegalArgumentException("Too many model roots for actor: " + roots.size()); + } + } + + if (!actorKeys.isEmpty() && actorKeys.size() != actorUuids.size()) { + throw new IllegalArgumentException("actorKeys size mismatch: " + actorKeys.size()); + } + + if (stages.size() > 16) { + throw new IllegalArgumentException("Too many stages: " + stages.size()); + } + + stageIndex = stages.isEmpty() ? 0 : Math.max(0, Math.min(stageIndex, stages.size() - 1)); + stageStartTick = Math.max(startTick, stageStartTick); + if (Double.isNaN(speed) || Double.isInfinite(speed) || speed <= 0.0) { + speed = 1.0; + } + + if (!lockOrientation) { + lockedYaw = 0.0F; + lockedHeadYaw = 0.0F; + lockedPitch = 0.0F; + } + + if (cameraOrbitTarget != null && !cameraOrbitTarget.isFinite()) { + cameraOrbitTarget = null; + } + } + + public Id getId() { + return ID; + } + + private static void encode(StartAnimationS2CPayload payload, RegistryByteBuf buf) { + Identifier.PACKET_CODEC.encode(buf, payload.animationId()); + Uuids.PACKET_CODEC.encode(buf, payload.instanceId()); + buf.writeVarInt(payload.actorUuids().size()); + + for (UUID uuid : payload.actorUuids()) { + Uuids.PACKET_CODEC.encode(buf, uuid); + } + + for (List roots : payload.modelRootsByActor()) { + buf.writeVarInt(roots.size()); + + for (Identifier root : roots) { + Identifier.PACKET_CODEC.encode(buf, root); + } + } + + buf.writeVarInt(payload.actorKeys().size()); + + for (String key : payload.actorKeys()) { + buf.writeString(key); + } + + buf.writeVarInt(payload.stages().size()); + + for (AnimationStageInfo stage : payload.stages()) { + AnimationStageInfo.CODEC.encode(buf, stage); + } + + buf.writeLong(payload.startTick()); + buf.writeVarInt(payload.stageIndex()); + buf.writeLong(payload.stageStartTick()); + buf.writeLong(payload.stageEndTick()); + buf.writeDouble(payload.speed()); + buf.writeBoolean(payload.lockOrientation()); + buf.writeFloat(payload.lockedYaw()); + buf.writeFloat(payload.lockedHeadYaw()); + buf.writeFloat(payload.lockedPitch()); + buf.writeBoolean(payload.cameraOrbitTarget() != null); + if (payload.cameraOrbitTarget() != null) { + Vec3d target = payload.cameraOrbitTarget(); + buf.writeDouble(target.x); + buf.writeDouble(target.y); + buf.writeDouble(target.z); + } + } + + private static StartAnimationS2CPayload decode(RegistryByteBuf buf) { + Identifier animationId = (Identifier)Identifier.PACKET_CODEC.decode(buf); + UUID instanceId = (UUID)Uuids.PACKET_CODEC.decode(buf); + int actorCount = buf.readVarInt(); + if (actorCount >= 0 && actorCount <= 16) { + List actorUuids = new ArrayList<>(actorCount); + + for (int i = 0; i < actorCount; i++) { + actorUuids.add((UUID)Uuids.PACKET_CODEC.decode(buf)); + } + + List> modelRootsByActor = new ArrayList<>(actorCount); + + for (int i = 0; i < actorCount; i++) { + int rootCount = buf.readVarInt(); + if (rootCount < 0 || rootCount > 32) { + throw new IllegalArgumentException("Too many model roots for actor: " + rootCount); + } + + List roots = new ArrayList<>(rootCount); + + for (int rootIndex = 0; rootIndex < rootCount; rootIndex++) { + roots.add((Identifier)Identifier.PACKET_CODEC.decode(buf)); + } + + modelRootsByActor.add(List.copyOf(roots)); + } + + int keyCount = buf.readVarInt(); + if (keyCount >= 0 && keyCount <= 16) { + List actorKeys = new ArrayList<>(keyCount); + + for (int i = 0; i < keyCount; i++) { + actorKeys.add(buf.readString(128)); + } + + int stageCount = buf.readVarInt(); + if (stageCount >= 0 && stageCount <= 16) { + List stages = new ArrayList<>(stageCount); + + for (int i = 0; i < stageCount; i++) { + stages.add((AnimationStageInfo)AnimationStageInfo.CODEC.decode(buf)); + } + + long startTick = buf.readLong(); + int stageIndex = buf.readVarInt(); + long stageStartTick = buf.readLong(); + long stageEndTick = buf.readLong(); + double speed = buf.readDouble(); + boolean lockOrientation = buf.readBoolean(); + float lockedYaw = buf.readFloat(); + float lockedHeadYaw = buf.readFloat(); + float lockedPitch = buf.readFloat(); + Vec3d cameraOrbitTarget = null; + if (buf.readBoolean()) { + cameraOrbitTarget = new Vec3d(buf.readDouble(), buf.readDouble(), buf.readDouble()); + } + + return new StartAnimationS2CPayload( + animationId, + modelRootsByActor, + instanceId, + actorUuids, + actorKeys, + stages, + startTick, + stageIndex, + stageStartTick, + stageEndTick, + speed, + lockOrientation, + lockedYaw, + lockedHeadYaw, + lockedPitch, + cameraOrbitTarget + ); + } else { + throw new IllegalArgumentException("Too many stages: " + stageCount); + } + } else { + throw new IllegalArgumentException("Too many actor keys: " + keyCount); + } + } else { + throw new IllegalArgumentException("Too many actor UUIDs: " + actorCount); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/StopAllAnimationsS2CPayload.java b/src/main/java/com/nonid/internal/animation/network/StopAllAnimationsS2CPayload.java new file mode 100644 index 0000000..aabaaa6 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/StopAllAnimationsS2CPayload.java @@ -0,0 +1,21 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record StopAllAnimationsS2CPayload(long stopTick) implements CustomPayload { + public static final Identifier STOP_ALL_ID = AnimationEngine.runtimeId("stop_all_animations"); + public static final Id ID = new Id(STOP_ALL_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.LONG, StopAllAnimationsS2CPayload::stopTick, StopAllAnimationsS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/network/StopAnimationS2CPayload.java b/src/main/java/com/nonid/internal/animation/network/StopAnimationS2CPayload.java new file mode 100644 index 0000000..193bbd3 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/network/StopAnimationS2CPayload.java @@ -0,0 +1,23 @@ +package com.nonid.internal.animation.network; + +import com.nonid.internal.animation.AnimationEngine; +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record StopAnimationS2CPayload(UUID instanceId, long stopTick) implements CustomPayload { + public static final Identifier STOP_ANIMATION_ID = AnimationEngine.runtimeId("stop_animation"); + public static final Id ID = new Id(STOP_ANIMATION_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, StopAnimationS2CPayload::instanceId, PacketCodecs.LONG, StopAnimationS2CPayload::stopTick, StopAnimationS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/internal/animation/server/AnimationSessionData.java b/src/main/java/com/nonid/internal/animation/server/AnimationSessionData.java new file mode 100644 index 0000000..7e4ce60 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/server/AnimationSessionData.java @@ -0,0 +1,4 @@ +package com.nonid.internal.animation.server; + +public interface AnimationSessionData { +} diff --git a/src/main/java/com/nonid/internal/animation/server/AnimationStageTiming.java b/src/main/java/com/nonid/internal/animation/server/AnimationStageTiming.java new file mode 100644 index 0000000..fdda925 --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/server/AnimationStageTiming.java @@ -0,0 +1,76 @@ +package com.nonid.internal.animation.server; + +import com.nonid.NeedsOfNature; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.util.AnimationIds; +import java.util.Objects; +import net.minecraft.util.Identifier; + +final class AnimationStageTiming { + static final long NO_AUTOMATIC_END = -1L; + + private AnimationStageTiming() { + } + + static long resolveDurationTicks(Identifier animationId, AnimationStageInfo stage, double speed) { + if (stage == null) { + return -1L; + } else { + double safeSpeed = Double.isFinite(speed) && speed > 0.0 ? speed : 1.0; + if (!stage.loop()) { + double authoredTicks = exactCycleTicks(stage); + return authoredTicks <= 0.0 ? -1L : Math.max(1L, (long)Math.ceil(authoredTicks / safeSpeed)); + } else { + long targetTicks = resolveLoopTargetTicks(animationId, stage.animationId()); + if (targetTicks < 0L) { + return -1L; + } else { + double cycleTicks = exactCycleTicks(stage); + if (cycleTicks <= 0.0) { + return targetTicks; + } else { + double worldCycleTicks = cycleTicks / safeSpeed; + if (Double.isFinite(worldCycleTicks) && !(worldCycleTicks <= 0.0)) { + long cycles = Math.max(1L, Math.round(targetTicks / worldCycleTicks)); + return Math.max(1L, Math.round(cycles * worldCycleTicks)); + } else { + return targetTicks; + } + } + } + } + } + } + + private static long resolveLoopTargetTicks(Identifier animationId, Identifier stageId) { + Identifier baseId = AnimationIds.baseId(animationId == null ? stageId : animationId); + Integer seconds = AnimationGameplayMetadata.stageSeconds(stageId); + if (seconds == null) { + seconds = AnimationGameplayMetadata.stageSeconds(baseId); + } + + if (seconds != null && seconds < 0) { + return -1L; + } + + if (seconds == null) { + Integer peakStage = AnimationGameplayMetadata.peakStage(baseId); + seconds = Objects.equals(AnimationIds.stageNumber(stageId), peakStage) + ? NeedsOfNature.getConfig().getPeakLoopProgressSeconds() + : NeedsOfNature.getConfig().getLoopProgressSeconds(); + } + + double multiplier = AnimationGameplayMetadata.stageDurationMultiplier(stageId); + return Math.max(1L, Math.round(seconds.intValue() * 20.0 * multiplier)); + } + + private static double exactCycleTicks(AnimationStageInfo stage) { + double metadataTicks = AnimationGameplayMetadata.cycleTicks(stage.animationId()); + if (metadataTicks > 0.0) { + return metadataTicks; + } else { + return Double.isFinite(stage.cycleTicks()) && stage.cycleTicks() > 0.0 ? stage.cycleTicks() : Math.max(0L, stage.lengthTicks()); + } + } +} diff --git a/src/main/java/com/nonid/internal/animation/server/AnimationStartContext.java b/src/main/java/com/nonid/internal/animation/server/AnimationStartContext.java new file mode 100644 index 0000000..e90bf9f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/server/AnimationStartContext.java @@ -0,0 +1,39 @@ +package com.nonid.internal.animation.server; + +import java.util.Map; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record AnimationStartContext( + Map addonMetadata, @Nullable AnimationSessionData sessionData, @Nullable Identifier joinPredecessor, boolean queueContinuation +) { + private static final AnimationStartContext EMPTY = new AnimationStartContext(Map.of(), null, null, false); + + public AnimationStartContext { + addonMetadata = addonMetadata != null && !addonMetadata.isEmpty() ? Map.copyOf(addonMetadata) : Map.of(); + } + + public static AnimationStartContext empty() { + return EMPTY; + } + + public static AnimationStartContext addonMetadata(@Nullable Map metadata) { + return metadata != null && !metadata.isEmpty() ? new AnimationStartContext(metadata, null, null, false) : EMPTY; + } + + public AnimationStartContext withSessionData(@Nullable AnimationSessionData data) { + return new AnimationStartContext(this.addonMetadata, data, this.joinPredecessor, this.queueContinuation); + } + + public AnimationStartContext withJoinPredecessor(@Nullable Identifier predecessor) { + return new AnimationStartContext(this.addonMetadata, this.sessionData, predecessor, this.queueContinuation); + } + + public AnimationStartContext asQueueContinuation() { + return this.queueContinuation ? this : new AnimationStartContext(this.addonMetadata, this.sessionData, this.joinPredecessor, true); + } + + public boolean isJoinReplacement() { + return this.joinPredecessor != null; + } +} diff --git a/src/main/java/com/nonid/internal/animation/server/ServerAnimationController.java b/src/main/java/com/nonid/internal/animation/server/ServerAnimationController.java new file mode 100644 index 0000000..3fd11dc --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/server/ServerAnimationController.java @@ -0,0 +1,4466 @@ +package com.nonid.internal.animation.server; + +import com.nonid.NeedsOfNature; +import com.nonid.NonDebugChatCategory; +import com.nonid.api.animation.NonAnimationDamageBehavior; +import com.nonid.internal.animation.AnimationEngine; +import com.nonid.internal.animation.api.AnimationLifecycleEvents; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AdvanceAnimationStageS2CPayload; +import com.nonid.internal.animation.network.AnimationSpeedUpdateS2CPayload; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.internal.animation.network.StartAnimationS2CPayload; +import com.nonid.internal.animation.network.StopAnimationS2CPayload; +import com.nonid.internal.animation.util.WeightedPicker; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDamage; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AfterDeath; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.ServerStarted; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.Disconnect; +import net.minecraft.block.BedBlock; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.SlabBlock; +import net.minecraft.block.enums.BedPart; +import net.minecraft.block.enums.SlabType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.damage.DamageTypes; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.tag.BlockTags; +import net.minecraft.registry.tag.FluidTags; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.state.property.Property; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.function.BooleanBiFunction; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult.Type; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.BlockPos.Mutable; +import net.minecraft.util.math.Direction.Axis; +import net.minecraft.util.math.random.Random; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.RaycastContext; +import net.minecraft.world.World; +import net.minecraft.world.RaycastContext.FluidHandling; +import net.minecraft.world.RaycastContext.ShapeType; +import org.jetbrains.annotations.Nullable; + +public final class ServerAnimationController { + private static final double DEBUG_MULTI_PLAYER_REQUESTER_MAX_DIST = 5.0; + private static final int BLOCK_SCAN_VERTICAL_RANGE = 1; + private static final double HALF_HEIGHT_SUPPORT_TOP_OFFSET = 0.5625; + private static final boolean BLOCK_REQUIRE_SOLID_BELOW = true; + private static final double FLOOR_MIN_HEIGHT = 0.875; + private static final double FLOOR_SUPPORT_INSET = 0.0625; + private static final double FLOOR_SUPPORT_THICKNESS = 0.0625; + private static final double WALL_CORE_INSET = 0.0625; + private static final double WALL_CORE_MIN_HEIGHT = 0.875; + private static final double WALL_EDGE_EPSILON = 1.0E-6; + private static final double WALL_ANCHOR_DISTANCE = 0.5; + private static final double WALL_BROAD_FACE_MIN_SPAN = 0.5; + private static final double GROUND_PLACEMENT_SEARCH_RADIUS = 2.0; + private static final double GROUND_PLACEMENT_SEARCH_STEP = 0.5; + private static final double GROUND_PLACEMENT_CLEARANCE_HALF_EXTENT = 0.75; + private static final double GROUND_PLACEMENT_CLEARANCE_HEIGHT = 2.0; + private static final double GROUND_PLACEMENT_FLOOR_EPSILON = 0.01; + private static final double PLAYER_LOCK_EPSILON_SQ = 4.0E-4; + private static final int IN_WALL_GRACE_TICKS = 5; + private static final int PLAYER_NUDGE_INTERVAL_TICKS = 4; + private static final int PLAYER_NUDGE_MAX_MOBS_PER_PLAYER = 6; + private static final double PLAYER_NUDGE_SCAN_RADIUS = 1.0; + private static final double PLAYER_NUDGE_MIN_DISTANCE = 0.85; + private static final double PLAYER_NUDGE_STRENGTH = 0.08; + private static final double PLAYER_NUDGE_EPSILON = 1.0E-6; + private static final int RECENT_NO_RESTORE_TRANSITION_TICKS = 20; + private static final Identifier BEDS_BLOCK_TAG_ID = Identifier.of("minecraft", "beds"); + private static final TagKey WALL_SURFACES_BLOCK_TAG = TagKey.of(RegistryKeys.BLOCK, AnimationEngine.packId("wall_surfaces")); + private static final Direction[] HORIZONTAL_DIRECTIONS = new Direction[]{Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST}; + private static final Map, Map> IN_WALL_GRACE_BY_WORLD = new HashMap<>(); + private static final Map, Map> RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD = new HashMap<>(); + private static final String ANIMATION_NOAI_TAG = "afw_noai"; + private static final String ANIMATION_NOAI_ORIG_TAG = "afw_noai_orig"; + private static final int NOAI_CLEANUP_INTERVAL_TICKS = 6000; + private static final int STARTUP_NOAI_CLEANUP_TICKS = 20; + private static final int STARTUP_START_BLOCK_TICKS = 40; + private static final List GROUND_PLACEMENT_OFFSETS = createGroundPlacementOffsets(); + private static boolean initialized = false; + private static int STARTUP_NOAI_CLEANUP_UNTIL_TICK = Integer.MIN_VALUE; + private static int STARTUP_START_BLOCK_UNTIL_TICK = Integer.MIN_VALUE; + private static final Map ACTIVE_INSTANCES = new ConcurrentHashMap<>(); + private static final Map, Map> ACTIVE_INSTANCE_BY_ACTOR = new ConcurrentHashMap<>(); + private static final Map ANIMATION_START_PROTECTION = new ConcurrentHashMap<>(); + private static final int PLAYER_QUEUE_CAP = 8; + private static final Map, Map> PLAYER_QUEUES_BY_WORLD = new HashMap<>(); + private static final Map, Map> QUEUED_MOB_TO_PLAYER_BY_WORLD = new HashMap<>(); + private static final Map, Map> DEFERRED_QUEUE_CONTEXT_BY_WORLD = new HashMap<>(); + private static final Map, Map> AI_DISABLE_STATE_BY_WORLD = new HashMap<>(); + private static final Map, Integer> NOAI_CLEANUP_TICKS_BY_WORLD = new HashMap<>(); + private static final Map, Map> PLAYER_LOCK_STATE_BY_WORLD = new HashMap<>(); + private static final int MAX_FLOOR_SNAP_DROP = 2; + + private ServerAnimationController() { + } + + public static void init() { + if (!initialized) { + initialized = true; + ServerLifecycleEvents.SERVER_STARTED.register((ServerStarted)server -> { + AI_DISABLE_STATE_BY_WORLD.clear(); + NOAI_CLEANUP_TICKS_BY_WORLD.clear(); + STARTUP_NOAI_CLEANUP_UNTIL_TICK = server.getTicks() + 20; + STARTUP_START_BLOCK_UNTIL_TICK = server.getTicks() + 40; + PLAYER_LOCK_STATE_BY_WORLD.clear(); + ACTIVE_INSTANCES.clear(); + ACTIVE_INSTANCE_BY_ACTOR.clear(); + PLAYER_QUEUES_BY_WORLD.clear(); + QUEUED_MOB_TO_PLAYER_BY_WORLD.clear(); + DEFERRED_QUEUE_CONTEXT_BY_WORLD.clear(); + ANIMATION_START_PROTECTION.clear(); + RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.clear(); + }); + ServerPlayConnectionEvents.DISCONNECT.register((Disconnect)(handler, server) -> server.execute(() -> { + ServerPlayerEntity player = handler.player; + if (player != null) { + if (player.getEntityWorld() instanceof ServerWorld world) { + stopActiveInstancesForDisconnectingPlayer(world, player.getUuid()); + releasePlayerLocks(world, List.of(player.getUuid())); + clearPlayerQueue(world, player); + clearDeferredQueueOriginal(world, player.getUuid()); + ANIMATION_START_PROTECTION.remove(player.getUuid()); + removePlayerFromInstanceSubscribers(player.getUuid()); + } + } + })); + ServerLivingEntityEvents.AFTER_DEATH.register((AfterDeath)(entity, damageSource) -> { + if (entity.getEntityWorld() instanceof ServerWorld world) { + stopActiveInstancesForDeadActor(world, entity); + } + }); + ServerLivingEntityEvents.AFTER_DAMAGE.register((AfterDamage)(entity, damageSource, baseDamageTaken, damageTaken, blocked) -> { + if (!blocked && !(damageTaken <= 0.0F)) { + if (entity.getEntityWorld() instanceof ServerWorld world) { + stopDamageSensitiveInstances(world, entity); + } + } + }); + ServerTickEvents.END_WORLD_TICK.register(ServerAnimationController::tickWorld); + } + } + + @Nullable + public static UUID startNowWithContext( + ServerWorld world, + @Nullable ServerPlayerEntity requester, + Identifier animationId, + List actorsSorted, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable UUID positionAnchorUuid, + @Nullable AnimationStartContext startContext, + boolean forceChat, + boolean allowMultiplePlayers + ) { + if (world == null || animationId == null || actorsSorted == null || actorsSorted.isEmpty()) { + return null; + } + + if (isWithinStartupStartBlockWindow(world)) { + return null; + } + + if (!AnimationDefinitions.isAnimationEnabled(animationId)) { + return null; + } + + List actors = new ArrayList<>(actorsSorted.size()); + actors.addAll(actorsSorted); + actors.sort(Comparator.comparingInt(Entity::getId)); + int players = 0; + List actorUuids = new ArrayList<>(actors.size()); + + for (Entity e : actors) { + if (e == null || e.getEntityWorld() != world) { + return null; + } + + if (e instanceof PlayerEntity) { + players++; + } else if (!(e instanceof MobEntity)) { + sendOrLogDebug(world, requester, NonDebugChatCategory.WARNING, dbg("matched_unpathable_actor", animationId), forceChat); + return null; + } + + actorUuids.add(e.getUuid()); + } + + boolean authorizedMultiPlayerRequest = players > 1 && requester != null && (allowMultiplePlayers || forceChat); + if (players > 1 && !authorizedMultiPlayerRequest) { + sendOrLogDebug(world, requester, NonDebugChatCategory.SETUP, dbg("matched_multiple_players_not_supported", animationId), forceChat); + return null; + } + + if (authorizedMultiPlayerRequest && !allSelectedPlayersNearRequester(actors, requester, 5.0)) { + sendOrLogDebug( + world, + requester, + NonDebugChatCategory.SETUP, + dbg("start_cancelled_players_too_far_from_requester", String.format(Locale.ROOT, "%.1f", 5.0)), + forceChat + ); + return null; + } + + if (shouldBlockStart(world, actorUuids)) { + return null; + } + + List safeActorKeys = actorKeys != null && actorKeys.size() == actorUuids.size() ? List.copyOf(actorKeys) : List.of(); + List safeStages = stages == null ? List.of() : List.copyOf(stages); + NonAnimationDamageBehavior safeBehavior = damageBehavior == null ? NonAnimationDamageBehavior.IGNORE_DAMAGE : damageBehavior; + return startAndBroadcast( + world, + requester, + animationId, + actorUuids, + safeActorKeys, + safeStages, + safeBehavior, + ignoreAttackers, + sanitizeStartContext(startContext), + forceChat, + positionAnchorUuid, + null, + null + ); + } + + @Nullable + public static ServerAnimationController.MatchedStartRequest startEligibleMatchedNow( + ServerWorld world, + @Nullable ServerPlayerEntity requester, + List actorsSorted, + @Nullable Set requiredAnimationTags, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable UUID positionAnchorUuid, + @Nullable AnimationStartContext startContext, + boolean forceChat + ) { + if (world != null && actorsSorted != null && !actorsSorted.isEmpty()) { + List actors = new ArrayList<>(actorsSorted.size()); + actors.addAll(actorsSorted); + actors.sort(Comparator.comparingInt(Entity::getId)); + ServerAnimationController.EligibleDefinition eligible = selectEligibleDefinition( + world, actors, requiredAnimationTags == null ? Set.of() : requiredAnimationTags, positionAnchorUuid + ); + if (eligible == null) { + return null; + } + + UUID instanceId = startNowWithContext( + world, + requester, + eligible.definition().id(), + actors, + eligible.actorKeys(), + eligible.definition().stages(), + damageBehavior, + ignoreAttackers, + positionAnchorUuid, + startContext, + forceChat, + false + ); + return new ServerAnimationController.MatchedStartRequest(instanceId, eligible.definition().id(), eligible.actorKeys(), eligible.definition().stages()); + } else { + return null; + } + } + + public static boolean isDefinitionStartEligible( + ServerWorld world, @Nullable AnimationDefinitions.Definition definition, List actorsSorted, @Nullable UUID positionAnchorUuid + ) { + if (world != null && definition != null && actorsSorted != null && !actorsSorted.isEmpty()) { + List actors = new ArrayList<>(actorsSorted.size()); + actors.addAll(actorsSorted); + actors.sort(Comparator.comparingInt(Entity::getId)); + + for (Entity actor : actors) { + if (actor == null || actor.getEntityWorld() != world) { + return false; + } + } + + List actorKeys = AnimationDefinitions.resolveActorKeys(definition, actors, world.getRandom()); + return isDefinitionStartEligible(world, definition, actors, actorKeys, positionAnchorUuid); + } else { + return false; + } + } + + public static boolean isDefinitionStartEligible( + ServerWorld world, + @Nullable AnimationDefinitions.Definition definition, + List actorsSorted, + List actorKeys, + @Nullable UUID positionAnchorUuid + ) { + if (world != null && definition != null && actorsSorted != null && !actorsSorted.isEmpty()) { + List actors = new ArrayList<>(actorsSorted.size()); + actors.addAll(actorsSorted); + actors.sort(Comparator.comparingInt(Entity::getId)); + + for (Entity actor : actors) { + if (actor == null || actor.getEntityWorld() != world) { + return false; + } + } + + if (actorKeys == null || actorKeys.size() != actors.size()) { + return false; + } + + if (!AnimationDefinitions.resolveAllActorKeyAssignments(definition, actors).contains(List.copyOf(actorKeys))) { + return false; + } + + List actorUuids = actors.stream().map(Entity::getUuid).toList(); + UUID effectiveAnchorUuid = resolveEffectivePositionAnchorUuid(world, definition.id(), actorUuids, actorKeys, positionAnchorUuid); + return checkStartRequirements(world, definition, actorUuids, effectiveAnchorUuid).valid(); + } else { + return false; + } + } + + public static void setAnimationStartProtection(@Nullable ServerPlayerEntity player, int ticks) { + if (player != null) { + if (player.getEntityWorld() instanceof ServerWorld world) { + if (ticks <= 0) { + ANIMATION_START_PROTECTION.remove(player.getUuid()); + } else { + long until = world.getTime() + ticks; + ANIMATION_START_PROTECTION.put(player.getUuid(), new ServerAnimationController.AnimationStartProtection(world.getRegistryKey(), until)); + } + } + } + } + + public static void clearAnimationStartProtection(@Nullable ServerPlayerEntity player) { + if (player != null) { + ANIMATION_START_PROTECTION.remove(player.getUuid()); + } + } + + public static boolean hasAnimationStartProtection(@Nullable ServerWorld world, @Nullable ServerPlayerEntity player) { + if (world != null && player != null) { + ServerAnimationController.AnimationStartProtection protection = ANIMATION_START_PROTECTION.get(player.getUuid()); + if (protection == null) { + return false; + } else if (!protection.worldKey.equals(world.getRegistryKey())) { + ANIMATION_START_PROTECTION.remove(player.getUuid()); + return false; + } else if (world.getTime() >= protection.untilTick) { + ANIMATION_START_PROTECTION.remove(player.getUuid()); + return false; + } else { + return true; + } + } else { + return false; + } + } + + public static boolean enqueueForPlayerWithContext( + ServerWorld world, + @Nullable ServerPlayerEntity requester, + Identifier animationId, + List actors, + int insertIndex, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + @Nullable AnimationStartContext startContext, + boolean forceChat + ) { + if (world != null && animationId != null && actors != null && !actors.isEmpty()) { + UUID playerUuid = inferSinglePlayerUuid(world, actors); + if (playerUuid == null) { + sendOrLogDebug(world, requester, NonDebugChatCategory.WARNING, dbg("queue_enqueue_failed_exactly_one_player"), forceChat); + return false; + } + + ServerAnimationController.PlayerQueueState queue = PLAYER_QUEUES_BY_WORLD.computeIfAbsent(world.getRegistryKey(), k -> new HashMap<>()) + .computeIfAbsent(playerUuid, ServerAnimationController.PlayerQueueState::new); + if (queue.entries.size() >= 8) { + sendOrLogDebug(world, requester, NonDebugChatCategory.WARNING, dbg("queue_enqueue_failed_full", 8), forceChat); + return false; + } + + List actorUuids = new ArrayList<>(); + List nonPlayers = new ArrayList<>(); + + for (Entity actor : actors) { + if (actor != null && actor.getEntityWorld() == world) { + UUID uuid = actor.getUuid(); + if (actor instanceof ServerPlayerEntity) { + if (!uuid.equals(playerUuid)) { + continue; + } + } else { + nonPlayers.add(uuid); + } + + actorUuids.add(uuid); + } + } + + if (!actorUuids.contains(playerUuid)) { + sendOrLogDebug(world, requester, NonDebugChatCategory.WARNING, dbg("queue_enqueue_failed_player_missing"), forceChat); + return false; + } + + Map mobOwner = QUEUED_MOB_TO_PLAYER_BY_WORLD.computeIfAbsent(world.getRegistryKey(), k -> new HashMap<>()); + + for (UUID mobUuid : nonPlayers) { + UUID existingOwner = mobOwner.get(mobUuid); + if (existingOwner != null) { + sendOrLogDebug( + world, + requester, + NonDebugChatCategory.WARNING, + dbg("queue_enqueue_denied_mob_already_queued", describeActorForDebug(world, mobUuid)), + forceChat + ); + return false; + } + } + + UUID entryId = UUID.randomUUID(); + ServerAnimationController.QueuedEntry entry = new ServerAnimationController.QueuedEntry( + entryId, + animationId, + List.copyOf(actorUuids), + damageBehavior == null ? NonAnimationDamageBehavior.IGNORE_DAMAGE : damageBehavior, + ignoreAttackers, + sanitizeStartContext(startContext) + ); + insertQueueEntry(queue.entries, entry, insertIndex); + captureDeferredQueueContextFromActiveInstance(world, playerUuid); + + for (UUID mobUuid : nonPlayers) { + mobOwner.put(mobUuid, playerUuid); + } + + sendOrLogDebug( + world, requester, NonDebugChatCategory.INFO, dbg("queued_at_position", animationId, describeQueuePosition(queue.entries, entryId)), forceChat + ); + return true; + } else { + return false; + } + } + + public static void clearPlayerQueue(ServerWorld world, @Nullable ServerPlayerEntity player) { + if (world != null && player != null) { + clearPlayerQueueByUuid(world, player.getUuid(), NonDebugChatCategory.INFO, dbg("queue_cleared_for_player")); + } + } + + private static void stopActiveInstancesForDisconnectingPlayer(ServerWorld world, UUID playerUuid) { + if (world != null && playerUuid != null && !ACTIVE_INSTANCES.isEmpty()) { + List instanceIds = new ArrayList<>(); + + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world && inst.actorUuidSet.contains(playerUuid)) { + instanceIds.add(entry.getKey()); + } + } + + for (UUID instanceId : instanceIds) { + stopInstance(world, instanceId, true, ServerAnimationController.StopReason.EXTERNAL_STOP); + } + } + } + + @Nullable + private static ServerAnimationController.EligibleDefinition selectEligibleDefinition( + ServerWorld world, List actorsSorted, Set requiredAnimationTags, @Nullable UUID positionAnchorUuid + ) { + if (world != null && actorsSorted != null && !actorsSorted.isEmpty()) { + AnimationDefinitions.MatchResult match = AnimationDefinitions.match(actorsSorted, requiredAnimationTags); + List candidates = match.candidatesSorted(); + if (candidates != null && !candidates.isEmpty()) { + List eligible = new ArrayList<>(); + List actorUuids = actorsSorted.stream().map(Entity::getUuid).toList(); + + for (AnimationDefinitions.Definition definition : candidates) { + List actorKeys = AnimationDefinitions.resolveActorKeys(definition, actorsSorted, world.getRandom()); + UUID effectiveAnchorUuid = resolveEffectivePositionAnchorUuid(world, definition.id(), actorUuids, actorKeys, positionAnchorUuid); + ServerAnimationController.RequirementCheck check = checkStartRequirements(world, definition, actorUuids, effectiveAnchorUuid); + if (check.valid()) { + eligible.add(new ServerAnimationController.EligibleDefinition(definition, actorKeys, check.blockPlacement())); + } + } + + return eligible.isEmpty() ? null : pickWeightedEligible(eligible, world.getRandom()); + } else { + return null; + } + } else { + return null; + } + } + + private static ServerAnimationController.RequirementCheck checkStartRequirements( + ServerWorld world, @Nullable AnimationDefinitions.Definition definition, List actorUuids, @Nullable UUID effectiveAnchorUuid + ) { + if (definition == null) { + return new ServerAnimationController.RequirementCheck(false, null); + } else { + AnimationDefinitions.BlockRequirements blockRequirements = definition.blockRequirements(); + AnimationDefinitions.WaterRequirement waterRequirement = definition.waterRequirement(); + if (blockRequirements != null && waterRequirement != AnimationDefinitions.WaterRequirement.NONE) { + return new ServerAnimationController.RequirementCheck(false, null); + } else if (blockRequirements != null) { + Entity anchor = pickAnchorEntity(world, actorUuids, effectiveAnchorUuid); + ServerAnimationController.BlockPlacement placement = anchor == null ? null : findBlockPlacement(world, anchor, blockRequirements); + return new ServerAnimationController.RequirementCheck(placement != null, placement); + } else { + return new ServerAnimationController.RequirementCheck(waterRequirementMet(world, definition, actorUuids), null); + } + } + } + + private static ServerAnimationController.EligibleDefinition pickWeightedEligible( + List definitions, Random random + ) { + return WeightedPicker.pick(definitions, eligible -> eligible.definition().weight(), random.nextDouble()); + } + + public static boolean advanceStage(ServerWorld world, UUID instanceId) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + if (inst != null && inst.world == world) { + int currentIndex = inst.stageTracker == null ? 0 : inst.stageTracker.stageIndex; + if (inst.stages != null && !inst.stages.isEmpty()) { + if (currentIndex + 1 >= inst.stages.size()) { + return stopInstance(world, instanceId, true, ServerAnimationController.StopReason.NATURAL_END); + } + + int targetIndex = currentIndex + 1; + return advanceToStageIndex(world, instanceId, inst, targetIndex); + } else { + return false; + } + } else { + return false; + } + } + + public static boolean advanceToStage(ServerWorld world, UUID instanceId, int stageNumber) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + if (inst == null || inst.world != world) { + return false; + } else { + return stageNumber <= 0 ? false : advanceToStageIndex(world, instanceId, inst, stageNumber - 1); + } + } + + private static boolean advanceToStageIndex(ServerWorld world, UUID instanceId, ServerAnimationController.ActiveInstance inst, int targetIndex) { + if (inst == null || inst.world != world || inst.stages == null || inst.stages.isEmpty()) { + return false; + } + + if (targetIndex >= 0 && targetIndex < inst.stages.size()) { + long advanceTick = world.getTime(); + if (inst.stageTracker != null) { + inst.stageTracker.stageIndex = targetIndex; + inst.stageTracker.stageStartTick = advanceTick; + inst.stageTracker.progressEndTickOverride = -1L; + } + + inst.speed = clampSpeed(resolveStageSpeed(inst.stages, inst.stageTracker == null ? 0 : inst.stageTracker.stageIndex)); + long stageEndTick = resolveStageEndTick(inst); + AdvanceAnimationStageS2CPayload payload = new AdvanceAnimationStageS2CPayload(instanceId, advanceTick, stageEndTick, targetIndex); + Set targets = computeInstanceBroadcastTargets(world, null, inst); + rememberInstanceSubscribers(inst, targets); + + for (ServerPlayerEntity p : targets) { + ServerPlayNetworking.send(p, payload); + ServerPlayNetworking.send(p, new AnimationSpeedUpdateS2CPayload(instanceId, inst.speed, advanceTick, stageEndTick)); + } + + ((AnimationLifecycleEvents.StageAdvance)AnimationLifecycleEvents.STAGE_ADVANCE.invoker()) + .onStageAdvance(new AnimationLifecycleEvents.StageChangedContext(world, snapshot(instanceId, inst), resolveCurrentStage(inst), advanceTick)); + return true; + } else { + return false; + } + } + + public static boolean stopInstance(ServerWorld world, UUID instanceId) { + return stopInstance(world, instanceId, true, ServerAnimationController.StopReason.EXTERNAL_STOP); + } + + public static void adjustSpeed(ServerWorld world, UUID instanceId, double multiplier, @Nullable ServerPlayerEntity requester) { + adjustSpeed(world, instanceId, multiplier, requester, requester != null); + } + + public static void adjustSpeedSilently(ServerWorld world, UUID instanceId, double multiplier) { + adjustSpeed(world, instanceId, multiplier, null, false); + } + + private static void adjustSpeed(ServerWorld world, UUID instanceId, double multiplier, @Nullable ServerPlayerEntity requester, boolean notify) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + if (inst != null && inst.world == world) { + double clampedMultiplier = sanitizeMultiplier(multiplier); + double newSpeed = clampSpeed(inst.speed * clampedMultiplier); + double oldSpeed = inst.speed; + if (Math.abs(newSpeed - oldSpeed) < 1.0E-4) { + if (notify) { + sendOrLogDebug(world, requester, NonDebugChatCategory.ALWAYS, dbg("speed_unchanged"), true); + } + } else { + long now = world.getTime(); + AnimationStageInfo currentStage = resolveCurrentStage(inst); + long duration = Math.max(0L, currentStage.lengthTicks()); + if (inst.stageTracker != null) { + if (duration > 0L) { + long elapsed = Math.max(0L, now - inst.stageTracker.stageStartTick); + double normalizedProgress = elapsed * oldSpeed; + long adjustedStart = now - Math.round(normalizedProgress / newSpeed); + inst.stageTracker.stageStartTick = Math.max(inst.startTick, adjustedStart); + } + + inst.stageTracker.progressEndTickOverride = -1L; + } + + inst.speed = newSpeed; + long stageStartTick = inst.stageTracker == null ? inst.startTick : inst.stageTracker.stageStartTick; + long stageEndTick = resolveStageEndTick(inst); + AnimationSpeedUpdateS2CPayload payload = new AnimationSpeedUpdateS2CPayload(instanceId, newSpeed, stageStartTick, stageEndTick); + Set targets = computeInstanceBroadcastTargets(world, requester, inst); + rememberInstanceSubscribers(inst, targets); + + for (ServerPlayerEntity p : targets) { + ServerPlayNetworking.send(p, payload); + } + + if (notify) { + sendOrLogDebug(world, requester, NonDebugChatCategory.ALWAYS, dbg("speed_set", String.format(Locale.ROOT, "%.2fx", newSpeed)), true); + } + } + } else { + if (notify) { + sendOrLogDebug(world, requester, NonDebugChatCategory.ALWAYS, dbg("no_active_instance_to_adjust"), true); + } + } + } + + public static boolean setCurrentStageProgressDuration(ServerWorld world, UUID instanceId, long durationTicks) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + if (inst != null && inst.world == world && inst.stageTracker != null && durationTicks > 0L) { + long stageStartTick = inst.stageTracker.stageStartTick; + inst.stageTracker.progressEndTickOverride = stageStartTick + Math.max(1L, durationTicks); + AnimationSpeedUpdateS2CPayload payload = new AnimationSpeedUpdateS2CPayload( + instanceId, inst.speed, stageStartTick, inst.stageTracker.progressEndTickOverride + ); + Set targets = computeInstanceBroadcastTargets(world, null, inst); + rememberInstanceSubscribers(inst, targets); + + for (ServerPlayerEntity player : targets) { + ServerPlayNetworking.send(player, payload); + } + + return true; + } else { + return false; + } + } + + private static void stopActiveInstancesForDeadActor(ServerWorld world, LivingEntity actor) { + if (world != null && actor != null) { + UUID actorUuid = actor.getUuid(); + stopActiveInstancesForDeadActor(world, actorUuid); + clearRecentNoRestoreTransition(world, actorUuid); + ANIMATION_START_PROTECTION.remove(actorUuid); + if (actor instanceof ServerPlayerEntity) { + clearPlayerQueueByUuid(world, actorUuid, null, null); + } + } + } + + private static void stopActiveInstancesForDeadActor(ServerWorld world, UUID actorUuid) { + if (world != null && actorUuid != null && !ACTIVE_INSTANCES.isEmpty()) { + List instanceIds = new ArrayList<>(); + + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world && inst.actorUuidSet.contains(actorUuid)) { + instanceIds.add(entry.getKey()); + } + } + + for (UUID instanceId : instanceIds) { + stopInstance(world, instanceId, true, ServerAnimationController.StopReason.ACTOR_DEATH, Set.of(actorUuid)); + } + } + } + + public static boolean stopInstance(ServerWorld world, UUID instanceId, boolean restoreTransforms) { + return stopInstance(world, instanceId, restoreTransforms, ServerAnimationController.StopReason.EXTERNAL_STOP); + } + + public static boolean stopInstance(ServerWorld world, UUID instanceId, boolean restoreTransforms, ServerAnimationController.StopReason reason) { + return stopInstance(world, instanceId, restoreTransforms, reason, null); + } + + private static boolean stopInstance( + ServerWorld world, UUID instanceId, boolean restoreTransforms, ServerAnimationController.StopReason reason, @Nullable Set skipRestoreActors + ) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + if (inst != null && inst.world == world) { + long stopTick = world.getTime() + 1L; + boolean chained = false; + if (reason == ServerAnimationController.StopReason.NATURAL_END && inst.playerUuid != null) { + chained = startNextQueuedForPlayerDeferringRestore(world, inst, instanceId); + } + + StopAnimationS2CPayload payload = new StopAnimationS2CPayload(instanceId, stopTick); + + for (ServerPlayerEntity p : computeInstanceBroadcastTargets(world, null, inst)) { + ServerPlayNetworking.send(p, payload); + } + + fireStopEvent(instanceId, inst); + if (restoreTransforms) { + Set effectiveSkipActors = skipRestoreActors; + if (chained && inst.playerUuid != null) { + if (effectiveSkipActors != null && !effectiveSkipActors.isEmpty()) { + Set combined = new HashSet<>(effectiveSkipActors); + combined.add(inst.playerUuid); + effectiveSkipActors = combined; + } else { + effectiveSkipActors = Set.of(inst.playerUuid); + } + } + + restoreTransforms(inst, effectiveSkipActors); + } else { + cacheNoRestoreTransition(world, inst); + } + + restoreAiForActors(inst.world, inst.actorUuids); + releasePlayerLocks(inst.world, inst.actorUuids); + processPendingAiRestores(inst.world); + removeActiveInstance(instanceId); + if (reason == ServerAnimationController.StopReason.NATURAL_END) { + if (chained) { + sendOrLogDebug(world, null, NonDebugChatCategory.INFO, dbg("queue_continued_for_player"), false); + } + } else { + handleQueueOnStop(world, inst, reason); + } + + return true; + } else { + return false; + } + } + + public static boolean shouldAllowActorDamage(ServerWorld world, LivingEntity actor, DamageSource source) { + UUID actorUuid = actor.getUuid(); + long now = world.getTime(); + if (source != null && source.isOf(DamageTypes.IN_WALL)) { + Map grace = IN_WALL_GRACE_BY_WORLD.get(world.getRegistryKey()); + if (grace != null) { + Long until = grace.get(actorUuid); + if (until != null) { + if (now <= until) { + return false; + } + + grace.remove(actorUuid); + } + } + + if (ACTIVE_INSTANCES.isEmpty()) { + return true; + } + + for (ServerAnimationController.ActiveInstance inst : ACTIVE_INSTANCES.values()) { + if (inst.world == world && now >= inst.startTick && inst.actorUuidSet.contains(actorUuid)) { + return false; + } + } + } + + if (source != null && source.isOf(DamageTypes.DROWN) && isUnderwaterBreathingProtected(world, actorUuid)) { + return false; + } + + if (ACTIVE_INSTANCES.isEmpty()) { + return true; + } + + for (Entry entry : List.copyOf(ACTIVE_INSTANCES.entrySet())) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world && now >= inst.startTick && inst.actorUuidSet.contains(actorUuid)) { + if (inst.ignoreAttackers && source != null && source.getAttacker() instanceof MobEntity) { + return false; + } + + if (inst.damageBehavior == NonAnimationDamageBehavior.BLOCK_DAMAGE) { + return false; + } + } + } + + return true; + } + + private static void stopDamageSensitiveInstances(ServerWorld world, LivingEntity actor) { + if (!ACTIVE_INSTANCES.isEmpty()) { + UUID actorUuid = actor.getUuid(); + long now = world.getTime(); + List stopInstances = null; + + for (Entry entry : List.copyOf(ACTIVE_INSTANCES.entrySet())) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world + && now >= inst.startTick + && inst.actorUuidSet.contains(actorUuid) + && inst.damageBehavior == NonAnimationDamageBehavior.STOP_ON_DAMAGE) { + if (stopInstances == null) { + stopInstances = new ArrayList<>(); + } + + stopInstances.add(entry.getKey()); + } + } + + if (stopInstances != null) { + for (UUID id : stopInstances) { + stopInstance(world, id, true, ServerAnimationController.StopReason.DAMAGE_STOP); + } + } + } + } + + public static boolean shouldIgnoreAttackers(ServerWorld world, LivingEntity actor) { + if (ACTIVE_INSTANCES.isEmpty()) { + return false; + } + + UUID actorUuid = actor.getUuid(); + + for (ServerAnimationController.ActiveInstance inst : ACTIVE_INSTANCES.values()) { + if (inst.world == world && inst.ignoreAttackers && inst.actorUuidSet.contains(actorUuid)) { + return true; + } + } + + return false; + } + + private static boolean isUnderwaterBreathingProtected(ServerWorld world, UUID actorUuid) { + if (world != null && actorUuid != null && !ACTIVE_INSTANCES.isEmpty()) { + for (ServerAnimationController.ActiveInstance inst : ACTIVE_INSTANCES.values()) { + if (inst.world == world && inst.underwaterBreathing && inst.actorUuidSet.contains(actorUuid)) { + return true; + } + } + + return false; + } else { + return false; + } + } + + private static ServerAnimationController.ActiveInstance findActiveInstanceForActors(ServerWorld world, List actorUuids) { + if (actorUuids != null && !actorUuids.isEmpty()) { + if (ACTIVE_INSTANCES.isEmpty()) { + return null; + } + + for (ServerAnimationController.ActiveInstance inst : ACTIVE_INSTANCES.values()) { + if (inst.world == world) { + for (UUID uuid : actorUuids) { + if (inst.actorUuidSet.contains(uuid)) { + return inst; + } + } + } + } + + return null; + } else { + return null; + } + } + + public static void stopInstanceAndBroadcast(ServerPlayerEntity requester, UUID instanceId) { + if (requester.getEntityWorld() instanceof ServerWorld requesterWorld) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + if (inst == null) { + sendOrLogDebug(requesterWorld, requester, NonDebugChatCategory.ALWAYS, dbg("no_such_active_instance"), true); + } else if (inst.world != requesterWorld) { + sendOrLogDebug(requesterWorld, requester, NonDebugChatCategory.ALWAYS, dbg("instance_in_different_world"), true); + } else { + long now = requesterWorld.getTime(); + long stopTick = now + 1L; + StopAnimationS2CPayload payload = new StopAnimationS2CPayload(instanceId, stopTick); + + for (ServerPlayerEntity p : computeInstanceBroadcastTargets(requesterWorld, requester, inst)) { + ServerPlayNetworking.send(p, payload); + } + + boolean chained = false; + if (inst.playerUuid != null) { + chained = startNextQueuedForPlayer(requesterWorld, inst.playerUuid, instanceId, resolveTransitionYaw(inst)); + } + + fireStopEvent(instanceId, inst); + restoreTransforms(inst); + restoreAiForActors(inst.world, inst.actorUuids); + releasePlayerLocks(inst.world, inst.actorUuids); + processPendingAiRestores(inst.world); + removeActiveInstance(instanceId); + sendOrLogDebug(requesterWorld, requester, NonDebugChatCategory.INFO, dbg("broadcasting_stop"), false); + if (chained) { + sendOrLogDebug(requesterWorld, requester, NonDebugChatCategory.INFO, dbg("queue_continued_for_player"), false); + } + + AnimationEngine.logRuntime( + "Broadcast STOP: instance={} stopTick={} actors={} requester={}", instanceId, stopTick, inst.actorUuids.size(), requester.getName().getString() + ); + } + } + } + + public static void clearAllInstancesInWorld(ServerWorld world) { + List toRemove = new ArrayList<>(); + + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world) { + fireStopEvent(entry.getKey(), inst); + restoreTransforms(inst); + restoreAiForActors(world, inst.actorUuids); + releasePlayerLocks(world, inst.actorUuids); + toRemove.add(entry.getKey()); + handleQueueOnStop(world, inst, ServerAnimationController.StopReason.STOP_ALL); + } + } + + for (UUID id : toRemove) { + removeActiveInstance(id); + } + + Map queues = PLAYER_QUEUES_BY_WORLD.remove(world.getRegistryKey()); + if (queues != null) { + Map byMob = QUEUED_MOB_TO_PLAYER_BY_WORLD.get(world.getRegistryKey()); + if (byMob != null) { + byMob.clear(); + } + + DEFERRED_QUEUE_CONTEXT_BY_WORLD.remove(world.getRegistryKey()); + + for (UUID playerUuid : queues.keySet()) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(playerUuid); + sendOrLogDebug(world, player, NonDebugChatCategory.INFO, dbg("queue_cleared"), false); + } + } + + Map grace = IN_WALL_GRACE_BY_WORLD.get(world.getRegistryKey()); + if (grace != null) { + grace.clear(); + } + + processPendingAiRestores(world); + clearAllPlayerLocksInWorld(world); + RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.remove(world.getRegistryKey()); + } + + private static void tickWorld(ServerWorld world) { + cleanupInWallGrace(world, world.getTime()); + cleanupNoRestoreTransitions(world, world.getTime()); + tickActiveInstances(world); + tickNoAiCleanup(world); + tickPlayerQueues(world); + } + + private static void cacheNoRestoreTransition(ServerWorld world, ServerAnimationController.ActiveInstance inst) { + if (world != null && inst != null && inst.sharedTransform != null && inst.actorUuidSet != null && !inst.actorUuidSet.isEmpty()) { + long expiresTick = world.getTime() + 20L; + ServerAnimationController.RecentNoRestoreTransition transition = new ServerAnimationController.RecentNoRestoreTransition( + expiresTick, inst.sharedTransform, inst.blockPlacement + ); + Map byActor = RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.computeIfAbsent( + world.getRegistryKey(), ignored -> new HashMap<>() + ); + + for (UUID actorUuid : inst.actorUuidSet) { + if (actorUuid != null) { + byActor.put(actorUuid, transition); + } + } + } + } + + @Nullable + private static ServerAnimationController.RecentNoRestoreTransition consumeNoRestoreTransition( + ServerWorld world, List actorUuids, AnimationStartContext startContext + ) { + if (world == null || actorUuids == null || actorUuids.isEmpty() || startContext == null) { + return null; + } + + if (!startContext.isJoinReplacement()) { + return null; + } + + Map byActor = RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.get(world.getRegistryKey()); + if (byActor != null && !byActor.isEmpty()) { + long now = world.getTime(); + ServerAnimationController.RecentNoRestoreTransition selected = null; + + for (UUID actorUuid : actorUuids) { + ServerAnimationController.RecentNoRestoreTransition candidate = byActor.get(actorUuid); + if (candidate != null) { + if (candidate.expiresTick() >= now) { + selected = candidate; + break; + } + + byActor.remove(actorUuid); + } + } + + for (UUID actorUuid : actorUuids) { + byActor.remove(actorUuid); + } + + if (byActor.isEmpty()) { + RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.remove(world.getRegistryKey()); + } + + return selected; + } else { + return null; + } + } + + private static void cleanupNoRestoreTransitions(ServerWorld world, long now) { + Map byActor = RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.get(world.getRegistryKey()); + if (byActor != null && !byActor.isEmpty()) { + byActor.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().expiresTick() < now); + if (byActor.isEmpty()) { + RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.remove(world.getRegistryKey()); + } + } + } + + private static void clearRecentNoRestoreTransition(ServerWorld world, UUID actorUuid) { + if (world != null && actorUuid != null) { + Map byActor = RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.get(world.getRegistryKey()); + if (byActor != null) { + byActor.remove(actorUuid); + if (byActor.isEmpty()) { + RECENT_NO_RESTORE_TRANSITIONS_BY_WORLD.remove(world.getRegistryKey()); + } + } + } + } + + private static boolean shouldBlockStart(ServerWorld world, List actorUuids) { + if (actorUuids != null && !actorUuids.isEmpty()) { + for (UUID uuid : actorUuids) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(uuid); + if (player != null && player.getEntityWorld() == world && hasAnimationStartProtection(world, player)) { + return true; + } + } + + return false; + } else { + return false; + } + } + + private static Text dbg(String key, Object... args) { + return Text.translatable("debug.needsofnature.animation." + key, sanitizeTranslatableArgs(args)); + } + + private static Object[] sanitizeTranslatableArgs(Object... args) { + if (args != null && args.length != 0) { + Object[] safe = new Object[args.length]; + + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (arg instanceof Identifier id) { + safe[i] = id.toString(); + } else { + safe[i] = !(arg instanceof Text) && arg != null ? String.valueOf(arg) : arg; + } + } + + return safe; + } else { + return new Object[0]; + } + } + + private static void sendOrLogDebug( + ServerWorld world, @Nullable ServerPlayerEntity requester, NonDebugChatCategory category, String message, boolean forceChat + ) { + sendOrLogDebug(world, requester, category, Text.literal(message), forceChat); + } + + private static void sendOrLogDebug(ServerWorld world, @Nullable ServerPlayerEntity requester, NonDebugChatCategory category, Text message, boolean forceChat) { + AnimationEngine.logRuntime("{}", message.getString()); + if (requester != null) { + if (AnimationEngine.shouldSendDebugChat(requester, category, forceChat)) { + AnimationEngine.sendDebugChat(requester, category, message, forceChat); + } + } else if (world != null) { + for (ServerPlayerEntity player : world.getPlayers()) { + if (AnimationEngine.shouldSendDebugChat(player, category, forceChat)) { + AnimationEngine.sendDebugChat(player, category, message, forceChat); + } + } + } + } + + private static void tickActiveInstances(ServerWorld world) { + long now = world.getTime(); + List toRemove = new ArrayList<>(); + Set actorsToFreeze = new HashSet<>(); + Set activePlayerUuids = new HashSet<>(); + Map playersToLock = new HashMap<>(); + Set underwaterPlayers = new HashSet<>(); + List stageAdvances = new ArrayList<>(); + Map deadActors = new LinkedHashMap<>(); + + for (ServerAnimationController.ActiveInstance inst : ACTIVE_INSTANCES.values()) { + if (inst.world == world) { + LivingEntity deadActor = findDeadLoadedActor(inst); + if (deadActor != null) { + deadActors.putIfAbsent(deadActor.getUuid(), deadActor); + } + } + } + + for (LivingEntity deadActor : deadActors.values()) { + stopActiveInstancesForDeadActor(world, deadActor); + } + + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world && !maybeAdvanceStages(world, entry.getKey(), inst, now, stageAdvances, toRemove)) { + actorsToFreeze.addAll(inst.actorUuidSet); + if (inst.playerUuid != null) { + activePlayerUuids.add(inst.playerUuid); + } + + if (inst.sharedTransform != null) { + for (UUID uuid : inst.actorUuidSet) { + Entity e = world.getEntity(uuid); + if (e instanceof ServerPlayerEntity) { + playersToLock.put(uuid, inst.sharedTransform); + if (inst.underwaterBreathing) { + underwaterPlayers.add(uuid); + } + } + } + } + } + } + + for (ServerAnimationController.StageAdvance adv : stageAdvances) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(adv.instanceId()); + if (inst != null && inst.world == world) { + AdvanceAnimationStageS2CPayload payload = new AdvanceAnimationStageS2CPayload( + adv.instanceId(), adv.advanceTick(), adv.stageEndTick(), adv.stageIndex() + ); + Set targets = computeInstanceBroadcastTargets(world, null, inst); + rememberInstanceSubscribers(inst, targets); + + for (ServerPlayerEntity p : targets) { + ServerPlayNetworking.send(p, payload); + ServerPlayNetworking.send(p, new AnimationSpeedUpdateS2CPayload(adv.instanceId(), inst.speed, adv.advanceTick(), adv.stageEndTick())); + } + + ((AnimationLifecycleEvents.StageAdvance)AnimationLifecycleEvents.STAGE_ADVANCE.invoker()) + .onStageAdvance( + new AnimationLifecycleEvents.StageChangedContext(world, snapshot(adv.instanceId(), inst), resolveCurrentStage(inst), adv.advanceTick()) + ); + } + } + + freezeActiveActors(world, actorsToFreeze); + lockActivePlayers(world, playersToLock, underwaterPlayers); + nudgeNonActiveMobsAwayFromAnimatedPlayers(world, activePlayerUuids, now); + cleanupStalePlayerLocks(world, playersToLock.keySet()); + + for (UUID id : toRemove) { + removeActiveInstance(id); + } + + processPendingAiRestores(world); + } + + @Nullable + private static LivingEntity findDeadLoadedActor(ServerAnimationController.ActiveInstance inst) { + for (UUID actorUuid : inst.actorUuidSet) { + if (inst.world.getEntity(actorUuid) instanceof LivingEntity living && (!living.isAlive() || living.isRemoved())) { + return living; + } + } + + return null; + } + + @Nullable + private static UUID startAndBroadcast( + ServerWorld world, + @Nullable ServerPlayerEntity requester, + Identifier animationId, + List actorUuids, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext, + boolean forceChat, + @Nullable Float transitionYaw + ) { + return startAndBroadcast( + world, requester, animationId, actorUuids, actorKeys, stages, damageBehavior, ignoreAttackers, startContext, forceChat, null, null, transitionYaw + ); + } + + @Nullable + private static UUID startAndBroadcast( + ServerWorld world, + @Nullable ServerPlayerEntity requester, + Identifier animationId, + List actorUuids, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext, + boolean forceChat, + @Nullable UUID preferredAnchorUuid, + @Nullable Float transitionYaw + ) { + return startAndBroadcast( + world, + requester, + animationId, + actorUuids, + actorKeys, + stages, + damageBehavior, + ignoreAttackers, + startContext, + forceChat, + preferredAnchorUuid, + null, + transitionYaw + ); + } + + @Nullable + private static UUID startAndBroadcast( + ServerWorld world, + @Nullable ServerPlayerEntity requester, + Identifier animationId, + List actorUuids, + List actorKeys, + List stages, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext, + boolean forceChat, + @Nullable UUID preferredAnchorUuid, + @Nullable ServerAnimationController.BlockPlacement cachedBlockPlacement, + @Nullable Float transitionYaw + ) { + long startTick = world.getTime() + 1L; + UUID instanceId = UUID.randomUUID(); + Map originalTransforms = captureOriginalTransforms(world, actorUuids); + AnimationStartContext safeContext = sanitizeStartContext(startContext); + UUID effectivePositionAnchorUuid = resolveEffectivePositionAnchorUuid(world, animationId, actorUuids, actorKeys, preferredAnchorUuid); + ServerAnimationController.RecentNoRestoreTransition joinTransition = consumeNoRestoreTransition(world, actorUuids, safeContext); + Float effectiveTransitionYaw = transitionYaw; + if (effectiveTransitionYaw == null && joinTransition != null) { + effectiveTransitionYaw = joinTransition.sharedTransform().yaw(); + } + + ServerAnimationController.SharedTransform sharedTransform = joinTransition != null + ? joinTransition.sharedTransform() + : pickSharedTransform(actorUuids, originalTransforms, effectivePositionAnchorUuid); + AnimationDefinitions.Definition def = AnimationDefinitions.getDefinition(animationId); + AnimationDefinitions.AnimationPackInfo packInfo = def == null ? null : def.packInfo(); + List> modelRootsByActor = resolveModelRootsByActor(world, actorUuids, packInfo); + AnimationDefinitions.BlockRequirements blockRequirements = def == null ? null : def.blockRequirements(); + AnimationDefinitions.WaterRequirement waterRequirement = def == null ? AnimationDefinitions.WaterRequirement.NONE : def.waterRequirement(); + ServerAnimationController.BlockPlacement placement = cachedBlockPlacement != null + ? cachedBlockPlacement + : (joinTransition == null ? null : joinTransition.blockPlacement()); + boolean lockOrientation = false; + float lockedYaw = 0.0F; + float lockedHeadYaw = 0.0F; + float lockedPitch = 0.0F; + if (blockRequirements != null && waterRequirement != AnimationDefinitions.WaterRequirement.NONE) { + sendOrLogDebug(world, requester, NonDebugChatCategory.SETUP, dbg("start_cancelled_block_and_water_requirement"), forceChat); + return null; + } + + if (blockRequirements != null) { + Entity anchor = pickAnchorEntity(world, actorUuids, effectivePositionAnchorUuid); + if (placement == null) { + placement = anchor == null ? null : findBlockPlacement(world, anchor, blockRequirements); + } + + if (placement == null) { + sendOrLogDebug( + world, requester, NonDebugChatCategory.WARNING, blockRequirementFailureMessage(blockRequirements, animationId, world, actorUuids), forceChat + ); + return null; + } + + sharedTransform = placement.sharedTransform(); + lockOrientation = true; + lockedYaw = sharedTransform.yaw(); + lockedHeadYaw = sharedTransform.headYaw(); + lockedPitch = sharedTransform.pitch(); + } else if (waterRequirement != AnimationDefinitions.WaterRequirement.NONE) { + Entity waterAnchor = findWaterAnchor(world, actorUuids, waterRequirement); + if (waterAnchor == null) { + sendOrLogDebug(world, requester, NonDebugChatCategory.WARNING, dbg("start_cancelled_water_requirement_not_met"), forceChat); + return null; + } + + ServerAnimationController.SharedTransform base = sharedTransform; + sharedTransform = new ServerAnimationController.SharedTransform(waterAnchor.getEntityPos(), base.yaw(), base.pitch(), base.headYaw(), base.bodyYaw()); + } else { + if (anyActorOnWaterFooting(world, actorUuids)) { + sendOrLogDebug(world, requester, NonDebugChatCategory.WARNING, dbg("start_cancelled_not_allowed_in_water"), forceChat); + return null; + } + + UUID queuedPlayerUuid = resolvePlayerUuid(world, actorUuids); + ServerAnimationController.SharedTransform groundTransform = resolveQueuedPlacementTransform(world, queuedPlayerUuid, sharedTransform, safeContext); + if (groundTransform == null) { + groundTransform = findSafeGroundPlacement(world, sharedTransform); + } + + if (groundTransform == null) { + ServerAnimationController.SharedTransform dismountTransform = findMountedPlayerDismountTransform(world, actorUuids, sharedTransform); + if (dismountTransform != null) { + groundTransform = findSafeGroundPlacement(world, dismountTransform); + } + } + + if (groundTransform == null) { + sendOrLogDebug( + world, + requester, + NonDebugChatCategory.WARNING, + dbg("start_cancelled_not_enough_space", describeAnimationIdForDebug(animationId), describeActorsForDebug(world, actorUuids)), + forceChat + ); + return null; + } + + sharedTransform = groundTransform; + } + + if (effectivePositionAnchorUuid != null && !lockOrientation) { + lockOrientation = true; + lockedYaw = sharedTransform.yaw(); + lockedHeadYaw = sharedTransform.headYaw(); + lockedPitch = sharedTransform.pitch(); + } + + boolean preservePlayerLook = false; + if (effectiveTransitionYaw != null && blockRequirements == null) { + float yaw = MathHelper.wrapDegrees(effectiveTransitionYaw); + sharedTransform = new ServerAnimationController.SharedTransform(sharedTransform.pos(), yaw, 0.0F, yaw, yaw); + lockOrientation = true; + lockedYaw = yaw; + lockedHeadYaw = yaw; + lockedPitch = 0.0F; + preservePlayerLook = true; + } + + Set actorUuidSet = Set.copyOf(new HashSet<>(actorUuids)); + List safeActorKeys = actorKeys != null && actorKeys.size() == actorUuids.size() ? List.copyOf(actorKeys) : List.of(); + List safeStages = stages == null ? List.of() : List.copyOf(stages); + if (!((AnimationLifecycleEvents.AllowStart)AnimationLifecycleEvents.ALLOW_START.invoker()) + .allowStart( + new AnimationLifecycleEvents.AllowStartContext( + world, animationId, actorUuids, safeActorKeys, safeStages, requester, damageBehavior, ignoreAttackers, safeContext + ) + )) { + return null; + } + + double initialSpeed = clampSpeed(resolveStageSpeed(stages, 0)); + AnimationStageInfo initialStage = safeStages.isEmpty() ? new AnimationStageInfo(animationId, true, 0L, true, initialSpeed) : safeStages.getFirst(); + long initialStageEndTick = resolveStageEndTick(animationId, initialStage, startTick, initialSpeed); + Vec3d cameraOrbitTarget = placement == null ? null : placement.cameraOrbitTarget(); + StartAnimationS2CPayload payload = new StartAnimationS2CPayload( + animationId, + modelRootsByActor, + instanceId, + actorUuids, + safeActorKeys, + safeStages, + startTick, + 0, + startTick, + initialStageEndTick, + initialSpeed, + lockOrientation, + lockedYaw, + lockedHeadYaw, + lockedPitch, + cameraOrbitTarget + ); + disableAiForActors(world, actorUuids); + if (placement != null) { + markInWallGrace(world, actorUuids); + } + + applySharedTransform(world, actorUuids, sharedTransform, preservePlayerLook); + if (ignoreAttackers) { + clearAttackTargets(world, actorUuidSet); + } + + Set targets = computeBroadcastTargets(world, requester, actorUuids); + + for (ServerPlayerEntity p : targets) { + ServerPlayNetworking.send(p, payload); + } + + UUID playerUuid = resolvePlayerUuid(world, actorUuids); + originalTransforms = applyDeferredQueueOriginal(world, playerUuid, originalTransforms, safeContext); + boolean underwaterBreathing = waterRequirement == AnimationDefinitions.WaterRequirement.UNDERWATER; + ServerAnimationController.ActiveInstance active = new ServerAnimationController.ActiveInstance( + world, + animationId, + modelRootsByActor, + List.copyOf(actorUuids), + safeActorKeys, + safeStages, + startTick, + originalTransforms, + sharedTransform, + lockOrientation, + lockedYaw, + lockedHeadYaw, + lockedPitch, + damageBehavior, + ignoreAttackers, + safeContext, + underwaterBreathing, + playerUuid, + actorUuidSet, + new ServerAnimationController.StageTracker(startTick), + placement, + cameraOrbitTarget, + initialSpeed + ); + registerActiveInstance(instanceId, active); + rememberInstanceSubscribers(active, targets); + acquirePlayerLocks(world, actorUuids); + ((AnimationLifecycleEvents.Start)AnimationLifecycleEvents.START.invoker()) + .onStart(new AnimationLifecycleEvents.StartedContext(world, snapshot(instanceId, active), requester)); + sendOrLogDebug(world, requester, NonDebugChatCategory.INFO, dbg("actors_immediate_broadcast_start"), false); + String requesterName = requester == null ? "api" : requester.getName().getString(); + AnimationEngine.logRuntime( + "Broadcast START: animation={} instance={} startTick={} actors={} requester={}", + describeAnimationIdForDebug(animationId), + instanceId, + startTick, + actorUuids.size(), + requesterName + ); + return instanceId; + } + + private static Set computeBroadcastTargets(ServerWorld world, @Nullable ServerPlayerEntity requester, List actorUuids) { + Set targets = new HashSet<>(); + if (requester != null) { + targets.add(requester); + } + + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e != null) { + targets.addAll(PlayerLookup.tracking(e)); + if (e instanceof ServerPlayerEntity sp) { + targets.add(sp); + } + } else { + ServerPlayerEntity p = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(uuid); + if (p != null && p.getEntityWorld() == world) { + targets.add(p); + targets.addAll(PlayerLookup.tracking(p)); + } + } + } + + return targets; + } + + private static Set computeInstanceBroadcastTargets( + ServerWorld world, @Nullable ServerPlayerEntity requester, ServerAnimationController.ActiveInstance inst + ) { + Set targets = computeBroadcastTargets(world, requester, inst == null ? List.of() : inst.actorUuids); + if (world != null && inst != null && !inst.subscribedPlayerUuids.isEmpty()) { + for (UUID playerUuid : inst.subscribedPlayerUuids) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(playerUuid); + if (player != null && player.getEntityWorld() == world) { + targets.add(player); + } + } + + return targets; + } else { + return targets; + } + } + + private static void rememberInstanceSubscribers(ServerAnimationController.ActiveInstance inst, Set targets) { + if (inst != null && targets != null && !targets.isEmpty()) { + for (ServerPlayerEntity player : targets) { + if (player != null && player.getEntityWorld() == inst.world) { + inst.subscribedPlayerUuids.add(player.getUuid()); + } + } + } + } + + private static void removePlayerFromInstanceSubscribers(UUID playerUuid) { + if (playerUuid != null && !ACTIVE_INSTANCES.isEmpty()) { + for (ServerAnimationController.ActiveInstance inst : ACTIVE_INSTANCES.values()) { + if (inst != null) { + inst.subscribedPlayerUuids.remove(playerUuid); + } + } + } + } + + private static void fireStopEvent(UUID instanceId, ServerAnimationController.ActiveInstance inst) { + if (inst != null) { + ((AnimationLifecycleEvents.Stop)AnimationLifecycleEvents.STOP.invoker()) + .onStop(new AnimationLifecycleEvents.StoppedContext(inst.world, snapshot(instanceId, inst))); + } + } + + private static boolean allSelectedPlayersNearRequester(List actors, ServerPlayerEntity requester, double maxDistance) { + if (actors != null && requester != null) { + double maxDistanceSq = maxDistance * maxDistance; + + for (Entity actor : actors) { + if (actor instanceof PlayerEntity playerActor && playerActor.squaredDistanceTo(requester) > maxDistanceSq) { + return false; + } + } + + return true; + } else { + return false; + } + } + + private static Vec3d computeCentroid(List entities) { + double x = 0.0; + double y = 0.0; + double z = 0.0; + + for (Entity e : entities) { + Vec3d p = e.getEntityPos(); + x += p.x; + y += p.y; + z += p.z; + } + + double n = entities.size(); + return new Vec3d(x / n, y / n, z / n); + } + + private static Map captureOriginalTransforms(ServerWorld world, List actorUuids) { + Map originals = new HashMap<>(); + + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e instanceof LivingEntity living) { + Vec3d restorePos = living instanceof ServerPlayerEntity player ? resolveMountedPlayerRestorePosition(player) : null; + originals.put( + uuid, + new ServerAnimationController.OriginalTransform( + world.getRegistryKey(), living.getEntityPos(), living.getYaw(), living.getPitch(), living.getHeadYaw(), living.getBodyYaw(), restorePos + ) + ); + } else if (e != null) { + originals.put( + uuid, + new ServerAnimationController.OriginalTransform(world.getRegistryKey(), e.getEntityPos(), e.getYaw(), e.getPitch(), e.getYaw(), e.getYaw()) + ); + } + } + + return originals; + } + + @Nullable + private static UUID resolveEffectivePositionAnchorUuid( + ServerWorld world, Identifier animationId, List actorUuids, List actorKeys, @Nullable UUID preferredAnchorUuid + ) { + Entity explicitAnchor = resolveAnchorEntity(world, actorUuids, preferredAnchorUuid); + if (explicitAnchor != null) { + return explicitAnchor.getUuid(); + } + + AnimationDefinitions.Definition def = AnimationDefinitions.getDefinition(animationId); + String anchorActorKey = def == null ? null : normalizeActorKey(def.positionAnchorActor()); + if (anchorActorKey == null) { + return null; + } + + if (actorKeys != null && actorKeys.size() == actorUuids.size()) { + for (int i = 0; i < actorUuids.size(); i++) { + if (anchorActorKey.equals(normalizeActorKey(actorKeys.get(i)))) { + Entity anchor = resolveAnchorEntity(world, actorUuids, actorUuids.get(i)); + return anchor == null ? null : anchor.getUuid(); + } + } + + return null; + } else { + return null; + } + } + + @Nullable + private static String normalizeActorKey(@Nullable String actorKey) { + if (actorKey == null) { + return null; + } + + String value = actorKey.trim(); + return value.isEmpty() ? null : value.toLowerCase(Locale.ROOT); + } + + @Nullable + private static Entity resolveAnchorEntity(ServerWorld world, List actorUuids, @Nullable UUID anchorUuid) { + if (world == null || actorUuids == null || actorUuids.isEmpty() || anchorUuid == null) { + return null; + } + + if (!actorUuids.contains(anchorUuid)) { + return null; + } + + Entity entity = world.getEntity(anchorUuid); + if (entity != null) { + return entity; + } + + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(anchorUuid); + return player != null && player.getEntityWorld() == world ? player : null; + } + + private static ServerAnimationController.SharedTransform pickSharedTransform( + List actorUuids, Map originals, @Nullable UUID anchorUuid + ) { + if (anchorUuid != null) { + ServerAnimationController.OriginalTransform anchored = originals.get(anchorUuid); + if (anchored != null) { + return new ServerAnimationController.SharedTransform(anchored.pos(), anchored.yaw(), anchored.pitch(), anchored.headYaw(), anchored.bodyYaw()); + } + } + + for (UUID uuid : actorUuids) { + ServerAnimationController.OriginalTransform ot = originals.get(uuid); + if (ot != null) { + return new ServerAnimationController.SharedTransform(ot.pos(), ot.yaw(), ot.pitch(), ot.headYaw(), ot.bodyYaw()); + } + } + + return new ServerAnimationController.SharedTransform(Vec3d.ZERO, 0.0F, 0.0F, 0.0F, 0.0F); + } + + private static Entity pickAnchorEntity(ServerWorld world, List actorUuids, @Nullable UUID preferredAnchorUuid) { + Entity anchor = resolveAnchorEntity(world, actorUuids, preferredAnchorUuid); + return anchor != null ? anchor : pickAnchorEntity(world, actorUuids); + } + + private static Entity pickAnchorEntity(ServerWorld world, List actorUuids) { + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e instanceof PlayerEntity) { + return e; + } + } + + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e != null) { + return e; + } + } + + return null; + } + + private static boolean waterRequirementMet(ServerWorld world, @Nullable AnimationDefinitions.Definition def, List actorUuids) { + AnimationDefinitions.WaterRequirement req = def == null ? AnimationDefinitions.WaterRequirement.NONE : def.waterRequirement(); + return req == AnimationDefinitions.WaterRequirement.NONE ? !anyActorOnWaterFooting(world, actorUuids) : findWaterAnchor(world, actorUuids, req) != null; + } + + private static boolean anyActorOnWaterFooting(ServerWorld world, List actorUuids) { + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e != null) { + BlockPos underFeet = e.getBlockPos().down(); + if (world.getFluidState(underFeet).isIn(FluidTags.WATER)) { + return true; + } + } + } + + return false; + } + + @Nullable + private static Entity findWaterAnchor(ServerWorld world, List actorUuids, AnimationDefinitions.WaterRequirement requirement) { + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e != null && meetsWaterRequirement(e, requirement)) { + return e; + } + } + + return null; + } + + private static boolean meetsWaterRequirement(Entity e, AnimationDefinitions.WaterRequirement requirement) { + return switch (requirement) { + case SURFACE -> e.isTouchingWater() && !e.isSubmergedInWater(); + case UNDERWATER -> e.isSubmergedInWater(); + case NONE -> true; + }; + } + + @Nullable + private static ServerAnimationController.BlockPlacement findWallPlacement( + ServerWorld world, Entity anchor, AnimationDefinitions.BlockRequirements requirements + ) { + Vec3d anchorPos = anchor.getEntityPos(); + BlockPos anchorBlock = BlockPos.ofFloored(anchorPos); + int radius = getBlockScanRadius(); + double bestDistSq = Double.MAX_VALUE; + ServerAnimationController.BlockPlacement best = null; + int minY = anchorBlock.getY() - 1; + int maxY = anchorBlock.getY() + 1; + + for (int y = minY; y <= maxY; y++) { + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + BlockPos pos = new BlockPos(anchorBlock.getX() + dx, y, anchorBlock.getZ() + dz); + + for (Direction facing : HORIZONTAL_DIRECTIONS) { + ServerAnimationController.WallSegment segment = resolveWallSegment(world, pos, facing, requirements); + if (segment != null && checkWallClearance(world, segment, facing, requirements.clearance())) { + Vec3d sharedPos = segment.surfaceCenter(); + double distSq = sharedPos.squaredDistanceTo(anchorPos); + if (!(distSq >= bestDistSq) && hasBlockRequirementLineOfSight(world, anchor, sharedPos)) { + float yaw = directionToYaw(facing.getOpposite()); + bestDistSq = distSq; + ServerAnimationController.SharedTransform transform = new ServerAnimationController.SharedTransform(sharedPos, yaw, 0.0F, yaw, yaw); + best = new ServerAnimationController.BlockPlacement(segment.columns().getFirst().pos(), facing, transform, null, false); + } + } + } + } + } + } + + return best; + } + + @Nullable + private static ServerAnimationController.BlockPlacement findBlockPlacement( + ServerWorld world, Entity anchor, AnimationDefinitions.BlockRequirements requirements + ) { + if (requirements == null) { + return null; + } else if (requirements.isWall()) { + return findWallPlacement(world, anchor, requirements); + } else { + return requirements.isCenterSupport() ? findCenterSupportPlacement(world, anchor, requirements) : null; + } + } + + @Nullable + private static ServerAnimationController.BlockPlacement findCenterSupportPlacement( + ServerWorld world, Entity anchor, AnimationDefinitions.BlockRequirements requirements + ) { + Vec3d anchorPos = anchor.getEntityPos(); + BlockPos anchorBlock = BlockPos.ofFloored(anchorPos); + int radius = getBlockScanRadius(); + double bestDistSq = Double.MAX_VALUE; + ServerAnimationController.BlockPlacement best = null; + int minY = anchorBlock.getY() - 1; + int maxY = anchorBlock.getY() + 1; + + for (int y = minY; y <= maxY; y++) { + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + BlockPos supportPos = new BlockPos(anchorBlock.getX() + dx, y, anchorBlock.getZ() + dz); + Double supportTopOffset = supportTopOffset(world, supportPos, requirements.support(), requirements.blocks()); + if (supportTopOffset != null) { + Direction[] directions = directionsByDistanceToAnchor(anchorPos, supportPos, supportTopOffset); + if (requirements.placement() != AnimationDefinitions.CenterPlacement.SURFACE_FOOTPRINT) { + if (checkCenterSupportSurfaceRadius(world, supportPos, supportTopOffset, requirements.surfaceRadius(), requirements.clearance())) { + for (Direction direction : directions) { + if (checkCenterDirectionalClearance(world, supportPos, direction, supportTopOffset, requirements.clearance())) { + BlockPos sidePos = supportPos.offset(direction); + Vec3d sideCenter = new Vec3d(sidePos.getX() + 0.5, supportPos.getY() + supportTopOffset, sidePos.getZ() + 0.5); + double distSq = sideCenter.squaredDistanceTo(anchorPos); + if (distSq < bestDistSq) { + float yaw = directionToYaw(direction.getOpposite()); + Vec3d sharedPos = new Vec3d(supportPos.getX() + 0.5, supportPos.getY(), supportPos.getZ() + 0.5); + if (hasBlockRequirementLineOfSight(world, anchor, sideCenter)) { + bestDistSq = distSq; + ServerAnimationController.SharedTransform transform = new ServerAnimationController.SharedTransform( + sharedPos, yaw, 0.0F, yaw, yaw + ); + best = new ServerAnimationController.BlockPlacement(supportPos, direction, transform, null, true); + } + } + } + } + } + } else { + for (Direction direction : directions) { + for (int lateralSign : new int[]{1, -1}) { + if (checkCenterSurfaceFootprint(world, supportPos, direction, lateralSign, supportTopOffset, requirements)) { + ServerAnimationController.BedFootprintFacing bedFacing = resolveBedFootprintFacing( + world, supportPos, direction, lateralSign, requirements + ); + if (!bedFacing.bedFootprint() || bedFacing.facing() != null) { + Vec3d footprintCenter = centerSurfaceFootprint( + supportPos, direction, lateralSign, supportTopOffset, requirements.surfaceFootprint() + ); + double distSq = footprintCenter.squaredDistanceTo(anchorPos); + if (distSq < bestDistSq) { + Direction placementFacing = bedFacing.facing() == null ? direction : bedFacing.facing().getOpposite(); + BlockPos placementPos = supportPos; + if (bedFacing.facing() != null) { + AnimationDefinitions.SurfaceFootprint footprint = requirements.surfaceFootprint(); + Direction side = lateralSign >= 0 ? direction.rotateYCounterclockwise() : direction.rotateYClockwise(); + placementPos = supportPos.offset(bedFacing.facing(), Math.max(0, footprint.depth() - 1)) + .offset(side, Math.max(0, footprint.width() - 1)); + } + + float yaw = directionToYaw(placementFacing.getOpposite()); + Vec3d sharedPos = new Vec3d(placementPos.getX() + 0.5, placementPos.getY(), placementPos.getZ() + 0.5); + if (hasBlockRequirementLineOfSight(world, anchor, footprintCenter)) { + bestDistSq = distSq; + ServerAnimationController.SharedTransform transform = new ServerAnimationController.SharedTransform( + sharedPos, yaw, 0.0F, yaw, yaw + ); + best = new ServerAnimationController.BlockPlacement(placementPos, placementFacing, transform, null, true); + } + } + } + } + } + } + } + } + } + } + } + + return best; + } + + private static Direction[] directionsByDistanceToAnchor(Vec3d anchorPos, BlockPos supportPos, double supportTopOffset) { + Direction[] directions = new Direction[]{Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST}; + Arrays.sort(directions, Comparator.comparingDouble(direction -> { + BlockPos side = supportPos.offset(direction); + Vec3d center = new Vec3d(side.getX() + 0.5, supportPos.getY() + supportTopOffset, side.getZ() + 0.5); + return center.squaredDistanceTo(anchorPos); + })); + return directions; + } + + @Nullable + private static Double supportTopOffset( + ServerWorld world, BlockPos pos, AnimationDefinitions.CenterSupport support, @Nullable AnimationDefinitions.BlockPredicate blocks + ) { + if (support == null) { + return null; + } + + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + return null; + } + + if (!matchesBlockPredicate(state, blocks)) { + return null; + } + + if (support == AnimationDefinitions.CenterSupport.SURFACE) { + if (blocks != null && !blocks.isEmpty()) { + VoxelShape shape = state.getCollisionShape(world, pos); + return shape.isEmpty() ? null : shape.getMax(Axis.Y); + } else { + return null; + } + } else { + if (support == AnimationDefinitions.CenterSupport.HALF_HEIGHT) { + return isHalfHeightSupport(state) ? 0.5625 : null; + } + + if (state.getBlock() instanceof SlabBlock) { + SlabType type = (SlabType)state.get(SlabBlock.TYPE); + if (support == AnimationDefinitions.CenterSupport.SLAB) { + return type == SlabType.BOTTOM ? 0.5 : null; + } + + if (support == AnimationDefinitions.CenterSupport.FULL_BLOCK) { + return type != SlabType.TOP && type != SlabType.DOUBLE ? null : 1.0; + } + } + + return support == AnimationDefinitions.CenterSupport.FULL_BLOCK && isFullHeightSupport(world, pos, state) ? 1.0 : null; + } + } + + private static List> resolveModelRootsByActor( + ServerWorld world, List actorUuids, @Nullable AnimationDefinitions.AnimationPackInfo packInfo + ) { + if (actorUuids != null && !actorUuids.isEmpty()) { + if (packInfo == null) { + return Collections.nCopies(actorUuids.size(), List.of()); + } + + List> rootsByActor = new ArrayList<>(actorUuids.size()); + + for (UUID actorUuid : actorUuids) { + Entity actor = resolveAnchorEntity(world, actorUuids, actorUuid); + Identifier entityTypeId = actor == null ? null : Registries.ENTITY_TYPE.getId(actor.getType()); + rootsByActor.add(packInfo.modelRootsFor(entityTypeId)); + } + + return List.copyOf(rootsByActor); + } else { + return List.of(); + } + } + + private static boolean isHalfHeightSupport(BlockState state) { + return state.getBlock() instanceof BedBlock ? true : state.getBlock() instanceof SlabBlock && state.get(SlabBlock.TYPE) == SlabType.BOTTOM; + } + + private static boolean isFullHeightSupport(ServerWorld world, BlockPos pos, BlockState state) { + if (state.isAir() || state.isReplaceable()) { + return false; + } + + if (state.isSolidBlock(world, pos)) { + return true; + } + + VoxelShape shape = state.getCollisionShape(world, pos); + return !shape.isEmpty() && shape.getMax(Axis.Y) >= 0.999; + } + + private static boolean checkCenterDirectionalClearance( + ServerWorld world, BlockPos supportPos, Direction facing, double supportTopOffset, AnimationDefinitions.Clearance clearance + ) { + if (clearance == null) { + return true; + } + + int lateralRadius = Math.max(1, clearance.width()) - 1; + int height = Math.max(1, clearance.height()); + int depth = Math.max(1, clearance.depth()); + Direction left = facing.rotateYCounterclockwise(); + + for (int d = 1; d <= depth; d++) { + for (int w = -lateralRadius; w <= lateralRadius; w++) { + BlockPos check = supportPos.offset(facing, d).offset(left, w); + if (!isDirectionalClearanceFree(world, check, supportPos.getY(), height)) { + return false; + } + + if (!isSolidFloorBlock(world, check.down())) { + return false; + } + } + } + + return true; + } + + private static boolean checkCenterSurfaceFootprint( + ServerWorld world, BlockPos supportPos, Direction facing, int lateralSign, double supportTopOffset, AnimationDefinitions.BlockRequirements requirements + ) { + AnimationDefinitions.SurfaceFootprint footprint = requirements.surfaceFootprint(); + if (footprint == null) { + return false; + } + + int width = Math.max(1, footprint.width()); + int depth = Math.max(1, footprint.depth()); + int height = Math.max(1, footprint.height()); + int margin = Math.max(0, footprint.margin()); + Direction side = lateralSign >= 0 ? facing.rotateYCounterclockwise() : facing.rotateYClockwise(); + double supportTopY = supportPos.getY() + supportTopOffset; + + for (int d = -margin; d < depth + margin; d++) { + for (int w = -margin; w < width + margin; w++) { + BlockPos check = supportPos.offset(facing, d).offset(side, w); + boolean insideFootprint = d >= 0 && d < depth && w >= 0 && w < width; + if (insideFootprint) { + Double checkTopOffset = supportTopOffset(world, check, requirements.support(), requirements.blocks()); + if (checkTopOffset == null || Math.abs(checkTopOffset - supportTopOffset) > 1.0E-4) { + return false; + } + + if (!isSpaceClearAboveSupport(world, check, supportTopY, height)) { + return false; + } + } else if (!isSurfaceFootprintMarginClear(world, check, supportTopY, height)) { + return false; + } + } + } + + return true; + } + + private static ServerAnimationController.BedFootprintFacing resolveBedFootprintFacing( + ServerWorld world, BlockPos supportPos, Direction facing, int lateralSign, AnimationDefinitions.BlockRequirements requirements + ) { + AnimationDefinitions.SurfaceFootprint footprint = requirements.surfaceFootprint(); + if (footprint == null) { + return ServerAnimationController.BedFootprintFacing.none(); + } + + List positions = centerSurfaceFootprintPositions(supportPos, facing, lateralSign, footprint); + if (positions.isEmpty()) { + return ServerAnimationController.BedFootprintFacing.none(); + } + + int bedCount = 0; + + for (BlockPos pos : positions) { + if (world.getBlockState(pos).getBlock() instanceof BedBlock) { + bedCount++; + } + } + + if (bedCount == 0) { + return ServerAnimationController.BedFootprintFacing.none(); + } + + boolean bedOnlyPredicate = isBedOnlyBlockPredicate(requirements.blocks()); + if (bedCount != positions.size()) { + return bedOnlyPredicate ? ServerAnimationController.BedFootprintFacing.invalid() : ServerAnimationController.BedFootprintFacing.none(); + } + + Set footprintPositions = new HashSet<>(positions); + Set pairedPositions = new HashSet<>(); + Direction expectedFacing = null; + + for (BlockPos pos : positions) { + BlockState state = world.getBlockState(pos); + Direction bedFacing = (Direction)state.get(BedBlock.FACING); + BedPart part = (BedPart)state.get(BedBlock.PART); + if (expectedFacing == null) { + expectedFacing = bedFacing; + } else if (expectedFacing != bedFacing) { + return ServerAnimationController.BedFootprintFacing.invalid(); + } + + BlockPos otherPos = part == BedPart.FOOT ? pos.offset(bedFacing) : pos.offset(bedFacing.getOpposite()); + if (!footprintPositions.contains(otherPos)) { + return ServerAnimationController.BedFootprintFacing.invalid(); + } + + BlockState otherState = world.getBlockState(otherPos); + if (!(otherState.getBlock() instanceof BedBlock)) { + return ServerAnimationController.BedFootprintFacing.invalid(); + } + + if (otherState.get(BedBlock.FACING) != bedFacing) { + return ServerAnimationController.BedFootprintFacing.invalid(); + } + + if (otherState.get(BedBlock.PART) == part) { + return ServerAnimationController.BedFootprintFacing.invalid(); + } + + pairedPositions.add(pos); + pairedPositions.add(otherPos); + } + + if (!pairedPositions.containsAll(footprintPositions) || expectedFacing == null) { + return ServerAnimationController.BedFootprintFacing.invalid(); + } else { + return facing == expectedFacing && lateralSign >= 0 + ? ServerAnimationController.BedFootprintFacing.valid(expectedFacing) + : ServerAnimationController.BedFootprintFacing.invalid(); + } + } + + private static List centerSurfaceFootprintPositions( + BlockPos supportPos, Direction facing, int lateralSign, AnimationDefinitions.SurfaceFootprint footprint + ) { + int width = Math.max(1, footprint == null ? 1 : footprint.width()); + int depth = Math.max(1, footprint == null ? 1 : footprint.depth()); + Direction side = lateralSign >= 0 ? facing.rotateYCounterclockwise() : facing.rotateYClockwise(); + List positions = new ArrayList<>(width * depth); + + for (int d = 0; d < depth; d++) { + for (int w = 0; w < width; w++) { + positions.add(supportPos.offset(facing, d).offset(side, w)); + } + } + + return positions; + } + + private static boolean isBedOnlyBlockPredicate(@Nullable AnimationDefinitions.BlockPredicate blocks) { + if (blocks != null && !blocks.isEmpty()) { + for (Identifier tagId : blocks.tagIds()) { + if (!BEDS_BLOCK_TAG_ID.equals(tagId)) { + return false; + } + } + + for (Identifier blockId : blocks.blockIds()) { + if (!(Registries.BLOCK.get(blockId) instanceof BedBlock)) { + return false; + } + } + + return !blocks.tagIds().isEmpty() || !blocks.blockIds().isEmpty(); + } else { + return false; + } + } + + private static boolean isSurfaceFootprintMarginClear(ServerWorld world, BlockPos pos, double supportTopY, int height) { + Box clearanceBox = new Box(pos.getX(), supportTopY + 1.0E-4, pos.getZ(), pos.getX() + 1.0, supportTopY + Math.max(1, height), pos.getZ() + 1.0); + return world.isSpaceEmpty(clearanceBox) && !containsFluid(world, clearanceBox); + } + + private static Vec3d centerSurfaceFootprint( + BlockPos supportPos, Direction facing, int lateralSign, double supportTopOffset, AnimationDefinitions.SurfaceFootprint footprint + ) { + int width = Math.max(1, footprint == null ? 1 : footprint.width()); + int depth = Math.max(1, footprint == null ? 1 : footprint.depth()); + Direction side = lateralSign >= 0 ? facing.rotateYCounterclockwise() : facing.rotateYClockwise(); + double forwardOffset = (depth - 1) * 0.5; + double sideOffset = (width - 1) * 0.5; + return new Vec3d( + supportPos.getX() + 0.5 + facing.getOffsetX() * forwardOffset + side.getOffsetX() * sideOffset, + supportPos.getY() + supportTopOffset, + supportPos.getZ() + 0.5 + facing.getOffsetZ() * forwardOffset + side.getOffsetZ() * sideOffset + ); + } + + private static boolean checkCenterSupportSurfaceRadius( + ServerWorld world, BlockPos supportPos, double supportTopOffset, int surfaceRadius, AnimationDefinitions.Clearance clearance + ) { + int radius = Math.max(0, surfaceRadius); + int height = Math.max(1, clearance == null ? 2 : clearance.height()); + double supportTopY = supportPos.getY() + supportTopOffset; + + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + BlockPos check = supportPos.add(dx, 0, dz); + if (!isSameOrLowerSurface(world, check, supportTopOffset)) { + return false; + } + + if (!isSpaceClearAboveSupport(world, check, supportTopY, height)) { + return false; + } + } + } + + return true; + } + + private static boolean isSameOrLowerSurface(ServerWorld world, BlockPos pos, double supportTopOffset) { + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + return false; + } else if (!state.isAir() && !state.isReplaceable()) { + VoxelShape shape = state.getCollisionShape(world, pos); + return shape.isEmpty() ? true : shape.getMax(Axis.Y) <= supportTopOffset + 1.0E-4; + } else { + return true; + } + } + + private static boolean isDirectionalClearanceFree(ServerWorld world, BlockPos pos, int supportY, int height) { + Box clearanceBox = new Box(pos.getX(), supportY + 1.0E-4, pos.getZ(), pos.getX() + 1.0, supportY + Math.max(1, height), pos.getZ() + 1.0); + return world.isSpaceEmpty(clearanceBox) && !containsFluid(world, clearanceBox); + } + + private static boolean isSpaceClearAboveSupport(ServerWorld world, BlockPos pos, double supportTopY, int height) { + Box clearanceBox = new Box(pos.getX(), supportTopY + 1.0E-4, pos.getZ(), pos.getX() + 1.0, supportTopY + Math.max(1, height), pos.getZ() + 1.0); + return world.isSpaceEmpty(clearanceBox) && !containsFluid(world, clearanceBox); + } + + private static Text blockRequirementFailureMessage( + AnimationDefinitions.BlockRequirements requirements, Identifier animationId, ServerWorld world, List actorUuids + ) { + String animation = describeAnimationIdForDebug(animationId); + String actors = describeActorsForDebug(world, actorUuids); + return requirements != null && requirements.isCenterSupport() + ? dbg("start_cancelled_no_valid_center_support", animation, actors) + : dbg("start_cancelled_no_valid_wall", animation, actors); + } + + private static boolean checkWallClearance( + ServerWorld world, ServerAnimationController.WallSegment segment, Direction facing, AnimationDefinitions.Clearance clearance + ) { + if (clearance == null) { + return true; + } + + if (segment != null && !segment.columns().isEmpty()) { + int sidePadding = Math.max(1, clearance.width()) - 1; + int height = Math.max(1, clearance.height()); + int depth = Math.max(1, clearance.depth()); + Direction left = facing.rotateYCounterclockwise(); + BlockPos firstWallPos = segment.columns().getFirst().pos(); + int wallWidth = segment.columns().size(); + + for (int dy = 0; dy < height; dy++) { + for (int d = 1; d <= depth; d++) { + for (int w = -sidePadding; w < wallWidth + sidePadding; w++) { + BlockPos check = firstWallPos.offset(facing, d).offset(left, w).up(dy); + if (!isFreeSpace(world, check)) { + return false; + } + + if (dy == 0 && !isSolidFloorBlock(world, check.down())) { + return false; + } + } + } + } + + return true; + } else { + return false; + } + } + + @Nullable + private static ServerAnimationController.WallSegment resolveWallSegment( + ServerWorld world, BlockPos firstPos, Direction facing, AnimationDefinitions.BlockRequirements requirements + ) { + if (world != null && firstPos != null && facing != null && requirements != null) { + AnimationDefinitions.WallWidth width = requirements.width(); + int minimum = Math.max(1, width == null ? 1 : width.min()); + Direction lateral = facing.rotateYCounterclockwise(); + List columns = new ArrayList<>(minimum); + + for (int index = 0; index < minimum; index++) { + ServerAnimationController.WallColumn column = resolveWallColumn(world, firstPos.offset(lateral, index), facing, requirements); + if (column == null) { + return null; + } + + columns.add(column); + } + + Integer maximum = width == null ? null : width.max(); + if (maximum != null && exceedsMaximumWallRun(world, firstPos, facing, requirements, maximum)) { + return null; + } + + double x = 0.0; + double y = 0.0; + double z = 0.0; + + for (ServerAnimationController.WallColumn column : columns) { + x += column.surfaceAnchor().x; + y += column.surfaceAnchor().y; + z += column.surfaceAnchor().z; + } + + double divisor = columns.size(); + return new ServerAnimationController.WallSegment(List.copyOf(columns), new Vec3d(x / divisor, y / divisor, z / divisor)); + } else { + return null; + } + } + + @Nullable + private static ServerAnimationController.WallColumn resolveWallColumn( + ServerWorld world, BlockPos pos, Direction facing, AnimationDefinitions.BlockRequirements requirements + ) { + if (!isSolidFloorBlock(world, pos.down())) { + return null; + } + + if (!isWallRequirementBlock(world, pos, facing, requirements)) { + return null; + } + + int wallHeight = countWallHeight(world, pos, facing, requirements.height(), requirements); + if (matchesWallHeight(wallHeight, requirements.height()) && hasWallHeightCapClearance(world, pos, wallHeight, requirements.height())) { + VoxelShape wallShape = world.getBlockState(pos).getCollisionShape(world, pos); + if (wallShape.isEmpty()) { + return null; + } + + EnumMap broadSurfaces = findBroadWallSurfaces(wallShape.getBoundingBoxes()); + Double broadSurface = broadSurfaces.get(facing); + return !broadSurfaces.isEmpty() && broadSurface == null + ? null + : new ServerAnimationController.WallColumn(pos, wallSurfaceAnchor(pos, wallShape, facing, broadSurface)); + } else { + return null; + } + } + + private static boolean exceedsMaximumWallRun( + ServerWorld world, BlockPos origin, Direction facing, AnimationDefinitions.BlockRequirements requirements, int maximum + ) { + int count = 1; + Direction lateral = facing.rotateYCounterclockwise(); + + for (Direction scanDirection : new Direction[]{lateral, lateral.getOpposite()}) { + for (BlockPos cursor = origin.offset(scanDirection); + count <= maximum && resolveWallColumn(world, cursor, facing, requirements) != null; + cursor = cursor.offset(scanDirection) + ) { + if (++count > maximum) { + return true; + } + } + } + + return false; + } + + private static int getBlockScanRadius() { + int radius = NeedsOfNature.getConfig().blockSearchRadius(); + return Math.max(1, radius); + } + + private static boolean hasBlockRequirementLineOfSight(ServerWorld world, Entity anchor, Vec3d target) { + if (world != null && anchor != null && target != null) { + Vec3d start = anchor.getEyePos(); + Vec3d end = target.add(0.0, 0.75, 0.0); + if (start.squaredDistanceTo(end) < 1.0E-6) { + return true; + } + + BlockHitResult result = world.raycast(new RaycastContext(start, end, ShapeType.COLLIDER, FluidHandling.NONE, anchor)); + return result == null || result.getType() == Type.MISS; + } else { + return false; + } + } + + private static void markInWallGrace(ServerWorld world, List actorUuids) { + if (actorUuids != null && !actorUuids.isEmpty()) { + long until = world.getTime() + 5L; + Map grace = IN_WALL_GRACE_BY_WORLD.computeIfAbsent(world.getRegistryKey(), k -> new HashMap<>()); + + for (UUID uuid : actorUuids) { + Long existing = grace.get(uuid); + if (existing == null || existing < until) { + grace.put(uuid, until); + } + } + } + } + + private static void cleanupInWallGrace(ServerWorld world, long now) { + Map grace = IN_WALL_GRACE_BY_WORLD.get(world.getRegistryKey()); + if (grace != null && !grace.isEmpty()) { + grace.entrySet().removeIf(entry -> entry.getValue() < now); + } + } + + private static EnumMap findBroadWallSurfaces(List boxes) { + EnumMap surfaces = new EnumMap<>(Direction.class); + + for (Direction direction : HORIZONTAL_DIRECTIONS) { + Double surface = findBroadWallSurface(boxes, direction); + if (surface != null) { + surfaces.put(direction, surface); + } + } + + return surfaces; + } + + @Nullable + private static Double findBroadWallSurface(List boxes, Direction facing) { + Axis lateralAxis = facing.getAxis() == Axis.X ? Axis.Z : Axis.X; + boolean positive = facing == Direction.EAST || facing == Direction.SOUTH; + Double bestSurface = null; + + for (Box candidate : boxes) { + double surface = boxBoundary(candidate, facing); + if (!(coveredSpanAtSurface(boxes, facing, lateralAxis, surface) + 1.0E-6 < 0.5) + && (bestSurface == null || (positive ? surface > bestSurface : surface < bestSurface))) { + bestSurface = surface; + } + } + + return bestSurface; + } + + private static double coveredSpanAtSurface(List boxes, Direction facing, Axis lateralAxis, double surface) { + List surfaceBoxes = new ArrayList<>(); + + for (Box box : boxes) { + if (Math.abs(boxBoundary(box, facing) - surface) <= 1.0E-6) { + surfaceBoxes.add(box); + } + } + + surfaceBoxes.sort(Comparator.comparingDouble(boxx -> boxMin(boxx, lateralAxis))); + double covered = 0.0; + double intervalEnd = Double.NEGATIVE_INFINITY; + + for (Box box : surfaceBoxes) { + double min = boxMin(box, lateralAxis); + double max = boxMax(box, lateralAxis); + if (min > intervalEnd) { + covered += max - min; + intervalEnd = max; + } else if (max > intervalEnd) { + covered += max - intervalEnd; + intervalEnd = max; + } + } + + return covered; + } + + private static Vec3d wallSurfaceAnchor(BlockPos pos, VoxelShape shape, Direction facing, @Nullable Double broadSurface) { + double centerX = pos.getX() + 0.5; + double centerZ = pos.getZ() + 0.5; + double surface = broadSurface != null ? broadSurface : wallShapeBoundary(shape, facing); + + return switch (facing) { + case EAST -> new Vec3d(pos.getX() + surface + 0.5, pos.getY(), centerZ); + case WEST -> new Vec3d(pos.getX() + surface - 0.5, pos.getY(), centerZ); + case SOUTH -> new Vec3d(centerX, pos.getY(), pos.getZ() + surface + 0.5); + case NORTH -> new Vec3d(centerX, pos.getY(), pos.getZ() + surface - 0.5); + default -> throw new IllegalArgumentException("Wall facing must be horizontal: " + facing); + }; + } + + private static double wallShapeBoundary(VoxelShape shape, Direction facing) { + return switch (facing) { + case EAST -> shape.getMax(Axis.X); + case WEST -> shape.getMin(Axis.X); + case SOUTH -> shape.getMax(Axis.Z); + case NORTH -> shape.getMin(Axis.Z); + default -> throw new IllegalArgumentException("Wall facing must be horizontal: " + facing); + }; + } + + private static double boxBoundary(Box box, Direction facing) { + return switch (facing) { + case EAST -> box.maxX; + case WEST -> box.minX; + case SOUTH -> box.maxZ; + case NORTH -> box.minZ; + default -> throw new IllegalArgumentException("Wall facing must be horizontal: " + facing); + }; + } + + private static double boxMin(Box box, Axis axis) { + return switch (axis) { + case X -> box.minX; + case Y -> box.minY; + case Z -> box.minZ; + default -> throw new MatchException(null, null); + }; + } + + private static double boxMax(Box box, Axis axis) { + return switch (axis) { + case X -> box.maxX; + case Y -> box.maxY; + case Z -> box.maxZ; + default -> throw new MatchException(null, null); + }; + } + + private static float directionToYaw(Direction dir) { + return switch (dir) { + case EAST -> -90.0F; + case WEST -> 90.0F; + default -> 0.0F; + case NORTH -> 180.0F; + }; + } + + private static int countWallHeight( + ServerWorld world, BlockPos pos, Direction facing, AnimationDefinitions.WallHeight height, AnimationDefinitions.BlockRequirements requirements + ) { + if (height == null) { + return 0; + } + + int min = Math.max(1, height.min()); + Integer max = height.max(); + int limit = max != null ? max + 1 : min; + int actual = 0; + + for (BlockPos cursor = pos; actual < limit && isWallRequirementBlock(world, cursor, facing, requirements); cursor = cursor.up()) { + actual++; + } + + return actual; + } + + private static boolean matchesWallHeight(int actual, AnimationDefinitions.WallHeight height) { + if (height == null) { + return false; + } + + int min = Math.max(1, height.min()); + Integer max = height.max(); + return actual < min ? false : max == null || actual <= max; + } + + private static boolean hasWallHeightCapClearance(ServerWorld world, BlockPos pos, int actual, AnimationDefinitions.WallHeight height) { + if (height == null || height.max() == null) { + return true; + } else { + return actual != height.max() ? true : isFreeSpace(world, pos.up(actual)); + } + } + + private static boolean isSolidWallBlock(ServerWorld world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + return false; + } + + if (state.isAir() || state.isReplaceable()) { + return false; + } + + if (!state.isIn(BlockTags.FENCES) && !state.isIn(BlockTags.WALLS) && !state.isIn(WALL_SURFACES_BLOCK_TAG)) { + VoxelShape shape = state.getCollisionShape(world, pos); + if (shape.isEmpty()) { + return false; + } else if (!(shape.getMin(Axis.X) > 1.0E-6) + && !(shape.getMax(Axis.X) < 0.999999) + && !(shape.getMin(Axis.Z) > 1.0E-6) + && !(shape.getMax(Axis.Z) < 0.999999)) { + VoxelShape requiredCore = VoxelShapes.cuboid(0.0625, 0.0, 0.0625, 0.9375, 0.875, 0.9375); + return !VoxelShapes.matchesAnywhere(requiredCore, shape, BooleanBiFunction.ONLY_FIRST); + } else { + return false; + } + } else { + return true; + } + } + + private static boolean isWallRequirementBlock( + ServerWorld world, BlockPos pos, Direction wallSurfaceFacing, AnimationDefinitions.BlockRequirements requirements + ) { + AnimationDefinitions.BlockPredicate blocks = requirements == null ? null : requirements.blocks(); + if (blocks != null && !blocks.isEmpty()) { + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + return false; + } else if (!matchesBlockPredicate(state, blocks)) { + return false; + } else { + return !matchesWallFacing(state, wallSurfaceFacing, requirements.facing()) ? false : !state.getCollisionShape(world, pos).isEmpty(); + } + } else { + return isSolidWallBlock(world, pos); + } + } + + private static boolean matchesWallFacing(BlockState state, Direction wallSurfaceFacing, @Nullable AnimationDefinitions.WallFacing requiredFacing) { + if (requiredFacing == null) { + return true; + } + + Direction blockFacing = null; + + for (Entry, Comparable> entry : state.getEntries().entrySet()) { + if ("facing".equals(entry.getKey().getName()) && entry.getValue() instanceof Direction direction) { + blockFacing = direction; + break; + } + } + + return requiredFacing.matches(blockFacing, wallSurfaceFacing); + } + + private static boolean matchesBlockPredicate(BlockState state, @Nullable AnimationDefinitions.BlockPredicate blocks) { + if (blocks != null && !blocks.isEmpty()) { + Identifier blockId = Registries.BLOCK.getId(state.getBlock()); + if (blocks.blockIds().contains(blockId)) { + return true; + } + + for (Identifier tagId : blocks.tagIds()) { + if (state.isIn(TagKey.of(RegistryKeys.BLOCK, tagId))) { + return true; + } + } + + return false; + } else { + return true; + } + } + + private static boolean isSolidFloorBlock(ServerWorld world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + return false; + } + + if (state.isAir() || state.isReplaceable()) { + return false; + } + + if (state.isIn(BlockTags.FENCES) || state.isIn(BlockTags.WALLS)) { + return false; + } + + if (state.isSolidBlock(world, pos)) { + return true; + } + + VoxelShape shape = state.getCollisionShape(world, pos); + if (shape.isEmpty()) { + return false; + } + + double top = shape.getMax(Axis.Y); + if (top < 0.875) { + return false; + } + + VoxelShape requiredSupport = VoxelShapes.cuboid(0.0625, top - 0.0625, 0.0625, 0.9375, top, 0.9375); + return !VoxelShapes.matchesAnywhere(requiredSupport, shape, BooleanBiFunction.ONLY_FIRST); + } + + private static boolean isFreeSpace(ServerWorld world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + return !state.getFluidState().isEmpty() ? false : state.isAir() || state.isReplaceable(); + } + + private static void applySharedTransform( + ServerWorld world, List actorUuids, ServerAnimationController.SharedTransform sharedTransform, boolean preservePlayerLook + ) { + Map states = AI_DISABLE_STATE_BY_WORLD.get(world.getRegistryKey()); + + for (UUID uuid : actorUuids) { + Entity e = world.getEntity(uuid); + if (e != null) { + switch (e) { + case ServerPlayerEntity player: + preparePlayerForAnimationTeleport(player); + float teleportYaw = preservePlayerLook ? player.getYaw() : sharedTransform.yaw(); + float teleportPitch = preservePlayerLook ? player.getPitch() : sharedTransform.pitch(); + player.networkHandler.requestTeleport(sharedTransform.pos().x, sharedTransform.pos().y, sharedTransform.pos().z, teleportYaw, teleportPitch); + player.setHeadYaw(sharedTransform.headYaw()); + player.setBodyYaw(sharedTransform.bodyYaw()); + break; + case LivingEntity living: + living.refreshPositionAndAngles( + sharedTransform.pos().x, sharedTransform.pos().y, sharedTransform.pos().z, sharedTransform.yaw(), sharedTransform.pitch() + ); + living.setHeadYaw(sharedTransform.headYaw()); + living.setBodyYaw(sharedTransform.bodyYaw()); + break; + default: + e.refreshPositionAndAngles( + sharedTransform.pos().x, sharedTransform.pos().y, sharedTransform.pos().z, sharedTransform.yaw(), sharedTransform.pitch() + ); + } + + e.setVelocity(Vec3d.ZERO); + e.fallDistance = 0.0; + if (e instanceof MobEntity mob && states != null) { + ServerAnimationController.AiDisableState state = states.get(uuid); + if (state != null) { + state.lockedYaw = sharedTransform.yaw(); + state.lockedHeadYaw = sharedTransform.headYaw(); + state.lockedBodyYaw = sharedTransform.bodyYaw(); + state.lockedPitch = sharedTransform.pitch(); + } + } + } + } + } + + private static void preparePlayerForAnimationTeleport(ServerPlayerEntity player) { + if (player != null) { + if (player.hasVehicle()) { + player.stopRiding(); + } + + if (player.isSleeping()) { + player.wakeUp(true, true); + } + + player.setVelocity(Vec3d.ZERO); + player.fallDistance = 0.0; + } + } + + @Nullable + private static ServerAnimationController.SharedTransform findSafeGroundPlacement( + ServerWorld world, ServerAnimationController.SharedTransform sharedTransform + ) { + Vec3d startPos = sharedTransform.pos(); + ServerAnimationController.FloorHit baseFloor = findNearestFloor(world, startPos, 2); + if (!baseFloor.found) { + return null; + } + + ServerAnimationController.SharedTransform base = withSharedPosition(sharedTransform, baseFloor.pos); + if (isGroundPlacementClear(world, baseFloor.pos)) { + return base; + } + + for (ServerAnimationController.GroundPlacementOffset offset : GROUND_PLACEMENT_OFFSETS) { + Vec3d candidateStart = startPos.add(offset.dx(), 0.0, offset.dz()); + ServerAnimationController.FloorHit candidateFloor = findNearestFloor(world, candidateStart, 2); + if (candidateFloor.found && isGroundPlacementClear(world, candidateFloor.pos)) { + return withSharedPosition(sharedTransform, candidateFloor.pos); + } + } + + return null; + } + + private static ServerAnimationController.SharedTransform withSharedPosition(ServerAnimationController.SharedTransform sharedTransform, Vec3d pos) { + return new ServerAnimationController.SharedTransform( + pos, sharedTransform.yaw(), sharedTransform.pitch(), sharedTransform.headYaw(), sharedTransform.bodyYaw() + ); + } + + @Nullable + private static ServerAnimationController.SharedTransform findMountedPlayerDismountTransform( + ServerWorld world, List actorUuids, ServerAnimationController.SharedTransform base + ) { + if (world != null && actorUuids != null && !actorUuids.isEmpty() && base != null) { + for (UUID actorUuid : actorUuids) { + if (world.getEntity(actorUuid) instanceof ServerPlayerEntity player && player.hasVehicle()) { + Entity vehicle = player.getVehicle(); + if (vehicle != null && !vehicle.isRemoved()) { + Vec3d dismountPos = vehicle.updatePassengerForDismount(player); + if (dismountPos == null) { + dismountPos = vehicle.getEntityPos(); + } + + return withSharedPosition(base, dismountPos); + } + } + } + + return null; + } else { + return null; + } + } + + @Nullable + private static Vec3d resolveMountedPlayerRestorePosition(ServerPlayerEntity player) { + if (player != null && player.hasVehicle()) { + Entity vehicle = player.getVehicle(); + if (vehicle != null && !vehicle.isRemoved()) { + Vec3d dismountPos = vehicle.updatePassengerForDismount(player); + if (dismountPos == null || vehicle.getBoundingBox().expand(0.05).contains(dismountPos)) { + Box box = vehicle.getBoundingBox(); + dismountPos = new Vec3d(vehicle.getX(), box.maxY + 0.05, vehicle.getZ()); + } + + return dismountPos; + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + private static ServerAnimationController.SharedTransform resolveQueuedPlacementTransform( + ServerWorld world, @Nullable UUID playerUuid, ServerAnimationController.SharedTransform base, AnimationStartContext startContext + ) { + if (world == null || playerUuid == null || base == null) { + return null; + } + + if (startContext != null && startContext.queueContinuation()) { + ServerAnimationController.DeferredQueueContext context = getDeferredQueueContext(world, playerUuid); + if (context != null && context.previousPlacement() != null) { + ServerAnimationController.BlockPlacement previousPlacement = context.previousPlacement(); + if (!isCenterSupportPlacement(previousPlacement)) { + ServerAnimationController.SharedTransform previousTransform = previousPlacement.sharedTransform(); + return isGroundPlacementClear(world, previousTransform.pos()) ? withSharedPosition(base, previousTransform.pos()) : null; + } + + double supportTopOffset = supportSurfaceTopOffset(world, previousPlacement.wallPos()); + if (supportTopOffset <= 0.0) { + return null; + } + + Vec3d topPos = new Vec3d( + previousPlacement.wallPos().getX() + 0.5, previousPlacement.wallPos().getY() + supportTopOffset, previousPlacement.wallPos().getZ() + 0.5 + ); + return !isGroundPlacementClear(world, topPos) ? null : withSharedPosition(base, topPos); + } else { + return null; + } + } else { + return null; + } + } + + private static double supportSurfaceTopOffset(ServerWorld world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + return -1.0; + } + + VoxelShape shape = state.getCollisionShape(world, pos); + return shape.isEmpty() ? -1.0 : shape.getMax(Axis.Y); + } + + private static boolean isGroundPlacementClear(ServerWorld world, Vec3d pos) { + Box clearanceBox = new Box(pos.x - 0.75, pos.y + 0.01, pos.z - 0.75, pos.x + 0.75, pos.y + 2.0, pos.z + 0.75); + return world.isSpaceEmpty(clearanceBox) && !containsFluid(world, clearanceBox); + } + + private static boolean containsFluid(ServerWorld world, Box box) { + int minX = MathHelper.floor(box.minX); + int maxX = MathHelper.floor(box.maxX - 1.0E-7); + int minY = MathHelper.floor(box.minY); + int maxY = MathHelper.floor(box.maxY - 1.0E-7); + int minZ = MathHelper.floor(box.minZ); + int maxZ = MathHelper.floor(box.maxZ - 1.0E-7); + Mutable mutable = new Mutable(); + + for (int y = minY; y <= maxY; y++) { + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + mutable.set(x, y, z); + if (!world.getFluidState(mutable).isEmpty()) { + return true; + } + } + } + } + + return false; + } + + private static ServerAnimationController.FloorHit findNearestFloor(ServerWorld world, Vec3d startPos, int maxDropBlocks) { + BlockPos startBlock = BlockPos.ofFloored(startPos); + Mutable cursor = new Mutable(startBlock.getX(), startBlock.getY(), startBlock.getZ()); + int minY = Math.max(world.getBottomY(), startBlock.getY() - maxDropBlocks); + double maxFloorY = startPos.y + 0.001; + + for (int y = startBlock.getY(); y >= minY; y--) { + cursor.setY(y); + BlockState state = world.getBlockState(cursor); + if (!state.isAir()) { + VoxelShape shape = state.getCollisionShape(world, cursor); + if (!shape.isEmpty()) { + double top = shape.getMax(Axis.Y); + if (!(top <= 0.0)) { + double floorY = y + top; + if (floorY <= maxFloorY) { + return new ServerAnimationController.FloorHit(true, new Vec3d(startPos.x, floorY, startPos.z)); + } + } + } + } + } + + return new ServerAnimationController.FloorHit(false, startPos); + } + + private static List createGroundPlacementOffsets() { + int steps = Math.max(1, MathHelper.ceil(4.0)); + double maxDistSq = 4.0; + List offsets = new ArrayList<>(); + + for (int ix = -steps; ix <= steps; ix++) { + for (int iz = -steps; iz <= steps; iz++) { + if (ix != 0 || iz != 0) { + double dx = ix * 0.5; + double dz = iz * 0.5; + double distSq = dx * dx + dz * dz; + if (!(distSq > maxDistSq)) { + offsets.add(new ServerAnimationController.GroundPlacementOffset(dx, dz, distSq)); + } + } + } + } + + offsets.sort(Comparator.comparingDouble(ServerAnimationController.GroundPlacementOffset::distSq)); + return List.copyOf(offsets); + } + + private static void restoreTransforms(ServerAnimationController.ActiveInstance inst) { + restoreTransforms(inst, null); + } + + private static void restoreTransforms(ServerAnimationController.ActiveInstance inst, @Nullable Set skipActors) { + Map originals = inst.originalTransforms; + if (originals != null && !originals.isEmpty()) { + Map states = AI_DISABLE_STATE_BY_WORLD.get(inst.world.getRegistryKey()); + + for (Entry entry : originals.entrySet()) { + if (skipActors == null || !skipActors.contains(entry.getKey())) { + ServerAnimationController.OriginalTransform ot = entry.getValue(); + if (ot.worldKey() == inst.world.getRegistryKey()) { + Entity e = inst.world.getEntity(entry.getKey()); + if (e != null) { + switch (e) { + case ServerPlayerEntity player: + preparePlayerForAnimationTeleport(player); + Vec3d restorePos = ot.restorePosOrPos(); + player.networkHandler.requestTeleport(restorePos.x, restorePos.y, restorePos.z, player.getYaw(), player.getPitch()); + clearDeferredQueueOriginal(inst.world, entry.getKey()); + break; + case LivingEntity living: + living.refreshPositionAndAngles(ot.pos().x, ot.pos().y, ot.pos().z, ot.yaw(), ot.pitch()); + living.setHeadYaw(ot.headYaw()); + living.setBodyYaw(ot.bodyYaw()); + break; + default: + e.refreshPositionAndAngles(ot.pos().x, ot.pos().y, ot.pos().z, ot.yaw(), ot.pitch()); + } + + e.setVelocity(Vec3d.ZERO); + e.fallDistance = 0.0; + if (e instanceof MobEntity && states != null) { + ServerAnimationController.AiDisableState state = states.get(entry.getKey()); + if (state != null) { + state.lockedYaw = ot.yaw(); + state.lockedHeadYaw = ot.headYaw(); + state.lockedBodyYaw = ot.bodyYaw(); + state.lockedPitch = ot.pitch(); + } + } + } + } + } + } + } + } + + @Nullable + private static ServerAnimationController.OriginalTransform getOriginalTransformForActor(ServerAnimationController.ActiveInstance inst, UUID actorUuid) { + return inst != null && actorUuid != null && inst.originalTransforms != null ? inst.originalTransforms.get(actorUuid) : null; + } + + private static boolean startNextQueuedForPlayerDeferringRestore( + ServerWorld world, ServerAnimationController.ActiveInstance inst, @Nullable UUID ignoreInstanceId + ) { + if (world != null && inst != null && inst.playerUuid != null) { + boolean inserted = captureDeferredQueueContext(world, inst.playerUuid, inst); + boolean started = startNextQueuedForPlayer(world, inst.playerUuid, ignoreInstanceId, resolveTransitionYaw(inst)); + if (!started && inserted) { + clearDeferredQueueOriginal(world, inst.playerUuid); + } + + return started; + } else { + return false; + } + } + + private static boolean captureDeferredQueueContextFromActiveInstance(ServerWorld world, UUID playerUuid) { + ServerAnimationController.ActiveInstance active = findActiveInstanceForActors(world, List.of(playerUuid)); + return captureDeferredQueueContext(world, playerUuid, active); + } + + private static boolean captureDeferredQueueContext(ServerWorld world, UUID playerUuid, @Nullable ServerAnimationController.ActiveInstance inst) { + if (world != null && playerUuid != null && inst != null) { + ServerAnimationController.OriginalTransform original = getOriginalTransformForActor(inst, playerUuid); + if (original == null) { + return false; + } + + Map byPlayer = DEFERRED_QUEUE_CONTEXT_BY_WORLD.computeIfAbsent( + world.getRegistryKey(), ignored -> new HashMap<>() + ); + ServerAnimationController.DeferredQueueContext context = new ServerAnimationController.DeferredQueueContext( + original, queueContinuationPlacementForInstance(inst) + ); + return byPlayer.putIfAbsent(playerUuid, context) == null; + } else { + return false; + } + } + + private static Map applyDeferredQueueOriginal( + ServerWorld world, @Nullable UUID playerUuid, Map originals, AnimationStartContext startContext + ) { + if (world == null || playerUuid == null || originals == null || originals.isEmpty()) { + return originals; + } + + if (startContext != null && startContext.queueContinuation()) { + ServerAnimationController.OriginalTransform deferred = getDeferredQueueOriginal(world, playerUuid); + if (deferred == null) { + return originals; + } + + Map carried = new HashMap<>(originals); + carried.put(playerUuid, deferred); + return carried; + } else { + return originals; + } + } + + @Nullable + private static ServerAnimationController.OriginalTransform getDeferredQueueOriginal(ServerWorld world, UUID playerUuid) { + ServerAnimationController.DeferredQueueContext context = getDeferredQueueContext(world, playerUuid); + return context == null ? null : context.originalTransform(); + } + + private static void clearDeferredQueueOriginal(ServerWorld world, UUID playerUuid) { + Map byPlayer = DEFERRED_QUEUE_CONTEXT_BY_WORLD.get(world.getRegistryKey()); + if (byPlayer != null) { + byPlayer.remove(playerUuid); + if (byPlayer.isEmpty()) { + DEFERRED_QUEUE_CONTEXT_BY_WORLD.remove(world.getRegistryKey()); + } + } + } + + @Nullable + private static ServerAnimationController.DeferredQueueContext getDeferredQueueContext(ServerWorld world, UUID playerUuid) { + Map byPlayer = DEFERRED_QUEUE_CONTEXT_BY_WORLD.get(world.getRegistryKey()); + return byPlayer == null ? null : byPlayer.get(playerUuid); + } + + @Nullable + private static ServerAnimationController.BlockPlacement queueContinuationPlacementForInstance(ServerAnimationController.ActiveInstance inst) { + return inst != null && inst.blockPlacement != null ? inst.blockPlacement : null; + } + + private static boolean isCenterSupportPlacement(ServerAnimationController.BlockPlacement placement) { + return placement != null && placement.centerSupport(); + } + + private static void disableAiForActors(ServerWorld world, List actorUuids) { + if (!actorUuids.isEmpty()) { + Map states = AI_DISABLE_STATE_BY_WORLD.computeIfAbsent(world.getRegistryKey(), k -> new HashMap<>()); + + for (UUID uuid : actorUuids) { + if (world.getEntity(uuid) instanceof MobEntity mob) { + ServerAnimationController.AiDisableState state = states.get(uuid); + if (state == null) { + boolean originalAiDisabled = mob.isAiDisabled(); + state = new ServerAnimationController.AiDisableState(originalAiDisabled); + states.put(uuid, state); + } else { + state.locks++; + state.needsRestore = false; + } + + captureOrientation(mob, state); + applyNoAiTags(mob, state); + mob.setAiDisabled(true); + mob.getNavigation().stop(); + mob.setVelocity(Vec3d.ZERO); + mob.fallDistance = 0.0; + mob.setYaw(state.lockedYaw); + mob.setPitch(state.lockedPitch); + mob.setHeadYaw(state.lockedHeadYaw); + mob.setBodyYaw(state.lockedBodyYaw); + } + } + } + } + + private static void restoreAiForActors(ServerWorld world, List actorUuids) { + if (!actorUuids.isEmpty()) { + Map states = AI_DISABLE_STATE_BY_WORLD.get(world.getRegistryKey()); + if (states != null) { + for (UUID uuid : actorUuids) { + ServerAnimationController.AiDisableState state = states.get(uuid); + if (state != null) { + if (state.locks > 1) { + state.locks--; + } else { + state.locks = 0; + state.needsRestore = true; + tryRestoreAi(world, uuid, state, states); + } + } + } + + if (states.isEmpty()) { + AI_DISABLE_STATE_BY_WORLD.remove(world.getRegistryKey()); + } + } + } + } + + private static void tryRestoreAi( + ServerWorld world, UUID actorUuid, ServerAnimationController.AiDisableState state, Map states + ) { + Entity e = world.getEntity(actorUuid); + if (e instanceof MobEntity mob) { + restoreAiState(mob, state); + states.remove(actorUuid); + } else if (e != null) { + states.remove(actorUuid); + } + } + + private static void processPendingAiRestores(ServerWorld world) { + Map states = AI_DISABLE_STATE_BY_WORLD.get(world.getRegistryKey()); + if (states != null && !states.isEmpty()) { + Iterator> it = states.entrySet().iterator(); + + while (it.hasNext()) { + Entry entry = it.next(); + ServerAnimationController.AiDisableState state = entry.getValue(); + if (state.locks <= 0 && state.needsRestore) { + Entity e = world.getEntity(entry.getKey()); + if (e instanceof MobEntity mob) { + restoreAiState(mob, state); + it.remove(); + } else if (e != null) { + it.remove(); + } + } + } + + if (states.isEmpty()) { + AI_DISABLE_STATE_BY_WORLD.remove(world.getRegistryKey()); + } + } + } + + private static void freezeActiveActors(ServerWorld world, Set actorUuids) { + if (!actorUuids.isEmpty()) { + Map states = AI_DISABLE_STATE_BY_WORLD.get(world.getRegistryKey()); + if (states != null && !states.isEmpty()) { + for (UUID uuid : actorUuids) { + ServerAnimationController.AiDisableState state = states.get(uuid); + if (state != null && world.getEntity(uuid) instanceof MobEntity mob) { + freezeMobDuringAnimation(mob, state); + } + } + } + } + } + + private static void captureOrientation(MobEntity mob, ServerAnimationController.AiDisableState state) { + state.lockedYaw = mob.getYaw(); + state.lockedHeadYaw = mob.getHeadYaw(); + state.lockedBodyYaw = mob.getBodyYaw(); + state.lockedPitch = mob.getPitch(); + } + + private static void freezeMobDuringAnimation(MobEntity mob, ServerAnimationController.AiDisableState state) { + mob.setVelocity(Vec3d.ZERO); + mob.fallDistance = 0.0; + mob.setYaw(state.lockedYaw); + mob.setPitch(state.lockedPitch); + mob.setHeadYaw(state.lockedHeadYaw); + mob.setBodyYaw(state.lockedBodyYaw); + } + + private static void applyNoAiTags(MobEntity mob, ServerAnimationController.AiDisableState state) { + mob.addCommandTag("afw_noai"); + if (state.originalAiDisabled) { + mob.addCommandTag("afw_noai_orig"); + } else { + mob.removeCommandTag("afw_noai_orig"); + } + } + + public static void sendActiveInstancesToPlayer(ServerPlayerEntity player) { + if (player.getEntityWorld() instanceof ServerWorld world) { + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world) { + List safeKeys = sanitizeActorKeys(inst.actorKeys, inst.actorUuids.size()); + List safeStages = inst.stages == null ? List.of() : List.copyOf(inst.stages); + int stageIndex = inst.stageTracker == null ? 0 : inst.stageTracker.stageIndex; + long stageStartTick = inst.stageTracker == null ? inst.startTick : inst.stageTracker.stageStartTick; + StartAnimationS2CPayload payload = new StartAnimationS2CPayload( + inst.animationId, + inst.modelRootsByActor, + entry.getKey(), + inst.actorUuids, + safeKeys, + safeStages, + inst.startTick, + stageIndex, + stageStartTick, + resolvePublishedStageEndTick(inst), + inst.speed, + inst.lockOrientation, + inst.lockedYaw, + inst.lockedHeadYaw, + inst.lockedPitch, + inst.cameraOrbitTarget + ); + ServerPlayNetworking.send(player, payload); + inst.subscribedPlayerUuids.add(player.getUuid()); + } + } + } + } + + private static void tickPlayerQueues(ServerWorld world) { + Map queues = PLAYER_QUEUES_BY_WORLD.get(world.getRegistryKey()); + if (queues != null && !queues.isEmpty()) { + List removePlayers = new ArrayList<>(); + + for (UUID playerUuid : List.copyOf(queues.keySet())) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(playerUuid); + if (player == null || player.getEntityWorld() != world || !player.isAlive() || player.isRemoved()) { + removePlayers.add(playerUuid); + } else if (!isActorActiveIgnoringInstance(world, playerUuid, null)) { + startNextQueuedForPlayer(world, playerUuid, null, null); + ServerAnimationController.PlayerQueueState state = queues.get(playerUuid); + if (state == null || state.entries.isEmpty()) { + removePlayers.add(playerUuid); + } + } + } + + for (UUID playerUuid : removePlayers) { + clearPlayerQueueByUuid(world, playerUuid, null, null); + } + + if (queues.isEmpty()) { + PLAYER_QUEUES_BY_WORLD.remove(world.getRegistryKey(), queues); + } + } + } + + @Nullable + private static UUID inferSinglePlayerUuid(ServerWorld world, List actors) { + UUID found = null; + + for (Entity actor : actors) { + if (actor instanceof ServerPlayerEntity player && player.getEntityWorld() == world) { + if (found != null && !found.equals(player.getUuid())) { + return null; + } + + found = player.getUuid(); + } + } + + return found; + } + + @Nullable + private static UUID resolvePlayerUuid(ServerWorld world, List actorUuids) { + if (actorUuids != null && !actorUuids.isEmpty()) { + UUID found = null; + + for (UUID uuid : actorUuids) { + Entity entity = world.getEntity(uuid); + if (entity instanceof ServerPlayerEntity) { + if (found != null && !found.equals(uuid)) { + return null; + } + + found = uuid; + } + } + + return found; + } else { + return null; + } + } + + private static void insertQueueEntry(Deque entries, ServerAnimationController.QueuedEntry entry, int insertIndex) { + List list = new ArrayList<>(entries); + int pos = resolveInsertPosition(list.size(), insertIndex); + list.add(pos, entry); + entries.clear(); + entries.addAll(list); + } + + private static int resolveInsertPosition(int size, int insertIndex) { + if (insertIndex == 0) { + return size; + } + + if (insertIndex > 0) { + return Math.max(0, Math.min(size, insertIndex - 1)); + } + + int fromEnd = size + insertIndex + 1; + return Math.max(0, Math.min(size, fromEnd)); + } + + private static int describeQueuePosition(Deque entries, UUID entryId) { + int idx = 1; + + for (ServerAnimationController.QueuedEntry entry : entries) { + if (entry.entryId.equals(entryId)) { + return idx; + } + + idx++; + } + + return Math.max(1, entries.size()); + } + + private static String describeActorForDebug(@Nullable ServerWorld world, @Nullable UUID actorUuid) { + if (actorUuid != null && world != null) { + Entity entity = world.getEntity(actorUuid); + if (entity == null && world.getServer() != null) { + ServerPlayerEntity player = world.getServer().getPlayerManager().getPlayer(actorUuid); + if (player != null && player.getEntityWorld() == world) { + entity = player; + } + } + + if (entity == null) { + String uuid = actorUuid.toString(); + return "missing:" + uuid.substring(0, 8); + } else { + String display = entity.getDisplayName().getString(); + Identifier typeId = Registries.ENTITY_TYPE.getId(entity.getType()); + return display != null && !display.isBlank() ? display : typeId.getPath(); + } + } else { + return "(unknown)"; + } + } + + private static String describeActorsForDebug(@Nullable ServerWorld world, @Nullable List actorUuids) { + if (actorUuids != null && !actorUuids.isEmpty()) { + List labels = new ArrayList<>(); + + for (UUID actorUuid : actorUuids) { + if (actorUuid != null) { + labels.add(describeActorForDebug(world, actorUuid)); + } + } + + if (labels.isEmpty()) { + return "(none)"; + } + + int cap = 4; + return labels.size() <= cap ? String.join(", ", labels) : String.join(", ", labels.subList(0, cap)) + " +" + (labels.size() - cap) + " more"; + } else { + return "(none)"; + } + } + + private static String describeAnimationIdForDebug(@Nullable Identifier animationId) { + return animationId == null ? "(unknown)" : animationId.toString(); + } + + private static boolean isActorActiveIgnoringInstance(ServerWorld world, UUID actorUuid, @Nullable UUID ignoreInstanceId) { + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + if (ignoreInstanceId == null || !ignoreInstanceId.equals(entry.getKey())) { + ServerAnimationController.ActiveInstance inst = entry.getValue(); + if (inst.world == world && inst.actorUuidSet.contains(actorUuid)) { + return true; + } + } + } + + return false; + } + + private static boolean startNextQueuedForPlayer(ServerWorld world, UUID playerUuid, @Nullable UUID ignoreInstanceId, @Nullable Float transitionYaw) { + Map queues = PLAYER_QUEUES_BY_WORLD.get(world.getRegistryKey()); + if (queues == null) { + return false; + } + + ServerAnimationController.PlayerQueueState queue = queues.get(playerUuid); + if (queue != null && !queue.entries.isEmpty()) { + while (!queue.entries.isEmpty()) { + ServerAnimationController.QueuedEntry entry = queue.entries.peekFirst(); + if (entry == null) { + queue.entries.pollFirst(); + } else { + if (isActorActiveIgnoringInstance(world, playerUuid, ignoreInstanceId)) { + return false; + } + + ServerAnimationController.QueueStartResult result = tryStartQueuedEntry(world, queue, entry, transitionYaw); + if (result == ServerAnimationController.QueueStartResult.RETRY) { + return false; + } + + queue.entries.pollFirst(); + clearQueuedMobMappings(world, queue.playerUuid, entry); + if (result == ServerAnimationController.QueueStartResult.STARTED) { + if (queue.entries.isEmpty()) { + queues.remove(queue.playerUuid); + } + + return true; + } + } + } + + queues.remove(queue.playerUuid); + return false; + } else { + return false; + } + } + + private static ServerAnimationController.QueueStartResult tryStartQueuedEntry( + ServerWorld world, ServerAnimationController.PlayerQueueState queue, ServerAnimationController.QueuedEntry entry, @Nullable Float transitionYaw + ) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(queue.playerUuid); + if (player != null && player.getEntityWorld() == world && player.isAlive() && !player.isRemoved()) { + List resolvedActors = new ArrayList<>(); + boolean missingNonPlayer = false; + + for (UUID actorUuid : entry.actorUuids) { + if (actorUuid.equals(queue.playerUuid)) { + resolvedActors.add(player); + } else { + Entity entity = world.getEntity(actorUuid); + if (!(entity instanceof LivingEntity living && living.isAlive() && !living.isRemoved())) { + missingNonPlayer = true; + break; + } + + resolvedActors.add(entity); + } + } + + if (missingNonPlayer) { + sendOrLogDebug( + world, + player, + NonDebugChatCategory.WARNING, + dbg("queue_skipped_missing_non_player_actor", describeAnimationIdForDebug(entry.animationId), describeActorsForDebug(world, entry.actorUuids)), + false + ); + return ServerAnimationController.QueueStartResult.SKIP; + } else if (resolvedActors.isEmpty()) { + sendOrLogDebug( + world, + player, + NonDebugChatCategory.WARNING, + dbg("queue_skipped_no_actors_resolved", describeAnimationIdForDebug(entry.animationId), describeActorsForDebug(world, entry.actorUuids)), + false + ); + return ServerAnimationController.QueueStartResult.SKIP; + } else if (hasAnimationStartProtection(world, player)) { + return ServerAnimationController.QueueStartResult.RETRY; + } else { + AnimationDefinitions.Definition def = AnimationDefinitions.getDefinition(entry.animationId); + if (def == null) { + sendOrLogDebug( + world, + player, + NonDebugChatCategory.WARNING, + dbg("queue_skipped_missing_definition", describeAnimationIdForDebug(entry.animationId), describeActorsForDebug(world, entry.actorUuids)), + false + ); + return ServerAnimationController.QueueStartResult.SKIP; + } else { + List sorted = new ArrayList<>(resolvedActors); + sorted.sort(Comparator.comparingInt(Entity::getId)); + List uuids = sorted.stream().map(Entity::getUuid).toList(); + if (!waterRequirementMet(world, def, uuids)) { + sendOrLogDebug( + world, + player, + NonDebugChatCategory.WARNING, + dbg("queue_skipped_requirements_not_met", describeAnimationIdForDebug(entry.animationId), describeActorsForDebug(world, entry.actorUuids)), + false + ); + return ServerAnimationController.QueueStartResult.SKIP; + } else { + List actorKeys = AnimationDefinitions.resolveActorKeys(def, sorted, world.getRandom()); + AnimationStartContext nextContext = sanitizeStartContext(entry.startContext()).asQueueContinuation(); + UUID instanceId = startAndBroadcast( + world, player, def.id(), uuids, actorKeys, def.stages(), entry.damageBehavior, entry.ignoreAttackers, nextContext, false, transitionYaw + ); + if (instanceId == null) { + sendOrLogDebug( + world, + player, + NonDebugChatCategory.WARNING, + dbg("queue_skipped_start_failed", describeAnimationIdForDebug(entry.animationId), describeActorsForDebug(world, entry.actorUuids)), + false + ); + return ServerAnimationController.QueueStartResult.SKIP; + } else { + sendOrLogDebug( + world, + player, + NonDebugChatCategory.INFO, + dbg("queued_animation_started", describeAnimationIdForDebug(def.id()), describeActorsForDebug(world, uuids)), + false + ); + return ServerAnimationController.QueueStartResult.STARTED; + } + } + } + } + } else { + sendOrLogDebug( + world, + null, + NonDebugChatCategory.WARNING, + dbg("queue_skipped_player_unavailable", describeAnimationIdForDebug(entry.animationId), describeActorsForDebug(world, entry.actorUuids)), + false + ); + return ServerAnimationController.QueueStartResult.SKIP; + } + } + + private static void clearQueuedMobMappings(ServerWorld world, UUID playerUuid, ServerAnimationController.QueuedEntry entry) { + Map byMob = QUEUED_MOB_TO_PLAYER_BY_WORLD.get(world.getRegistryKey()); + if (byMob != null && !byMob.isEmpty() && entry != null) { + for (UUID actorUuid : entry.actorUuids) { + if (actorUuid != null && !actorUuid.equals(playerUuid)) { + UUID owner = byMob.get(actorUuid); + if (playerUuid.equals(owner)) { + byMob.remove(actorUuid); + } + } + } + } + } + + private static void clearPlayerQueueByUuid(ServerWorld world, UUID playerUuid, @Nullable NonDebugChatCategory category, @Nullable Text debugMessage) { + if (world != null && playerUuid != null) { + clearDeferredQueueOriginal(world, playerUuid); + Map queues = PLAYER_QUEUES_BY_WORLD.get(world.getRegistryKey()); + if (queues != null) { + ServerAnimationController.PlayerQueueState queue = queues.remove(playerUuid); + if (queue != null) { + for (ServerAnimationController.QueuedEntry entry : queue.entries) { + clearQueuedMobMappings(world, playerUuid, entry); + } + + if (queues.isEmpty()) { + PLAYER_QUEUES_BY_WORLD.remove(world.getRegistryKey()); + } + + if (category != null && debugMessage != null) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(playerUuid); + sendOrLogDebug(world, player, category, debugMessage, false); + } + } + } + } + } + + private static void handleQueueOnStop(ServerWorld world, ServerAnimationController.ActiveInstance inst, ServerAnimationController.StopReason reason) { + if (world != null && inst != null && inst.playerUuid != null) { + switch (reason) { + case NATURAL_END: + boolean started = startNextQueuedForPlayerDeferringRestore(world, inst, null); + if (started) { + sendOrLogDebug(world, null, NonDebugChatCategory.INFO, dbg("queue_continued_for_player"), false); + } + break; + case EXTERNAL_STOP: + case DAMAGE_STOP: + case ACTOR_DEATH: + case STOP_ALL: + clearPlayerQueueByUuid(world, inst.playerUuid, NonDebugChatCategory.INFO, dbg("queue_cleared")); + case JOIN_REPLACE: + } + } + } + + public static boolean isActorPendingOrActive(ServerWorld world, UUID actorUuid) { + if (isActorActiveIgnoringInstance(world, actorUuid, null)) { + return true; + } + + Map queues = PLAYER_QUEUES_BY_WORLD.get(world.getRegistryKey()); + if (queues != null && !queues.isEmpty()) { + if (queues.containsKey(actorUuid)) { + return true; + } + + for (ServerAnimationController.PlayerQueueState queue : queues.values()) { + for (ServerAnimationController.QueuedEntry entry : queue.entries) { + if (entry.actorUuids.contains(actorUuid)) { + return true; + } + } + } + + return false; + } else { + return false; + } + } + + private static void tickNoAiCleanup(ServerWorld world) { + RegistryKey key = world.getRegistryKey(); + boolean startupWindowCleanup = isWithinStartupNoAiCleanupWindow(world); + if (startupWindowCleanup || !world.getPlayers().isEmpty()) { + if (!startupWindowCleanup) { + int ticksLeft = NOAI_CLEANUP_TICKS_BY_WORLD.getOrDefault(key, 0) - 1; + if (ticksLeft > 0) { + NOAI_CLEANUP_TICKS_BY_WORLD.put(key, ticksLeft); + return; + } + + NOAI_CLEANUP_TICKS_BY_WORLD.put(key, 6000); + } + + Map states = AI_DISABLE_STATE_BY_WORLD.get(key); + + for (Entity entity : world.iterateEntities()) { + if (entity instanceof MobEntity mob && (mob.getCommandTags().contains("afw_noai") || mob.getCommandTags().contains("afw_noai_orig"))) { + UUID uuid = mob.getUuid(); + if (startupWindowCleanup || !isActorActive(world, uuid)) { + ServerAnimationController.AiDisableState state = states == null ? null : states.get(uuid); + if (state != null) { + state.locks = 0; + state.needsRestore = true; + tryRestoreAi(world, uuid, state, states); + } else { + boolean originalDisabled = mob.getCommandTags().contains("afw_noai_orig"); + mob.setAiDisabled(originalDisabled); + clearNoAiTags(mob); + } + } + } + } + + if (states != null && states.isEmpty()) { + AI_DISABLE_STATE_BY_WORLD.remove(key); + } + } + } + + private static boolean isWithinStartupNoAiCleanupWindow(ServerWorld world) { + if (world != null && world.getServer() != null) { + int now = world.getServer().getTicks(); + return now <= STARTUP_NOAI_CLEANUP_UNTIL_TICK; + } else { + return false; + } + } + + private static boolean isWithinStartupStartBlockWindow(ServerWorld world) { + if (world != null && world.getServer() != null) { + int now = world.getServer().getTicks(); + return now <= STARTUP_START_BLOCK_UNTIL_TICK; + } else { + return false; + } + } + + private static List sanitizeActorKeys(List actorKeys, int actorCount) { + if (actorKeys != null && !actorKeys.isEmpty()) { + return actorKeys.size() != actorCount ? List.of() : List.copyOf(actorKeys); + } else { + return List.of(); + } + } + + private static Map sanitizeMetadata(@Nullable Map metadata) { + if (metadata != null && !metadata.isEmpty()) { + LinkedHashMap out = new LinkedHashMap<>(); + + for (Entry entry : metadata.entrySet()) { + if (entry != null) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key != null && !key.isBlank() && value != null) { + out.put(key, value); + } + } + } + + return out.isEmpty() ? Map.of() : Map.copyOf(out); + } else { + return Map.of(); + } + } + + private static AnimationStartContext sanitizeStartContext(@Nullable AnimationStartContext context) { + return context == null + ? AnimationStartContext.empty() + : new AnimationStartContext(sanitizeMetadata(context.addonMetadata()), context.sessionData(), context.joinPredecessor(), context.queueContinuation()); + } + + private static void registerActiveInstance(UUID instanceId, ServerAnimationController.ActiveInstance instance) { + ACTIVE_INSTANCES.put(instanceId, instance); + Map actorIndex = ACTIVE_INSTANCE_BY_ACTOR.computeIfAbsent(instance.world.getRegistryKey(), ignored -> new ConcurrentHashMap<>()); + + for (UUID actorUuid : instance.actorUuidSet) { + actorIndex.put(actorUuid, instanceId); + } + } + + @Nullable + private static ServerAnimationController.ActiveInstance removeActiveInstance(UUID instanceId) { + ServerAnimationController.ActiveInstance removed = ACTIVE_INSTANCES.remove(instanceId); + if (removed == null) { + return null; + } + + Map actorIndex = ACTIVE_INSTANCE_BY_ACTOR.get(removed.world.getRegistryKey()); + if (actorIndex != null) { + for (UUID actorUuid : removed.actorUuidSet) { + actorIndex.remove(actorUuid, instanceId); + } + + if (actorIndex.isEmpty()) { + ACTIVE_INSTANCE_BY_ACTOR.remove(removed.world.getRegistryKey(), actorIndex); + } + } + + return removed; + } + + @Nullable + public static UUID findActiveInstanceId(ServerWorld world, UUID actorUuid) { + if (world != null && actorUuid != null) { + Map actorIndex = ACTIVE_INSTANCE_BY_ACTOR.get(world.getRegistryKey()); + if (actorIndex == null) { + return null; + } + + UUID instanceId = actorIndex.get(actorUuid); + if (instanceId == null) { + return null; + } + + ServerAnimationController.ActiveInstance instance = ACTIVE_INSTANCES.get(instanceId); + if (instance != null && instance.world == world && instance.actorUuidSet.contains(actorUuid)) { + return instanceId; + } + + actorIndex.remove(actorUuid, instanceId); + return null; + } else { + return null; + } + } + + public static boolean isActorActive(ServerWorld world, UUID actorUuid) { + return findActiveInstanceId(world, actorUuid) != null; + } + + private static void clearNoAiTags(MobEntity mob) { + mob.removeCommandTag("afw_noai"); + mob.removeCommandTag("afw_noai_orig"); + } + + private static void restoreAiState(MobEntity mob, ServerAnimationController.AiDisableState state) { + mob.setVelocity(Vec3d.ZERO); + mob.fallDistance = 0.0; + mob.setAiDisabled(state.originalAiDisabled); + clearNoAiTags(mob); + } + + private static void acquirePlayerLocks(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + Map states = PLAYER_LOCK_STATE_BY_WORLD.computeIfAbsent(world.getRegistryKey(), k -> new HashMap<>()); + + for (UUID uuid : actorUuids) { + if (world.getEntity(uuid) instanceof ServerPlayerEntity player) { + ServerAnimationController.PlayerLockState existing = states.get(uuid); + if (existing == null) { + states.put(uuid, new ServerAnimationController.PlayerLockState(player.hasNoGravity())); + } else { + existing.locks++; + } + + player.setNoGravity(true); + player.setVelocity(Vec3d.ZERO); + player.fallDistance = 0.0; + } + } + } + } + + private static void releasePlayerLocks(ServerWorld world, List actorUuids) { + if (world != null && actorUuids != null && !actorUuids.isEmpty()) { + Map states = PLAYER_LOCK_STATE_BY_WORLD.get(world.getRegistryKey()); + if (states != null && !states.isEmpty()) { + for (UUID uuid : actorUuids) { + ServerAnimationController.PlayerLockState state = states.get(uuid); + if (state != null) { + if (state.locks > 1) { + state.locks--; + } else { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(uuid); + if (player != null) { + player.setNoGravity(state.originalNoGravity); + player.fallDistance = 0.0; + } + + states.remove(uuid); + } + } + } + + if (states.isEmpty()) { + PLAYER_LOCK_STATE_BY_WORLD.remove(world.getRegistryKey()); + } + } + } + } + + private static void clearAllPlayerLocksInWorld(ServerWorld world) { + if (world != null) { + Map states = PLAYER_LOCK_STATE_BY_WORLD.remove(world.getRegistryKey()); + if (states != null && !states.isEmpty()) { + for (Entry entry : states.entrySet()) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(entry.getKey()); + if (player != null) { + player.setNoGravity(entry.getValue().originalNoGravity); + player.fallDistance = 0.0; + } + } + } + } + } + + private static void cleanupStalePlayerLocks(ServerWorld world, Set currentlyLockedPlayers) { + if (world != null) { + Set lockedNow = currentlyLockedPlayers == null ? Set.of() : currentlyLockedPlayers; + Map states = PLAYER_LOCK_STATE_BY_WORLD.get(world.getRegistryKey()); + if (states != null && !states.isEmpty()) { + Iterator> it = states.entrySet().iterator(); + + while (it.hasNext()) { + Entry entry = it.next(); + UUID uuid = entry.getKey(); + if (!lockedNow.contains(uuid) && !isActorActive(world, uuid)) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(uuid); + if (player != null) { + player.setNoGravity(entry.getValue().originalNoGravity); + player.fallDistance = 0.0; + } + + it.remove(); + } + } + + if (states.isEmpty()) { + PLAYER_LOCK_STATE_BY_WORLD.remove(world.getRegistryKey()); + } + } + + for (ServerPlayerEntity player : world.getPlayers()) { + UUID uuid = player.getUuid(); + if (!lockedNow.contains(uuid) && !isActorActive(world, uuid) && player.hasNoGravity() && !player.isSpectator() && !player.getAbilities().flying) { + player.setNoGravity(false); + player.fallDistance = 0.0; + } + } + } + } + + private static void lockActivePlayers(ServerWorld world, Map playersToLock, Set underwaterPlayers) { + if (!playersToLock.isEmpty()) { + for (Entry entry : playersToLock.entrySet()) { + if (world.getEntity(entry.getKey()) instanceof ServerPlayerEntity player) { + preparePlayerForAnimationTeleport(player); + ServerAnimationController.SharedTransform sharedTransform = entry.getValue(); + Vec3d targetPos = sharedTransform.pos(); + double dx = player.getX() - targetPos.x; + double dy = player.getY() - targetPos.y; + double dz = player.getZ() - targetPos.z; + if (dx * dx + dy * dy + dz * dz > 4.0E-4) { + player.setPosition(targetPos.x, targetPos.y, targetPos.z); + player.networkHandler.requestTeleport(targetPos.x, targetPos.y, targetPos.z, player.getYaw(), player.getPitch()); + } + + player.setNoGravity(true); + if (underwaterPlayers.contains(player.getUuid())) { + player.setAir(player.getMaxAir()); + } + + player.setVelocity(Vec3d.ZERO); + player.fallDistance = 0.0; + } + } + } + } + + private static void nudgeNonActiveMobsAwayFromAnimatedPlayers(ServerWorld world, Set activePlayerUuids, long nowTick) { + if (world != null && activePlayerUuids != null && !activePlayerUuids.isEmpty()) { + if (nowTick % 4L == 0L) { + for (UUID playerUuid : activePlayerUuids) { + ServerPlayerEntity player = Objects.requireNonNull(world.getServer()).getPlayerManager().getPlayer(playerUuid); + if (player != null && player.getEntityWorld() == world && player.isAlive() && !player.isRemoved()) { + Box searchBox = player.getBoundingBox().expand(1.0, 0.5, 1.0); + List nearby = world.getOtherEntities( + player, + searchBox, + entityx -> entityx instanceof MobEntity mobx && mobx.isAlive() && !mobx.isRemoved() && !isActorPendingOrActive(world, mobx.getUuid()) + ); + if (!nearby.isEmpty()) { + int nudged = 0; + + for (Entity entity : nearby) { + if (entity instanceof MobEntity mob && !isActorPendingOrActive(world, mob.getUuid())) { + if (nudged >= 6) { + break; + } + + if (nudgeMobAwayFromPlayer(player, mob)) { + nudged++; + } + } + } + } + } + } + } + } + } + + private static boolean nudgeMobAwayFromPlayer(ServerPlayerEntity player, MobEntity mob) { + double dx = mob.getX() - player.getX(); + double dz = mob.getZ() - player.getZ(); + double horizontalDistSq = dx * dx + dz * dz; + double minDistSq = 0.7224999999999999; + if (horizontalDistSq >= minDistSq) { + return false; + } + + Vec3d dir = resolveHorizontalNudgeDirection(mob, dx, dz, horizontalDistSq); + double dist = Math.sqrt(Math.max(horizontalDistSq, 1.0E-6)); + double overlap = Math.max(0.0, 0.85 - dist); + double strength = 0.08 * (0.6 + overlap / 0.85); + mob.addVelocity(dir.x * strength, 0.0, dir.z * strength); + mob.fallDistance = 0.0; + return true; + } + + private static Vec3d resolveHorizontalNudgeDirection(MobEntity mob, double dx, double dz, double horizontalDistSq) { + if (horizontalDistSq > 1.0E-6) { + double invLen = 1.0 / Math.sqrt(horizontalDistSq); + return new Vec3d(dx * invLen, 0.0, dz * invLen); + } else { + long bits = mob.getUuid().getLeastSignificantBits() ^ mob.getUuid().getMostSignificantBits(); + double angle = (bits & 65535L) * 9.587379924285257E-5; + return new Vec3d(Math.cos(angle), 0.0, Math.sin(angle)); + } + } + + private static void clearAttackTargets(ServerWorld world, Set actorUuids) { + if (!actorUuids.isEmpty()) { + for (Entity entity : world.iterateEntities()) { + if (entity instanceof MobEntity mob) { + LivingEntity target = mob.getTarget(); + if (target != null && actorUuids.contains(target.getUuid())) { + mob.setTarget(null); + mob.getNavigation().stop(); + } + } + } + } + } + + private static double clampSpeed(double speed) { + return !Double.isNaN(speed) && !Double.isInfinite(speed) && !(speed <= 0.0) ? Math.max(0.1, Math.min(speed, 4.0)) : 1.0; + } + + private static double resolveStageSpeed(List stages, int index) { + if (stages != null && !stages.isEmpty()) { + int clamped = Math.max(0, Math.min(index, stages.size() - 1)); + double speed = stages.get(clamped).speed(); + return !Double.isNaN(speed) && !Double.isInfinite(speed) && !(speed <= 0.0) ? speed : 1.0; + } else { + return 1.0; + } + } + + private static double sanitizeMultiplier(double multiplier) { + return !Double.isNaN(multiplier) && !Double.isInfinite(multiplier) && !(multiplier <= 0.0) ? Math.max(0.1, Math.min(multiplier, 4.0)) : 1.0; + } + + private static boolean maybeAdvanceStages( + ServerWorld world, + UUID instanceId, + ServerAnimationController.ActiveInstance inst, + long now, + List stageAdvances, + List toRemove + ) { + List stages = inst.stages; + ServerAnimationController.StageTracker tracker = inst.stageTracker; + if (stages != null && !stages.isEmpty() && tracker != null) { + int idx = Math.max(0, Math.min(tracker.stageIndex, stages.size() - 1)); + long stageStart = tracker.stageStartTick; + double speed = clampSpeed(inst.speed); + + while (true) { + AnimationStageInfo current = stages.get(idx); + long duration = AnimationStageTiming.resolveDurationTicks(inst.animationId, current, speed); + if (duration < 0L) { + return false; + } + + long targetEnd = stageStart + duration; + if (now < targetEnd) { + return false; + } + + if (idx + 1 >= stages.size()) { + long stopTick = now + 1L; + boolean chained = false; + if (inst.playerUuid != null) { + chained = startNextQueuedForPlayerDeferringRestore(world, inst, instanceId); + } + + StopAnimationS2CPayload payload = new StopAnimationS2CPayload(instanceId, stopTick); + + for (ServerPlayerEntity p : computeInstanceBroadcastTargets(world, null, inst)) { + ServerPlayNetworking.send(p, payload); + } + + fireStopEvent(instanceId, inst); + restoreTransforms(inst, chained && inst.playerUuid != null ? Set.of(inst.playerUuid) : null); + restoreAiForActors(world, inst.actorUuids); + releasePlayerLocks(world, inst.actorUuids); + toRemove.add(instanceId); + if (chained) { + sendOrLogDebug(world, null, NonDebugChatCategory.INFO, dbg("queue_continued_for_player"), false); + } + + return true; + } + + idx++; + stageStart = targetEnd; + tracker.stageIndex = idx; + tracker.stageStartTick = stageStart; + tracker.progressEndTickOverride = -1L; + inst.speed = clampSpeed(resolveStageSpeed(stages, idx)); + speed = clampSpeed(inst.speed); + long nextStageEnd = resolveStageEndTick(inst.animationId, stages.get(idx), stageStart, speed); + stageAdvances.add(new ServerAnimationController.StageAdvance(instanceId, stageStart, nextStageEnd, idx)); + } + } else { + return false; + } + } + + private static long resolveStageEndTick(ServerAnimationController.ActiveInstance instance) { + return instance != null && instance.stageTracker != null + ? resolveStageEndTick(instance.animationId, resolveCurrentStage(instance), instance.stageTracker.stageStartTick, instance.speed) + : -1L; + } + + private static long resolvePublishedStageEndTick(ServerAnimationController.ActiveInstance instance) { + if (instance != null && instance.stageTracker != null) { + long override = instance.stageTracker.progressEndTickOverride; + return override > instance.stageTracker.stageStartTick ? override : resolveStageEndTick(instance); + } else { + return -1L; + } + } + + private static long resolveStageEndTick(Identifier animationId, AnimationStageInfo stage, long stageStartTick, double speed) { + long duration = AnimationStageTiming.resolveDurationTicks(animationId, stage, speed); + return duration < 0L ? -1L : stageStartTick + duration; + } + + private static AnimationStageInfo resolveCurrentStage(ServerAnimationController.ActiveInstance inst) { + if (inst.stages != null && !inst.stages.isEmpty()) { + int idx = inst.stageTracker == null ? 0 : Math.max(0, Math.min(inst.stageTracker.stageIndex, inst.stages.size() - 1)); + return inst.stages.get(idx); + } else { + return new AnimationStageInfo(inst.animationId, true, 0L, true, 1.0); + } + } + + @Nullable + public static AnimationStageInfo getCurrentStage(ServerWorld world, UUID instanceId) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + return inst != null && inst.world == world ? resolveCurrentStage(inst) : null; + } + + @Nullable + public static ServerAnimationController.ActiveInstanceSnapshot getActiveInstanceSnapshot(ServerWorld world, UUID instanceId) { + ServerAnimationController.ActiveInstance inst = ACTIVE_INSTANCES.get(instanceId); + return inst != null && inst.world == world ? snapshot(instanceId, inst) : null; + } + + public static List getActiveInstanceSnapshots(ServerWorld world) { + if (world != null && !ACTIVE_INSTANCES.isEmpty()) { + List snapshots = new ArrayList<>(); + + for (Entry entry : ACTIVE_INSTANCES.entrySet()) { + if (entry.getValue().world == world) { + snapshots.add(snapshot(entry.getKey(), entry.getValue())); + } + } + + return List.copyOf(snapshots); + } else { + return List.of(); + } + } + + private static ServerAnimationController.ActiveInstanceSnapshot snapshot(UUID instanceId, ServerAnimationController.ActiveInstance instance) { + int stageIndex = instance.stageTracker == null ? 0 : instance.stageTracker.stageIndex; + long stageStartTick = instance.stageTracker == null ? instance.startTick : instance.stageTracker.stageStartTick; + return new ServerAnimationController.ActiveInstanceSnapshot( + instanceId, + instance.animationId, + List.copyOf(instance.actorUuids), + sanitizeActorKeys(instance.actorKeys, instance.actorUuids.size()), + List.copyOf(instance.stages), + instance.startTick, + stageIndex, + stageStartTick, + resolvePublishedStageEndTick(instance), + instance.speed, + instance.damageBehavior, + instance.ignoreAttackers, + instance.startContext + ); + } + + @Nullable + private static Float resolveTransitionYaw(@Nullable ServerAnimationController.ActiveInstance inst) { + return inst != null && inst.sharedTransform != null ? inst.sharedTransform.yaw() : null; + } + + private static final class ActiveInstance { + final ServerWorld world; + final Identifier animationId; + final List> modelRootsByActor; + final List actorUuids; + final List actorKeys; + final List stages; + final long startTick; + final Map originalTransforms; + final ServerAnimationController.SharedTransform sharedTransform; + final boolean lockOrientation; + final float lockedYaw; + final float lockedHeadYaw; + final float lockedPitch; + final NonAnimationDamageBehavior damageBehavior; + final boolean ignoreAttackers; + final AnimationStartContext startContext; + final boolean underwaterBreathing; + @Nullable + final UUID playerUuid; + final Set actorUuidSet; + final ServerAnimationController.StageTracker stageTracker; + @Nullable + final ServerAnimationController.BlockPlacement blockPlacement; + @Nullable + final Vec3d cameraOrbitTarget; + final Set subscribedPlayerUuids = ConcurrentHashMap.newKeySet(); + double speed; + + ActiveInstance( + ServerWorld world, + Identifier animationId, + List> modelRootsByActor, + List actorUuids, + List actorKeys, + List stages, + long startTick, + Map originalTransforms, + ServerAnimationController.SharedTransform sharedTransform, + boolean lockOrientation, + float lockedYaw, + float lockedHeadYaw, + float lockedPitch, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext, + boolean underwaterBreathing, + @Nullable UUID playerUuid, + Set actorUuidSet, + ServerAnimationController.StageTracker stageTracker, + @Nullable ServerAnimationController.BlockPlacement blockPlacement, + @Nullable Vec3d cameraOrbitTarget, + double speed + ) { + this.world = world; + this.animationId = animationId; + this.modelRootsByActor = modelRootsByActor.stream().map(List::copyOf).toList(); + this.actorUuids = actorUuids; + this.actorKeys = actorKeys; + this.stages = stages; + this.startTick = startTick; + this.originalTransforms = originalTransforms; + this.sharedTransform = sharedTransform; + this.lockOrientation = lockOrientation; + this.lockedYaw = lockedYaw; + this.lockedHeadYaw = lockedHeadYaw; + this.lockedPitch = lockedPitch; + this.damageBehavior = damageBehavior; + this.ignoreAttackers = ignoreAttackers; + this.startContext = startContext; + this.underwaterBreathing = underwaterBreathing; + this.playerUuid = playerUuid; + this.actorUuidSet = actorUuidSet; + this.stageTracker = stageTracker; + this.blockPlacement = blockPlacement; + this.cameraOrbitTarget = cameraOrbitTarget; + this.speed = speed; + } + } + + public record ActiveInstanceSnapshot( + UUID instanceId, + Identifier animationId, + List actorUuids, + List actorKeys, + List stages, + long startTick, + int stageIndex, + long stageStartTick, + long stageEndTick, + double speed, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext + ) { + public Map metadata() { + return this.startContext.addonMetadata(); + } + + @Nullable + public AnimationSessionData sessionData() { + return this.startContext.sessionData(); + } + } + + private static final class AiDisableState { + final boolean originalAiDisabled; + int locks; + boolean needsRestore; + float lockedYaw; + float lockedHeadYaw; + float lockedBodyYaw; + float lockedPitch; + + AiDisableState(boolean originalAiDisabled) { + this.originalAiDisabled = originalAiDisabled; + this.locks = 1; + this.needsRestore = false; + this.lockedYaw = 0.0F; + this.lockedHeadYaw = 0.0F; + this.lockedBodyYaw = 0.0F; + this.lockedPitch = 0.0F; + } + } + + private record AnimationStartProtection(RegistryKey worldKey, long untilTick) { + } + + private record BedFootprintFacing(boolean bedFootprint, @Nullable Direction facing) { + static ServerAnimationController.BedFootprintFacing none() { + return new ServerAnimationController.BedFootprintFacing(false, null); + } + + static ServerAnimationController.BedFootprintFacing invalid() { + return new ServerAnimationController.BedFootprintFacing(true, null); + } + + static ServerAnimationController.BedFootprintFacing valid(Direction facing) { + return new ServerAnimationController.BedFootprintFacing(true, facing); + } + } + + private record BlockPlacement( + BlockPos wallPos, Direction facing, ServerAnimationController.SharedTransform sharedTransform, @Nullable Vec3d cameraOrbitTarget, boolean centerSupport + ) { + } + + private record DeferredQueueContext( + ServerAnimationController.OriginalTransform originalTransform, @Nullable ServerAnimationController.BlockPlacement previousPlacement + ) { + } + + private record EligibleDefinition( + AnimationDefinitions.Definition definition, List actorKeys, @Nullable ServerAnimationController.BlockPlacement blockPlacement + ) { + } + + private record FloorHit(boolean found, Vec3d pos) { + } + + private record GroundPlacementOffset(double dx, double dz, double distSq) { + } + + public record MatchedStartRequest(@Nullable UUID instanceId, Identifier animationId, List actorKeys, List stages) { + } + + private record OriginalTransform(RegistryKey worldKey, Vec3d pos, float yaw, float pitch, float headYaw, float bodyYaw, @Nullable Vec3d restorePos) { + private OriginalTransform(RegistryKey worldKey, Vec3d pos, float yaw, float pitch, float headYaw, float bodyYaw) { + this(worldKey, pos, yaw, pitch, headYaw, bodyYaw, null); + } + + private Vec3d restorePosOrPos() { + return this.restorePos == null ? this.pos : this.restorePos; + } + } + + private static final class PlayerLockState { + final boolean originalNoGravity; + int locks; + + PlayerLockState(boolean originalNoGravity) { + this.originalNoGravity = originalNoGravity; + this.locks = 1; + } + } + + private static final class PlayerQueueState { + final UUID playerUuid; + final Deque entries = new ArrayDeque<>(); + + PlayerQueueState(UUID playerUuid) { + this.playerUuid = playerUuid; + } + } + + private enum QueueStartResult { + STARTED, + RETRY, + SKIP; + } + + private record QueuedEntry( + UUID entryId, + Identifier animationId, + List actorUuids, + NonAnimationDamageBehavior damageBehavior, + boolean ignoreAttackers, + AnimationStartContext startContext + ) { + } + + private record RecentNoRestoreTransition( + long expiresTick, ServerAnimationController.SharedTransform sharedTransform, @Nullable ServerAnimationController.BlockPlacement blockPlacement + ) { + } + + private record RequirementCheck(boolean valid, @Nullable ServerAnimationController.BlockPlacement blockPlacement) { + } + + private record SharedTransform(Vec3d pos, float yaw, float pitch, float headYaw, float bodyYaw) { + } + + private record StageAdvance(UUID instanceId, long advanceTick, long stageEndTick, int stageIndex) { + } + + private static final class StageTracker { + int stageIndex = 0; + long stageStartTick; + long progressEndTickOverride; + + StageTracker(long stageStartTick) { + this.stageStartTick = stageStartTick; + this.progressEndTickOverride = -1L; + } + } + + private enum StopReason { + NATURAL_END, + EXTERNAL_STOP, + DAMAGE_STOP, + JOIN_REPLACE, + ACTOR_DEATH, + STOP_ALL; + } + + private record WallColumn(BlockPos pos, Vec3d surfaceAnchor) { + } + + private record WallSegment(List columns, Vec3d surfaceCenter) { + } +} diff --git a/src/main/java/com/nonid/internal/animation/util/AnimationIds.java b/src/main/java/com/nonid/internal/animation/util/AnimationIds.java new file mode 100644 index 0000000..f52bf4e --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/util/AnimationIds.java @@ -0,0 +1,74 @@ +package com.nonid.internal.animation.util; + +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class AnimationIds { + private AnimationIds() { + } + + @Nullable + public static Identifier baseId(@Nullable Identifier animationId) { + if (animationId == null) { + return null; + } + + Integer stage = stageNumber(animationId); + if (stage == null) { + return animationId; + } + + String path = animationId.getPath(); + return Identifier.of(animationId.getNamespace(), path.substring(0, path.lastIndexOf(".p"))); + } + + @Nullable + public static Integer stageNumber(@Nullable Identifier animationId) { + if (animationId == null) { + return null; + } + + String path = animationId.getPath(); + int marker = path.lastIndexOf(".p"); + if (marker > 0 && marker + 2 < path.length()) { + String raw = path.substring(marker + 2); + int value = 0; + + for (int i = 0; i < raw.length(); i++) { + char c = raw.charAt(i); + if (c < '0' || c > '9') { + return null; + } + + int digit = c - '0'; + if (value > (Integer.MAX_VALUE - digit) / 10) { + return null; + } + + value = value * 10 + digit; + } + + return value > 0 ? value : null; + } else { + return null; + } + } + + public static int stageNumberOrDefault(@Nullable Identifier animationId, int fallback) { + Integer stage = stageNumber(animationId); + return stage == null ? fallback : stage; + } + + public static Identifier stageId(Identifier animationId, int stage) { + if (animationId == null) { + throw new IllegalArgumentException("animationId cannot be null"); + } + + if (stage <= 0) { + throw new IllegalArgumentException("stage must be positive"); + } + + Identifier base = baseId(animationId); + return Identifier.of(base.getNamespace(), base.getPath() + ".p" + stage); + } +} diff --git a/src/main/java/com/nonid/internal/animation/util/EntityVariants.java b/src/main/java/com/nonid/internal/animation/util/EntityVariants.java new file mode 100644 index 0000000..c83152f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/util/EntityVariants.java @@ -0,0 +1,30 @@ +package com.nonid.internal.animation.util; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.SlimeEntity; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class EntityVariants { + private EntityVariants() { + } + + @Nullable + public static String resolveVariant(Entity entity) { + return entity instanceof SlimeEntity slime ? "size_" + Math.max(0, slime.getSize() - 1) : null; + } + + @Nullable + public static Identifier resolveVariantModel(LivingEntity entity, @Nullable Identifier baseModelId) { + String variant = resolveVariant(entity); + return variant == null ? null : variantModelId(baseModelId, variant); + } + + @Nullable + public static Identifier variantModelId(@Nullable Identifier baseModelId, @Nullable String variant) { + return baseModelId != null && variant != null && !variant.isBlank() + ? Identifier.of(baseModelId.getNamespace(), baseModelId.getPath() + "_" + variant) + : null; + } +} diff --git a/src/main/java/com/nonid/internal/animation/util/StageTimeWarp.java b/src/main/java/com/nonid/internal/animation/util/StageTimeWarp.java new file mode 100644 index 0000000..c89f63c --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/util/StageTimeWarp.java @@ -0,0 +1,78 @@ +package com.nonid.internal.animation.util; + +public final class StageTimeWarp { + private static final double EPSILON_TICKS = 1.0E-4; + + private StageTimeWarp() { + } + + public static double offsetSecondsToTicks(double offsetSeconds) { + return !Double.isFinite(offsetSeconds) ? 0.0 : offsetSeconds * 20.0; + } + + public static double clampOffsetSeconds(double cycleTicks, double offsetSeconds) { + return clampOffsetTicks(cycleTicks, offsetSecondsToTicks(offsetSeconds)) / 20.0; + } + + public static double clampOffsetTicks(double cycleTicks, double offsetTicks) { + if (!Double.isFinite(cycleTicks) || cycleTicks <= 0.0) { + return 0.0; + } + + if (!Double.isFinite(offsetTicks)) { + return 0.0; + } + + double midpoint = cycleTicks * 0.5; + double minOffset = -midpoint + 1.0E-4; + double maxOffset = midpoint - 1.0E-4; + return Math.max(minOffset, Math.min(maxOffset, offsetTicks)); + } + + public static boolean hasWarp(double cycleTicks, double offsetTicks) { + return Double.isFinite(cycleTicks) && !(cycleTicks <= 0.0) ? Math.abs(clampOffsetTicks(cycleTicks, offsetTicks)) > 1.0E-4 : false; + } + + public static double authoredToWarpedCycleTicks(double authoredTicks, double cycleTicks, double offsetTicks) { + if (!hasWarp(cycleTicks, offsetTicks)) { + return clampCyclePosition(authoredTicks, cycleTicks); + } + + double position = clampCyclePosition(authoredTicks, cycleTicks); + double midpoint = cycleTicks * 0.5; + double shiftedMidpoint = midpoint + clampOffsetTicks(cycleTicks, offsetTicks); + return position <= midpoint + ? position * (shiftedMidpoint / midpoint) + : shiftedMidpoint + (position - midpoint) * ((cycleTicks - shiftedMidpoint) / (cycleTicks - midpoint)); + } + + public static double warpedToAuthoredCycleTicks(double warpedTicks, double cycleTicks, double offsetTicks) { + if (!hasWarp(cycleTicks, offsetTicks)) { + return clampCyclePosition(warpedTicks, cycleTicks); + } + + double position = clampCyclePosition(warpedTicks, cycleTicks); + double midpoint = cycleTicks * 0.5; + double shiftedMidpoint = midpoint + clampOffsetTicks(cycleTicks, offsetTicks); + return position <= shiftedMidpoint + ? position * (midpoint / shiftedMidpoint) + : midpoint + (position - shiftedMidpoint) * ((cycleTicks - midpoint) / (cycleTicks - shiftedMidpoint)); + } + + private static double clampCyclePosition(double ticks, double cycleTicks) { + if (!Double.isFinite(cycleTicks) || cycleTicks <= 0.0) { + return 0.0; + } + + if (!Double.isFinite(ticks)) { + return 0.0; + } + + double position = ticks % cycleTicks; + if (position < 0.0) { + position += cycleTicks; + } + + return position > cycleTicks ? cycleTicks : position; + } +} diff --git a/src/main/java/com/nonid/internal/animation/util/WeightedPicker.java b/src/main/java/com/nonid/internal/animation/util/WeightedPicker.java new file mode 100644 index 0000000..cf0127f --- /dev/null +++ b/src/main/java/com/nonid/internal/animation/util/WeightedPicker.java @@ -0,0 +1,53 @@ +package com.nonid.internal.animation.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.ToDoubleFunction; +import org.jetbrains.annotations.Nullable; + +public final class WeightedPicker { + private WeightedPicker() { + } + + @Nullable + public static T pick(List candidates, ToDoubleFunction weight, double randomUnit) { + if (candidates != null && !candidates.isEmpty()) { + List valid = new ArrayList<>(candidates.size()); + double total = 0.0; + + for (T candidate : candidates) { + if (candidate != null) { + valid.add(candidate); + total += sanitizeWeight(weight.applyAsDouble(candidate)); + } + } + + if (valid.isEmpty()) { + return null; + } + + double unit = Double.isFinite(randomUnit) ? Math.max(0.0, Math.min(Math.nextDown(1.0), randomUnit)) : 0.0; + if (Double.isFinite(total) && !(total <= 0.0)) { + double roll = unit * total; + double running = 0.0; + + for (T candidate : valid) { + running += sanitizeWeight(weight.applyAsDouble(candidate)); + if (roll < running) { + return candidate; + } + } + + return valid.getLast(); + } else { + return valid.get(Math.min(valid.size() - 1, (int)Math.floor(unit * valid.size()))); + } + } else { + return null; + } + } + + public static double sanitizeWeight(double value) { + return Double.isFinite(value) && value > 0.0 ? value : 1.0; + } +} diff --git a/src/main/java/com/nonid/item/EnergyGainAdjustItem.java b/src/main/java/com/nonid/item/EnergyGainAdjustItem.java new file mode 100644 index 0000000..3f00b1f --- /dev/null +++ b/src/main/java/com/nonid/item/EnergyGainAdjustItem.java @@ -0,0 +1,88 @@ +package com.nonid.item; + +import com.nonid.EnergyHolder; +import com.nonid.particle.NonParticles; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Item.Settings; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; + +public class EnergyGainAdjustItem extends Item { + private final float delta; + + public EnergyGainAdjustItem(Settings settings, float delta) { + super(settings); + this.delta = delta; + } + + public ActionResult useOnEntity(ItemStack stack, PlayerEntity user, LivingEntity entity, Hand hand) { + if (!(entity instanceof MobEntity mob && mob instanceof EnergyHolder holder)) { + return ActionResult.PASS; + } else { + if (user.getEntityWorld().isClient()) { + return ActionResult.SUCCESS; + } + + boolean removedStabilization = false; + if (this.delta > 0.0F && mob.getCommandTags().contains("non.energy_stabilized")) { + mob.removeCommandTag("non.energy_stabilized"); + removedStabilization = true; + } + + float current = holder.getEnergyGainMultiplier(); + float target = clamp(current + this.delta, 0.4F, 6.0F); + if (Math.abs(target - current) < 1.0E-6F && !removedStabilization) { + return ActionResult.FAIL; + } + + if (Math.abs(target - current) >= 1.0E-6F) { + holder.setEnergyGainMultiplier(target); + } + + if (user.getEntityWorld() instanceof ServerWorld serverWorld) { + this.playFeedback(serverWorld, mob, removedStabilization, target); + } + + if (!user.isInCreativeMode()) { + stack.decrement(1); + } + + return ActionResult.SUCCESS; + } + } + + private void playFeedback(ServerWorld world, MobEntity mob, boolean removedStabilization, float appliedMultiplier) { + if (removedStabilization) { + world.spawnParticles(ParticleTypes.WITCH, mob.getX(), mob.getBodyY(0.6), mob.getZ(), 12, 0.35, 0.35, 0.35, 0.01); + world.playSound(null, mob.getX(), mob.getY(), mob.getZ(), SoundEvents.BLOCK_BEACON_DEACTIVATE, SoundCategory.NEUTRAL, 0.9F, 0.95F); + } else { + boolean augmenter = this.delta > 0.0F; + ParticleEffect particle = augmenter ? NonParticles.SMALLHEART : ParticleTypes.SMOKE; + SoundEvent sound = augmenter ? SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP : SoundEvents.BLOCK_BREWING_STAND_BREW; + float pitch = pitchForMultiplier(appliedMultiplier); + world.spawnParticles(particle, mob.getX(), mob.getBodyY(0.6), mob.getZ(), 10, 0.35, 0.35, 0.35, 0.01); + world.playSound(null, mob.getX(), mob.getY(), mob.getZ(), sound, SoundCategory.NEUTRAL, 0.9F, pitch); + } + } + + private static float pitchForMultiplier(float multiplier) { + float clamped = clamp(multiplier, 0.4F, 6.0F); + float span = 5.6F; + float t = (clamped - 0.4F) / span; + return 0.65F + 0.9F * t; + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } +} diff --git a/src/main/java/com/nonid/item/EnergyStabilizerItem.java b/src/main/java/com/nonid/item/EnergyStabilizerItem.java new file mode 100644 index 0000000..e066317 --- /dev/null +++ b/src/main/java/com/nonid/item/EnergyStabilizerItem.java @@ -0,0 +1,128 @@ +package com.nonid.item; + +import com.nonid.EnergyHolder; +import com.nonid.NeedsOfNature; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.Angerable; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Item.Settings; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.registry.Registries; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public class EnergyStabilizerItem extends Item { + private static final String STABILIZER_ENTITY_DATA_KEY = "stabilizer_entity"; + + public EnergyStabilizerItem(Settings settings) { + super(settings); + } + + public Text getName(ItemStack stack) { + Identifier target = getTargetEntityTypeId(stack); + if (target == null) { + return super.getName(stack); + } + + Text entityName = Registries.ENTITY_TYPE.getOptionalValue(target).map(EntityType::getName).orElseGet(() -> Text.literal(target.toString())); + return Text.translatable("item.needsofnature.entity_energy_stabilizer", new Object[]{entityName}); + } + + public ActionResult useOnEntity(ItemStack stack, PlayerEntity user, LivingEntity entity, Hand hand) { + if (entity instanceof MobEntity mob && mob instanceof EnergyHolder) { + Identifier target = getTargetEntityTypeId(stack); + if (target == null) { + return ActionResult.FAIL; + } + + Identifier mobType = Registries.ENTITY_TYPE.getId(mob.getType()); + if (!target.equals(mobType)) { + return ActionResult.FAIL; + } + + if (mob.getCommandTags().contains("non.energy_stabilized")) { + return ActionResult.FAIL; + } + + if (user.getEntityWorld().isClient()) { + return ActionResult.SUCCESS; + } + + EnergyHolder holder = (EnergyHolder)mob; + mob.addCommandTag("non.energy_stabilized"); + holder.setEnergy(Math.min(holder.getEnergy(), 199)); + mob.setTarget(null); + if (mob instanceof Angerable angerable) { + angerable.stopAnger(); + } + + if (user.getEntityWorld() instanceof ServerWorld serverWorld) { + playFeedback(serverWorld, mob); + } + + if (!user.isInCreativeMode()) { + stack.decrement(1); + } + + return ActionResult.SUCCESS; + } else { + return ActionResult.PASS; + } + } + + public static ItemStack createStackForEntity(@Nullable Identifier entityTypeId) { + ItemStack stack = new ItemStack(NeedsOfNature.ENERGY_STABILIZER); + if (entityTypeId != null) { + setTargetEntityTypeId(stack, entityTypeId); + } + + return stack; + } + + public static void setTargetEntityTypeId(ItemStack stack, @Nullable Identifier entityTypeId) { + if (stack != null && !stack.isEmpty()) { + NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> { + String value = entityTypeId == null ? "" : entityTypeId.toString(); + nbt.putString("stabilizer_entity", value); + }); + } + } + + @Nullable + public static Identifier getTargetEntityTypeId(ItemStack stack) { + if (stack != null && !stack.isEmpty() && stack.isOf(NeedsOfNature.ENERGY_STABILIZER)) { + NbtComponent custom = (NbtComponent)stack.get(DataComponentTypes.CUSTOM_DATA); + if (custom != null && !custom.isEmpty()) { + NbtCompound nbt = custom.copyNbt(); + if (!nbt.contains("stabilizer_entity")) { + return null; + } + + String raw = nbt.getString("stabilizer_entity", ""); + return raw != null && !raw.isBlank() ? Identifier.tryParse(raw) : null; + } else { + return null; + } + } else { + return null; + } + } + + private static void playFeedback(ServerWorld world, MobEntity mob) { + world.spawnParticles(ParticleTypes.END_ROD, mob.getX(), mob.getBodyY(0.65), mob.getZ(), 12, 0.4, 0.4, 0.4, 0.02); + world.playSound(null, mob.getX(), mob.getY(), mob.getZ(), SoundEvents.BLOCK_BEACON_POWER_SELECT, SoundCategory.NEUTRAL, 0.9F, 1.0F); + } +} diff --git a/src/main/java/com/nonid/item/HorseLiquidCollectorItem.java b/src/main/java/com/nonid/item/HorseLiquidCollectorItem.java new file mode 100644 index 0000000..ca03e7f --- /dev/null +++ b/src/main/java/com/nonid/item/HorseLiquidCollectorItem.java @@ -0,0 +1,98 @@ +package com.nonid.item; + +import com.nonid.NeedsOfNature; +import com.nonid.client.HorseLiquidCollectorItemRenderer; +import com.nonid.entity.HorseLiquidCollectorEntity; +import java.util.function.Consumer; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Item.Settings; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.stat.Stats; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult.Type; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import net.minecraft.world.RaycastContext.FluidHandling; +import net.minecraft.world.event.GameEvent; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.animatable.GeoItem; +import software.bernie.geckolib.animatable.client.GeoRenderProvider; +import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.manager.AnimatableManager.ControllerRegistrar; +import software.bernie.geckolib.renderer.GeoItemRenderer; +import software.bernie.geckolib.util.GeckoLibUtil; + +public class HorseLiquidCollectorItem extends Item implements GeoItem { + private AnimatableInstanceCache animatableCache; + private boolean syncedAnimatableRegistered; + + public HorseLiquidCollectorItem(Settings settings) { + super(settings); + } + + public ActionResult use(World world, PlayerEntity user, Hand hand) { + ItemStack stack = user.getStackInHand(hand); + BlockHitResult hit = raycast(world, user, FluidHandling.ANY); + if (hit.getType() != Type.BLOCK) { + return ActionResult.PASS; + } + + Vec3d hitPos = hit.getPos(); + HorseLiquidCollectorEntity collector = new HorseLiquidCollectorEntity(NeedsOfNature.HORSE_LIQUID_COLLECTOR_ENTITY_TYPE, world); + collector.refreshPositionAndAngles(hitPos.x, hitPos.y + 0.01, hitPos.z, user.getYaw(), 0.0F); + collector.setLockedOrientation(user.getYaw(), 0.0F); + if (!world.isSpaceEmpty(collector, collector.getBoundingBox())) { + return ActionResult.FAIL; + } + + if (world.isClient()) { + return ActionResult.SUCCESS; + } + + world.spawnEntity(collector); + world.playSound(null, collector.getX(), collector.getY(), collector.getZ(), SoundEvents.ENTITY_ARMOR_STAND_PLACE, SoundCategory.PLAYERS, 1.0F, 1.0F); + world.emitGameEvent(user, GameEvent.ENTITY_PLACE, collector.getBlockPos()); + user.incrementStat(Stats.USED.getOrCreateStat(this)); + if (!user.isInCreativeMode()) { + stack.decrement(1); + } + + return ActionResult.SUCCESS; + } + + public void registerControllers(@NotNull ControllerRegistrar controllers) { + } + + @NotNull + public AnimatableInstanceCache getAnimatableInstanceCache() { + if (!this.syncedAnimatableRegistered) { + GeoItem.registerSyncedAnimatable(this); + this.syncedAnimatableRegistered = true; + } + + if (this.animatableCache == null) { + this.animatableCache = GeckoLibUtil.createInstanceCache(this); + } + + return this.animatableCache; + } + + public void createGeoRenderer(Consumer consumer) { + consumer.accept(new GeoRenderProvider() { + private GeoItemRenderer<@NotNull HorseLiquidCollectorItem> renderer; + + public GeoItemRenderer getGeoItemRenderer() { + if (this.renderer == null) { + this.renderer = new HorseLiquidCollectorItemRenderer(); + } + + return this.renderer; + } + }); + } +} diff --git a/src/main/java/com/nonid/item/NonDataAccessoryItem.java b/src/main/java/com/nonid/item/NonDataAccessoryItem.java new file mode 100644 index 0000000..4312940 --- /dev/null +++ b/src/main/java/com/nonid/item/NonDataAccessoryItem.java @@ -0,0 +1,45 @@ +package com.nonid.item; + +import com.nonid.data.NonAccessoryLiquidFilling; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.StackReference; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.Item.Settings; +import net.minecraft.screen.slot.Slot; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.ClickType; + +public final class NonDataAccessoryItem extends Item { + public NonDataAccessoryItem(Settings settings) { + super(settings); + } + + public boolean onClicked(ItemStack stack, ItemStack otherStack, Slot slot, ClickType clickType, PlayerEntity player, StackReference cursorStackReference) { + if (clickType != ClickType.LEFT || !slot.canTakePartial(player)) { + return false; + } + + if (!NonAccessoryLiquidFilling.fillInPlace(stack, otherStack)) { + return false; + } + + otherStack.decrement(1); + if (otherStack.isEmpty()) { + cursorStackReference.set(new ItemStack(Items.GLASS_BOTTLE)); + } else { + cursorStackReference.set(otherStack); + ItemStack glassBottle = new ItemStack(Items.GLASS_BOTTLE); + if (!player.getInventory().insertStack(glassBottle) && !player.getEntityWorld().isClient()) { + player.dropItem(glassBottle, false); + } + } + + if (player.getEntityWorld().isClient()) { + player.playSound(SoundEvents.ITEM_BOTTLE_FILL, 1.0F, 1.0F); + } + + return true; + } +} diff --git a/src/main/java/com/nonid/mixin/AnimalEntityBreedingMixin.java b/src/main/java/com/nonid/mixin/AnimalEntityBreedingMixin.java new file mode 100644 index 0000000..4bb726b --- /dev/null +++ b/src/main/java/com/nonid/mixin/AnimalEntityBreedingMixin.java @@ -0,0 +1,40 @@ +package com.nonid.mixin; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import net.minecraft.entity.passive.AnimalEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(AnimalEntity.class) +public abstract class AnimalEntityBreedingMixin { + @Inject(method = "canBreedWith", at = @At("HEAD"), cancellable = true) + private void non$skipInvalidGenderPairWhenSelectingMate(AnimalEntity other, CallbackInfoReturnable cir) { + if (other != null) { + NonConfig config = NeedsOfNature.getConfig(); + if (config != null && config.useAnimationBreeding() && config.requireMaleFemaleForBreeding()) { + AnimalEntity self = (AnimalEntity)(Object)this; + boolean selfMale = self.getCommandTags().contains("gender.male"); + boolean selfFemale = self.getCommandTags().contains("gender.female"); + boolean otherMale = other.getCommandTags().contains("gender.male"); + boolean otherFemale = other.getCommandTags().contains("gender.female"); + boolean validPair = selfMale && otherFemale || selfFemale && otherMale; + if (!validPair) { + cir.setReturnValue(false); + } + } + } + } + + @Inject(method = "breed(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/entity/passive/AnimalEntity;)V", at = @At("HEAD"), cancellable = true) + private void non$overrideVanillaBreeding(ServerWorld world, AnimalEntity other, CallbackInfo ci) { + AnimalEntity self = (AnimalEntity)(Object)this; + if (NeedsOfNature.handleAnimationBreedingAttempt(world, self, other)) { + ci.cancel(); + } + } +} diff --git a/src/main/java/com/nonid/mixin/AnimalMateGoalAnimationBreedingMixin.java b/src/main/java/com/nonid/mixin/AnimalMateGoalAnimationBreedingMixin.java new file mode 100644 index 0000000..465a1e8 --- /dev/null +++ b/src/main/java/com/nonid/mixin/AnimalMateGoalAnimationBreedingMixin.java @@ -0,0 +1,44 @@ +package com.nonid.mixin; + +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import net.minecraft.entity.ai.goal.AnimalMateGoal; +import net.minecraft.entity.passive.AnimalEntity; +import net.minecraft.server.world.ServerWorld; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AnimalMateGoal.class) +public abstract class AnimalMateGoalAnimationBreedingMixin { + @Final + @Shadow + protected AnimalEntity animal; + @Final + @Shadow + protected ServerWorld world; + @Shadow + @Nullable + protected AnimalEntity mate; + + @Inject(method = "tick", at = @At("HEAD"), cancellable = true) + private void non$startAnimationBreedingWhenClose(CallbackInfo ci) { + NonConfig config = NeedsOfNature.getConfig(); + if (config != null && config.useAnimationBreeding()) { + AnimalEntity other = this.mate; + if (other != null && other.isAlive() && !other.isRemoved()) { + if (this.animal.isInLove() && other.isInLove()) { + if (!(this.animal.squaredDistanceTo(other) >= 9.0)) { + if (NeedsOfNature.handleAnimationBreedingAttempt(this.world, this.animal, other)) { + ci.cancel(); + } + } + } + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/BrewingRecipeRegistryMixin.java b/src/main/java/com/nonid/mixin/BrewingRecipeRegistryMixin.java new file mode 100644 index 0000000..4b5522a --- /dev/null +++ b/src/main/java/com/nonid/mixin/BrewingRecipeRegistryMixin.java @@ -0,0 +1,24 @@ +package com.nonid.mixin; + +import com.nonid.potion.NonPotions; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.BrewingRecipeRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BrewingRecipeRegistry.class) +public abstract class BrewingRecipeRegistryMixin { + @Inject(method = "hasItemRecipe", at = @At("HEAD"), cancellable = true) + private void non$blockSplashConversionsForLiquidBottles(ItemStack input, ItemStack ingredient, CallbackInfoReturnable cir) { + if (NonPotions.isLiquidBottle(input)) { + if (ingredient != null && !ingredient.isEmpty()) { + if (ingredient.isOf(Items.GUNPOWDER) || ingredient.isOf(Items.DRAGON_BREATH)) { + cir.setReturnValue(false); + } + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/CreeperEntityNoNAnimationMixin.java b/src/main/java/com/nonid/mixin/CreeperEntityNoNAnimationMixin.java new file mode 100644 index 0000000..f8f5456 --- /dev/null +++ b/src/main/java/com/nonid/mixin/CreeperEntityNoNAnimationMixin.java @@ -0,0 +1,159 @@ +package com.nonid.mixin; + +import com.nonid.NeedsOfNature; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.server.ServerAnimationController; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Box; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(CreeperEntity.class) +public abstract class CreeperEntityNoNAnimationMixin { + @Unique + private static final double IGNORED_ACTOR_FUSE_RADIUS = 7.0; + @Shadow + private int currentFuseTime; + @Shadow + private int lastFuseTime; + @Shadow + private int fuseTime; + @Shadow + private static TrackedData FUSE_SPEED; + @Shadow + private static TrackedData IGNITED; + @Unique + private boolean non$wasAnimationControlled; + + @Inject(method = "tick", at = @At("HEAD")) + private void non$resetFuseAfterAnimationControlEnds(CallbackInfo ci) { + boolean controlled = this.non$isAnimationControlled(); + if (this.non$wasAnimationControlled && !controlled) { + this.non$resetFuse(); + } + + this.non$wasAnimationControlled = controlled; + if (!controlled && this.non$hasIgnoredAnimationActorNearby()) { + this.non$resetFuse(); + } + } + + @Inject(method = "getFuseSpeed", at = @At("HEAD"), cancellable = true) + private void non$stopFuseDuringAnimation(CallbackInfoReturnable cir) { + if (this.non$shouldSuppressFuse()) { + this.non$resetFuse(); + cir.setReturnValue(0); + } + } + + @Inject(method = "setFuseSpeed", at = @At("HEAD"), cancellable = true) + private void non$stopFuseSpeedDuringAnimation(int fuseSpeed, CallbackInfo ci) { + if (this.non$shouldSuppressFuse()) { + this.non$resetFuse(); + ci.cancel(); + } + } + + @Inject(method = "isIgnited", at = @At("HEAD"), cancellable = true) + private void non$ignoreIgnitedDuringAnimation(CallbackInfoReturnable cir) { + if (this.non$shouldSuppressFuse()) { + this.non$resetFuse(); + cir.setReturnValue(false); + } + } + + @Inject(method = "ignite", at = @At("HEAD"), cancellable = true) + private void non$ignoreManualIgnitionDuringAnimation(CallbackInfo ci) { + if (this.non$shouldSuppressFuse()) { + this.non$resetFuse(); + ci.cancel(); + } + } + + @Inject(method = "explode", at = @At("HEAD"), cancellable = true) + private void non$blockExplosionDuringGatherOrAnimation(CallbackInfo ci) { + if (this.non$shouldSuppressFuse()) { + this.non$resetFuse(); + ci.cancel(); + } else { + if (this.non$isNoNGathering()) { + this.non$clampGatherFuse(); + ci.cancel(); + } + } + } + + @Unique + private boolean non$shouldSuppressFuse() { + return this.non$isAnimationControlled() || this.non$hasIgnoredAnimationActorNearby(); + } + + @Unique + private boolean non$isAnimationControlled() { + CreeperEntity self = (CreeperEntity)(Object)this; + return self.getEntityWorld() instanceof ServerWorld world ? AnimationService.isActorPendingOrActive(world, self.getUuid()) : false; + } + + @Unique + private boolean non$hasIgnoredAnimationActorNearby() { + CreeperEntity self = (CreeperEntity)(Object)this; + if (self.getEntityWorld() instanceof ServerWorld world) { + LivingEntity target = self.getTarget(); + if (target != null && ServerAnimationController.shouldIgnoreAttackers(world, target)) { + self.setTarget(null); + self.getNavigation().stop(); + return true; + } else { + Box searchBox = self.getBoundingBox().expand(7.0); + return !world.getEntitiesByClass( + LivingEntity.class, + searchBox, + actor -> actor != self + && actor.isAlive() + && !actor.isRemoved() + && self.squaredDistanceTo(actor) <= 49.0 + && ServerAnimationController.shouldIgnoreAttackers(world, actor) + ) + .isEmpty(); + } + } else { + return false; + } + } + + @Unique + private boolean non$isNoNGathering() { + CreeperEntity self = (CreeperEntity)(Object)this; + return self.getEntityWorld() instanceof ServerWorld world ? NeedsOfNature.isNoNGatheringActor(world, self.getUuid()) : false; + } + + @Unique + private void non$resetFuse() { + CreeperEntity self = (CreeperEntity)(Object)this; + this.currentFuseTime = 0; + this.lastFuseTime = 0; + if (FUSE_SPEED != null) { + self.getDataTracker().set(FUSE_SPEED, 0); + } + + if (IGNITED != null) { + self.getDataTracker().set(IGNITED, false); + } + } + + @Unique + private void non$clampGatherFuse() { + int maxFuse = Math.max(1, this.fuseTime); + int safeFuse = Math.max(0, maxFuse - 1); + this.currentFuseTime = Math.min(Math.max(0, this.currentFuseTime), safeFuse); + this.lastFuseTime = Math.min(Math.max(0, this.lastFuseTime), this.currentFuseTime); + } +} diff --git a/src/main/java/com/nonid/mixin/EntityEnergyPersistenceMixin.java b/src/main/java/com/nonid/mixin/EntityEnergyPersistenceMixin.java new file mode 100644 index 0000000..d42410a --- /dev/null +++ b/src/main/java/com/nonid/mixin/EntityEnergyPersistenceMixin.java @@ -0,0 +1,52 @@ +package com.nonid.mixin; + +import com.mojang.serialization.Codec; +import com.nonid.EnergyHolder; +import java.util.Optional; +import net.minecraft.entity.Entity; +import net.minecraft.storage.ReadView; +import net.minecraft.storage.WriteView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityEnergyPersistenceMixin { + @Inject(method = "writeData", at = @At("TAIL")) + private void needsOfNature$writeEnergy(WriteView storage, CallbackInfo ci) { + if (this instanceof EnergyHolder holder) { + storage.putInt("NeedsOfNatureEnergy", holder.getEnergy()); + storage.putFloat("NeedsOfNatureEnergyGainMult", holder.getEnergyGainMultiplier()); + storage.putFloat("NeedsOfNatureEnergyGainDrift", holder.getEnergyGainDrift()); + } + } + + @Inject(method = "readData", at = @At("TAIL")) + private void needsOfNature$readEnergy(ReadView storage, CallbackInfo ci) { + if (this instanceof EnergyHolder holder) { + boolean loaded = false; + Optional energyOpt = storage.getOptionalInt("NeedsOfNatureEnergy"); + if (energyOpt.isPresent()) { + holder.setEnergy(energyOpt.get()); + loaded = true; + } + + Optional multOpt = storage.read("NeedsOfNatureEnergyGainMult", Codec.FLOAT); + if (multOpt.isPresent()) { + holder.setEnergyGainMultiplier(multOpt.get()); + loaded = true; + } + + Optional driftOpt = storage.read("NeedsOfNatureEnergyGainDrift", Codec.FLOAT); + if (driftOpt.isPresent()) { + holder.setEnergyGainDrift(driftOpt.get()); + loaded = true; + } + + if (loaded) { + holder.markEnergyInitialized(); + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/EntityGenderPersistenceMixin.java b/src/main/java/com/nonid/mixin/EntityGenderPersistenceMixin.java new file mode 100644 index 0000000..2ada73b --- /dev/null +++ b/src/main/java/com/nonid/mixin/EntityGenderPersistenceMixin.java @@ -0,0 +1,51 @@ +package com.nonid.mixin; + +import com.nonid.DestroyedSkinHolder; +import com.nonid.GenderHolder; +import com.nonid.MessHolder; +import net.minecraft.entity.Entity; +import net.minecraft.storage.ReadView; +import net.minecraft.storage.WriteView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityGenderPersistenceMixin { + @Inject(method = "writeData", at = @At("TAIL")) + private void needsOfNature$writeGender(WriteView storage, CallbackInfo ci) { + if (this instanceof GenderHolder holder) { + storage.putInt("NeedsOfNatureGender", holder.getGenderMask()); + } + + if (this instanceof DestroyedSkinHolder holder) { + storage.putInt("NeedsOfNatureDestroyedSkinStage", holder.getDestroyedSkinStage()); + storage.putInt("NeedsOfNatureDestroyedSkinDamage", holder.getDestroyedSkinDamage()); + } + + if (this instanceof MessHolder holder) { + storage.putInt("NeedsOfNatureMessV", holder.getVMess()); + storage.putInt("NeedsOfNatureMessA", holder.getAMess()); + storage.putInt("NeedsOfNatureMessM", holder.getMMess()); + } + } + + @Inject(method = "readData", at = @At("TAIL")) + private void needsOfNature$readGender(ReadView storage, CallbackInfo ci) { + if (this instanceof GenderHolder holder) { + storage.getOptionalInt("NeedsOfNatureGender").ifPresent(holder::setGenderMask); + } + + if (this instanceof DestroyedSkinHolder holder) { + storage.getOptionalInt("NeedsOfNatureDestroyedSkinStage").ifPresent(holder::setDestroyedSkinStage); + storage.getOptionalInt("NeedsOfNatureDestroyedSkinDamage").ifPresent(holder::setDestroyedSkinDamage); + } + + if (this instanceof MessHolder holder) { + storage.getOptionalInt("NeedsOfNatureMessV").ifPresent(holder::setVMess); + storage.getOptionalInt("NeedsOfNatureMessA").ifPresent(holder::setAMess); + storage.getOptionalInt("NeedsOfNatureMessM").ifPresent(holder::setMMess); + } + } +} diff --git a/src/main/java/com/nonid/mixin/FileResourcePackProviderAccessor.java b/src/main/java/com/nonid/mixin/FileResourcePackProviderAccessor.java new file mode 100644 index 0000000..1428d9d --- /dev/null +++ b/src/main/java/com/nonid/mixin/FileResourcePackProviderAccessor.java @@ -0,0 +1,16 @@ +package com.nonid.mixin; + +import net.minecraft.resource.FileResourcePackProvider; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.path.SymlinkFinder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(FileResourcePackProvider.class) +public interface FileResourcePackProviderAccessor { + @Accessor("type") + ResourceType non$getType(); + + @Accessor("symlinkFinder") + SymlinkFinder non$getSymlinkFinder(); +} diff --git a/src/main/java/com/nonid/mixin/FleeEntityGoalMixin.java b/src/main/java/com/nonid/mixin/FleeEntityGoalMixin.java new file mode 100644 index 0000000..e08ebd0 --- /dev/null +++ b/src/main/java/com/nonid/mixin/FleeEntityGoalMixin.java @@ -0,0 +1,45 @@ +package com.nonid.mixin; + +import com.nonid.NonPlayerAvoidanceState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.goal.FleeEntityGoal; +import net.minecraft.entity.mob.PathAwareEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(FleeEntityGoal.class) +public abstract class FleeEntityGoalMixin { + @Shadow + @Final + protected PathAwareEntity mob; + @Shadow + protected LivingEntity targetEntity; + + @Inject(method = "canStart", at = @At("RETURN"), cancellable = true) + private void non$suppressPlayerAvoidanceStart(CallbackInfoReturnable cir) { + if (cir.getReturnValueZ() && this.non$shouldSuppressPlayerAvoidance()) { + cir.setReturnValue(false); + } + } + + @Inject(method = "shouldContinue", at = @At("RETURN"), cancellable = true) + private void non$suppressPlayerAvoidanceContinuation(CallbackInfoReturnable cir) { + if (cir.getReturnValueZ() && this.non$shouldSuppressPlayerAvoidance()) { + this.mob.getNavigation().stop(); + cir.setReturnValue(false); + } + } + + @Unique + private boolean non$shouldSuppressPlayerAvoidance() { + return this.targetEntity instanceof ServerPlayerEntity player + && this.mob instanceof NonPlayerAvoidanceState state + && state.shouldSuppressPlayerAvoidance(player); + } +} diff --git a/src/main/java/com/nonid/mixin/LivingEntityLiquidBottleConsumeMixin.java b/src/main/java/com/nonid/mixin/LivingEntityLiquidBottleConsumeMixin.java new file mode 100644 index 0000000..34dadf9 --- /dev/null +++ b/src/main/java/com/nonid/mixin/LivingEntityLiquidBottleConsumeMixin.java @@ -0,0 +1,35 @@ +package com.nonid.mixin; + +import com.nonid.NeedsOfNature; +import com.nonid.potion.NonPotions; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityLiquidBottleConsumeMixin { + @Inject(method = "consumeItem", at = @At("HEAD")) + private void non$grantEmergencySnackForEntityLiquidBottle(CallbackInfo ci) { + if ((Object)this instanceof ServerPlayerEntity player) { + ItemStack stack = player.getActiveItem(); + boolean entityLiquidBottle = NonPotions.isLiquidBottle(stack) && NonPotions.getLiquidBottleEntityTypeId(stack) != null; + if (entityLiquidBottle) { + NeedsOfNature.grantEmergencySnackAdvancement(player); + } else { + if (stack.isOf(Items.MILK_BUCKET)) { + NeedsOfNature.grantUdderlyUnfortunateAdvancementIfPregnant(player); + } + + if (stack.contains(DataComponentTypes.FOOD) || stack.contains(DataComponentTypes.CONSUMABLE)) { + NeedsOfNature.resetStableDietStreak(player); + } + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/LivingEntityLiquidJumpMixin.java b/src/main/java/com/nonid/mixin/LivingEntityLiquidJumpMixin.java new file mode 100644 index 0000000..bc17288 --- /dev/null +++ b/src/main/java/com/nonid/mixin/LivingEntityLiquidJumpMixin.java @@ -0,0 +1,57 @@ +package com.nonid.mixin; + +import com.nonid.LiquidHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonTrinketsIntegration; +import com.nonid.effect.NonStatusEffects; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.Registries; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityLiquidJumpMixin { + @Inject(method = "jump", at = @At("TAIL")) + private void needsOfNature$applyLiquidJumpSlowdown(CallbackInfo ci) { + LivingEntity self = (LivingEntity)(Object)this; + if (self instanceof PlayerEntity) { + if (self instanceof LiquidHolder) { + float multiplier = 1.0F; + StatusEffectInstance effect = self.getStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FILLED)); + if (effect != null) { + int amplifier = effect.getAmplifier(); + float liquidMultiplier = amplifier >= 2 + ? NeedsOfNature.getConfig().getFilledStageThreeJumpMult() + : (amplifier >= 1 ? NeedsOfNature.getConfig().getFilledStageTwoJumpMult() : NeedsOfNature.getConfig().getFilledStageOneJumpMult()); + liquidMultiplier = non$applyFilledAccessoryMultiplier(self, liquidMultiplier); + multiplier *= liquidMultiplier; + } + + StatusEffectInstance pregnancy = self.getStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT)); + if (pregnancy != null) { + multiplier *= NeedsOfNature.getPregnancyMovementMultiplier(pregnancy.getDuration()); + } + + if (!(Math.abs(multiplier - 1.0F) < 1.0E-4F)) { + Vec3d velocity = self.getVelocity(); + self.setVelocity(velocity.x * multiplier, velocity.y * multiplier, velocity.z * multiplier); + } + } + } + } + + private static float non$applyFilledAccessoryMultiplier(LivingEntity entity, float multiplier) { + double effectMultiplier = NonTrinketsIntegration.getActiveTankAccessoryEffects(entity).filledEffectMultiplier(); + if (!Double.isFinite(effectMultiplier)) { + return multiplier; + } + + double penalty = 1.0 - multiplier; + return (float)Math.max(0.0, Math.min(2.0, 1.0 - penalty * effectMultiplier)); + } +} diff --git a/src/main/java/com/nonid/mixin/MobEntityEnergyMixin.java b/src/main/java/com/nonid/mixin/MobEntityEnergyMixin.java new file mode 100644 index 0000000..89e8832 --- /dev/null +++ b/src/main/java/com/nonid/mixin/MobEntityEnergyMixin.java @@ -0,0 +1,1003 @@ +package com.nonid.mixin; + +import com.nonid.EnergyHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonAnimationMatcher; +import com.nonid.NonDebugChatCategory; +import com.nonid.NonGatherSystem; +import com.nonid.NonMobEnergySystem; +import com.nonid.NonMobGatherEligibility; +import com.nonid.NonPlayerAvoidanceState; +import com.nonid.data.NonEntityProfiles; +import com.nonid.internal.animation.api.AnimationService; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.network.AnimationStageInfo; +import com.nonid.particle.NonParticles; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityData; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.SpawnReason; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.data.DataTracker.Builder; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.registry.Registries; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.world.LocalDifficulty; +import net.minecraft.world.ServerWorldAccess; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MobEntity.class) +public abstract class MobEntityEnergyMixin extends LivingEntity implements EnergyHolder, NonPlayerAvoidanceState { + @Unique + private static final int PLAYER_AVOIDANCE_ELIGIBILITY_CACHE_TICKS = 20; + @Unique + private static final int LINGER_MATCH_CACHE_TICKS = 10; + @Unique + private static final TrackedData ENERGY = DataTracker.registerData(MobEntity.class, TrackedDataHandlerRegistry.INTEGER); + @Unique + private int non$energyTickCounter; + @Unique + private boolean non$energyInitialized; + @Unique + private float non$energyGainMult = -1.0F; + @Unique + private float non$energyGainDrift = -1.0F; + @Unique + private double non$energyAuraCarry; + @Unique + private int non$nearAnimCheckCooldown; + @Unique + private boolean non$nearActiveAnimation; + @Unique + private double non$nearActiveAnimationGainMultiplier = 1.0; + @Unique + @Nullable + private UUID non$lingerLookTargetUuid; + @Unique + private int non$lingerMatchCooldown; + @Unique + private int non$heartParticleCooldown; + @Unique + private int non$playerSearchCooldown; + @Unique + private int non$mobSearchCooldown; + @Unique + private long non$lastEnergyProcessedTick = Long.MIN_VALUE; + @Unique + private boolean non$attackFailsafeWarned; + @Unique + private final Map non$partnerCooldowns = new HashMap<>(); + @Unique + private final Map non$bypassablePartnerCooldowns = new HashMap<>(); + @Unique + private final Map non$multiActorJoinCooldowns = new HashMap<>(); + @Unique + @Nullable + private UUID non$avoidancePlayerUuid; + @Unique + @Nullable + private UUID non$avoidanceInstanceUuid; + @Unique + private long non$avoidanceEligibilityExpiresTick = Long.MIN_VALUE; + @Unique + private boolean non$avoidanceEligible; + + protected MobEntityEnergyMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(method = "initDataTracker", at = @At("TAIL")) + private void needsOfNature$initEnergyTracker(Builder builder, CallbackInfo ci) { + builder.add(ENERGY, 0); + } + + @Inject(method = "tick", at = @At("TAIL")) + private void needsOfNature$tickEnergy(CallbackInfo ci) { + if (this.getEntityWorld() instanceof ServerWorld serverWorld) { + long worldTick = serverWorld.getTime(); + if (this.non$lastEnergyProcessedTick != worldTick) { + this.non$lastEnergyProcessedTick = worldTick; + this.non$ensureEnergyInitialized(serverWorld); + boolean non$hasAnimationSupport = NonMobEnergySystem.hasGenericAnimationSupport(this.getType(), this.isBaby()); + boolean attackFailsafeActive = NeedsOfNature.isMobAttackFailsafeActive(serverWorld, this.getUuid()); + if (attackFailsafeActive) { + this.non$lingerLookTargetUuid = null; + if (!this.non$attackFailsafeWarned) { + NeedsOfNature.sendDebugChatToNearby( + serverWorld, + this, + 20.0, + NonDebugChatCategory.WARNING, + Text.translatable("debug.needsofnature.energy_locked_attack_cooldown", new Object[]{Text.translatable(this.getType().getTranslationKey())}) + ); + this.non$attackFailsafeWarned = true; + } + + if (this.getEnergy() != 0) { + this.setEnergy(0); + } + + this.non$energyTickCounter = 0; + this.non$heartParticleCooldown = 0; + this.non$playerSearchCooldown = 40; + this.non$mobSearchCooldown = 40; + } else { + this.non$attackFailsafeWarned = false; + if (!non$hasAnimationSupport) { + this.non$lingerLookTargetUuid = null; + this.non$nearActiveAnimation = false; + this.non$nearActiveAnimationGainMultiplier = 1.0; + if (this.getEnergy() >= 200) { + this.setEnergy(199); + } + + this.non$energyTickCounter = 0; + this.non$heartParticleCooldown = 0; + } else if (this.non$nearAnimCheckCooldown-- <= 0) { + this.non$nearActiveAnimationGainMultiplier = NeedsOfNature.getNearbyVisibleAnimationGainMultiplier(serverWorld, (MobEntity)(Object)this); + this.non$nearActiveAnimation = this.non$nearActiveAnimationGainMultiplier > 1.0; + this.non$nearAnimCheckCooldown = 20; + } + + if (non$hasAnimationSupport) { + int energy = this.getEnergy(); + if (energy >= 200 && NonMobEnergySystem.isEnergyAttackStabilized(this)) { + this.setEnergy(199); + energy = this.getEnergy(); + } + + int ticksPerGain = NonMobEnergySystem.ticksPerGain( + (MobEntity)(Object)this, + this.getEnergyGainMultiplier(), + this.getEnergyGainDrift(), + this.non$nearActiveAnimation, + this.non$nearActiveAnimationGainMultiplier + ); + if (energy < 200) { + this.non$energyTickCounter++; + if (this.non$energyTickCounter >= ticksPerGain) { + this.non$energyTickCounter = 0; + this.setEnergy(Math.min(200, energy + 1)); + } + + this.non$heartParticleCooldown = 0; + } else { + this.non$energyTickCounter = 0; + this.non$spawnMaxEnergyHeartParticle(serverWorld); + } + + this.non$maybeStartEnergyAnimation(serverWorld, energy); + } + } + } + } + } + + @Inject(method = "tickNewAi", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/ai/control/LookControl;tick()V")) + private void needsOfNature$applyLingerLookTarget(CallbackInfo ci) { + UUID targetUuid = this.non$lingerLookTargetUuid; + if (targetUuid != null) { + if (this.getEntityWorld() instanceof ServerWorld world) { + MobEntity self = (MobEntity)(Object)this; + if (NonEntityProfiles.resolve(self).lookAtNearbyAnimations()) { + if (world.getEntity(targetUuid) instanceof ServerPlayerEntity player + && player.isAlive() + && !player.isRemoved() + && !player.isSpectator() + && !player.isCreative() + && NeedsOfNature.getActivePlayerInstance(player) != null + && !NeedsOfNature.isActorPendingOrActive(world, self.getUuid())) { + double lookY = Math.max(player.getY() + 0.6, player.getEyeY() - 2.0); + self.getLookControl().lookAt(player.getX(), lookY, player.getZ(), 30.0F, 30.0F); + } else { + this.non$lingerLookTargetUuid = null; + } + } + } else { + this.non$lingerLookTargetUuid = null; + } + } + } + + @Inject(method = "initialize", at = @At("TAIL")) + private void needsOfNature$initEnergyOnSpawn( + ServerWorldAccess world, LocalDifficulty difficulty, SpawnReason spawnReason, @Nullable EntityData entityData, CallbackInfoReturnable cir + ) { + if (world instanceof ServerWorld serverWorld) { + this.non$ensureEnergyInitialized(serverWorld); + } + } + + @Override + public int getEnergy() { + return (Integer)this.dataTracker.get(ENERGY); + } + + @Override + public void setEnergy(int value) { + int maxEnergy = NonMobEnergySystem.isEnergyAttackStabilized(this) ? 199 : 200; + this.dataTracker.set(ENERGY, Math.min(maxEnergy, Math.max(0, value))); + } + + @Override + public float getEnergyGainMultiplier() { + return this.non$energyGainMult <= 0.0F ? 1.0F : this.non$energyGainMult; + } + + @Override + public void setEnergyGainMultiplier(float value) { + if (Float.isFinite(value)) { + this.non$energyGainMult = Math.max(0.4F, Math.min(6.0F, value)); + } + } + + @Override + public float getEnergyGainDrift() { + return this.non$energyGainDrift <= 0.0F ? 1.0F : this.non$energyGainDrift; + } + + @Override + public void setEnergyGainDrift(float value) { + if (Float.isFinite(value)) { + this.non$energyGainDrift = Math.max(0.8F, Math.min(1.2F, value)); + } + } + + @Override + public double getEnergyAuraCarry() { + return this.non$energyAuraCarry > 0.0 ? this.non$energyAuraCarry : 0.0; + } + + @Override + public void setEnergyAuraCarry(double value) { + if (!Double.isFinite(value)) { + this.non$energyAuraCarry = 0.0; + } else { + this.non$energyAuraCarry = Math.max(0.0, value); + } + } + + @Override + public boolean isEnergyInitialized() { + return this.non$energyInitialized; + } + + @Override + public void markEnergyInitialized() { + this.non$energyInitialized = true; + } + + @Override + public void ensureEnergyInitialized(ServerWorld world) { + if (world != null) { + this.non$ensureEnergyInitialized(world); + } + } + + @Override + public int getMaxEnergy() { + return 200; + } + + @Override + public void onNonAnimationStarted() { + this.non$partnerCooldowns.clear(); + this.non$bypassablePartnerCooldowns.clear(); + this.non$multiActorJoinCooldowns.clear(); + } + + @Override + public void applyPartnerCooldown(UUID partnerId, long nowTick, int cooldownTicks) { + if (partnerId != null) { + if (cooldownTicks > 0) { + long until = nowTick + cooldownTicks; + this.non$partnerCooldowns.put(partnerId, until); + this.non$bypassablePartnerCooldowns.put(partnerId, until); + } + } + } + + @Override + public List getActiveCooldownDebugLines(ServerWorld world) { + List lines = new ArrayList<>(); + if (world == null) { + return lines; + } + + long nowTick = world.getTime(); + this.non$pruneCooldownMaps(nowTick); + if (this.non$playerSearchCooldown > 0) { + lines.add("player search delay: " + NeedsOfNature.formatTicksForDebug(this.non$playerSearchCooldown)); + } + + if (this.non$mobSearchCooldown > 0) { + lines.add("mob search delay: " + NeedsOfNature.formatTicksForDebug(this.non$mobSearchCooldown)); + } + + NonMobEnergySystem.appendCooldownMapDebug(lines, world, nowTick, "partner", this.non$partnerCooldowns, this.non$bypassablePartnerCooldowns, true); + NonMobEnergySystem.appendCooldownMapDebug(lines, world, nowTick, "3+ join", this.non$multiActorJoinCooldowns, this.non$bypassablePartnerCooldowns, false); + return lines; + } + + @Override + public void requestAttackOnPlayer(ServerWorld world, ServerPlayerEntity player) { + if (world != null && player != null) { + if (this.getEntityWorld() == world) { + if (!NonMobEnergySystem.isEnergyAttackStabilized(this) || this.getEnergy() < 200) { + if (NonMobEnergySystem.hasGenericAnimationSupport(this.getType(), this.isBaby())) { + if (NonMobGatherEligibility.canInitiateAutoGather((MobEntity)(Object)this)) { + if (NeedsOfNature.canMobAttackPlayer(player)) { + if (!NeedsOfNature.isActorPendingOrActive(world, this.getUuid())) { + if (!NeedsOfNature.isMobAttackFailsafeActive(world, this.getUuid())) { + this.non$playerSearchCooldown = 0; + this.non$partnerCooldowns.remove(player.getUuid()); + this.non$bypassablePartnerCooldowns.remove(player.getUuid()); + this.non$multiActorJoinCooldowns.remove(player.getUuid()); + this.non$tryPlayerInteraction(world, this.getEnergy(), true, true, player); + } + } + } + } + } + } + } + } + } + + @Override + public boolean shouldSuppressPlayerAvoidance(ServerPlayerEntity player) { + if (this.getEntityWorld() instanceof ServerWorld world && player != null) { + MobEntity self = (MobEntity)(Object)this; + if (player.getEntityWorld() != world || !player.isAlive() || player.isRemoved() || player.isSpectator()) { + return false; + } + + if (NonGatherSystem.isMobGatheringWithPlayer(world, self.getUuid(), player.getUuid())) { + return true; + } + + if (this.getEnergy() >= 200 + && NonMobGatherEligibility.canInitiateAutoGather(self) + && !NeedsOfNature.isActorPendingOrActive(world, self.getUuid()) + && !NeedsOfNature.isMobAttackFailsafeActive(world, self.getUuid())) { + UUID activeInstance = NeedsOfNature.getActivePlayerInstance(player); + boolean interactionIsAttack = activeInstance == null || NeedsOfNature.isInstanceAttack(world, activeInstance); + if (interactionIsAttack && !NeedsOfNature.canMobAttackPlayer(player)) { + return false; + } + + if (activeInstance == null && NonGatherSystem.isAutomaticMobPlayerStartBlockedByMount(player)) { + return false; + } + + long nowTick = world.getTime(); + if (player.getUuid().equals(this.non$avoidancePlayerUuid) + && Objects.equals(activeInstance, this.non$avoidanceInstanceUuid) + && nowTick < this.non$avoidanceEligibilityExpiresTick) { + return this.non$avoidanceEligible; + } + + this.non$avoidancePlayerUuid = player.getUuid(); + this.non$avoidanceInstanceUuid = activeInstance; + this.non$avoidanceEligibilityExpiresTick = nowTick + 20L; + this.non$avoidanceEligible = this.non$hasEligiblePlayerInteraction(world, self, player, activeInstance); + return this.non$avoidanceEligible; + } else { + return false; + } + } else { + return false; + } + } + + @Unique + private boolean non$hasEligiblePlayerInteraction(ServerWorld world, MobEntity self, ServerPlayerEntity player, @Nullable UUID activeInstance) { + List actors; + Entity requiredActiveActor; + Identifier predecessorAnimationId; + boolean attackSelection; + if (activeInstance != null) { + AnimationStageInfo stage = AnimationService.getCurrentStage(world, activeInstance); + if (stage != null && !stage.allowJoin()) { + return false; + } + + predecessorAnimationId = AnimationService.getActiveAnimationId(world, activeInstance); + if (predecessorAnimationId == null) { + return false; + } + + actors = this.non$resolveJoinActors(world, activeInstance); + attackSelection = NeedsOfNature.isInstanceAttack(world, activeInstance); + requiredActiveActor = attackSelection ? self : null; + } else { + if (AnimationService.isActorPendingOrActive(world, player.getUuid())) { + return false; + } + + actors = new ArrayList<>(2); + actors.add(self); + actors.add(player); + actors.sort(Comparator.comparingInt(Entity::getId)); + requiredActiveActor = self; + predecessorAnimationId = null; + attackSelection = true; + } + + List eligible = attackSelection + ? NonAnimationMatcher.findEligibleAutomaticAttack(world, actors, Set.of(), player, true, requiredActiveActor, Set.of(), predecessorAnimationId) + : NonAnimationMatcher.findEligibleAutomatic(world, actors, Set.of(), player, true, requiredActiveActor, Set.of(), predecessorAnimationId); + return !eligible.isEmpty(); + } + + @Unique + private void non$maybeStartEnergyAnimation(ServerWorld world, int currentEnergy) { + if (!NonMobEnergySystem.hasGenericAnimationSupport(this.getType(), this.isBaby())) { + this.non$lingerLookTargetUuid = null; + } else { + MobEntity self = (MobEntity)(Object)this; + if (!NonMobGatherEligibility.canInitiateAutoGather(self)) { + this.non$lingerLookTargetUuid = null; + } else { + long nowTick = world.getTime(); + this.non$pruneCooldownMaps(nowTick); + if (NeedsOfNature.isActorPendingOrActive(world, this.getUuid())) { + this.non$lingerLookTargetUuid = null; + } else { + boolean canJoin = currentEnergy >= 70; + boolean canMobMatch = currentEnergy >= 120 && currentEnergy < 200; + boolean isAttack = currentEnergy >= 200; + if (!canJoin) { + this.non$lingerLookTargetUuid = null; + } else { + ServerPlayerEntity lingerPlayer = null; + lingerPlayer = this.non$findNearestActiveAnimationPlayerForLinger(world); + if (lingerPlayer != null) { + this.non$applyPlayerAnimationLinger(lingerPlayer); + } else { + this.non$lingerLookTargetUuid = null; + } + + if (this.non$playerSearchCooldown > 0) { + this.non$playerSearchCooldown--; + } else { + this.non$playerSearchCooldown = 40; + if (NeedsOfNature.consumeScanBudget(world) && this.non$tryPlayerInteraction(world, currentEnergy, true, isAttack, null)) { + this.non$playerSearchCooldown = 100; + return; + } + } + + if (this.non$mobSearchCooldown > 0) { + this.non$mobSearchCooldown--; + } else { + this.non$mobSearchCooldown = 40; + if (NeedsOfNature.consumeScanBudget(world)) { + if (lingerPlayer == null) { + if (canMobMatch || isAttack) { + Identifier selfTypeId = Registries.ENTITY_TYPE.getId(this.getType()); + boolean selfIsBaby = this.isBaby(); + if (AnimationDefinitions.hasEnabledGenericMobPairAnimationForEntityType(selfTypeId, selfIsBaby)) { + Box searchBox = this.getBoundingBox().expand(20.0); + List candidates = world.getEntitiesByClass( + MobEntity.class, + searchBox, + mob -> !mob.getUuid().equals(this.getUuid()) + && NonMobGatherEligibility.canBeAutoGatherTarget(mob) + && mob.isAlive() + && !mob.isRemoved() + && mob.getEntityWorld() == world + && AnimationDefinitions.areEnabledGenericMobPairTypesCompatible( + selfTypeId, selfIsBaby, Registries.ENTITY_TYPE.getId(mob.getType()), mob.isBaby() + ) + ); + if (!candidates.isEmpty()) { + candidates.sort(Comparator.comparing(candidatex -> !NonMobEnergySystem.bypassesEntityEntityRoll(this, candidatex))); + boolean entityEntityRollChecked = false; + boolean entityEntityRollPassed = false; + + for (MobEntity candidate : candidates) { + boolean bypassEntityEntityRoll = NonMobEnergySystem.bypassesEntityEntityRoll(this, candidate); + if (!bypassEntityEntityRoll && !entityEntityRollChecked) { + entityEntityRollChecked = true; + entityEntityRollPassed = NonMobEnergySystem.passesEntityEntityRoll(world, this); + if (!entityEntityRollPassed) { + this.non$mobSearchCooldown = 1200; + return; + } + } + + if (candidate instanceof EnergyHolder holder && !NeedsOfNature.isActorPendingOrActive(world, candidate.getUuid())) { + int candidateEnergy = holder.getEnergy(); + if (isAttack || candidateEnergy >= 70) { + List actors = new ArrayList<>(2); + actors.add(this); + actors.add(candidate); + actors.sort(Comparator.comparingInt(Entity::getId)); + if (!this.non$partnerCooldowns.containsKey(candidate.getUuid())) { + NonGatherSystem.GatherCandidate gatherCandidate = NonGatherSystem.findGatherCandidate( + world, actors, isAttack, (Entity)this, null + ); + if (gatherCandidate != null) { + if (!NonMobEnergySystem.passesSameGenderRoll(world, this, actors)) { + this.non$applyRollCooldown(candidate.getUuid(), nowTick); + this.non$mobSearchCooldown = 100; + } else { + Text mobName = this.getDisplayName(); + Text partnerName = candidate.getDisplayName(); + NeedsOfNature.sendDebugChatToNearby( + world, + this, + 20.0, + NonDebugChatCategory.INFO, + Text.translatable( + "debug.needsofnature.mob_pair_attempt", + new Object[]{ + mobName, partnerName, currentEnergy, candidateEnergy, gatherCandidate.animationId().toString() + } + ) + ); + NonGatherSystem.startMobMobGather( + world, + (MobEntity)(Object)this, + candidate, + gatherCandidate.animationId(), + gatherCandidate.actorKeys(), + gatherCandidate.stages(), + isAttack + ); + NeedsOfNature.sendDebugChatToNearby( + world, + this, + 20.0, + NonDebugChatCategory.INFO, + Text.translatable( + "debug.needsofnature.mob_pair_gathering", + new Object[]{mobName, partnerName, gatherCandidate.animationId().toString()} + ) + ); + this.non$mobSearchCooldown = 100; + } + break; + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + @Unique + private boolean non$tryPlayerInteraction(ServerWorld world, int currentEnergy, boolean canJoin, boolean isAttack, @Nullable ServerPlayerEntity forcedPlayer) { + MobEntity self = (MobEntity)(Object)this; + if (!NonMobGatherEligibility.canInitiateAutoGather(self)) { + return false; + } + + long nowTick = world.getTime(); + double maxDistSq = 400.0; + Text mobName = this.getDisplayName(); + String mobTypeId = Registries.ENTITY_TYPE.getId(this.getType()).toString(); + boolean attackFailsafeActive = isAttack && NeedsOfNature.isMobAttackFailsafeActive(world, this.getUuid()); + if (forcedPlayer == null && world.getPlayers().isEmpty()) { + return false; + } + + if (attackFailsafeActive) { + return false; + } + + int mobChunkX = (int)Math.floor(this.getX()) >> 4; + int mobChunkZ = (int)Math.floor(this.getZ()) >> 4; + boolean playerNearbyChunk = false; + if (forcedPlayer == null) { + for (ServerPlayerEntity player : world.getPlayers()) { + if (player.isAlive() && !player.isRemoved()) { + UUID inst = NeedsOfNature.getActivePlayerInstance(player); + boolean interactionIsAttack = inst == null ? isAttack : NeedsOfNature.isInstanceAttack(world, inst); + if (!interactionIsAttack || NeedsOfNature.canMobAttackPlayer(player)) { + double distSq = player.squaredDistanceTo(this); + boolean bypassCooldown = this.non$shouldBypassPlayerCooldown(player, distSq); + if (!this.non$partnerCooldowns.containsKey(player.getUuid()) || bypassCooldown) { + if (bypassCooldown) { + this.non$partnerCooldowns.remove(player.getUuid()); + this.non$bypassablePartnerCooldowns.remove(player.getUuid()); + } + + int px = (int)Math.floor(player.getX()) >> 4; + int pz = (int)Math.floor(player.getZ()) >> 4; + if (Math.abs(px - mobChunkX) <= 1 && Math.abs(pz - mobChunkZ) <= 1) { + playerNearbyChunk = true; + break; + } + } + } + } + } + + if (!playerNearbyChunk) { + return false; + } + } + + ServerPlayerEntity closest = null; + double closestDistSq = Double.MAX_VALUE; + if (forcedPlayer != null) { + if (!forcedPlayer.isAlive() || forcedPlayer.isRemoved() || forcedPlayer.getEntityWorld() != world) { + return false; + } + + UUID inst = NeedsOfNature.getActivePlayerInstance(forcedPlayer); + boolean interactionIsAttack = inst == null ? isAttack : NeedsOfNature.isInstanceAttack(world, inst); + if (interactionIsAttack && !NeedsOfNature.canMobAttackPlayer(forcedPlayer)) { + return false; + } + + double distSq = forcedPlayer.squaredDistanceTo(this); + if (distSq > maxDistSq) { + return false; + } + + closest = forcedPlayer; + closestDistSq = distSq; + } else { + for (ServerPlayerEntity player : world.getPlayers()) { + if (player.isAlive() && !player.isRemoved()) { + UUID inst = NeedsOfNature.getActivePlayerInstance(player); + boolean interactionIsAttack = inst == null ? isAttack : NeedsOfNature.isInstanceAttack(world, inst); + if (!interactionIsAttack || NeedsOfNature.canMobAttackPlayer(player)) { + double distSq = player.squaredDistanceTo(this); + boolean bypassCooldown = this.non$shouldBypassPlayerCooldown(player, distSq); + if (!this.non$partnerCooldowns.containsKey(player.getUuid()) || bypassCooldown) { + if (bypassCooldown) { + this.non$partnerCooldowns.remove(player.getUuid()); + this.non$bypassablePartnerCooldowns.remove(player.getUuid()); + } + + if (!(distSq > maxDistSq) && (inst != null || isAttack) && distSq < closestDistSq) { + closestDistSq = distSq; + closest = player; + } + } + } + } + } + } + + if (closest == null) { + return false; + } + + UUID activeInstance = NeedsOfNature.getActivePlayerInstance(closest); + AnimationStageInfo activeStage = activeInstance == null ? null : AnimationService.getCurrentStage(world, activeInstance); + if (activeInstance != null) { + boolean activeIsAttack = NeedsOfNature.isInstanceAttack(world, activeInstance); + boolean joinAsAttack = isAttack && activeIsAttack; + if (activeStage != null && !activeStage.allowJoin()) { + return false; + } else { + Identifier predecessorAnimationId = AnimationService.getActiveAnimationId(world, activeInstance); + if (predecessorAnimationId == null) { + return true; + } else { + List combinedActors = this.non$resolveJoinActors(world, activeInstance); + AnimationService.MatchedAnimation combinedMatch = activeIsAttack + ? NonAnimationMatcher.findAutomaticAttack( + world, combinedActors, Set.of(), closest, true, joinAsAttack ? (Entity)this : null, Set.of(), null, predecessorAnimationId + ) + : NonAnimationMatcher.findAutomatic( + world, combinedActors, Set.of(), closest, true, joinAsAttack ? (Entity)this : null, Set.of(), null, predecessorAnimationId + ); + if (combinedMatch == null) { + NeedsOfNature.logRuntime( + "Energy join skipped: mobType={} player={} instance={} no expanded match", mobTypeId, closest.getName().getString(), activeInstance + ); + NeedsOfNature.sendDebugChat( + closest, NonDebugChatCategory.WARNING, Text.translatable("debug.needsofnature.mob_join_no_expanded_match", new Object[]{mobName}) + ); + return true; + } else { + String playerName = closest.getName().getString(); + String joinAnimationId = combinedMatch.animationId().toString(); + NeedsOfNature.logRuntime( + "Energy join attempt: mobType={} energy={} -> player={} instance={} distanceSq={}", + mobTypeId, + currentEnergy, + playerName, + activeInstance, + closestDistSq + ); + NeedsOfNature.sendDebugChat( + closest, + NonDebugChatCategory.INFO, + Text.translatable("debug.needsofnature.mob_wants_join", new Object[]{mobName, currentEnergy, joinAnimationId}) + ); + int multiActorJoinChance = NonMobEnergySystem.resolveMultiActorJoinChancePercent(this); + if (combinedActors.size() > 2 && multiActorJoinChance < 100 && this.non$multiActorJoinCooldowns.containsKey(closest.getUuid())) { + return true; + } else if (!NonMobEnergySystem.passesMultiActorJoinRoll(world, this, combinedActors.size(), multiActorJoinChance)) { + this.non$applyNonBypassRollCooldown(closest.getUuid(), nowTick); + return true; + } else { + NonGatherSystem.startJoinGather(world, (MobEntity)(Object)this, closest, activeInstance, combinedMatch.animationId()); + NeedsOfNature.logRuntime( + "Energy join gathering: mobType={} -> player={} instance={} anim={}", + mobTypeId, + playerName, + activeInstance, + combinedMatch.animationId() + ); + NeedsOfNature.sendDebugChat( + closest, + NonDebugChatCategory.INFO, + Text.translatable("debug.needsofnature.join_request_accepted", new Object[]{mobName, joinAnimationId}) + ); + return true; + } + } + } + } + } else if (isAttack && canJoin) { + List actors = new ArrayList<>(2); + actors.add(this); + actors.add(closest); + actors.sort(Comparator.comparingInt(Entity::getId)); + NonGatherSystem.GatherCandidate gatherCandidate = NonGatherSystem.findGatherCandidate(world, actors, true, (Entity)this, closest); + if (gatherCandidate == null) { + return false; + } else { + String playerName = closest.getName().getString(); + NeedsOfNature.logRuntime( + "Energy start attempt (200): mobType={} -> player={} anim={} distanceSq={}", mobTypeId, playerName, gatherCandidate.animationId(), closestDistSq + ); + NeedsOfNature.sendDebugChat( + closest, NonDebugChatCategory.INFO, Text.translatable("debug.needsofnature.starting_animation_with_mob_200", new Object[]{mobName}) + ); + boolean gatherStarted = NonGatherSystem.startMobPlayerGather( + world, (MobEntity)(Object)this, closest, gatherCandidate.animationId(), gatherCandidate.actorKeys(), gatherCandidate.stages() + ); + if (!gatherStarted) { + this.non$playerSearchCooldown = 100; + return true; + } else { + NeedsOfNature.logRuntime("Energy start gathering: mobType={} player={} anim={}", mobTypeId, playerName, gatherCandidate.animationId()); + NeedsOfNature.sendDebugChat( + closest, NonDebugChatCategory.INFO, Text.translatable("debug.needsofnature.gathering_with_mob", new Object[]{mobName}) + ); + this.non$playerSearchCooldown = 100; + return true; + } + } + } else { + return false; + } + } + + @Unique + private void non$spawnMaxEnergyHeartParticle(ServerWorld world) { + if (world != null) { + this.non$heartParticleCooldown++; + if (this.non$heartParticleCooldown >= 10) { + this.non$heartParticleCooldown = 0; + world.spawnParticles(NonParticles.SMALLHEART, this.getX(), this.getBodyY(0.6), this.getZ(), 1, 0.3, 0.3, 0.3, 0.0); + } + } + } + + @Unique + private boolean non$shouldBypassPlayerCooldown(ServerPlayerEntity player, double distSq) { + if (player == null) { + return false; + } + + if (!this.non$bypassablePartnerCooldowns.containsKey(player.getUuid())) { + return false; + } + + if (distSq > 64.0) { + return false; + } + + MobEntity self = (MobEntity)(Object)this; + return self.canSee(player); + } + + @Unique + @Nullable + private ServerPlayerEntity non$findNearestActiveAnimationPlayerForLinger(ServerWorld world) { + if (world != null && !world.getPlayers().isEmpty()) { + MobEntity self = (MobEntity)(Object)this; + double maxDistSq = 400.0; + ServerPlayerEntity cachedPlayer = null; + if (this.non$lingerLookTargetUuid != null) { + if (world.getEntity(this.non$lingerLookTargetUuid) instanceof ServerPlayerEntity player + && this.non$isBasicPlayerAnimationLingerTarget(player, self, maxDistSq)) { + cachedPlayer = player; + } else { + this.non$lingerLookTargetUuid = null; + } + } + + if (this.non$lingerMatchCooldown > 0) { + this.non$lingerMatchCooldown--; + return cachedPlayer; + } + + this.non$lingerMatchCooldown = 10; + if (!NeedsOfNature.consumeScanBudget(world)) { + return cachedPlayer; + } + + if (cachedPlayer != null && this.non$hasPairAnimationWithPlayer(cachedPlayer)) { + return cachedPlayer; + } + + this.non$lingerLookTargetUuid = null; + ServerPlayerEntity nearest = null; + double nearestDistSq = Double.MAX_VALUE; + + for (ServerPlayerEntity player : world.getPlayers()) { + double distSq = player.squaredDistanceTo(self); + if (!(distSq > maxDistSq) + && !(distSq >= nearestDistSq) + && this.non$isBasicPlayerAnimationLingerTarget(player, self, maxDistSq) + && this.non$hasPairAnimationWithPlayer(player)) { + nearest = player; + nearestDistSq = distSq; + } + } + + return nearest; + } else { + return null; + } + } + + @Unique + private boolean non$isBasicPlayerAnimationLingerTarget(ServerPlayerEntity player, MobEntity self, double maxDistSq) { + if (player == null || self == null || !player.isAlive() || player.isRemoved()) { + return false; + } else if (!player.isSpectator() && !player.isCreative()) { + return player.squaredDistanceTo(self) > maxDistSq ? false : NeedsOfNature.getActivePlayerInstance(player) != null; + } else { + return false; + } + } + + @Unique + private boolean non$hasPairAnimationWithPlayer(ServerPlayerEntity player) { + if (player == null) { + return false; + } else if (player.getEntityWorld() instanceof ServerWorld world) { + List pair = new ArrayList<>(2); + pair.add(this); + pair.add(player); + pair.sort(Comparator.comparingInt(Entity::getId)); + return NonAnimationMatcher.findAutomatic(world, pair, Set.of(), null, false, null, Set.of()) != null; + } else { + return false; + } + } + + @Unique + private void non$applyPlayerAnimationLinger(ServerPlayerEntity player) { + if (player != null) { + MobEntity self = (MobEntity)(Object)this; + this.non$lingerLookTargetUuid = player.getUuid(); + double distSq = self.squaredDistanceTo(player); + if (distSq > 9.0 || distSq < 2.25) { + double targetX = player.getX(); + double targetZ = player.getZ(); + double dx = self.getX() - player.getX(); + double dz = self.getZ() - player.getZ(); + double length = Math.sqrt(dx * dx + dz * dz); + if (length < 1.0E-4) { + double angle = Math.toRadians(Math.floorMod(self.getId() * 47, 360)); + dx = Math.cos(angle); + dz = Math.sin(angle); + length = 1.0; + } + + double ringDistance = 2.25; + targetX += dx / length * ringDistance; + targetZ += dz / length * ringDistance; + self.getNavigation().startMovingTo(targetX, player.getY(), targetZ, 1.1); + } + } + } + + @Unique + private List non$resolveJoinActors(ServerWorld world, UUID instanceId) { + List combined = new ArrayList<>(); + if (world != null && instanceId != null) { + for (UUID uuid : NeedsOfNature.getInstanceActors(world, instanceId)) { + Entity e = world.getEntity(uuid); + if (e != null) { + combined.add(e); + } + } + } + + if (combined.stream().noneMatch(ex -> ex.getUuid().equals(this.getUuid()))) { + combined.add(this); + } + + combined.sort(Comparator.comparingInt(Entity::getId)); + return combined; + } + + @Unique + private void non$applyRollCooldown(UUID partnerId, long nowTick) { + if (partnerId != null) { + long until = nowTick + 1200L; + this.non$partnerCooldowns.put(partnerId, until); + this.non$bypassablePartnerCooldowns.put(partnerId, until); + } + } + + @Unique + private void non$applyNonBypassRollCooldown(UUID partnerId, long nowTick) { + if (partnerId != null) { + long until = nowTick + 1200L; + this.non$multiActorJoinCooldowns.put(partnerId, until); + } + } + + @Unique + private void non$pruneCooldownMaps(long nowTick) { + this.non$partnerCooldowns.entrySet().removeIf(e -> e.getValue() <= nowTick); + this.non$bypassablePartnerCooldowns.entrySet().removeIf(e -> e.getValue() <= nowTick); + this.non$multiActorJoinCooldowns.entrySet().removeIf(e -> e.getValue() <= nowTick); + } + + @Unique + private void non$ensureEnergyInitialized(ServerWorld world) { + if (!this.non$energyInitialized) { + this.non$energyInitialized = true; + if (this.non$energyGainMult <= 0.0F) { + this.non$energyGainMult = NonMobEnergySystem.randomGainMultiplier(world); + } + + if (this.non$energyGainDrift <= 0.0F) { + this.non$energyGainDrift = NonMobEnergySystem.randomGainDrift(world); + } + + int maxInitial = NeedsOfNature.getConfig().getInitialEnergyMax(); + if (maxInitial > 0 && this.getEnergy() == 0) { + int initial = world.getRandom().nextInt(maxInitial + 1); + this.setEnergy(initial); + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/MobEntityGenderMixin.java b/src/main/java/com/nonid/mixin/MobEntityGenderMixin.java new file mode 100644 index 0000000..92197bd --- /dev/null +++ b/src/main/java/com/nonid/mixin/MobEntityGenderMixin.java @@ -0,0 +1,41 @@ +package com.nonid.mixin; + +import com.nonid.GenderHolder; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.data.DataTracker.Builder; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MobEntity.class) +public abstract class MobEntityGenderMixin extends LivingEntity implements GenderHolder { + @Unique + private static final TrackedData NON_GENDER = DataTracker.registerData(MobEntity.class, TrackedDataHandlerRegistry.INTEGER); + + protected MobEntityGenderMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(method = "initDataTracker", at = @At("TAIL")) + private void needsOfNature$initGenderTracker(Builder builder, CallbackInfo ci) { + builder.add(NON_GENDER, 0); + } + + @Override + public int getGenderMask() { + return (Integer)this.dataTracker.get(NON_GENDER); + } + + @Override + public void setGenderMask(int mask) { + this.dataTracker.set(NON_GENDER, mask & 3); + } +} diff --git a/src/main/java/com/nonid/mixin/MobEntityPregnancyFriendlyTargetMixin.java b/src/main/java/com/nonid/mixin/MobEntityPregnancyFriendlyTargetMixin.java new file mode 100644 index 0000000..32222b2 --- /dev/null +++ b/src/main/java/com/nonid/mixin/MobEntityPregnancyFriendlyTargetMixin.java @@ -0,0 +1,37 @@ +package com.nonid.mixin; + +import com.nonid.NeedsOfNature; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.Angerable; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MobEntity.class) +public abstract class MobEntityPregnancyFriendlyTargetMixin { + @Inject(method = "setTarget", at = @At("HEAD"), cancellable = true) + private void non$blockPlayerTargetingForPregnancyFriendly(LivingEntity target, CallbackInfo ci) { + if (target instanceof PlayerEntity) { + MobEntity mob = (MobEntity)(Object)this; + if (NeedsOfNature.isPregnancyFriendlyMob(mob)) { + ci.cancel(); + } + } + } + + @Inject(method = "mobTick", at = @At("HEAD")) + private void non$clearPlayerTargetForPregnancyFriendly(CallbackInfo ci) { + MobEntity mob = (MobEntity)(Object)this; + if (NeedsOfNature.isPregnancyFriendlyMob(mob)) { + if (mob.getTarget() instanceof PlayerEntity) { + mob.setTarget(null); + if (mob instanceof Angerable angerable) { + angerable.stopAnger(); + } + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/NonMixinPlugin.java b/src/main/java/com/nonid/mixin/NonMixinPlugin.java new file mode 100644 index 0000000..b397a98 --- /dev/null +++ b/src/main/java/com/nonid/mixin/NonMixinPlugin.java @@ -0,0 +1,36 @@ +package com.nonid.mixin; + +import java.util.List; +import java.util.Set; +import net.fabricmc.loader.api.FabricLoader; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +public final class NonMixinPlugin implements IMixinConfigPlugin { + private static final String TRINKETS_SLOT_STATE_MIXIN = "com.nonid.mixin.client.HandledScreenTrinketSlotStateMixin"; + + public void onLoad(String mixinPackage) { + } + + public String getRefMapperConfig() { + return null; + } + + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return "com.nonid.mixin.client.HandledScreenTrinketSlotStateMixin".equals(mixinClassName) ? FabricLoader.getInstance().isModLoaded("trinkets") : true; + } + + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + public List getMixins() { + return null; + } + + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } +} diff --git a/src/main/java/com/nonid/mixin/PlayerEntityDestroyedSkinMixin.java b/src/main/java/com/nonid/mixin/PlayerEntityDestroyedSkinMixin.java new file mode 100644 index 0000000..294a93c --- /dev/null +++ b/src/main/java/com/nonid/mixin/PlayerEntityDestroyedSkinMixin.java @@ -0,0 +1,102 @@ +package com.nonid.mixin; + +import com.nonid.DestroyedSkinHolder; +import com.nonid.MessHolder; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.data.DataTracker.Builder; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityDestroyedSkinMixin implements DestroyedSkinHolder, MessHolder { + @Unique + private static final TrackedData NON_DESTROYED_SKIN_STAGE = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + @Unique + private static final TrackedData NON_DESTROYED_SKIN_DAMAGE = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + @Unique + private static final TrackedData NON_MESS_V = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + @Unique + private static final TrackedData NON_MESS_A = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + @Unique + private static final TrackedData NON_MESS_M = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + + @Inject(method = "initDataTracker", at = @At("TAIL")) + private void non$initDestroyedSkinTracker(Builder builder, CallbackInfo ci) { + builder.add(NON_DESTROYED_SKIN_STAGE, 0); + builder.add(NON_DESTROYED_SKIN_DAMAGE, 0); + builder.add(NON_MESS_V, 0); + builder.add(NON_MESS_A, 0); + builder.add(NON_MESS_M, 0); + } + + @Override + public int getDestroyedSkinStage() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(NON_DESTROYED_SKIN_STAGE); + } + + @Override + public void setDestroyedSkinStage(int stage) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(NON_DESTROYED_SKIN_STAGE, Math.max(0, Math.min(4, stage))); + } + + @Override + public int getDestroyedSkinDamage() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(NON_DESTROYED_SKIN_DAMAGE); + } + + @Override + public void setDestroyedSkinDamage(int damage) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(NON_DESTROYED_SKIN_DAMAGE, Math.max(0, Math.min(10, damage))); + } + + @Override + public int getVMess() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(NON_MESS_V); + } + + @Override + public void setVMess(int value) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(NON_MESS_V, clampMess(value)); + } + + @Override + public int getAMess() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(NON_MESS_A); + } + + @Override + public void setAMess(int value) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(NON_MESS_A, clampMess(value)); + } + + @Override + public int getMMess() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(NON_MESS_M); + } + + @Override + public void setMMess(int value) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(NON_MESS_M, clampMess(value)); + } + + @Unique + private static int clampMess(int value) { + return Math.max(0, Math.min(10, value)); + } +} diff --git a/src/main/java/com/nonid/mixin/PlayerEntityEnergyMixin.java b/src/main/java/com/nonid/mixin/PlayerEntityEnergyMixin.java new file mode 100644 index 0000000..11614d0 --- /dev/null +++ b/src/main/java/com/nonid/mixin/PlayerEntityEnergyMixin.java @@ -0,0 +1,181 @@ +package com.nonid.mixin; + +import com.nonid.EnergyHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonTrinketsIntegration; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.data.DataTracker.Builder; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityEnergyMixin implements EnergyHolder { + @Unique + private static final TrackedData ENERGY = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + @Unique + private static final int DEFAULT_TICKS_PER_ENERGY = 120; + @Unique + private static final int WATER_ENERGY_DECAY_TICKS = 10; + @Unique + private int non$energyTickCounter; + @Unique + private int non$waterEnergyDecayTickCounter; + @Unique + private boolean non$energyInitialized; + @Unique + private float non$energyGainMult = 1.0F; + @Unique + private float non$energyGainDrift = 1.0F; + + @Inject(method = "initDataTracker", at = @At("TAIL")) + private void needsOfNature$initEnergyTracker(Builder builder, CallbackInfo ci) { + builder.add(ENERGY, 0); + } + + @Inject(method = "tick", at = @At("TAIL")) + private void needsOfNature$tickEnergy(CallbackInfo ci) { + PlayerEntity self = (PlayerEntity)(Object)this; + if (!self.getEntityWorld().isClient()) { + if (self.getEntityWorld() instanceof ServerWorld serverWorld) { + this.ensureEnergyInitialized(serverWorld); + if (self instanceof ServerPlayerEntity serverPlayer) { + NonTrinketsIntegration.syncAccessoryStateIfChanged(serverPlayer); + } + + int energy = this.getEnergy(); + energy = this.non$applyWaterEnergyDecay(self, energy); + int ticksPerGain = this.non$getTicksPerGain(); + if (energy >= 200) { + this.non$energyTickCounter = 0; + if (self instanceof ServerPlayerEntity serverPlayer) { + NeedsOfNature.updateEnergizedEffect(serverPlayer, this); + NeedsOfNature.tickPlayerEnergyAura(serverWorld, serverPlayer); + } + } else { + this.non$energyTickCounter++; + if (this.non$energyTickCounter >= ticksPerGain) { + this.non$energyTickCounter = 0; + this.setEnergy(Math.min(200, energy + 1)); + } + + if (self instanceof ServerPlayerEntity serverPlayer) { + NeedsOfNature.updateEnergizedEffect(serverPlayer, this); + NeedsOfNature.tickPlayerEnergyAura(serverWorld, serverPlayer); + } + } + } + } + } + + @Unique + private int non$applyWaterEnergyDecay(PlayerEntity self, int energy) { + if (self.isTouchingWater() && energy > 0) { + this.non$waterEnergyDecayTickCounter++; + if (this.non$waterEnergyDecayTickCounter < 10) { + return energy; + } + + this.non$waterEnergyDecayTickCounter = 0; + int next = Math.max(0, energy - 1); + this.setEnergy(next); + return next; + } else { + this.non$waterEnergyDecayTickCounter = 0; + return energy; + } + } + + @Override + public int getEnergy() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(ENERGY); + } + + @Override + public int getMaxEnergy() { + return 200; + } + + @Override + public void setEnergy(int value) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(ENERGY, Math.min(200, Math.max(0, value))); + } + + @Override + public float getEnergyGainMultiplier() { + return this.non$energyGainMult <= 0.0F ? 1.0F : this.non$energyGainMult; + } + + @Override + public void setEnergyGainMultiplier(float value) { + if (Float.isFinite(value)) { + this.non$energyGainMult = Math.max(0.1F, Math.min(10.0F, value)); + } + } + + @Override + public float getEnergyGainDrift() { + return this.non$energyGainDrift <= 0.0F ? 1.0F : this.non$energyGainDrift; + } + + @Override + public void setEnergyGainDrift(float value) { + if (Float.isFinite(value)) { + this.non$energyGainDrift = Math.max(0.1F, Math.min(10.0F, value)); + } + } + + @Override + public boolean isEnergyInitialized() { + return this.non$energyInitialized; + } + + @Override + public void markEnergyInitialized() { + this.non$energyInitialized = true; + } + + @Override + public void ensureEnergyInitialized(ServerWorld world) { + if (!this.non$energyInitialized) { + this.non$energyInitialized = true; + if (this.non$energyGainMult <= 0.0F) { + this.non$energyGainMult = 1.0F; + } + + if (this.non$energyGainDrift <= 0.0F) { + this.non$energyGainDrift = 1.0F; + } + + if (this.getEnergy() < 0) { + this.setEnergy(0); + } + } + } + + @Unique + private int non$getTicksPerGain() { + double rate = NeedsOfNature.getConfig().getEnergyGainRate(); + float scale = this.getEnergyGainMultiplier() * this.getEnergyGainDrift() * (float)rate; + PlayerEntity self = (PlayerEntity)(Object)this; + if (self instanceof ServerPlayerEntity serverPlayer) { + scale = (float)(scale * NonTrinketsIntegration.getAccessoryEffects(serverPlayer).playerEnergyGainMultiplier()); + } + + if (Float.isFinite(scale) && !(scale <= 0.0F)) { + int effective = Math.round(120.0F / scale); + return Math.max(1, effective); + } else { + return 120; + } + } +} diff --git a/src/main/java/com/nonid/mixin/PlayerEntityGenderMixin.java b/src/main/java/com/nonid/mixin/PlayerEntityGenderMixin.java new file mode 100644 index 0000000..327ca58 --- /dev/null +++ b/src/main/java/com/nonid/mixin/PlayerEntityGenderMixin.java @@ -0,0 +1,36 @@ +package com.nonid.mixin; + +import com.nonid.GenderHolder; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.data.DataTracker.Builder; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityGenderMixin implements GenderHolder { + @Unique + private static final TrackedData NON_GENDER = DataTracker.registerData(PlayerEntity.class, TrackedDataHandlerRegistry.INTEGER); + + @Inject(method = "initDataTracker", at = @At("TAIL")) + private void non$initGenderTracker(Builder builder, CallbackInfo ci) { + builder.add(NON_GENDER, 0); + } + + @Override + public int getGenderMask() { + PlayerEntity self = (PlayerEntity)(Object)this; + return (Integer)self.getDataTracker().get(NON_GENDER); + } + + @Override + public void setGenderMask(int mask) { + PlayerEntity self = (PlayerEntity)(Object)this; + self.getDataTracker().set(NON_GENDER, mask & 3); + } +} diff --git a/src/main/java/com/nonid/mixin/PlayerLiquidMixin.java b/src/main/java/com/nonid/mixin/PlayerLiquidMixin.java new file mode 100644 index 0000000..0855ec4 --- /dev/null +++ b/src/main/java/com/nonid/mixin/PlayerLiquidMixin.java @@ -0,0 +1,177 @@ +package com.nonid.mixin; + +import com.nonid.LiquidDonorHistory; +import com.nonid.LiquidHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import com.nonid.NonTrinketsIntegration; +import java.util.List; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.storage.ReadView; +import net.minecraft.storage.WriteView; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class PlayerLiquidMixin implements LiquidHolder { + @Unique + private int nonLiquidStored = 0; + @Unique + private boolean nonLiquidMixed = false; + @Unique + private Identifier nonLiquidEntityType = null; + @Unique + private NeedsOfNature.PregnancyVariantData nonLiquidDonorVariantData = null; + @Unique + private List nonRecentLiquidDonors = List.of(); + + @Override + public int getLiquidStored() { + return this.nonLiquidStored; + } + + @Override + public int getLiquidCapacity() { + NonConfig config = NeedsOfNature.getConfig(); + if (config != null && !config.isLiquidTankEnabled()) { + return 0; + } + + int baseCapacity = config != null ? config.getLiquidTankCapacityMl() : 200; + if ((Object)this instanceof PlayerEntity player) { + baseCapacity += NonTrinketsIntegration.getActiveTankAccessoryEffects(player).liquidCapacityAdd(); + } + + return Math.max(0, baseCapacity); + } + + @Override + public void setLiquidStored(int value) { + int cap = this.getLiquidCapacity(); + if (cap <= 0) { + this.nonLiquidStored = 0; + this.clearLiquidComposition(); + this.clearRecentLiquidDonors(); + } else { + this.nonLiquidStored = Math.max(0, Math.min(cap, value)); + if (this.nonLiquidStored == 0) { + this.clearLiquidComposition(); + this.clearRecentLiquidDonors(); + } + } + } + + @Override + public LiquidHolder.LiquidComposition getLiquidComposition() { + if (this.nonLiquidStored == 0) { + return LiquidHolder.LiquidComposition.EMPTY; + } else if (this.nonLiquidMixed) { + return LiquidHolder.LiquidComposition.MIXED; + } else { + return this.nonLiquidEntityType != null ? LiquidHolder.LiquidComposition.ENTITY : LiquidHolder.LiquidComposition.MIXED; + } + } + + @Override + public Identifier getLiquidEntityTypeId() { + return this.nonLiquidEntityType; + } + + @Override + public NeedsOfNature.PregnancyVariantData getLiquidDonorVariantData() { + return this.nonLiquidDonorVariantData; + } + + @Override + public void setLiquidCompositionMixed() { + this.nonLiquidMixed = true; + this.nonLiquidEntityType = null; + this.nonLiquidDonorVariantData = null; + } + + @Override + public void setLiquidCompositionEntity(Identifier entityTypeId) { + if (entityTypeId == null) { + this.setLiquidCompositionMixed(); + } else { + this.nonLiquidMixed = false; + this.nonLiquidEntityType = entityTypeId; + } + } + + @Override + public void setLiquidDonorVariantData(NeedsOfNature.PregnancyVariantData variantData) { + this.nonLiquidDonorVariantData = NeedsOfNature.normalizePregnancyVariantData(variantData); + } + + @Override + public List getRecentLiquidDonors() { + return this.nonRecentLiquidDonors; + } + + @Override + public void setRecentLiquidDonors(List donors) { + this.nonRecentLiquidDonors = LiquidDonorHistory.normalize(donors); + } + + @Override + public void clearLiquidComposition() { + this.nonLiquidMixed = false; + this.nonLiquidEntityType = null; + this.nonLiquidDonorVariantData = null; + } + + @Inject(method = "writeData", at = @At("TAIL")) + private void needsOfNature$writeLiquid(WriteView storage, CallbackInfo ci) { + if ((Object)this instanceof PlayerEntity) { + if (this.nonLiquidStored > 0) { + storage.putInt("NeedsOfNatureLiquid", this.nonLiquidStored); + if (this.nonLiquidMixed) { + storage.putString("NeedsOfNatureLiquidType", "mixed"); + } else if (this.nonLiquidEntityType != null) { + storage.putString("NeedsOfNatureLiquidType", this.nonLiquidEntityType.toString()); + String variantJson = NeedsOfNature.encodePregnancyVariantData(this.nonLiquidDonorVariantData); + if (variantJson != null) { + storage.putString("NeedsOfNatureLiquidDonorVariantData", variantJson); + } + } + + String donorHistoryJson = LiquidDonorHistory.encode(this.nonRecentLiquidDonors); + if (donorHistoryJson != null) { + storage.putString("NeedsOfNatureLiquidDonorHistory", donorHistoryJson); + } + } + } + } + + @Inject(method = "readData", at = @At("TAIL")) + private void needsOfNature$readLiquid(ReadView storage, CallbackInfo ci) { + if ((Object)this instanceof PlayerEntity) { + int stored = storage.getOptionalInt("NeedsOfNatureLiquid").orElse(0); + this.setLiquidStored(stored); + if (this.nonLiquidStored > 0) { + String rawType = storage.getOptionalString("NeedsOfNatureLiquidType").orElse(""); + if (!rawType.isBlank() && !"mixed".equalsIgnoreCase(rawType)) { + Identifier entityTypeId = Identifier.tryParse(rawType); + if (entityTypeId != null) { + this.setLiquidCompositionEntity(entityTypeId); + this.setLiquidDonorVariantData( + NeedsOfNature.decodePregnancyVariantData(storage.getOptionalString("NeedsOfNatureLiquidDonorVariantData").orElse("")) + ); + } else { + this.setLiquidCompositionMixed(); + } + } else { + this.setLiquidCompositionMixed(); + } + + this.setRecentLiquidDonors(LiquidDonorHistory.decode(storage.getOptionalString("NeedsOfNatureLiquidDonorHistory").orElse(""))); + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/PlayerLiquidMovementMixin.java b/src/main/java/com/nonid/mixin/PlayerLiquidMovementMixin.java new file mode 100644 index 0000000..d7c18a1 --- /dev/null +++ b/src/main/java/com/nonid/mixin/PlayerLiquidMovementMixin.java @@ -0,0 +1,70 @@ +package com.nonid.mixin; + +import com.nonid.LiquidHolder; +import com.nonid.NeedsOfNature; +import com.nonid.NonTrinketsIntegration; +import com.nonid.effect.NonStatusEffects; +import net.minecraft.entity.attribute.EntityAttributeInstance; +import net.minecraft.entity.attribute.EntityAttributeModifier; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.attribute.EntityAttributeModifier.Operation; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntity.class) +public abstract class PlayerLiquidMovementMixin { + @Unique + private static final Identifier LIQUID_SPEED_MOD = NeedsOfNature.id("liquid_speed"); + @Unique + private static final Identifier PREGNANCY_SPEED_MOD = NeedsOfNature.id("pregnancy_speed"); + + @Inject(method = "tickMovement", at = @At("TAIL")) + private void needsOfNature$applyLiquidMovementSlowdown(CallbackInfo ci) { + PlayerEntity player = (PlayerEntity)(Object)this; + if (player instanceof LiquidHolder) { + EntityAttributeInstance attribute = player.getAttributeInstance(EntityAttributes.MOVEMENT_SPEED); + if (attribute != null) { + StatusEffectInstance effect = player.getStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FILLED)); + if (effect == null) { + attribute.removeModifier(LIQUID_SPEED_MOD); + } else { + int amplifier = effect.getAmplifier(); + float multiplier = amplifier >= 2 + ? NeedsOfNature.getConfig().getFilledStageThreeSpeedMult() + : (amplifier >= 1 ? NeedsOfNature.getConfig().getFilledStageTwoSpeedMult() : NeedsOfNature.getConfig().getFilledStageOneSpeedMult()); + multiplier = non$applyFilledAccessoryMultiplier(player, multiplier); + double value = multiplier - 1.0; + attribute.updateModifier(new EntityAttributeModifier(LIQUID_SPEED_MOD, value, Operation.ADD_MULTIPLIED_TOTAL)); + } + + if (!player.getEntityWorld().isClient()) { + StatusEffectInstance pregnancy = player.getStatusEffect(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.PREGNANT)); + if (pregnancy == null) { + attribute.removeModifier(PREGNANCY_SPEED_MOD); + } else { + float pregnancyMultiplier = NeedsOfNature.getPregnancyMovementMultiplier(pregnancy.getDuration()); + attribute.updateModifier(new EntityAttributeModifier(PREGNANCY_SPEED_MOD, pregnancyMultiplier - 1.0, Operation.ADD_MULTIPLIED_TOTAL)); + } + } + } + } + } + + @Unique + private static float non$applyFilledAccessoryMultiplier(PlayerEntity player, float multiplier) { + double effectMultiplier = NonTrinketsIntegration.getActiveTankAccessoryEffects(player).filledEffectMultiplier(); + if (!Double.isFinite(effectMultiplier)) { + return multiplier; + } + + double penalty = 1.0 - multiplier; + return (float)Math.max(0.0, Math.min(2.0, 1.0 - penalty * effectMultiplier)); + } +} diff --git a/src/main/java/com/nonid/mixin/PlayerPregnancyMixin.java b/src/main/java/com/nonid/mixin/PlayerPregnancyMixin.java new file mode 100644 index 0000000..043b0a6 --- /dev/null +++ b/src/main/java/com/nonid/mixin/PlayerPregnancyMixin.java @@ -0,0 +1,188 @@ +package com.nonid.mixin; + +import com.mojang.serialization.Codec; +import com.nonid.NeedsOfNature; +import com.nonid.PregnancyCycleHolder; +import com.nonid.PregnancyHolder; +import java.util.Optional; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.storage.ReadView; +import net.minecraft.storage.WriteView; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class PlayerPregnancyMixin implements PregnancyHolder, PregnancyCycleHolder { + @Unique + @Nullable + private Identifier nonPregnancyEntityType; + @Unique + private long nonPregnancyEndTick; + @Unique + private boolean nonPregnancyPendingHatch; + @Unique + @Nullable + private NeedsOfNature.PregnancyVariantData nonPregnancyVariantData; + @Unique + private int nonPregnancyOffspringCount; + @Unique + private boolean nonPregnancyCycleInitialized; + @Unique + private long nonPregnancyCycleProgressTicks; + @Unique + private long nonPregnancyCycleLastGameTick; + @Unique + private long nonPregnancyCycleLastTimeOfDay; + + @Nullable + @Override + public Identifier getPregnantEntityTypeId() { + return this.nonPregnancyEntityType; + } + + @Override + public long getPregnancyEndTick() { + return this.nonPregnancyEndTick; + } + + @Override + public boolean isPregnancyPendingHatch() { + return this.nonPregnancyPendingHatch; + } + + @Nullable + @Override + public NeedsOfNature.PregnancyVariantData getPregnancyVariantData() { + return this.nonPregnancyVariantData; + } + + @Override + public int getPregnancyOffspringCount() { + return this.nonPregnancyOffspringCount; + } + + @Override + public void setPregnancyOffspringCount(int offspringCount) { + this.nonPregnancyOffspringCount = Math.max(0, Math.min(16, offspringCount)); + } + + @Override + public void setPregnancyState( + @Nullable Identifier entityTypeId, long endTick, boolean pendingHatch, @Nullable NeedsOfNature.PregnancyVariantData variantData + ) { + if (entityTypeId == null) { + this.clearPregnancyState(); + } else { + this.nonPregnancyEntityType = entityTypeId; + this.nonPregnancyEndTick = Math.max(0L, endTick); + this.nonPregnancyPendingHatch = pendingHatch; + this.nonPregnancyVariantData = NeedsOfNature.normalizePregnancyVariantData(variantData); + } + } + + @Override + public void clearPregnancyState() { + this.nonPregnancyEntityType = null; + this.nonPregnancyEndTick = 0L; + this.nonPregnancyPendingHatch = false; + this.nonPregnancyVariantData = null; + this.nonPregnancyOffspringCount = 0; + } + + @Override + public boolean isPregnancyCycleInitialized() { + return this.nonPregnancyCycleInitialized; + } + + @Override + public long getPregnancyCycleProgressTicks() { + return this.nonPregnancyCycleProgressTicks; + } + + @Override + public long getPregnancyCycleLastGameTick() { + return this.nonPregnancyCycleLastGameTick; + } + + @Override + public long getPregnancyCycleLastTimeOfDay() { + return this.nonPregnancyCycleLastTimeOfDay; + } + + @Override + public void setPregnancyCycleState(long progressTicks, long lastGameTick, long lastTimeOfDay) { + this.nonPregnancyCycleInitialized = true; + this.nonPregnancyCycleProgressTicks = Math.max(0L, progressTicks); + this.nonPregnancyCycleLastGameTick = Math.max(0L, lastGameTick); + this.nonPregnancyCycleLastTimeOfDay = Math.max(0L, lastTimeOfDay); + } + + @Override + public void clearPregnancyCycleState() { + this.nonPregnancyCycleInitialized = false; + this.nonPregnancyCycleProgressTicks = 0L; + this.nonPregnancyCycleLastGameTick = 0L; + this.nonPregnancyCycleLastTimeOfDay = 0L; + } + + @Inject(method = "writeData", at = @At("TAIL")) + private void non$writePregnancy(WriteView storage, CallbackInfo ci) { + if ((Object)this instanceof PlayerEntity) { + if (this.nonPregnancyCycleInitialized) { + storage.put("NeedsOfNaturePregnancyCycleProgress", Codec.LONG, Math.max(0L, this.nonPregnancyCycleProgressTicks)); + storage.put("NeedsOfNaturePregnancyCycleLastGameTick", Codec.LONG, Math.max(0L, this.nonPregnancyCycleLastGameTick)); + storage.put("NeedsOfNaturePregnancyCycleLastTimeOfDay", Codec.LONG, Math.max(0L, this.nonPregnancyCycleLastTimeOfDay)); + } + + if (this.nonPregnancyEntityType != null) { + storage.putString("NeedsOfNaturePregnancyType", this.nonPregnancyEntityType.toString()); + storage.put("NeedsOfNaturePregnancyEndTick", Codec.LONG, Math.max(0L, this.nonPregnancyEndTick)); + storage.put("NeedsOfNaturePregnancyPending", Codec.BOOL, this.nonPregnancyPendingHatch); + if (this.nonPregnancyOffspringCount > 0) { + storage.putInt("NeedsOfNaturePregnancyOffspringCount", this.nonPregnancyOffspringCount); + } + + String variantJson = NeedsOfNature.encodePregnancyVariantData(this.nonPregnancyVariantData); + if (variantJson != null) { + storage.putString("NeedsOfNaturePregnancyVariantData", variantJson); + } + } + } + } + + @Inject(method = "readData", at = @At("TAIL")) + private void non$readPregnancy(ReadView storage, CallbackInfo ci) { + if ((Object)this instanceof PlayerEntity) { + Optional cycleProgress = storage.read("NeedsOfNaturePregnancyCycleProgress", Codec.LONG); + if (cycleProgress.isPresent()) { + this.setPregnancyCycleState( + cycleProgress.get(), + storage.read("NeedsOfNaturePregnancyCycleLastGameTick", Codec.LONG).orElse(0L), + storage.read("NeedsOfNaturePregnancyCycleLastTimeOfDay", Codec.LONG).orElse(0L) + ); + } else { + this.clearPregnancyCycleState(); + } + + String rawType = storage.getOptionalString("NeedsOfNaturePregnancyType").orElse(""); + Identifier entityTypeId = rawType.isBlank() ? null : Identifier.tryParse(rawType); + if (entityTypeId == null) { + this.clearPregnancyState(); + } else { + long endTick = Math.max(0L, storage.read("NeedsOfNaturePregnancyEndTick", Codec.LONG).orElse(0L)); + boolean pending = storage.read("NeedsOfNaturePregnancyPending", Codec.BOOL).orElse(false); + NeedsOfNature.PregnancyVariantData variantData = NeedsOfNature.decodePregnancyVariantData( + storage.getOptionalString("NeedsOfNaturePregnancyVariantData").orElse("") + ); + this.setPregnancyState(entityTypeId, endTick, pending, variantData); + this.setPregnancyOffspringCount(storage.getOptionalInt("NeedsOfNaturePregnancyOffspringCount").orElse(0)); + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/ProjectileEntityAnimationRescueMixin.java b/src/main/java/com/nonid/mixin/ProjectileEntityAnimationRescueMixin.java new file mode 100644 index 0000000..803aa8a --- /dev/null +++ b/src/main/java/com/nonid/mixin/ProjectileEntityAnimationRescueMixin.java @@ -0,0 +1,26 @@ +package com.nonid.mixin; + +import com.nonid.NonAnimationRescueSystem; +import net.minecraft.entity.Entity; +import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.hit.HitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(ProjectileEntity.class) +public abstract class ProjectileEntityAnimationRescueMixin { + @ModifyVariable(method = "hitOrDeflect", at = @At("HEAD"), argsOnly = true) + private HitResult non$redirectAnimatedPlayerHit(HitResult hitResult) { + ProjectileEntity projectile = (ProjectileEntity)(Object)this; + if (projectile.getEntityWorld() instanceof ServerWorld world && hitResult instanceof EntityHitResult entityHit) { + Entity var7 = entityHit.getEntity(); + Entity redirectedTarget = NonAnimationRescueSystem.redirectProjectileTarget(world, projectile, var7); + return (HitResult)(redirectedTarget == var7 ? hitResult : new EntityHitResult(redirectedTarget, entityHit.getPos())); + } else { + return hitResult; + } + } +} diff --git a/src/main/java/com/nonid/mixin/ResourcePackManagerNoNPackMixin.java b/src/main/java/com/nonid/mixin/ResourcePackManagerNoNPackMixin.java new file mode 100644 index 0000000..9b527d7 --- /dev/null +++ b/src/main/java/com/nonid/mixin/ResourcePackManagerNoNPackMixin.java @@ -0,0 +1,135 @@ +package com.nonid.mixin; + +import com.google.common.collect.ImmutableSet; +import com.nonid.pack.NonExternalPackProvider; +import com.nonid.pack.NonGeneratedAccessoryModelPackProvider; +import com.nonid.pack.NonGeneratedAccessoryTagPackProvider; +import com.nonid.pack.NonGeneratedCemPackProvider; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Set; +import net.minecraft.resource.FileResourcePackProvider; +import net.minecraft.resource.ResourcePackManager; +import net.minecraft.resource.ResourcePackProvider; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.path.SymlinkFinder; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ResourcePackManager.class) +public abstract class ResourcePackManagerNoNPackMixin { + @Shadow + @Final + @Mutable + private Set providers; + + @Inject(method = "", at = @At("RETURN")) + private void non$injectExternalNoNPacks(ResourcePackProvider[] originalProviders, CallbackInfo ci) { + boolean needsClientProvider = false; + boolean needsServerProvider = false; + SymlinkFinder clientSymlinkFinder = null; + SymlinkFinder serverSymlinkFinder = null; + + for (ResourcePackProvider provider : this.providers) { + if (provider instanceof FileResourcePackProvider fileProvider) { + FileResourcePackProviderAccessor accessor = (FileResourcePackProviderAccessor)fileProvider; + ResourceType type = accessor.non$getType(); + if (type == ResourceType.CLIENT_RESOURCES) { + needsClientProvider = true; + if (clientSymlinkFinder == null) { + clientSymlinkFinder = accessor.non$getSymlinkFinder(); + } + } else if (type == ResourceType.SERVER_DATA) { + needsServerProvider = true; + if (serverSymlinkFinder == null) { + serverSymlinkFinder = accessor.non$getSymlinkFinder(); + } + } + } + } + + if (needsClientProvider || needsServerProvider) { + Path nonPackDir = NonExternalPackProvider.resolveDefaultPacksDir(); + LinkedHashSet updated = new LinkedHashSet<>(this.providers); + if (needsClientProvider && !containsNoNProvider(updated, ResourceType.CLIENT_RESOURCES)) { + updated.add( + new NonExternalPackProvider( + nonPackDir, ResourceType.CLIENT_RESOURCES, clientSymlinkFinder != null ? clientSymlinkFinder : new SymlinkFinder(path -> true) + ) + ); + } + + if (needsClientProvider && !containsGeneratedCemProvider(updated)) { + updated.add(new NonGeneratedCemPackProvider(nonPackDir)); + } + + if (needsClientProvider && !containsGeneratedAccessoryModelProvider(updated)) { + updated.add(new NonGeneratedAccessoryModelPackProvider()); + } + + if (needsServerProvider && !containsNoNProvider(updated, ResourceType.SERVER_DATA)) { + updated.add( + new NonExternalPackProvider( + nonPackDir, ResourceType.SERVER_DATA, serverSymlinkFinder != null ? serverSymlinkFinder : new SymlinkFinder(path -> true) + ) + ); + } + + if (needsServerProvider && !containsGeneratedAccessoryTagProvider(updated)) { + updated.add(new NonGeneratedAccessoryTagPackProvider()); + } + + this.providers = ImmutableSet.copyOf(updated); + } + } + + @Unique + private static boolean containsNoNProvider(Set providers, ResourceType type) { + for (ResourcePackProvider provider : providers) { + if (provider instanceof NonExternalPackProvider nonProvider && nonProvider.getType() == type) { + return true; + } + } + + return false; + } + + @Unique + private static boolean containsGeneratedCemProvider(Set providers) { + for (ResourcePackProvider provider : providers) { + if (provider instanceof NonGeneratedCemPackProvider) { + return true; + } + } + + return false; + } + + @Unique + private static boolean containsGeneratedAccessoryTagProvider(Set providers) { + for (ResourcePackProvider provider : providers) { + if (provider instanceof NonGeneratedAccessoryTagPackProvider) { + return true; + } + } + + return false; + } + + @Unique + private static boolean containsGeneratedAccessoryModelProvider(Set providers) { + for (ResourcePackProvider provider : providers) { + if (provider instanceof NonGeneratedAccessoryModelPackProvider) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/nonid/mixin/ServerPlayerEntityDamageMixin.java b/src/main/java/com/nonid/mixin/ServerPlayerEntityDamageMixin.java new file mode 100644 index 0000000..2b38209 --- /dev/null +++ b/src/main/java/com/nonid/mixin/ServerPlayerEntityDamageMixin.java @@ -0,0 +1,19 @@ +package com.nonid.mixin; + +import com.nonid.NeedsOfNature; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ServerPlayerEntity.class) +public abstract class ServerPlayerEntityDamageMixin { + @Redirect( + method = "damage", + at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayerEntity;shouldDamagePlayer(Lnet/minecraft/entity/player/PlayerEntity;)Z") + ) + private boolean non$allowDefeatedFinishDamage(ServerPlayerEntity victim, PlayerEntity attacker) { + return NeedsOfNature.shouldBypassVanillaPvpForDefeatedFinish(victim, attacker) ? true : victim.shouldDamagePlayer(attacker); + } +} diff --git a/src/main/java/com/nonid/mixin/client/EntityRendererDebugNameMixin.java b/src/main/java/com/nonid/mixin/client/EntityRendererDebugNameMixin.java new file mode 100644 index 0000000..afc059b --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/EntityRendererDebugNameMixin.java @@ -0,0 +1,51 @@ +package com.nonid.mixin.client; + +import com.nonid.EnergyHolder; +import com.nonid.client.NonDebugNameHelper; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityAttachmentType; +import net.minecraft.text.Text; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(EntityRenderer.class) +public abstract class EntityRendererDebugNameMixin { + @Inject(method = "hasLabel", at = @At("HEAD"), cancellable = true) + private void needsOfNature$showDebugLabel(Entity entity, double distance, CallbackInfoReturnable cir) { + if (NonDebugNameHelper.shouldShowDebugLabel(entity)) { + cir.setReturnValue(true); + } + } + + @Inject(method = "getDisplayName", at = @At("HEAD"), cancellable = true) + private void needsOfNature$debugDisplayName(Entity entity, CallbackInfoReturnable cir) { + if (NonDebugNameHelper.shouldShowDebugLabel(entity)) { + if (entity instanceof EnergyHolder holder) { + String energy = holder.getEnergy() + "/" + holder.getMaxEnergy(); + cir.setReturnValue(Text.literal("T:" + energy + " " + NonDebugNameHelper.resolveGenderSymbol(entity))); + } + } + } + + @Inject(method = "updateRenderState", at = @At("TAIL")) + private void needsOfNature$debugLabelState(Entity entity, EntityRenderState state, float tickDelta, CallbackInfo ci) { + if (NonDebugNameHelper.shouldShowDebugLabel(entity)) { + if (entity instanceof EnergyHolder holder) { + String energy = holder.getEnergy() + "/" + holder.getMaxEnergy(); + state.displayName = Text.literal("T:" + energy + " " + NonDebugNameHelper.resolveGenderSymbol(entity)); + Vec3d labelPos = entity.getAttachments().getPointNullable(EntityAttachmentType.NAME_TAG, 0, entity.getLerpedYaw(tickDelta)); + if (labelPos == null) { + labelPos = new Vec3d(entity.getX(), entity.getY() + entity.getHeight() + 0.5, entity.getZ()); + } + + state.nameLabelPos = labelPos; + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/GameRendererReactiveImpactFovMixin.java b/src/main/java/com/nonid/mixin/client/GameRendererReactiveImpactFovMixin.java new file mode 100644 index 0000000..dfd6db4 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/GameRendererReactiveImpactFovMixin.java @@ -0,0 +1,20 @@ +package com.nonid.mixin.client; + +import com.nonid.NeedsOfNatureClient; +import net.minecraft.client.render.GameRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(GameRenderer.class) +public class GameRendererReactiveImpactFovMixin { + @ModifyArg( + method = "renderWorld", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getBasicProjectionMatrix(F)Lorg/joml/Matrix4f;", ordinal = 0), + index = 0 + ) + private float non$applyReactiveImpactWorldFov(float fov) { + float offset = NeedsOfNatureClient.getReactiveImpactFovOffset(0.0F); + return Math.abs(offset) <= 1.0E-4F ? fov : Math.max(5.0F, fov + offset); + } +} diff --git a/src/main/java/com/nonid/mixin/client/HandledScreenAccessor.java b/src/main/java/com/nonid/mixin/client/HandledScreenAccessor.java new file mode 100644 index 0000000..677fd0f --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/HandledScreenAccessor.java @@ -0,0 +1,14 @@ +package com.nonid.mixin.client; + +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(HandledScreen.class) +public interface HandledScreenAccessor { + @Accessor("x") + int non$getLeft(); + + @Accessor("y") + int non$getTop(); +} diff --git a/src/main/java/com/nonid/mixin/client/HandledScreenTrinketSlotStateMixin.java b/src/main/java/com/nonid/mixin/client/HandledScreenTrinketSlotStateMixin.java new file mode 100644 index 0000000..d866486 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/HandledScreenTrinketSlotStateMixin.java @@ -0,0 +1,60 @@ +package com.nonid.mixin.client; + +import com.nonid.NonTrinketsIntegration; +import dev.emi.trinkets.TrinketSlot; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(HandledScreen.class) +public abstract class HandledScreenTrinketSlotStateMixin { + @Shadow + protected Slot focusedSlot; + + @Inject(method = "drawSlot", at = @At("TAIL")) + private void non$drawUnavailableTrinketSlotOverlay(DrawContext context, Slot slot, int mouseX, int mouseY, CallbackInfo ci) { + if (non$unavailableSlotTooltip(slot) != null) { + context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, -1728053248); + context.drawStrokedRectangle(slot.x, slot.y, 16, 16, -1435011209); + } + } + + @Inject(method = "drawMouseoverTooltip", at = @At("RETURN")) + private void non$drawUnavailableTrinketSlotTooltip(DrawContext context, int x, int y, CallbackInfo ci) { + Text tooltip = non$unavailableSlotTooltip(this.focusedSlot); + if (tooltip != null) { + context.drawTooltip(tooltip, x, y); + } + } + + private static Text non$unavailableSlotTooltip(Slot slot) { + if (slot instanceof TrinketSlot trinketSlot) { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null && trinketSlot.getType() != null) { + String slotId = trinketSlot.getType().getId(); + if (NonTrinketsIntegration.isSlotAvailableFor(player, slotId)) { + return null; + } else if ("legs/v".equals(slotId)) { + return Text.translatable("gui.needsofnature.trinket_slot.requires_female"); + } else { + return "legs/d".equals(slotId) + ? Text.translatable("gui.needsofnature.trinket_slot.requires_male") + : Text.translatable("gui.needsofnature.trinket_slot.unavailable"); + } + } else { + return null; + } + } else { + return null; + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/InGameHudMixin.java b/src/main/java/com/nonid/mixin/client/InGameHudMixin.java new file mode 100644 index 0000000..e605f26 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/InGameHudMixin.java @@ -0,0 +1,181 @@ +package com.nonid.mixin.client; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.nonid.client.NonEnergizedEffectTextures; +import com.nonid.client.NonFilledEffectTextures; +import com.nonid.client.NonHudOverlay; +import com.nonid.client.NonPregnantEffectTextures; +import com.nonid.effect.NonStatusEffects; +import java.util.ArrayList; +import java.util.Collection; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(InGameHud.class) +public class InGameHudMixin { + @Inject(method = "render", at = @At("TAIL")) + private void non$renderLiquidOverlay(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.isUiPreviewEnabled()) { + NonHudOverlay.renderUiPreview(context); + } else if (NonHudOverlay.shouldHideVanillaHud()) { + NonHudOverlay.renderOverlay(context); + } else { + NonHudOverlay.renderLiquidOverlay(context); + } + } + + @Inject(method = "renderMiscOverlays", at = @At("HEAD"), cancellable = true) + private void non$hideMiscOverlaysDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderCrosshair", at = @At("HEAD"), cancellable = true) + private void non$hideCrosshairDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderMainHud", at = @At("HEAD"), cancellable = true) + private void non$hideMainHudDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderStatusEffectOverlay", at = @At("HEAD"), cancellable = true) + private void non$hideStatusEffectsDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderBossBarHud", at = @At("HEAD"), cancellable = true) + private void non$hideBossBarsDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderSleepOverlay", at = @At("HEAD"), cancellable = true) + private void non$hideSleepOverlayDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderDemoTimer", at = @At("HEAD"), cancellable = true) + private void non$hideDemoTimerDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject( + method = "renderScoreboardSidebar(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V", + at = @At("HEAD"), + cancellable = true + ) + private void non$hideScoreboardDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderOverlayMessage", at = @At("HEAD"), cancellable = true) + private void non$hideOverlayMessageDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderTitleAndSubtitle", at = @At("HEAD"), cancellable = true) + private void non$hideTitleDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderChat", at = @At("HEAD"), cancellable = true) + private void non$hideChatDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderPlayerList", at = @At("HEAD"), cancellable = true) + private void non$hidePlayerListDuringAnimation(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Inject(method = "renderSubtitlesHud", at = @At("HEAD"), cancellable = true) + private void non$hideSubtitlesDuringAnimation(DrawContext context, boolean defer, CallbackInfo ci) { + if (NonHudOverlay.shouldHideVanillaHud()) { + ci.cancel(); + } + } + + @Redirect( + method = "renderStatusEffectOverlay", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;getStatusEffects()Ljava/util/Collection;") + ) + private Collection non$hideEnergizedFromHudOverlay(ClientPlayerEntity player) { + Collection effects = player.getStatusEffects(); + if (effects.isEmpty()) { + return effects; + } + + ArrayList filtered = new ArrayList<>(effects.size()); + + for (StatusEffectInstance effect : effects) { + if (effect.getEffectType().value() != NonStatusEffects.ENERGIZED) { + filtered.add(effect); + } + } + + return filtered; + } + + @Redirect( + method = "renderStatusEffectOverlay", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/DrawContext;drawGuiTexture(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/util/Identifier;IIIII)V" + ) + ) + private void non$drawFilledStatusEffectIcon(DrawContext context, RenderPipeline pipeline, Identifier sprite, int x, int y, int width, int height, int color) { + if (NonFilledEffectTextures.isFilledSprite(sprite)) { + Identifier texture = NonFilledEffectTextures.resolveTexture(MinecraftClient.getInstance().player); + context.drawTexture(RenderPipelines.GUI_TEXTURED, texture, x, y, 0.0F, 0.0F, width, height, width, height, color); + } else if (NonEnergizedEffectTextures.isEnergizedSprite(sprite)) { + Identifier texture = NonEnergizedEffectTextures.resolveTexture(MinecraftClient.getInstance().player); + context.drawTexture(RenderPipelines.GUI_TEXTURED, texture, x, y, 0.0F, 0.0F, width, height, width, height, color); + } else { + if (NonPregnantEffectTextures.shouldDrawSourceEgg(sprite)) { + ItemStack eggStack = NonPregnantEffectTextures.resolveSourceEgg(MinecraftClient.getInstance().player); + if (!eggStack.isEmpty()) { + NonPregnantEffectTextures.drawSourceEgg(context, eggStack, x, y, width, height); + return; + } + } + + context.drawGuiTexture(pipeline, sprite, x, y, width, height, color); + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/LivingEntityRendererDebugLabelMixin.java b/src/main/java/com/nonid/mixin/client/LivingEntityRendererDebugLabelMixin.java new file mode 100644 index 0000000..8d4a7d2 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/LivingEntityRendererDebugLabelMixin.java @@ -0,0 +1,19 @@ +package com.nonid.mixin.client; + +import com.nonid.client.NonDebugNameHelper; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LivingEntityRenderer.class) +public abstract class LivingEntityRendererDebugLabelMixin { + @Inject(method = "hasLabel(Lnet/minecraft/entity/LivingEntity;D)Z", at = @At("HEAD"), cancellable = true) + private void needsOfNature$forceDebugLabel(LivingEntity entity, double distance, CallbackInfoReturnable cir) { + if (NonDebugNameHelper.shouldShowDebugLabel(entity)) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/LivingEntityRendererPlayerTextureMixin.java b/src/main/java/com/nonid/mixin/client/LivingEntityRendererPlayerTextureMixin.java new file mode 100644 index 0000000..b1b0136 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/LivingEntityRendererPlayerTextureMixin.java @@ -0,0 +1,34 @@ +package com.nonid.mixin.client; + +import com.nonid.client.NonDebugSpinMode; +import com.nonid.client.NonDestroyedSkinClient; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(LivingEntityRenderer.class) +public abstract class LivingEntityRendererPlayerTextureMixin { + @ModifyVariable(method = "getRenderLayer", at = @At("STORE"), ordinal = 0) + private Identifier non$applyPlayerSkinEffectsToFinalTexture( + Identifier texture, LivingEntityRenderState state, boolean showBody, boolean translucent, boolean showOutline + ) { + if (!NonDebugSpinMode.isEnabled() && state instanceof PlayerEntityRenderState playerState) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.world != null) { + return client.world.getEntityById(playerState.id) instanceof LivingEntity living + ? NonDestroyedSkinClient.resolveRenderedTexture(living, texture) + : texture; + } else { + return texture; + } + } else { + return texture; + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/PlayerEntityRendererDestroyedSkinMixin.java b/src/main/java/com/nonid/mixin/client/PlayerEntityRendererDestroyedSkinMixin.java new file mode 100644 index 0000000..ea92e7a --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/PlayerEntityRendererDestroyedSkinMixin.java @@ -0,0 +1,51 @@ +package com.nonid.mixin.client; + +import com.nonid.client.NonDebugSpinMode; +import com.nonid.client.NonDestroyedSkinClient; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.entity.PlayerEntityRenderer; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(PlayerEntityRenderer.class) +public abstract class PlayerEntityRendererDestroyedSkinMixin { + @ModifyArg( + method = "renderRightArm", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/render/entity/PlayerEntityRenderer;renderArm(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/command/OrderedRenderCommandQueue;ILnet/minecraft/util/Identifier;Lnet/minecraft/client/model/ModelPart;Z)V" + ), + index = 3 + ) + private Identifier non$applyDestroyedSkinToRightArm(Identifier skinTexture) { + return non$resolveLocalHandTexture(skinTexture); + } + + @ModifyArg( + method = "renderLeftArm", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/render/entity/PlayerEntityRenderer;renderArm(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/command/OrderedRenderCommandQueue;ILnet/minecraft/util/Identifier;Lnet/minecraft/client/model/ModelPart;Z)V" + ), + index = 3 + ) + private Identifier non$applyDestroyedSkinToLeftArm(Identifier skinTexture) { + return non$resolveLocalHandTexture(skinTexture); + } + + private static Identifier non$resolveLocalHandTexture(Identifier skinTexture) { + if (NonDebugSpinMode.isEnabled()) { + return skinTexture; + } else { + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.player != null) { + Identifier replacement = NonDestroyedSkinClient.resolveBaseTexture(client.player, skinTexture); + return replacement != null ? replacement : skinTexture; + } else { + return skinTexture; + } + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/ResourcePackOrganizerHideNoNExternalMixin.java b/src/main/java/com/nonid/mixin/client/ResourcePackOrganizerHideNoNExternalMixin.java new file mode 100644 index 0000000..d1e0549 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/ResourcePackOrganizerHideNoNExternalMixin.java @@ -0,0 +1,48 @@ +package com.nonid.mixin.client; + +import java.util.List; +import net.minecraft.client.gui.screen.pack.ResourcePackOrganizer; +import net.minecraft.resource.ResourcePackProfile; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ResourcePackOrganizer.class) +public abstract class ResourcePackOrganizerHideNoNExternalMixin { + @Shadow + @Final + private List enabledPacks; + @Shadow + @Final + private List disabledPacks; + + @Inject(method = "", at = @At("RETURN")) + private void non$hideExternalNoNPacksOnInit(CallbackInfo ci) { + this.non$filterExternalNoNPacks(); + } + + @Inject(method = "refresh", at = @At("RETURN")) + private void non$hideExternalNoNPacksOnRefresh(CallbackInfo ci) { + this.non$filterExternalNoNPacks(); + } + + @Unique + private void non$filterExternalNoNPacks() { + this.enabledPacks.removeIf(ResourcePackOrganizerHideNoNExternalMixin::non$isNoNExternalPackProfile); + this.disabledPacks.removeIf(ResourcePackOrganizerHideNoNExternalMixin::non$isNoNExternalPackProfile); + } + + @Unique + private static boolean non$isNoNExternalPackProfile(ResourcePackProfile profile) { + if (profile == null) { + return false; + } + + String id = profile.getId(); + return id != null && !id.isBlank() ? id.startsWith("needsofnature/client/") || id.startsWith("needsofnature/server/") : false; + } +} diff --git a/src/main/java/com/nonid/mixin/client/StatusEffectsDisplayMixin.java b/src/main/java/com/nonid/mixin/client/StatusEffectsDisplayMixin.java new file mode 100644 index 0000000..bdaec6c --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/StatusEffectsDisplayMixin.java @@ -0,0 +1,45 @@ +package com.nonid.mixin.client; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.nonid.client.NonEnergizedEffectTextures; +import com.nonid.client.NonFilledEffectTextures; +import com.nonid.client.NonPregnantEffectTextures; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.StatusEffectsDisplay; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(StatusEffectsDisplay.class) +public class StatusEffectsDisplayMixin { + @Redirect( + method = "drawStatusEffects", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/DrawContext;drawGuiTexture(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/util/Identifier;IIII)V" + ) + ) + private void non$drawFilledEffectSprite(DrawContext context, RenderPipeline pipeline, Identifier sprite, int x, int y, int width, int height) { + if (NonFilledEffectTextures.isFilledSprite(sprite)) { + Identifier texture = NonFilledEffectTextures.resolveTexture(MinecraftClient.getInstance().player); + context.drawTexture(RenderPipelines.GUI_TEXTURED, texture, x, y, 0.0F, 0.0F, width, height, width, height); + } else if (NonEnergizedEffectTextures.isEnergizedSprite(sprite)) { + Identifier texture = NonEnergizedEffectTextures.resolveTexture(MinecraftClient.getInstance().player); + context.drawTexture(RenderPipelines.GUI_TEXTURED, texture, x, y, 0.0F, 0.0F, width, height, width, height); + } else { + if (NonPregnantEffectTextures.shouldDrawSourceEgg(sprite)) { + ItemStack eggStack = NonPregnantEffectTextures.resolveSourceEgg(MinecraftClient.getInstance().player); + if (!eggStack.isEmpty()) { + NonPregnantEffectTextures.drawSourceEgg(context, eggStack, x, y, width, height); + return; + } + } + + context.drawGuiTexture(pipeline, sprite, x, y, width, height); + } + } +} diff --git a/src/main/java/com/nonid/mixin/client/TranslationStorageStatLabelMixin.java b/src/main/java/com/nonid/mixin/client/TranslationStorageStatLabelMixin.java new file mode 100644 index 0000000..2608174 --- /dev/null +++ b/src/main/java/com/nonid/mixin/client/TranslationStorageStatLabelMixin.java @@ -0,0 +1,25 @@ +package com.nonid.mixin.client; + +import com.nonid.client.NonStatLabelResolver; +import net.minecraft.client.resource.language.TranslationStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(TranslationStorage.class) +public abstract class TranslationStorageStatLabelMixin { + @Shadow + public abstract boolean hasTranslation(String var1); + + @Inject(method = "get", at = @At("HEAD"), cancellable = true) + private void non$resolveDynamicStatLabel(String key, String fallback, CallbackInfoReturnable cir) { + if (key != null && !this.hasTranslation(key)) { + String resolved = NonStatLabelResolver.resolve(key); + if (resolved != null) { + cir.setReturnValue(resolved); + } + } + } +} diff --git a/src/main/java/com/nonid/network/AccessoryShedStateS2CPayload.java b/src/main/java/com/nonid/network/AccessoryShedStateS2CPayload.java new file mode 100644 index 0000000..a895f1d --- /dev/null +++ b/src/main/java/com/nonid/network/AccessoryShedStateS2CPayload.java @@ -0,0 +1,32 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record AccessoryShedStateS2CPayload(UUID playerUuid, boolean shedV, boolean shedA, boolean forcedV, boolean forcedA) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "accessory_shed_state"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + AccessoryShedStateS2CPayload::playerUuid, + PacketCodecs.BOOLEAN, + AccessoryShedStateS2CPayload::shedV, + PacketCodecs.BOOLEAN, + AccessoryShedStateS2CPayload::shedA, + PacketCodecs.BOOLEAN, + AccessoryShedStateS2CPayload::forcedV, + PacketCodecs.BOOLEAN, + AccessoryShedStateS2CPayload::forcedA, + AccessoryShedStateS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/AnimationCatalogChunkS2CPayload.java b/src/main/java/com/nonid/network/AnimationCatalogChunkS2CPayload.java new file mode 100644 index 0000000..971ca8b --- /dev/null +++ b/src/main/java/com/nonid/network/AnimationCatalogChunkS2CPayload.java @@ -0,0 +1,27 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record AnimationCatalogChunkS2CPayload(long revision, int index, int count, String data) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "animation_catalog_chunk")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.LONG, + AnimationCatalogChunkS2CPayload::revision, + PacketCodecs.VAR_INT, + AnimationCatalogChunkS2CPayload::index, + PacketCodecs.VAR_INT, + AnimationCatalogChunkS2CPayload::count, + PacketCodecs.STRING, + AnimationCatalogChunkS2CPayload::data, + AnimationCatalogChunkS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/AnimationCatalogData.java b/src/main/java/com/nonid/network/AnimationCatalogData.java new file mode 100644 index 0000000..1f134aa --- /dev/null +++ b/src/main/java/com/nonid/network/AnimationCatalogData.java @@ -0,0 +1,256 @@ +package com.nonid.network; + +import com.nonid.NonConfig; +import com.nonid.data.NonLiquidRoles; +import com.nonid.data.NonPeakStages; +import com.nonid.internal.animation.data.AnimationDefinitions; +import com.nonid.internal.animation.data.AnimationGameplayMetadata; +import com.nonid.internal.animation.network.AnimationStageInfo; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public record AnimationCatalogData(List animations) { + public static AnimationCatalogData capture(NonConfig config) { + Set disabledAnimations = config == null ? Set.of() : Set.copyOf(config.getDisabledAnimations()); + Set disabledPacks = config == null ? Set.of() : Set.copyOf(config.getDisabledAnimationPacks()); + List entries = new ArrayList<>(); + + for (AnimationDefinitions.Definition definition : AnimationDefinitions.getLoadedDefinitionsSnapshot()) { + entries.add(fromDefinition(definition, disabledAnimations, disabledPacks)); + } + + entries.sort(Comparator.comparing(AnimationCatalogData.Entry::id)); + return new AnimationCatalogData(List.copyOf(entries)); + } + + private static AnimationCatalogData.Entry fromDefinition( + AnimationDefinitions.Definition definition, Set disabledAnimations, Set disabledPacks + ) { + List actorKeys = AnimationDefinitions.resolveActorKeys(definition); + NonLiquidRoles.LiquidRoles liquidRoles = AnimationGameplayMetadata.liquidRoles(definition.id()); + List actors = new ArrayList<>(definition.actors().size()); + + for (int i = 0; i < definition.actors().size(); i++) { + AnimationDefinitions.ActorConstraint constraint = definition.actors().get(i); + List entityTypes = constraint.entityTypes().stream().filter(Objects::nonNull).map(Identifier::toString).sorted().toList(); + List requiredTags = constraint.requiredTags().stream().sorted().toList(); + List requiredTagsAny = constraint.requiredTagsAny().stream().sorted().toList(); + String key = i < actorKeys.size() ? actorKeys.get(i) : constraint.derivedKey(); + NonLiquidRoles.InjectorRole injectorRole = liquidRoles == null + ? NonLiquidRoles.InjectorRole.NONE + : liquidRoles.injectorRoles().getOrDefault(key, NonLiquidRoles.InjectorRole.NONE); + boolean receiver = liquidRoles != null && liquidRoles.receiverKeys().contains(key); + actors.add(new AnimationCatalogData.Actor(key, entityTypes, requiredTags, requiredTagsAny, injectorRole.name(), receiver)); + } + + List stages = new ArrayList<>(); + + for (AnimationStageInfo stage : definition.stages()) { + double cycleTicks = AnimationGameplayMetadata.cycleTicks(stage.animationId()); + if (cycleTicks <= 0.0) { + cycleTicks = stage.lengthTicks(); + } + + stages.add( + new AnimationCatalogData.Stage( + stage.effectiveAnimationId().toString(), + stage.loop(), + stage.lengthTicks(), + stage.speed(), + cycleTicks, + NonPeakStages.stageNumberFromId(stage.animationId()) + ) + ); + } + + if (stages.isEmpty()) { + stages.add(new AnimationCatalogData.Stage(definition.id().toString(), false, 0L, 1.0, 0.0, null)); + } + + AnimationDefinitions.AnimationPackInfo packInfo = definition.packInfo(); + AnimationCatalogData.Pack pack = packInfo == null + ? new AnimationCatalogData.Pack("unknown", "unknown", "Unknown Pack", "Unknown", null, null, null, List.of()) + : new AnimationCatalogData.Pack( + packInfo.id(), + packInfo.resourcePackId(), + packInfo.name(), + packInfo.author(), + packInfo.version(), + packInfo.description(), + packInfo.localModelRoot() == null ? null : packInfo.localModelRoot().toString(), + packInfo.modelImports() + .stream() + .map( + modelImport -> new AnimationCatalogData.ModelImport( + modelImport.packId(), + modelImport.modelRoot().toString(), + modelImport.entityTypes().stream().map(Identifier::toString).sorted().toList() + ) + ) + .toList() + ); + String id = definition.id().toString(); + return new AnimationCatalogData.Entry( + id, + definition.displayName(), + List.copyOf(actors), + List.copyOf(stages), + NonPeakStages.getPeakStage(definition.id()), + definition.contentTags() == null ? List.of() : List.copyOf(definition.contentTags()), + definition.attackEligible(), + summarizeBlockRequirements(definition.blockRequirements()), + definition.waterRequirement() == null ? null : definition.waterRequirement().name().toLowerCase(Locale.ROOT), + pack, + disabledAnimations.contains(id), + disabledPacks.contains(pack.id()) + ); + } + + @Nullable + private static String summarizeBlockRequirements(@Nullable AnimationDefinitions.BlockRequirements requirements) { + if (requirements == null) { + return null; + } + + List parts = new ArrayList<>(); + String type = requirements.type() != null && !requirements.type().isBlank() ? requirements.type() : "unknown"; + parts.add(localizeBlockType(type)); + if (requirements.isWall()) { + AnimationDefinitions.WallHeight height = requirements.height(); + if (height != null) { + parts.add(height.max() == null ? "高度 " + height.min() + "+" : "高度 " + height.min() + "-" + height.max()); + } + + AnimationDefinitions.WallWidth width = requirements.width(); + if (width != null && (width.min() > 1 || width.max() != null)) { + parts.add(width.max() == null ? "宽度 " + width.min() + "+" : "宽度 " + width.min() + "-" + width.max()); + } + + if (requirements.facing() != null) { + parts.add("朝向 " + localizeFacing(requirements.facing().id())); + } + } else if (requirements.isCenterSupport()) { + if (requirements.support() != null) { + parts.add(requirements.support().id()); + } + + if (requirements.placement() != null) { + parts.add(requirements.placement().id()); + } + + if (requirements.surfaceFootprint() != null) { + AnimationDefinitions.SurfaceFootprint footprint = requirements.surfaceFootprint(); + parts.add("占地 " + footprint.width() + "x" + footprint.depth() + "x" + footprint.height()); + if (footprint.margin() > 0) { + parts.add("边距 " + footprint.margin()); + } + } else if (requirements.surfaceRadius() > 0) { + parts.add("表面半径 " + requirements.surfaceRadius()); + } + } + + AnimationDefinitions.Clearance clearance = requirements.clearance(); + if (clearance != null) { + parts.add("净空 " + clearance.width() + "x" + clearance.height() + "x" + clearance.depth()); + } + + AnimationDefinitions.BlockPredicate blocks = requirements.blocks(); + if (blocks != null && !blocks.isEmpty()) { + parts.add("方块 " + summarizeBlockPredicate(blocks)); + } + + return "方块: " + String.join(" | ", parts); + } + + private static String localizeBlockType(String type) { + return switch (type) { + case "wall" -> "墙"; + case "center support" -> "中心支撑"; + case "surface" -> "表面"; + default -> type.replace('_', ' '); + }; + } + + private static String localizeFacing(String facing) { + return switch (facing) { + case "north" -> "北"; + case "south" -> "南"; + case "east" -> "东"; + case "west" -> "西"; + case "up" -> "上"; + case "down" -> "下"; + default -> facing; + }; + } + + private static String summarizeBlockPredicate(AnimationDefinitions.BlockPredicate blocks) { + LinkedHashSet values = new LinkedHashSet<>(); + if (blocks.blockIds() != null) { + blocks.blockIds().stream().filter(Objects::nonNull).map(Identifier::toString).sorted().forEach(values::add); + } + + if (blocks.tagIds() != null) { + blocks.tagIds().stream().filter(Objects::nonNull).map(id -> "#" + id).sorted().forEach(values::add); + } + + if (values.isEmpty()) { + return "-"; + } + + List list = List.copyOf(values); + int take = Math.min(3, list.size()); + String summary = String.join(", ", list.subList(0, take)); + int remaining = list.size() - take; + return remaining > 0 ? summary + ", +" + remaining : summary; + } + + public record Actor(String key, List entityTypes, List requiredTags, List requiredTagsAny, String injectorType, boolean receiver) { + } + + public record Entry( + String id, + @Nullable String displayName, + List actors, + List stages, + @Nullable Integer peakStage, + List contentTags, + boolean attackEligible, + @Nullable String blockRequirement, + @Nullable String waterRequirement, + AnimationCatalogData.Pack pack, + boolean animationDisabled, + boolean packDisabled + ) { + } + + public record ModelImport(String packId, String modelRoot, List entityTypes) { + public ModelImport { + entityTypes = entityTypes == null ? List.of() : List.copyOf(entityTypes); + } + } + + public record Pack( + String id, + String resourcePackId, + String name, + String author, + @Nullable String version, + @Nullable String description, + @Nullable String localModelRoot, + List modelImports + ) { + public Pack { + modelImports = modelImports == null ? List.of() : List.copyOf(modelImports); + } + } + + public record Stage(String animationId, boolean loop, long lengthTicks, double speed, double cycleTicks, @Nullable Integer stageNumber) { + } +} diff --git a/src/main/java/com/nonid/network/AttackStateS2CPayload.java b/src/main/java/com/nonid/network/AttackStateS2CPayload.java new file mode 100644 index 0000000..15ae3a7 --- /dev/null +++ b/src/main/java/com/nonid/network/AttackStateS2CPayload.java @@ -0,0 +1,84 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record AttackStateS2CPayload( + UUID instanceId, boolean attack, boolean escapable, AttackStateS2CPayload.EscapeProfile escapeProfile, AttackStateS2CPayload.PvpRole pvpRole, float progress +) implements CustomPayload { + public static final Identifier ATTACK_STATE_ID = Identifier.of("needsofnature", "attack_state"); + public static final Id ID = new Id(ATTACK_STATE_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + AttackStateS2CPayload::instanceId, + PacketCodecs.BOOLEAN, + AttackStateS2CPayload::attack, + PacketCodecs.BOOLEAN, + AttackStateS2CPayload::escapable, + PacketCodecs.INTEGER, + payload -> payload.escapeProfile().ordinal(), + PacketCodecs.INTEGER, + payload -> payload.pvpRole().ordinal(), + PacketCodecs.FLOAT, + AttackStateS2CPayload::progress, + AttackStateS2CPayload::fromPacket + ); + + public AttackStateS2CPayload { + if (escapeProfile == null) { + escapeProfile = AttackStateS2CPayload.EscapeProfile.NONE; + } + + if (pvpRole == null) { + pvpRole = AttackStateS2CPayload.PvpRole.NONE; + } + + if (!Float.isFinite(progress)) { + progress = 0.0F; + } + + progress = Math.max(0.0F, Math.min(1.0F, progress)); + } + + public AttackStateS2CPayload(UUID instanceId, boolean attack, boolean escapable, AttackStateS2CPayload.EscapeProfile escapeProfile) { + this(instanceId, attack, escapable, escapeProfile, AttackStateS2CPayload.PvpRole.NONE, 0.0F); + } + + private static AttackStateS2CPayload fromPacket(UUID instanceId, boolean attack, boolean escapable, int profileId, int roleId, float progress) { + return new AttackStateS2CPayload( + instanceId, attack, escapable, AttackStateS2CPayload.EscapeProfile.fromId(profileId), AttackStateS2CPayload.PvpRole.fromId(roleId), progress + ); + } + + public Id getId() { + return ID; + } + + public enum EscapeProfile { + NONE, + NORMAL, + DEFEATED; + + public static AttackStateS2CPayload.EscapeProfile fromId(int id) { + AttackStateS2CPayload.EscapeProfile[] values = values(); + return id >= 0 && id < values.length ? values[id] : NONE; + } + } + + public enum PvpRole { + NONE, + DEFENDER, + ATTACKER; + + public static AttackStateS2CPayload.PvpRole fromId(int id) { + AttackStateS2CPayload.PvpRole[] values = values(); + return id >= 0 && id < values.length ? values[id] : NONE; + } + } +} diff --git a/src/main/java/com/nonid/network/DestroyedSkinMaskSyncS2CPayload.java b/src/main/java/com/nonid/network/DestroyedSkinMaskSyncS2CPayload.java new file mode 100644 index 0000000..0f07912 --- /dev/null +++ b/src/main/java/com/nonid/network/DestroyedSkinMaskSyncS2CPayload.java @@ -0,0 +1,22 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DestroyedSkinMaskSyncS2CPayload(UUID playerUuid, int stage, byte[] pngBytes) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "destroyed_skin_mask_sync"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.ofStatic((buf, payload) -> { + buf.writeUuid(payload.playerUuid()); + buf.writeVarInt(payload.stage()); + buf.writeByteArray(payload.pngBytes()); + }, buf -> new DestroyedSkinMaskSyncS2CPayload(buf.readUuid(), buf.readVarInt(), buf.readByteArray(65536))); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/DestroyedSkinMaskUploadC2SPayload.java b/src/main/java/com/nonid/network/DestroyedSkinMaskUploadC2SPayload.java new file mode 100644 index 0000000..43326b9 --- /dev/null +++ b/src/main/java/com/nonid/network/DestroyedSkinMaskUploadC2SPayload.java @@ -0,0 +1,25 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DestroyedSkinMaskUploadC2SPayload(int stage, byte[] pngBytes) implements CustomPayload { + public static final int MAX_BYTES = 65536; + public static final Identifier ID_RAW = Identifier.of("needsofnature", "destroyed_skin_mask_upload"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.VAR_INT, + DestroyedSkinMaskUploadC2SPayload::stage, + PacketCodecs.byteArray(65536), + DestroyedSkinMaskUploadC2SPayload::pngBytes, + DestroyedSkinMaskUploadC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/DestroyedSkinParticlesS2CPayload.java b/src/main/java/com/nonid/network/DestroyedSkinParticlesS2CPayload.java new file mode 100644 index 0000000..bccde23 --- /dev/null +++ b/src/main/java/com/nonid/network/DestroyedSkinParticlesS2CPayload.java @@ -0,0 +1,25 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DestroyedSkinParticlesS2CPayload(UUID playerUuid, double x, double y, double z, int count, long seed) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "destroyed_skin_particles"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.ofStatic((buf, payload) -> { + buf.writeUuid(payload.playerUuid()); + buf.writeDouble(payload.x()); + buf.writeDouble(payload.y()); + buf.writeDouble(payload.z()); + buf.writeVarInt(payload.count()); + buf.writeLong(payload.seed()); + }, buf -> new DestroyedSkinParticlesS2CPayload(buf.readUuid(), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readVarInt(), buf.readLong())); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/DestroyedSkinStateS2CPayload.java b/src/main/java/com/nonid/network/DestroyedSkinStateS2CPayload.java new file mode 100644 index 0000000..bbf76cd --- /dev/null +++ b/src/main/java/com/nonid/network/DestroyedSkinStateS2CPayload.java @@ -0,0 +1,21 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DestroyedSkinStateS2CPayload(UUID playerUuid, int stage) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "destroyed_skin_state"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.ofStatic((buf, payload) -> { + buf.writeUuid(payload.playerUuid()); + buf.writeVarInt(payload.stage()); + }, buf -> new DestroyedSkinStateS2CPayload(buf.readUuid(), buf.readVarInt())); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/DestroyedSkinSyncS2CPayload.java b/src/main/java/com/nonid/network/DestroyedSkinSyncS2CPayload.java new file mode 100644 index 0000000..67328c5 --- /dev/null +++ b/src/main/java/com/nonid/network/DestroyedSkinSyncS2CPayload.java @@ -0,0 +1,22 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DestroyedSkinSyncS2CPayload(UUID playerUuid, int stage, byte[] pngBytes) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "destroyed_skin_sync"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.ofStatic((buf, payload) -> { + buf.writeUuid(payload.playerUuid()); + buf.writeVarInt(payload.stage()); + buf.writeByteArray(payload.pngBytes()); + }, buf -> new DestroyedSkinSyncS2CPayload(buf.readUuid(), buf.readVarInt(), buf.readByteArray(1048576))); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/DestroyedSkinUploadC2SPayload.java b/src/main/java/com/nonid/network/DestroyedSkinUploadC2SPayload.java new file mode 100644 index 0000000..be4dade --- /dev/null +++ b/src/main/java/com/nonid/network/DestroyedSkinUploadC2SPayload.java @@ -0,0 +1,24 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record DestroyedSkinUploadC2SPayload(byte[] pngBytes) implements CustomPayload { + public static final int MIN_DIMENSION = 64; + public static final int MAX_DIMENSION = 512; + public static final int DIMENSION_STEP = 64; + public static final int MAX_BYTES = 1048576; + public static final Identifier ID_RAW = Identifier.of("needsofnature", "destroyed_skin_upload"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.byteArray(1048576), DestroyedSkinUploadC2SPayload::pngBytes, DestroyedSkinUploadC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/GenderSelectionPromptS2CPayload.java b/src/main/java/com/nonid/network/GenderSelectionPromptS2CPayload.java new file mode 100644 index 0000000..1838bea --- /dev/null +++ b/src/main/java/com/nonid/network/GenderSelectionPromptS2CPayload.java @@ -0,0 +1,26 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record GenderSelectionPromptS2CPayload(int allowedMask, int currentMask, boolean permanent) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "gender_selection_prompt"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.INTEGER, + GenderSelectionPromptS2CPayload::allowedMask, + PacketCodecs.INTEGER, + GenderSelectionPromptS2CPayload::currentMask, + PacketCodecs.BOOLEAN, + GenderSelectionPromptS2CPayload::permanent, + GenderSelectionPromptS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/HostConfigSyncData.java b/src/main/java/com/nonid/network/HostConfigSyncData.java new file mode 100644 index 0000000..ee3fda0 --- /dev/null +++ b/src/main/java/com/nonid/network/HostConfigSyncData.java @@ -0,0 +1,337 @@ +package com.nonid.network; + +import com.nonid.NonConfig; +import com.nonid.data.NonPeakStages; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class HostConfigSyncData { + public int initialEnergyMax; + public double energyGainRate; + public double nearAnimationEnergyGainMult; + public double playerEnergyAuraMultLow; + public double playerEnergyAuraMultMid; + public double playerEnergyAuraMultHigh; + public double destroyedSkinAuraMultStage1; + public double destroyedSkinAuraMultStage2; + public double destroyedSkinAuraMultStage3; + public double destroyedSkinAuraMultStage4; + public int normalDamageDestroyedSkinChancePercent = 10; + public boolean lastDefeatedEnabled = true; + public int lastDefeatedEnergyThreshold = 70; + public int lastDefeatedSearchRadius = 6; + public int lastDefeatedCooldownSeconds = 120; + public int playerEnergyAuraRadiusMin; + public int playerEnergyAuraRadiusMax; + public int playerEnergyAuraPulseTicks; + public int scanBudgetPerTick; + public int animationBlockSearchRadius = 3; + public int loopProgressSeconds; + public int peakLoopProgressSeconds; + public int genderMaleChancePercent; + public int genderFemaleChancePercent; + public int genderBothChancePercent; + public int maleMaleChancePercent; + public int femaleFemaleChancePercent; + public int entityEntityChancePercent; + public int multiActorJoinChancePercent; + public boolean allowPlayerGenderChangeAnytime = true; + public boolean requirePlayerGenderSelectionOnJoin = false; + public int allowedStartingGenderMask = 7; + public int allowedPlayerInteractionProfilesMask = 15; + public int playerRequestMinEnergy = 70; + public boolean pregnancyEnabled; + public boolean liquidTankEnabled; + public boolean messSystemEnabled = true; + public boolean destroyedSkinSystemEnabled = true; + public boolean allowCraftingTableSkinRepair = false; + public boolean keepMessAfterDeath = false; + public boolean keepRippedSkinAfterDeath = false; + public boolean keepLiquidTankAfterDeath = false; + public boolean keepPregnancyAfterDeath = false; + public int liquidTankCapacityMl = 200; + public boolean vanillaOverridesEnabled; + public boolean useAnimationBreeding; + public boolean requireMaleFemaleForBreeding; + public boolean femaleCowOnlyMilking; + public boolean attackCreativePlayers; + public boolean convertMaleOnlyVInjectionsToA = true; + public int attackEscapeHits; + public double attackDecayPerSecond; + public int attackEscapeDamageDifficultyPercent = 100; + public int pvpFullHealthEscapeBonusPercent = 30; + public int pvpCriticalHealthMashStrengthPercent = 25; + public int pvpMaxHeatEscapeStrengthPercent = 70; + public int pvpZeroHeatAttackStrengthPercent = 70; + public int attackEscapeInvulnerabilitySeconds = 3; + public int attackEscapeAnimationProtectionSeconds = 3; + public int postEscapeGatherMaxMobs = 8; + public boolean blockAnimationsWhileRidingLivingEntities = true; + public boolean blockAnimationsWhileRidingVehicles = false; + public int attackOutcomeFailsafeThreshold = 3; + public int pregnancyChancePercent; + public int pregnancyDurationMinutes; + public boolean liquidBasedPregnancy = false; + public int pregnancyRiskyStartDay = 13; + public int pregnancyOvulationEndDay = 17; + public int pregnancySafeDayChancePercent = 10; + public int pregnancyRiskyChancePercent = 300; + public int pregnancyOvulationChancePercent = 500; + public Map pregnancyChanceByEntity = new LinkedHashMap<>(); + public Map pregnancyOffspringCountByEntity = new LinkedHashMap<>(); + public int pregnancyEggDefaultHatchSeconds; + public Map pregnancyEggEnabledByEntity = new LinkedHashMap<>(); + public Map pregnancyEggHatchSecondsByEntity = new LinkedHashMap<>(); + public boolean pregnancyAutoTameMobs = true; + public boolean pregnancyFriendlyMobsIgnorePlayers = false; + public Map entityProfilesByEntity = new LinkedHashMap<>(); + public double liquidDecayPerSecond; + public double peakXpPerMl = 0.06666666666666667; + public float filledStageOneSpeedMult; + public float filledStageTwoSpeedMult; + public float filledStageThreeSpeedMult; + public float filledStageOneJumpMult; + public float filledStageTwoJumpMult; + public float filledStageThreeJumpMult; + public Map genderSpawnByEntity = new LinkedHashMap<>(); + public Map liquidGainByEntity = new LinkedHashMap<>(); + public Map liquidColorByEntity = new LinkedHashMap<>(); + public List disabledAnimationPacks = List.of(); + public List disabledAnimations = List.of(); + public Map peakStageByAnimation = new LinkedHashMap<>(); + + public static HostConfigSyncData fromConfig(NonConfig config) { + HostConfigSyncData data = new HostConfigSyncData(); + if (config == null) { + return data; + } + + data.initialEnergyMax = config.getInitialEnergyMax(); + data.energyGainRate = config.getEnergyGainRate(); + data.nearAnimationEnergyGainMult = config.getNearAnimationEnergyGainMult(); + data.playerEnergyAuraMultLow = config.getPlayerEnergyAuraMultLow(); + data.playerEnergyAuraMultMid = config.getPlayerEnergyAuraMultMid(); + data.playerEnergyAuraMultHigh = config.getPlayerEnergyAuraMultHigh(); + data.destroyedSkinAuraMultStage1 = config.getDestroyedSkinAuraMultStage1(); + data.destroyedSkinAuraMultStage2 = config.getDestroyedSkinAuraMultStage2(); + data.destroyedSkinAuraMultStage3 = config.getDestroyedSkinAuraMultStage3(); + data.destroyedSkinAuraMultStage4 = config.getDestroyedSkinAuraMultStage4(); + data.normalDamageDestroyedSkinChancePercent = config.getNormalDamageDestroyedSkinChancePercent(); + data.lastDefeatedEnabled = config.isLastDefeatedEnabled(); + data.lastDefeatedEnergyThreshold = config.getLastDefeatedEnergyThreshold(); + data.lastDefeatedSearchRadius = config.getLastDefeatedSearchRadius(); + data.lastDefeatedCooldownSeconds = config.getLastDefeatedCooldownSeconds(); + data.playerEnergyAuraRadiusMin = config.getPlayerEnergyAuraRadiusMin(); + data.playerEnergyAuraRadiusMax = config.getPlayerEnergyAuraRadiusMax(); + data.playerEnergyAuraPulseTicks = config.getPlayerEnergyAuraPulseTicks(); + data.scanBudgetPerTick = config.getScanBudgetPerTick(); + data.animationBlockSearchRadius = config.blockSearchRadius(); + data.loopProgressSeconds = config.getLoopProgressSeconds(); + data.peakLoopProgressSeconds = config.getPeakLoopProgressSeconds(); + data.genderMaleChancePercent = config.getGenderMaleChancePercent(); + data.genderFemaleChancePercent = config.getGenderFemaleChancePercent(); + data.genderBothChancePercent = config.getGenderBothChancePercent(); + data.maleMaleChancePercent = config.getMaleMaleChancePercent(); + data.femaleFemaleChancePercent = config.getFemaleFemaleChancePercent(); + data.entityEntityChancePercent = config.getEntityEntityChancePercent(); + data.multiActorJoinChancePercent = config.getMultiActorJoinChancePercent(); + data.allowPlayerGenderChangeAnytime = config.allowPlayerGenderChangeAnytime(); + data.requirePlayerGenderSelectionOnJoin = config.requirePlayerGenderSelectionOnJoin(); + data.allowedStartingGenderMask = config.getAllowedStartingGenderMask(); + data.allowedPlayerInteractionProfilesMask = config.getAllowedPlayerInteractionProfilesMask(); + data.playerRequestMinEnergy = config.getPlayerRequestMinEnergy(); + data.pregnancyEnabled = config.isPregnancyEnabled(); + data.liquidTankEnabled = config.isLiquidTankEnabled(); + data.messSystemEnabled = config.isMessSystemEnabled(); + data.destroyedSkinSystemEnabled = config.isDestroyedSkinSystemEnabled(); + data.allowCraftingTableSkinRepair = config.isCraftingTableSkinRepairAllowed(); + data.keepMessAfterDeath = config.keepMessAfterDeath(); + data.keepRippedSkinAfterDeath = config.keepRippedSkinAfterDeath(); + data.keepLiquidTankAfterDeath = config.keepLiquidTankAfterDeath(); + data.keepPregnancyAfterDeath = config.keepPregnancyAfterDeath(); + data.liquidTankCapacityMl = config.getLiquidTankCapacityMl(); + data.vanillaOverridesEnabled = config.isVanillaOverridesEnabled(); + data.useAnimationBreeding = config.useAnimationBreeding(); + data.requireMaleFemaleForBreeding = config.requireMaleFemaleForBreeding(); + data.femaleCowOnlyMilking = config.femaleCowOnlyMilking(); + data.attackCreativePlayers = config.canAttackCreativePlayers(); + data.convertMaleOnlyVInjectionsToA = config.convertMaleOnlyVInjectionsToA(); + data.attackEscapeHits = config.getAttackEscapeHits(); + data.attackDecayPerSecond = config.getAttackDecayPerSecond(); + data.attackEscapeDamageDifficultyPercent = config.getAttackEscapeDamageDifficultyPercent(); + data.pvpFullHealthEscapeBonusPercent = config.getPvpFullHealthEscapeBonusPercent(); + data.pvpCriticalHealthMashStrengthPercent = config.getPvpCriticalHealthMashStrengthPercent(); + data.pvpMaxHeatEscapeStrengthPercent = config.getPvpMaxHeatEscapeStrengthPercent(); + data.pvpZeroHeatAttackStrengthPercent = config.getPvpZeroHeatAttackStrengthPercent(); + data.attackEscapeInvulnerabilitySeconds = config.getAttackEscapeInvulnerabilitySeconds(); + data.attackEscapeAnimationProtectionSeconds = config.getAttackEscapeAnimationProtectionSeconds(); + data.postEscapeGatherMaxMobs = config.getPostEscapeGatherMaxMobs(); + data.blockAnimationsWhileRidingLivingEntities = config.blockAnimationsWhileRidingLivingEntities(); + data.blockAnimationsWhileRidingVehicles = config.blockAnimationsWhileRidingVehicles(); + data.attackOutcomeFailsafeThreshold = config.getAttackOutcomeFailsafeThreshold(); + data.pregnancyChancePercent = config.getPregnancyChancePercent(); + data.pregnancyDurationMinutes = config.getPregnancyDurationMinutes(); + data.liquidBasedPregnancy = config.isLiquidBasedPregnancyEnabled(); + data.pregnancyRiskyStartDay = config.getPregnancyRiskyStartDay(); + data.pregnancyOvulationEndDay = config.getPregnancyOvulationEndDay(); + data.pregnancySafeDayChancePercent = config.getPregnancySafeDayChancePercent(); + data.pregnancyRiskyChancePercent = config.getPregnancyRiskyChancePercent(); + data.pregnancyOvulationChancePercent = config.getPregnancyOvulationChancePercent(); + data.pregnancyChanceByEntity = new LinkedHashMap<>(config.getPregnancyChanceByEntity()); + data.pregnancyOffspringCountByEntity = new LinkedHashMap<>(config.getPregnancyOffspringCountByEntity()); + data.pregnancyEggDefaultHatchSeconds = config.getPregnancyEggDefaultHatchSeconds(); + data.pregnancyEggEnabledByEntity = new LinkedHashMap<>(config.getPregnancyEggEnabledByEntity()); + data.pregnancyEggHatchSecondsByEntity = new LinkedHashMap<>(config.getPregnancyEggHatchSecondsByEntity()); + data.pregnancyAutoTameMobs = config.isPregnancyAutoTameMobs(); + data.pregnancyFriendlyMobsIgnorePlayers = config.pregnancyFriendlyMobsIgnorePlayers(); + data.entityProfilesByEntity = new LinkedHashMap<>(config.getEntityProfilesByEntity()); + data.liquidDecayPerSecond = config.getLiquidDecayPerSecond(); + data.peakXpPerMl = config.getPeakXpPerMl(); + data.filledStageOneSpeedMult = config.getFilledStageOneSpeedMult(); + data.filledStageTwoSpeedMult = config.getFilledStageTwoSpeedMult(); + data.filledStageThreeSpeedMult = config.getFilledStageThreeSpeedMult(); + data.filledStageOneJumpMult = config.getFilledStageOneJumpMult(); + data.filledStageTwoJumpMult = config.getFilledStageTwoJumpMult(); + data.filledStageThreeJumpMult = config.getFilledStageThreeJumpMult(); + data.genderSpawnByEntity = new LinkedHashMap<>(config.getGenderSpawnByEntity()); + data.liquidGainByEntity = new LinkedHashMap<>(config.getLiquidGainByEntity()); + data.liquidColorByEntity = new LinkedHashMap<>(config.getLiquidColorByEntity()); + data.disabledAnimationPacks = List.copyOf(config.getDisabledAnimationPacks()); + data.disabledAnimations = List.copyOf(config.getDisabledAnimations()); + data.peakStageByAnimation = new LinkedHashMap<>(NonPeakStages.snapshotPeakStagesForSync()); + return data; + } + + public static HostConfigSyncData fromEditableConfig(NonConfig config) { + HostConfigSyncData data = fromConfig(config); + data.peakStageByAnimation = new LinkedHashMap<>(); + return data; + } + + public void applyTo(NonConfig config) { + this.applyEditableTo(config); + NonPeakStages.applySyncedPeakStages(copyIntMap(this.peakStageByAnimation)); + } + + public void applyEditableTo(NonConfig config) { + if (config != null) { + config.setInitialEnergyMax(this.initialEnergyMax); + config.setEnergyGainRate(this.energyGainRate); + config.setNearAnimationEnergyGainMult(this.nearAnimationEnergyGainMult); + config.setPlayerEnergyAuraMultLow(this.playerEnergyAuraMultLow); + config.setPlayerEnergyAuraMultMid(this.playerEnergyAuraMultMid); + config.setPlayerEnergyAuraMultHigh(this.playerEnergyAuraMultHigh); + config.setDestroyedSkinAuraMultStage1(this.destroyedSkinAuraMultStage1); + config.setDestroyedSkinAuraMultStage2(this.destroyedSkinAuraMultStage2); + config.setDestroyedSkinAuraMultStage3(this.destroyedSkinAuraMultStage3); + config.setDestroyedSkinAuraMultStage4(this.destroyedSkinAuraMultStage4); + config.setNormalDamageDestroyedSkinChancePercent(this.normalDamageDestroyedSkinChancePercent); + config.setLastDefeatedEnabled(this.lastDefeatedEnabled); + config.setLastDefeatedEnergyThreshold(this.lastDefeatedEnergyThreshold); + config.setLastDefeatedSearchRadius(this.lastDefeatedSearchRadius); + config.setLastDefeatedCooldownSeconds(this.lastDefeatedCooldownSeconds); + config.setPlayerEnergyAuraRadiusMin(this.playerEnergyAuraRadiusMin); + config.setPlayerEnergyAuraRadiusMax(this.playerEnergyAuraRadiusMax); + config.setPlayerEnergyAuraPulseTicks(this.playerEnergyAuraPulseTicks); + config.setScanBudgetPerTick(this.scanBudgetPerTick); + config.setBlockSearchRadius(this.animationBlockSearchRadius); + config.setLoopProgressSeconds(this.loopProgressSeconds); + config.setPeakLoopProgressSeconds(this.peakLoopProgressSeconds); + config.setGenderMaleChancePercent(this.genderMaleChancePercent); + config.setGenderFemaleChancePercent(this.genderFemaleChancePercent); + config.setGenderBothChancePercent(this.genderBothChancePercent); + config.setMaleMaleChancePercent(this.maleMaleChancePercent); + config.setFemaleFemaleChancePercent(this.femaleFemaleChancePercent); + config.setEntityEntityChancePercent(this.entityEntityChancePercent); + config.setMultiActorJoinChancePercent(this.multiActorJoinChancePercent); + config.setAllowPlayerGenderChangeAnytime(this.allowPlayerGenderChangeAnytime); + config.setRequirePlayerGenderSelectionOnJoin(this.requirePlayerGenderSelectionOnJoin); + config.setAllowedStartingGenderMask(this.allowedStartingGenderMask); + config.setAllowedPlayerInteractionProfilesMask(this.allowedPlayerInteractionProfilesMask); + config.setPlayerRequestMinEnergy(this.playerRequestMinEnergy); + config.setPregnancyEnabled(this.pregnancyEnabled); + config.setLiquidTankEnabled(this.liquidTankEnabled); + config.setMessSystemEnabled(this.messSystemEnabled); + config.setDestroyedSkinSystemEnabled(this.destroyedSkinSystemEnabled); + config.setCraftingTableSkinRepairAllowed(this.allowCraftingTableSkinRepair); + config.setKeepMessAfterDeath(this.keepMessAfterDeath); + config.setKeepRippedSkinAfterDeath(this.keepRippedSkinAfterDeath); + config.setKeepLiquidTankAfterDeath(this.keepLiquidTankAfterDeath); + config.setKeepPregnancyAfterDeath(this.keepPregnancyAfterDeath); + config.setLiquidTankCapacityMl(this.liquidTankCapacityMl); + config.setVanillaOverridesEnabled(this.vanillaOverridesEnabled); + config.setUseAnimationBreeding(this.useAnimationBreeding); + config.setRequireMaleFemaleForBreeding(this.requireMaleFemaleForBreeding); + config.setFemaleCowOnlyMilking(this.femaleCowOnlyMilking); + config.setAttackCreativePlayers(this.attackCreativePlayers); + config.setConvertMaleOnlyVInjectionsToA(this.convertMaleOnlyVInjectionsToA); + config.setAttackEscapeHits(this.attackEscapeHits); + config.setAttackDecayPerSecond(this.attackDecayPerSecond); + config.setAttackEscapeDamageDifficultyPercent(this.attackEscapeDamageDifficultyPercent); + config.setPvpFullHealthEscapeBonusPercent(this.pvpFullHealthEscapeBonusPercent); + config.setPvpCriticalHealthMashStrengthPercent(this.pvpCriticalHealthMashStrengthPercent); + config.setPvpMaxHeatEscapeStrengthPercent(this.pvpMaxHeatEscapeStrengthPercent); + config.setPvpZeroHeatAttackStrengthPercent(this.pvpZeroHeatAttackStrengthPercent); + config.setAttackEscapeInvulnerabilitySeconds(this.attackEscapeInvulnerabilitySeconds); + config.setAttackEscapeAnimationProtectionSeconds(this.attackEscapeAnimationProtectionSeconds); + config.setPostEscapeGatherMaxMobs(this.postEscapeGatherMaxMobs); + config.setBlockAnimationsWhileRidingLivingEntities(this.blockAnimationsWhileRidingLivingEntities); + config.setBlockAnimationsWhileRidingVehicles(this.blockAnimationsWhileRidingVehicles); + config.setAttackOutcomeFailsafeThreshold(this.attackOutcomeFailsafeThreshold); + config.setPregnancyChancePercent(this.pregnancyChancePercent); + config.setPregnancyDurationMinutes(this.pregnancyDurationMinutes); + config.setLiquidBasedPregnancyEnabled(this.liquidBasedPregnancy); + config.setPregnancyRiskyStartDay(this.pregnancyRiskyStartDay); + config.setPregnancyOvulationEndDay(this.pregnancyOvulationEndDay); + config.setPregnancySafeDayChancePercent(this.pregnancySafeDayChancePercent); + config.setPregnancyRiskyChancePercent(this.pregnancyRiskyChancePercent); + config.setPregnancyOvulationChancePercent(this.pregnancyOvulationChancePercent); + config.setPregnancyChanceByEntity(copyIntMap(this.pregnancyChanceByEntity)); + config.setPregnancyOffspringCountByEntity(copyOffspringCountMap(this.pregnancyOffspringCountByEntity)); + config.setPregnancyEggDefaultHatchSeconds(this.pregnancyEggDefaultHatchSeconds); + config.setPregnancyEggEnabledByEntity(copyBoolMap(this.pregnancyEggEnabledByEntity)); + config.setPregnancyEggHatchSecondsByEntity(copyIntMap(this.pregnancyEggHatchSecondsByEntity)); + config.setPregnancyAutoTameMobs(this.pregnancyAutoTameMobs); + config.setPregnancyFriendlyMobsIgnorePlayers(this.pregnancyFriendlyMobsIgnorePlayers); + config.setEntityProfilesByEntity(copyEntityProfileMap(this.entityProfilesByEntity)); + config.setLiquidDecayPerSecond(this.liquidDecayPerSecond); + config.setPeakXpPerMl(this.peakXpPerMl); + config.setFilledStageOneSpeedMult(this.filledStageOneSpeedMult); + config.setFilledStageTwoSpeedMult(this.filledStageTwoSpeedMult); + config.setFilledStageThreeSpeedMult(this.filledStageThreeSpeedMult); + config.setFilledStageOneJumpMult(this.filledStageOneJumpMult); + config.setFilledStageTwoJumpMult(this.filledStageTwoJumpMult); + config.setFilledStageThreeJumpMult(this.filledStageThreeJumpMult); + config.setGenderSpawnByEntity(copyGenderSpawnMap(this.genderSpawnByEntity)); + config.setLiquidGainByEntity(copyIntMap(this.liquidGainByEntity)); + config.setLiquidColorByEntity(copyStringMap(this.liquidColorByEntity)); + config.setDisabledAnimationPacks(this.disabledAnimationPacks == null ? List.of() : this.disabledAnimationPacks); + config.setDisabledAnimations(this.disabledAnimations == null ? List.of() : this.disabledAnimations); + } + } + + private static Map copyGenderSpawnMap(Map source) { + return source != null && !source.isEmpty() ? new LinkedHashMap<>(source) : Map.of(); + } + + private static Map copyEntityProfileMap(Map source) { + return source != null && !source.isEmpty() ? new LinkedHashMap<>(source) : Map.of(); + } + + private static Map copyOffspringCountMap(Map source) { + return source != null && !source.isEmpty() ? new LinkedHashMap<>(source) : Map.of(); + } + + private static Map copyIntMap(Map source) { + return source != null && !source.isEmpty() ? new LinkedHashMap<>(source) : Map.of(); + } + + private static Map copyBoolMap(Map source) { + return source != null && !source.isEmpty() ? new LinkedHashMap<>(source) : Map.of(); + } + + private static Map copyStringMap(Map source) { + return source != null && !source.isEmpty() ? new LinkedHashMap<>(source) : Map.of(); + } +} diff --git a/src/main/java/com/nonid/network/HostConfigSyncS2CPayload.java b/src/main/java/com/nonid/network/HostConfigSyncS2CPayload.java new file mode 100644 index 0000000..f1c449b --- /dev/null +++ b/src/main/java/com/nonid/network/HostConfigSyncS2CPayload.java @@ -0,0 +1,28 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record HostConfigSyncS2CPayload(String json, String editableJson, long revision, boolean canEdit) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "host_config_sync"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.string(262144), + HostConfigSyncS2CPayload::json, + PacketCodecs.string(262144), + HostConfigSyncS2CPayload::editableJson, + PacketCodecs.LONG, + HostConfigSyncS2CPayload::revision, + PacketCodecs.BOOLEAN, + HostConfigSyncS2CPayload::canEdit, + HostConfigSyncS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/LiquidStateS2CPayload.java b/src/main/java/com/nonid/network/LiquidStateS2CPayload.java new file mode 100644 index 0000000..4f95366 --- /dev/null +++ b/src/main/java/com/nonid/network/LiquidStateS2CPayload.java @@ -0,0 +1,26 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record LiquidStateS2CPayload(int stored, int capacity, int tintRgb) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "liquid_state"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.INTEGER, + LiquidStateS2CPayload::stored, + PacketCodecs.INTEGER, + LiquidStateS2CPayload::capacity, + PacketCodecs.INTEGER, + LiquidStateS2CPayload::tintRgb, + LiquidStateS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/ManualDamageDestroyedSkinC2SPayload.java b/src/main/java/com/nonid/network/ManualDamageDestroyedSkinC2SPayload.java new file mode 100644 index 0000000..90e05df --- /dev/null +++ b/src/main/java/com/nonid/network/ManualDamageDestroyedSkinC2SPayload.java @@ -0,0 +1,17 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record ManualDamageDestroyedSkinC2SPayload() implements CustomPayload { + public static final Identifier MANUAL_DAMAGE_DESTROYED_SKIN_ID = Identifier.of("needsofnature", "manual_damage_destroyed_skin"); + public static final Id ID = new Id(MANUAL_DAMAGE_DESTROYED_SKIN_ID); + public static final PacketCodec CODEC = PacketCodec.unit(new ManualDamageDestroyedSkinC2SPayload()); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/MessStateS2CPayload.java b/src/main/java/com/nonid/network/MessStateS2CPayload.java new file mode 100644 index 0000000..647e7a8 --- /dev/null +++ b/src/main/java/com/nonid/network/MessStateS2CPayload.java @@ -0,0 +1,26 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record MessStateS2CPayload(UUID playerUuid, int vMess, int aMess, int mMess, int tintRgb, int liquidStored, int liquidCapacity) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "mess_state"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.ofStatic((buf, payload) -> { + buf.writeUuid(payload.playerUuid); + buf.writeVarInt(payload.vMess); + buf.writeVarInt(payload.aMess); + buf.writeVarInt(payload.mMess); + buf.writeVarInt(payload.tintRgb); + buf.writeVarInt(payload.liquidStored); + buf.writeVarInt(payload.liquidCapacity); + }, buf -> new MessStateS2CPayload(buf.readUuid(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt())); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAnimationCancelC2SPayload.java b/src/main/java/com/nonid/network/PlayerAnimationCancelC2SPayload.java new file mode 100644 index 0000000..85d9c40 --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAnimationCancelC2SPayload.java @@ -0,0 +1,20 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAnimationCancelC2SPayload(UUID requestId) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_animation_cancel")); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, PlayerAnimationCancelC2SPayload::requestId, PlayerAnimationCancelC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAnimationChoiceC2SPayload.java b/src/main/java/com/nonid/network/PlayerAnimationChoiceC2SPayload.java new file mode 100644 index 0000000..be1693e --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAnimationChoiceC2SPayload.java @@ -0,0 +1,25 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAnimationChoiceC2SPayload(UUID requestId, int choiceIndex) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_animation_choice")); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + PlayerAnimationChoiceC2SPayload::requestId, + PacketCodecs.INTEGER, + PlayerAnimationChoiceC2SPayload::choiceIndex, + PlayerAnimationChoiceC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAnimationConsentC2SPayload.java b/src/main/java/com/nonid/network/PlayerAnimationConsentC2SPayload.java new file mode 100644 index 0000000..01e2714 --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAnimationConsentC2SPayload.java @@ -0,0 +1,25 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAnimationConsentC2SPayload(UUID requestId, boolean accepted) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_animation_consent")); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + PlayerAnimationConsentC2SPayload::requestId, + PacketCodecs.BOOLEAN, + PlayerAnimationConsentC2SPayload::accepted, + PlayerAnimationConsentC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAnimationConsentS2CPayload.java b/src/main/java/com/nonid/network/PlayerAnimationConsentS2CPayload.java new file mode 100644 index 0000000..523b18e --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAnimationConsentS2CPayload.java @@ -0,0 +1,37 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAnimationConsentS2CPayload( + UUID requestId, UUID requesterUuid, String requesterName, String animationId, String displayName, String contentTagsJson, boolean joinRequest +) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_animation_consent_prompt")); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + PlayerAnimationConsentS2CPayload::requestId, + Uuids.PACKET_CODEC, + PlayerAnimationConsentS2CPayload::requesterUuid, + PacketCodecs.STRING, + PlayerAnimationConsentS2CPayload::requesterName, + PacketCodecs.STRING, + PlayerAnimationConsentS2CPayload::animationId, + PacketCodecs.STRING, + PlayerAnimationConsentS2CPayload::displayName, + PacketCodecs.STRING, + PlayerAnimationConsentS2CPayload::contentTagsJson, + PacketCodecs.BOOLEAN, + PlayerAnimationConsentS2CPayload::joinRequest, + PlayerAnimationConsentS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAnimationRequestStatusS2CPayload.java b/src/main/java/com/nonid/network/PlayerAnimationRequestStatusS2CPayload.java new file mode 100644 index 0000000..878efcd --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAnimationRequestStatusS2CPayload.java @@ -0,0 +1,27 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAnimationRequestStatusS2CPayload(UUID requestId, String state, String messageKey) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_animation_request_status")); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + PlayerAnimationRequestStatusS2CPayload::requestId, + PacketCodecs.STRING, + PlayerAnimationRequestStatusS2CPayload::state, + PacketCodecs.STRING, + PlayerAnimationRequestStatusS2CPayload::messageKey, + PlayerAnimationRequestStatusS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAnimationSelectionS2CPayload.java b/src/main/java/com/nonid/network/PlayerAnimationSelectionS2CPayload.java new file mode 100644 index 0000000..6899d1b --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAnimationSelectionS2CPayload.java @@ -0,0 +1,32 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAnimationSelectionS2CPayload(UUID requestId, UUID targetUuid, String targetName, String candidatesJson, boolean joinRequest) + implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_animation_selection")); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, + PlayerAnimationSelectionS2CPayload::requestId, + Uuids.PACKET_CODEC, + PlayerAnimationSelectionS2CPayload::targetUuid, + PacketCodecs.STRING, + PlayerAnimationSelectionS2CPayload::targetName, + PacketCodecs.STRING, + PlayerAnimationSelectionS2CPayload::candidatesJson, + PacketCodecs.BOOLEAN, + PlayerAnimationSelectionS2CPayload::joinRequest, + PlayerAnimationSelectionS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerAttackInputC2SPayload.java b/src/main/java/com/nonid/network/PlayerAttackInputC2SPayload.java new file mode 100644 index 0000000..f055f3d --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerAttackInputC2SPayload.java @@ -0,0 +1,22 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record PlayerAttackInputC2SPayload(UUID instanceId, int keyId) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "player_attack_input"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, PlayerAttackInputC2SPayload::instanceId, PacketCodecs.INTEGER, PlayerAttackInputC2SPayload::keyId, PlayerAttackInputC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerProfileActionC2SPayload.java b/src/main/java/com/nonid/network/PlayerProfileActionC2SPayload.java new file mode 100644 index 0000000..d3c8c5d --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerProfileActionC2SPayload.java @@ -0,0 +1,25 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record PlayerProfileActionC2SPayload(String action, String target, String mode) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_profile_action")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.STRING, + PlayerProfileActionC2SPayload::action, + PacketCodecs.STRING, + PlayerProfileActionC2SPayload::target, + PacketCodecs.STRING, + PlayerProfileActionC2SPayload::mode, + PlayerProfileActionC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerProfileSnapshotS2CPayload.java b/src/main/java/com/nonid/network/PlayerProfileSnapshotS2CPayload.java new file mode 100644 index 0000000..06607a1 --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerProfileSnapshotS2CPayload.java @@ -0,0 +1,23 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record PlayerProfileSnapshotS2CPayload(String json, String errorKey) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "player_profile_snapshot")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.STRING, + PlayerProfileSnapshotS2CPayload::json, + PacketCodecs.STRING, + PlayerProfileSnapshotS2CPayload::errorKey, + PlayerProfileSnapshotS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PlayerRuntimeSettingsS2CPayload.java b/src/main/java/com/nonid/network/PlayerRuntimeSettingsS2CPayload.java new file mode 100644 index 0000000..fea39d5 --- /dev/null +++ b/src/main/java/com/nonid/network/PlayerRuntimeSettingsS2CPayload.java @@ -0,0 +1,20 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record PlayerRuntimeSettingsS2CPayload(int attackEscapeHits) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "player_runtime_settings"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.INTEGER, PlayerRuntimeSettingsS2CPayload::attackEscapeHits, PlayerRuntimeSettingsS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PregnancyCycleStateS2CPayload.java b/src/main/java/com/nonid/network/PregnancyCycleStateS2CPayload.java new file mode 100644 index 0000000..b43b399 --- /dev/null +++ b/src/main/java/com/nonid/network/PregnancyCycleStateS2CPayload.java @@ -0,0 +1,35 @@ +package com.nonid.network; + +import com.nonid.PregnancyCyclePhase; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record PregnancyCycleStateS2CPayload(boolean enabled, int day, PregnancyCyclePhase phase) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "pregnancy_cycle_state"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.BOOLEAN, + PregnancyCycleStateS2CPayload::enabled, + PacketCodecs.INTEGER, + PregnancyCycleStateS2CPayload::day, + PacketCodecs.INTEGER, + payload -> payload.phase().ordinal(), + PregnancyCycleStateS2CPayload::fromPacket + ); + + public PregnancyCycleStateS2CPayload { + phase = phase == null ? PregnancyCyclePhase.NORMAL : phase; + } + + private static PregnancyCycleStateS2CPayload fromPacket(boolean enabled, int day, int phaseId) { + return new PregnancyCycleStateS2CPayload(enabled, day, PregnancyCyclePhase.fromNetworkId(phaseId)); + } + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/PregnancyStateS2CPayload.java b/src/main/java/com/nonid/network/PregnancyStateS2CPayload.java new file mode 100644 index 0000000..e4db307 --- /dev/null +++ b/src/main/java/com/nonid/network/PregnancyStateS2CPayload.java @@ -0,0 +1,20 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record PregnancyStateS2CPayload(String pregnantEntityTypeId) implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "pregnancy_state"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.STRING, PregnancyStateS2CPayload::pregnantEntityTypeId, PregnancyStateS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/RepairDestroyedSkinC2SPayload.java b/src/main/java/com/nonid/network/RepairDestroyedSkinC2SPayload.java new file mode 100644 index 0000000..bf7fc32 --- /dev/null +++ b/src/main/java/com/nonid/network/RepairDestroyedSkinC2SPayload.java @@ -0,0 +1,17 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record RepairDestroyedSkinC2SPayload() implements CustomPayload { + public static final Identifier REPAIR_DESTROYED_SKIN_ID = Identifier.of("needsofnature", "repair_destroyed_skin"); + public static final Id ID = new Id(REPAIR_DESTROYED_SKIN_ID); + public static final PacketCodec CODEC = PacketCodec.unit(new RepairDestroyedSkinC2SPayload()); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/ServerConfigRequestC2SPayload.java b/src/main/java/com/nonid/network/ServerConfigRequestC2SPayload.java new file mode 100644 index 0000000..2bb8225 --- /dev/null +++ b/src/main/java/com/nonid/network/ServerConfigRequestC2SPayload.java @@ -0,0 +1,16 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record ServerConfigRequestC2SPayload() implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "server_config_request")); + public static final PacketCodec CODEC = PacketCodec.unit(new ServerConfigRequestC2SPayload()); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/ServerConfigResultS2CPayload.java b/src/main/java/com/nonid/network/ServerConfigResultS2CPayload.java new file mode 100644 index 0000000..aa26a8a --- /dev/null +++ b/src/main/java/com/nonid/network/ServerConfigResultS2CPayload.java @@ -0,0 +1,25 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record ServerConfigResultS2CPayload(boolean success, long revision, String messageKey) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "server_config_result")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.BOOLEAN, + ServerConfigResultS2CPayload::success, + PacketCodecs.LONG, + ServerConfigResultS2CPayload::revision, + PacketCodecs.STRING, + ServerConfigResultS2CPayload::messageKey, + ServerConfigResultS2CPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/ServerConfigUpdateC2SPayload.java b/src/main/java/com/nonid/network/ServerConfigUpdateC2SPayload.java new file mode 100644 index 0000000..0f3449d --- /dev/null +++ b/src/main/java/com/nonid/network/ServerConfigUpdateC2SPayload.java @@ -0,0 +1,23 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record ServerConfigUpdateC2SPayload(long revision, String json) implements CustomPayload { + public static final Id ID = new Id(Identifier.of("needsofnature", "server_config_update")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.LONG, + ServerConfigUpdateC2SPayload::revision, + PacketCodecs.string(262144), + ServerConfigUpdateC2SPayload::json, + ServerConfigUpdateC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/SetPlayerGenderC2SPayload.java b/src/main/java/com/nonid/network/SetPlayerGenderC2SPayload.java new file mode 100644 index 0000000..01b7d40 --- /dev/null +++ b/src/main/java/com/nonid/network/SetPlayerGenderC2SPayload.java @@ -0,0 +1,20 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record SetPlayerGenderC2SPayload(int mask, boolean initialSelection) implements CustomPayload { + public static final Identifier SET_PLAYER_GENDER_ID = Identifier.of("needsofnature", "set_player_gender"); + public static final Id ID = new Id(SET_PLAYER_GENDER_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodecs.INTEGER, SetPlayerGenderC2SPayload::mask, PacketCodecs.BOOLEAN, SetPlayerGenderC2SPayload::initialSelection, SetPlayerGenderC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/SkinRippedInfoRequestS2CPayload.java b/src/main/java/com/nonid/network/SkinRippedInfoRequestS2CPayload.java new file mode 100644 index 0000000..8802591 --- /dev/null +++ b/src/main/java/com/nonid/network/SkinRippedInfoRequestS2CPayload.java @@ -0,0 +1,17 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record SkinRippedInfoRequestS2CPayload() implements CustomPayload { + public static final Identifier ID_RAW = Identifier.of("needsofnature", "skin_ripped_info_request"); + public static final Id ID = new Id(ID_RAW); + public static final PacketCodec CODEC = PacketCodec.unit(new SkinRippedInfoRequestS2CPayload()); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/StartManualPeakC2SPayload.java b/src/main/java/com/nonid/network/StartManualPeakC2SPayload.java new file mode 100644 index 0000000..adda962 --- /dev/null +++ b/src/main/java/com/nonid/network/StartManualPeakC2SPayload.java @@ -0,0 +1,17 @@ +package com.nonid.network; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; + +public record StartManualPeakC2SPayload() implements CustomPayload { + public static final Identifier START_MANUAL_PEAK_ID = Identifier.of("needsofnature", "start_manual_peak"); + public static final Id ID = new Id(START_MANUAL_PEAK_ID); + public static final PacketCodec CODEC = PacketCodec.unit(new StartManualPeakC2SPayload()); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/StopAttackAnimationC2SPayload.java b/src/main/java/com/nonid/network/StopAttackAnimationC2SPayload.java new file mode 100644 index 0000000..29d089b --- /dev/null +++ b/src/main/java/com/nonid/network/StopAttackAnimationC2SPayload.java @@ -0,0 +1,21 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record StopAttackAnimationC2SPayload(UUID instanceId) implements CustomPayload { + public static final Identifier STOP_ATTACK_ANIMATION_ID = Identifier.of("needsofnature", "stop_attack_animation"); + public static final Id ID = new Id(STOP_ATTACK_ANIMATION_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, StopAttackAnimationC2SPayload::instanceId, StopAttackAnimationC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/network/StopVoluntaryAnimationC2SPayload.java b/src/main/java/com/nonid/network/StopVoluntaryAnimationC2SPayload.java new file mode 100644 index 0000000..4ed2d39 --- /dev/null +++ b/src/main/java/com/nonid/network/StopVoluntaryAnimationC2SPayload.java @@ -0,0 +1,21 @@ +package com.nonid.network; + +import java.util.UUID; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.network.packet.CustomPayload.Id; +import net.minecraft.util.Identifier; +import net.minecraft.util.Uuids; + +public record StopVoluntaryAnimationC2SPayload(UUID instanceId) implements CustomPayload { + public static final Identifier STOP_VOLUNTARY_ANIMATION_ID = Identifier.of("needsofnature", "stop_voluntary_animation"); + public static final Id ID = new Id(STOP_VOLUNTARY_ANIMATION_ID); + public static final PacketCodec CODEC = PacketCodec.tuple( + Uuids.PACKET_CODEC, StopVoluntaryAnimationC2SPayload::instanceId, StopVoluntaryAnimationC2SPayload::new + ); + + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/nonid/pack/NonExternalPackCatalog.java b/src/main/java/com/nonid/pack/NonExternalPackCatalog.java new file mode 100644 index 0000000..b21676a --- /dev/null +++ b/src/main/java/com/nonid/pack/NonExternalPackCatalog.java @@ -0,0 +1,99 @@ +package com.nonid.pack; + +import com.nonid.NeedsOfNature; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public final class NonExternalPackCatalog { + private NonExternalPackCatalog() { + } + + public static List listFileNames(Path directory) { + return listPaths(directory).stream().map(NonExternalPackCatalog::fileName).toList(); + } + + public static List resolveHighToLow(Path directory, List configuredOrder) { + List paths = listPaths(directory); + if (paths.isEmpty()) { + return List.of(); + } + + Map pathByName = new LinkedHashMap<>(); + + for (Path path : paths) { + pathByName.put(fileName(path), path); + } + + List orderedNames = normalizeOrder(configuredOrder, List.copyOf(pathByName.keySet())); + List entries = new ArrayList<>(orderedNames.size()); + + for (String name : orderedNames) { + Path path = pathByName.get(name); + if (path != null) { + NonPackRootResolver.resolve(path).ifPresent(root -> entries.add(new NonExternalPackCatalog.Entry(name, path, root))); + } + } + + return List.copyOf(entries); + } + + public static List resolveLowToHigh(Path directory, List configuredOrder) { + List entries = new ArrayList<>(resolveHighToLow(directory, configuredOrder)); + Collections.reverse(entries); + return List.copyOf(entries); + } + + public static List normalizeOrder(List configuredOrder, List discoveredNames) { + List discovered = discoveredNames == null ? List.of() : discoveredNames; + if (discovered.isEmpty()) { + return List.of(); + } + + LinkedHashSet discoveredSet = new LinkedHashSet<>(discovered); + LinkedHashSet ordered = new LinkedHashSet<>(); + if (configuredOrder != null) { + for (String raw : configuredOrder) { + if (raw != null) { + String name = raw.trim(); + if (!name.isEmpty() && discoveredSet.contains(name)) { + ordered.add(name); + } + } + } + } + + ordered.addAll(discovered); + return List.copyOf(ordered); + } + + private static List listPaths(Path directory) { + if (directory != null && Files.isDirectory(directory)) { + try (Stream paths = Files.list(directory)) { + return paths.filter(NonPackRootResolver::isSupportedExternalPackPath) + .sorted((left, right) -> String.CASE_INSENSITIVE_ORDER.compare(fileName(left), fileName(right))) + .toList(); + } catch (IOException e) { + NeedsOfNature.logSetupWarning("[NoN] Failed to list external NoN packs in {}.", directory, e); + return List.of(); + } + } else { + return List.of(); + } + } + + private static String fileName(Path path) { + Path fileName = path == null ? null : path.getFileName(); + return fileName == null ? String.valueOf(path) : fileName.toString(); + } + + public record Entry(String fileName, Path path, NonPackRootResolver.ResolvedRoot root) { + } +} diff --git a/src/main/java/com/nonid/pack/NonExternalPackProvider.java b/src/main/java/com/nonid/pack/NonExternalPackProvider.java new file mode 100644 index 0000000..80d85f9 --- /dev/null +++ b/src/main/java/com/nonid/pack/NonExternalPackProvider.java @@ -0,0 +1,191 @@ +package com.nonid.pack; + +import com.mojang.logging.LogUtils; +import com.nonid.NeedsOfNature; +import com.nonid.NonConfig; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.resource.ResourcePackInfo; +import net.minecraft.resource.ResourcePackPosition; +import net.minecraft.resource.ResourcePackProfile; +import net.minecraft.resource.ResourcePackProvider; +import net.minecraft.resource.ResourcePackSource; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.DirectoryResourcePack.DirectoryBackedFactory; +import net.minecraft.resource.ResourcePackProfile.InsertionPosition; +import net.minecraft.resource.ResourcePackProfile.PackFactory; +import net.minecraft.resource.ZipResourcePack.ZipBackedFactory; +import net.minecraft.text.Text; +import net.minecraft.util.path.PathUtil; +import net.minecraft.util.path.SymlinkEntry; +import net.minecraft.util.path.SymlinkFinder; +import net.minecraft.util.path.SymlinkValidationException; +import org.slf4j.Logger; + +public final class NonExternalPackProvider implements ResourcePackProvider { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String EXTERNAL_PACKS_DIR_NAME = "needsofnature"; + private static final ResourcePackSource NON_SOURCE = ResourcePackSource.create(name -> Text.literal("[NoN] ").append(name), true); + private static final ResourcePackPosition ALWAYS_ENABLED_TOP = new ResourcePackPosition(true, InsertionPosition.TOP, false); + private final Path packsDir; + private final ResourceType type; + private final SymlinkFinder symlinkFinder; + + public NonExternalPackProvider(Path packsDir, ResourceType type, SymlinkFinder symlinkFinder) { + this.packsDir = packsDir; + this.type = type; + this.symlinkFinder = symlinkFinder; + } + + public ResourceType getType() { + return this.type; + } + + public static Path resolveDefaultPacksDir() { + return FabricLoader.getInstance().getGameDir().resolve("needsofnature"); + } + + public static List listExternalPackFileNames(Path dir) { + return NonExternalPackCatalog.listFileNames(dir); + } + + public static List normalizeConfiguredOrder(List configuredOrder, List discoveredNames) { + return NonExternalPackCatalog.normalizeOrder(configuredOrder, discoveredNames); + } + + public void register(Consumer profileAdder) { + try { + PathUtil.createDirectories(this.packsDir); + List discovered = NonExternalPackCatalog.resolveHighToLow(this.packsDir, resolveConfiguredOrder()) + .stream() + .filter(entry -> this.isSymlinkSafe(entry.root())) + .map(entry -> new NonExternalPackProvider.PackCandidate(entry.path(), createPackFactory(entry.root()))) + .toList(); + List normalizedOrder = discovered.stream().map(NonExternalPackProvider.PackCandidate::fileName).toList(); + Map indexByName = indexByName(normalizedOrder); + + for (int i = discovered.size() - 1; i >= 0; i--) { + NonExternalPackProvider.PackCandidate candidate = discovered.get(i); + int rank = indexByName.getOrDefault(candidate.fileName(), Integer.MAX_VALUE); + ResourcePackInfo info = this.createPackInfo(candidate.path(), rank); + ResourcePackProfile profile = ResourcePackProfile.create(info, candidate.packFactory(), this.type, ALWAYS_ENABLED_TOP); + if (profile != null) { + profileAdder.accept(profile); + } + } + } catch (IOException e) { + LOGGER.warn("Failed to list NoN packs in {}", this.packsDir, e); + } + } + + private boolean isSymlinkSafe(NonPackRootResolver.ResolvedRoot root) { + if (root != null && this.symlinkFinder != null) { + List entries = new ArrayList<>(); + + try { + if (root.isZip()) { + if (Files.isSymbolicLink(root.sourcePath())) { + this.symlinkFinder.validate(root.sourcePath(), entries); + } + } else { + this.symlinkFinder.validateRecursively(root.rootPath(), entries); + } + } catch (IOException e) { + LOGGER.warn("Failed to validate NoN pack symlinks in {}, ignoring", root.sourcePath(), e); + return false; + } + + if (!entries.isEmpty()) { + LOGGER.warn("Ignoring potential NoN pack entry: {}", SymlinkValidationException.getMessage(root.sourcePath(), entries)); + return false; + } else { + return true; + } + } else { + return true; + } + } + + private static PackFactory createPackFactory(NonPackRootResolver.ResolvedRoot root) { + if (root.isZip()) { + return (PackFactory)(root.isNested() + ? new NonPrefixedZipResourcePack.Factory(root.sourcePath(), root.zipPrefix()) + : new ZipBackedFactory(root.sourcePath())); + } else { + return new DirectoryBackedFactory(root.rootPath()); + } + } + + private ResourcePackInfo createPackInfo(Path path, int rank) { + String fileName = path.getFileName().toString(); + String channel = this.type == ResourceType.CLIENT_RESOURCES ? "client" : "server"; + String safeFileName = sanitizeForPackId(fileName); + String orderPrefix = formatOrderPrefix(rank); + String id = "needsofnature/" + channel + "/" + orderPrefix + "_" + safeFileName; + return new ResourcePackInfo(id, Text.literal(fileName), NON_SOURCE, Optional.empty()); + } + + private static List resolveConfiguredOrder() { + NonConfig config = NeedsOfNature.getConfig(); + return config != null ? config.getExternalNoNPackLoadOrder() : List.of(); + } + + private static Map indexByName(List names) { + Map index = new LinkedHashMap<>(); + if (names == null) { + return index; + } + + for (int i = 0; i < names.size(); i++) { + index.putIfAbsent(names.get(i), i); + } + + return index; + } + + private static String formatOrderPrefix(int rank) { + int clamped = rank; + if (clamped < 0) { + clamped = 0; + } + + if (clamped > 9999) { + clamped = 9999; + } + + int encoded = 9999 - clamped; + return String.format(Locale.ROOT, "%04d", encoded); + } + + private static String sanitizeForPackId(String fileName) { + if (fileName != null && !fileName.isBlank()) { + StringBuilder out = new StringBuilder(fileName.length()); + + for (int i = 0; i < fileName.length(); i++) { + char c = fileName.charAt(i); + boolean safe = c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '.' || c == '_' || c == '-'; + out.append(safe ? c : '_'); + } + + return out.toString(); + } else { + return "unknown"; + } + } + + private record PackCandidate(Path path, PackFactory packFactory) { + private String fileName() { + Path fileName = this.path.getFileName(); + return fileName == null ? this.path.toString() : fileName.toString(); + } + } +} diff --git a/src/main/java/com/nonid/pack/NonGeneratedAccessoryModelPackProvider.java b/src/main/java/com/nonid/pack/NonGeneratedAccessoryModelPackProvider.java new file mode 100644 index 0000000..be16416 --- /dev/null +++ b/src/main/java/com/nonid/pack/NonGeneratedAccessoryModelPackProvider.java @@ -0,0 +1,147 @@ +package com.nonid.pack; + +import com.nonid.data.NonAccessoryItemRegistry; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; +import net.minecraft.resource.ResourcePackProfile; +import net.minecraft.resource.ResourcePackProvider; +import net.minecraft.resource.ResourceType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public final class NonGeneratedAccessoryModelPackProvider implements ResourcePackProvider { + public void register(Consumer profileAdder) { + Map resources = buildGeneratedResources(); + if (!resources.isEmpty()) { + ResourcePackProfile profile = NonGeneratedInMemoryPack.createProfile( + "needsofnature/client/generated_accessory_models", + Text.literal("NoN 生成的配件模型"), + Text.literal("生成的 NoN 配件物品模型"), + ResourceType.CLIENT_RESOURCES, + resources + ); + if (profile != null) { + profileAdder.accept(profile); + } + } + } + + private static Map buildGeneratedResources() { + Map resources = new LinkedHashMap<>(); + + for (Entry entry : NonAccessoryItemRegistry.getClientModelsByItem().entrySet()) { + Identifier itemId = entry.getKey(); + NonAccessoryItemRegistry.ItemClientModel model = entry.getValue(); + if (model.itemModel() != null) { + addItemDefinition(resources, itemId, model); + } + + if (!model.layers().isEmpty()) { + addGeneratedItemModel(resources, itemId, model.layers()); + List emptyLayers = withoutStoredLiquidLayers(model.layers()); + if (emptyLayers.size() != model.layers().size()) { + addGeneratedItemModel(resources, Identifier.of(itemId.getNamespace(), itemId.getPath() + "_empty"), emptyLayers); + } + } + } + + return resources; + } + + private static void addItemDefinition(Map resources, Identifier itemId, NonAccessoryItemRegistry.ItemClientModel model) { + StringBuilder json = new StringBuilder("{\n \"model\": "); + List emptyLayers = withoutStoredLiquidLayers(model.layers()); + if (emptyLayers.size() != model.layers().size()) { + Identifier emptyModel = Identifier.of(itemId.getNamespace(), "item/" + itemId.getPath() + "_empty"); + json.append("{\n") + .append(" \"type\": \"minecraft:condition\",\n") + .append(" \"property\": \"minecraft:custom_model_data\",\n") + .append(" \"index\": 0,\n") + .append(" \"on_true\": ") + .append(basicModelJson(model.itemModel(), model.layers(), " ")) + .append(",\n \"on_false\": ") + .append(basicModelJson(emptyModel, emptyLayers, " ")) + .append("\n }"); + } else { + json.append(basicModelJson(model.itemModel(), model.layers(), " ")); + } + + json.append("\n}\n"); + resources.put(Identifier.of(itemId.getNamespace(), "items/" + itemId.getPath() + ".json"), json.toString().getBytes(StandardCharsets.UTF_8)); + } + + private static String basicModelJson(Identifier modelId, List layers, String indent) { + if (layers.isEmpty()) { + return "{\n" + indent + " \"type\": \"minecraft:empty\"\n" + indent + "}"; + } + + StringBuilder json = new StringBuilder("{\n") + .append(indent) + .append(" \"type\": \"minecraft:model\",\n") + .append(indent) + .append(" \"model\": \"") + .append(modelId) + .append('"'); + int lastTintIndex = lastTintIndex(layers); + if (lastTintIndex >= 0) { + json.append(",\n").append(indent).append(" \"tints\": [\n"); + + for (int i = 0; i <= lastTintIndex; i++) { + json.append(indent).append(" ").append(tintJson(layers.get(i).tintMode())); + if (i < lastTintIndex) { + json.append(','); + } + + json.append('\n'); + } + + json.append(indent).append(" ]\n").append(indent).append('}'); + } else { + json.append('\n').append(indent).append('}'); + } + + return json.toString(); + } + + private static void addGeneratedItemModel(Map resources, Identifier itemId, List layers) { + StringBuilder json = new StringBuilder("{\n \"parent\": \"minecraft:item/generated\",\n \"textures\": {\n"); + + for (int i = 0; i < layers.size(); i++) { + json.append(" \"layer").append(i).append("\": \"").append(layers.get(i).texture()).append('"'); + if (i + 1 < layers.size()) { + json.append(','); + } + + json.append('\n'); + } + + json.append(" }\n}\n"); + resources.put(Identifier.of(itemId.getNamespace(), "models/item/" + itemId.getPath() + ".json"), json.toString().getBytes(StandardCharsets.UTF_8)); + } + + private static int lastTintIndex(List layers) { + for (int i = layers.size() - 1; i >= 0; i--) { + if (layers.get(i).tintMode() != NonAccessoryItemRegistry.TintMode.NONE) { + return i; + } + } + + return -1; + } + + private static List withoutStoredLiquidLayers(List layers) { + return layers.stream().filter(layer -> layer.tintMode() != NonAccessoryItemRegistry.TintMode.STORED_LIQUID).toList(); + } + + private static String tintJson(NonAccessoryItemRegistry.TintMode tintMode) { + return switch (tintMode) { + case DYE -> "{ \"type\": \"minecraft:dye\", \"default\": -1 }"; + case STORED_LIQUID -> "{ \"type\": \"minecraft:custom_model_data\", \"index\": 0, \"default\": -1 }"; + case NONE -> "{ \"type\": \"minecraft:constant\", \"value\": -1 }"; + }; + } +} diff --git a/src/main/java/com/nonid/pack/NonGeneratedAccessoryTagPackProvider.java b/src/main/java/com/nonid/pack/NonGeneratedAccessoryTagPackProvider.java new file mode 100644 index 0000000..0a1bdfc --- /dev/null +++ b/src/main/java/com/nonid/pack/NonGeneratedAccessoryTagPackProvider.java @@ -0,0 +1,72 @@ +package com.nonid.pack; + +import com.nonid.NonTrinketsIntegration; +import com.nonid.data.NonAccessoryItemRegistry; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import net.minecraft.resource.ResourcePackProfile; +import net.minecraft.resource.ResourcePackProvider; +import net.minecraft.resource.ResourceType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public final class NonGeneratedAccessoryTagPackProvider implements ResourcePackProvider { + public void register(Consumer profileAdder) { + if (NonTrinketsIntegration.isTrinketsLoaded()) { + Map resources = buildGeneratedResources(); + if (!resources.isEmpty()) { + ResourcePackProfile profile = NonGeneratedInMemoryPack.createProfile( + "needsofnature/server/generated_accessory_tags", + Text.literal("NoN 生成的配件标签"), + Text.literal("生成的 NoN 配件饰品标签"), + ResourceType.SERVER_DATA, + resources + ); + if (profile != null) { + profileAdder.accept(profile); + } + } + } + } + + private static Map buildGeneratedResources() { + Map resources = new LinkedHashMap<>(); + addSlotTag(resources, "legs/v"); + addSlotTag(resources, "legs/a"); + addSlotTag(resources, "legs/d"); + addItemTag(resources, "minecraft", "dyeable", NonAccessoryItemRegistry.getDyeableItemIds()); + List accessoryIds = NonAccessoryItemRegistry.getRegisteredItemIds(); + addItemTag(resources, "minecraft", "enchantable/equippable", accessoryIds); + addItemTag(resources, "minecraft", "enchantable/vanishing", accessoryIds); + addItemTag(resources, "minecraft", "enchantable/durability", NonAccessoryItemRegistry.getDurabilityEnchantableItemIds()); + return resources; + } + + private static void addSlotTag(Map resources, String slotId) { + List itemIds = NonAccessoryItemRegistry.getItemIdsForTrinketsSlot(slotId); + addItemTag(resources, "trinkets", slotId, itemIds); + } + + private static void addItemTag(Map resources, String namespace, String path, List itemIds) { + if (!itemIds.isEmpty()) { + String resourcePath = "tags/item/" + path + ".json"; + StringBuilder json = new StringBuilder(); + json.append("{\n \"replace\": false,\n \"values\": [\n"); + + for (int i = 0; i < itemIds.size(); i++) { + json.append(" \"").append(itemIds.get(i)).append("\""); + if (i + 1 < itemIds.size()) { + json.append(','); + } + + json.append('\n'); + } + + json.append(" ]\n}\n"); + resources.put(Identifier.of(namespace, resourcePath), json.toString().getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/src/main/java/com/nonid/pack/NonGeneratedCemPackProvider.java b/src/main/java/com/nonid/pack/NonGeneratedCemPackProvider.java new file mode 100644 index 0000000..907110f --- /dev/null +++ b/src/main/java/com/nonid/pack/NonGeneratedCemPackProvider.java @@ -0,0 +1,425 @@ +package com.nonid.pack; + +import com.nonid.NeedsOfNature; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.Map.Entry; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.resource.ResourcePackProfile; +import net.minecraft.resource.ResourcePackProvider; +import net.minecraft.resource.ResourceType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public final class NonGeneratedCemPackProvider implements ResourcePackProvider { + private static final String CEM_ROOT = "assets/minecraft/optifine/cem/"; + private static final byte[] VANILLA_FALLBACK_JEM_BYTES = "{\n \"textureSize\": [64, 64],\n \"models\": []\n}\n".getBytes(StandardCharsets.UTF_8); + private final Path packsDir; + + public NonGeneratedCemPackProvider(Path packsDir) { + this.packsDir = packsDir; + } + + public void register(Consumer profileAdder) { + Map generatedResources = this.buildGeneratedResources(); + if (!generatedResources.isEmpty()) { + ResourcePackProfile profile = NonGeneratedInMemoryPack.createProfile( + "needsofnature/client/generated_cem", + Text.literal("NoN 生成的 CEM 规则"), + Text.literal("生成的 NoN CEM 性别与年龄规则"), + ResourceType.CLIENT_RESOURCES, + generatedResources + ); + if (profile != null) { + profileAdder.accept(profile); + } + } + } + + private Map buildGeneratedResources() { + List orderedSources = listOrderedScanSources(this.packsDir); + if (orderedSources.isEmpty()) { + return Map.of(); + } + + Map resolved = new LinkedHashMap<>(); + + for (NonGeneratedCemPackProvider.ScanSource source : orderedSources) { + NonGeneratedCemPackProvider.PackScan scan = scanSource(source); + if (!scan.isEmpty()) { + LinkedHashSet bases = new LinkedHashSet<>(); + bases.addAll(scan.explicitProperties); + bases.addAll(scan.variants.keySet()); + + for (String base : bases) { + if (!resolved.containsKey(base)) { + if (scan.explicitProperties.contains(base)) { + resolved.put(base, NonGeneratedCemPackProvider.Resolution.explicit()); + } else { + NonGeneratedCemPackProvider.CemVariants variants = scan.variants.get(base); + if (variants != null && variants.needsGeneratedProperties()) { + resolved.put(base, NonGeneratedCemPackProvider.Resolution.generated(variants)); + } + } + } + } + } + } + + if (resolved.isEmpty()) { + return Map.of(); + } + + Map generated = new LinkedHashMap<>(); + + for (Entry entry : resolved.entrySet()) { + String base = entry.getKey(); + NonGeneratedCemPackProvider.Resolution resolution = entry.getValue(); + if (resolution.kind != NonGeneratedCemPackProvider.ResolutionKind.EXPLICIT) { + generated.put(cemIdentifier(base + ".properties"), generatedPropertiesBytes(resolution.variants)); + generated.put(cemIdentifier(base + ".jem"), VANILLA_FALLBACK_JEM_BYTES); + putGeneratedVariant(generated, base, 3, resolution.variants.adultPrimaryBytes()); + putGeneratedVariant(generated, base, 2, resolution.variants.adultTwo); + putGeneratedVariant(generated, base, 5, resolution.variants.babyPrimaryBytes()); + putGeneratedVariant(generated, base, 4, resolution.variants.babyTwo); + } + } + + return generated; + } + + private static void putGeneratedVariant(Map generated, String base, int suffix, byte[] bytes) { + if (bytes != null && bytes.length != 0) { + generated.put(cemIdentifier(base + suffix + ".jem"), bytes); + } + } + + private static List listOrderedScanSources(Path externalDir) { + List ordered = new ArrayList<>(); + ordered.addAll(listOrderedExternalPackSources(externalDir)); + resolveBuiltinCemRoot().ifPresent(cemRoot -> ordered.add(NonGeneratedCemPackProvider.ScanSource.cemRoot("builtin", cemRoot))); + return List.copyOf(ordered); + } + + private static List listOrderedExternalPackSources(Path dir) { + List ordered = new ArrayList<>(); + + for (NonExternalPackCatalog.Entry entry : NonExternalPackCatalog.resolveHighToLow(dir, NeedsOfNature.getConfig().getExternalNoNPackLoadOrder())) { + ordered.add(NonGeneratedCemPackProvider.ScanSource.packRoot(entry.fileName(), entry.root())); + } + + return List.copyOf(ordered); + } + + private static Optional resolveBuiltinCemRoot() { + Optional container = FabricLoader.getInstance().getModContainer("needsofnature"); + return container.isEmpty() + ? Optional.empty() + : container.get().findPath("assets/minecraft/optifine/cem/".substring(0, "assets/minecraft/optifine/cem/".length() - 1)); + } + + private static NonGeneratedCemPackProvider.PackScan scanSource(NonGeneratedCemPackProvider.ScanSource source) { + if (source == null) { + return NonGeneratedCemPackProvider.PackScan.EMPTY; + } + + try { + if (source.kind == NonGeneratedCemPackProvider.ScanSourceKind.CEM_ROOT) { + return scanCemRoot(source.path); + } else if (source.packRoot == null) { + return NonGeneratedCemPackProvider.PackScan.EMPTY; + } else { + return source.packRoot.isZip() ? scanZipPack(source.packRoot) : scanDirectoryPack(source.packRoot.rootPath()); + } + } catch (IOException e) { + NeedsOfNature.LOGGER.debug("[NoN] Failed to scan {} for generated CEM rules.", source.debugName, e); + return NonGeneratedCemPackProvider.PackScan.EMPTY; + } + } + + private static NonGeneratedCemPackProvider.PackScan scanDirectoryPack(Path packPath) throws IOException { + Path cemRoot = packPath.resolve("assets/minecraft/optifine/cem/"); + return !Files.isDirectory(cemRoot) ? NonGeneratedCemPackProvider.PackScan.EMPTY : scanCemRoot(cemRoot); + } + + private static NonGeneratedCemPackProvider.PackScan scanCemRoot(Path cemRoot) throws IOException { + Map variants = new LinkedHashMap<>(); + Set explicitProperties = new LinkedHashSet<>(); + + try (Stream paths = Files.walk(cemRoot)) { + for (Path path : paths.filter(x$0 -> Files.isRegularFile(x$0)).toList()) { + String relative = cemRoot.relativize(path).toString().replace('\\', '/'); + byte[] bytes = relative.endsWith(".jem") ? Files.readAllBytes(path) : null; + processCemPath(relative, bytes, explicitProperties, variants); + } + } + + return new NonGeneratedCemPackProvider.PackScan(explicitProperties, variants); + } + + private static NonGeneratedCemPackProvider.PackScan scanZipPack(NonPackRootResolver.ResolvedRoot packRoot) throws IOException { + Set explicitProperties = new LinkedHashSet<>(); + Map variants = new LinkedHashMap<>(); + + try (ZipFile zipFile = new ZipFile(packRoot.sourcePath().toFile())) { + Enumeration entries = zipFile.entries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory()) { + String name = packRoot.stripZipPrefix(entry.getName()); + if (name != null && name.startsWith("assets/minecraft/optifine/cem/")) { + String relative = name.substring("assets/minecraft/optifine/cem/".length()); + byte[] bytes = relative.endsWith(".jem") ? zipFile.getInputStream(entry).readAllBytes() : null; + processCemPath(relative, bytes, explicitProperties, variants); + } + } + } + } + + return new NonGeneratedCemPackProvider.PackScan(explicitProperties, variants); + } + + private static void processCemPath( + String relativePath, byte[] bytes, Set explicitProperties, Map variants + ) { + if (relativePath != null && !relativePath.isBlank()) { + String normalized = relativePath.replace('\\', '/'); + if (normalized.endsWith(".properties")) { + String base = normalized.substring(0, normalized.length() - ".properties".length()); + if (!base.isBlank()) { + explicitProperties.add(base); + } + } else if (normalized.endsWith(".jem")) { + String stem = normalized.substring(0, normalized.length() - ".jem".length()); + boolean baby = false; + if (stem.endsWith("_baby") && stem.length() > "_baby".length()) { + stem = stem.substring(0, stem.length() - "_baby".length()); + baby = true; + } + + String base; + NonGeneratedCemPackProvider.CemVariantKind kind; + if (stem.endsWith("1") && stem.length() > 1) { + base = stem.substring(0, stem.length() - 1); + kind = NonGeneratedCemPackProvider.CemVariantKind.ONE; + } else if (stem.endsWith("2") && stem.length() > 1) { + base = stem.substring(0, stem.length() - 1); + kind = NonGeneratedCemPackProvider.CemVariantKind.TWO; + } else { + base = stem; + kind = NonGeneratedCemPackProvider.CemVariantKind.BASE; + } + + if (!base.isBlank()) { + NonGeneratedCemPackProvider.CemVariants entry = variants.computeIfAbsent(base, key -> new NonGeneratedCemPackProvider.CemVariants()); + entry.mark(kind, baby, bytes); + } + } + } + } + + private static Identifier cemIdentifier(String relativePath) { + return Identifier.of("minecraft", "optifine/cem/" + relativePath); + } + + private static byte[] generatedPropertiesBytes(NonGeneratedCemPackProvider.CemVariants variants) { + StringBuilder content = new StringBuilder(); + int rule = 1; + if (variants.hasAdultPrimary()) { + rule = appendPrimaryGenderRules(content, rule, 3, false); + } + + if (variants.hasAdultTwo) { + rule = appendAgeGenderRule(content, rule, 2, false, 2, true); + } + + if (variants.hasBabyPrimary()) { + rule = appendPrimaryGenderRules(content, rule, 5, true); + } + + if (variants.hasBabyTwo) { + appendAgeGenderRule(content, rule, 4, true, 2, true); + } + + return content.toString().getBytes(StandardCharsets.UTF_8); + } + + private static int appendPrimaryGenderRules(StringBuilder content, int rule, int modelSuffix, boolean baby) { + rule = appendAgeGenderRule(content, rule, modelSuffix, baby, 1, true); + return appendAgeGenderRule(content, rule, modelSuffix, baby, 3, true); + } + + private static int appendAgeGenderRule(StringBuilder content, int rule, int modelSuffix, boolean baby, int gender, boolean enabled) { + if (!enabled) { + return rule; + } + + appendRuleHeader(content, rule, modelSuffix); + content.append("nbt.").append(rule).append('.').append("NeedsOfNatureGender").append('=').append(gender).append('\n'); + content.append("baby.").append(rule).append('=').append(baby).append('\n'); + return rule + 1; + } + + private static void appendRuleHeader(StringBuilder content, int rule, int modelSuffix) { + if (!content.isEmpty()) { + content.append('\n'); + } + + content.append("models.").append(rule).append('=').append(modelSuffix).append('\n'); + } + + private enum CemVariantKind { + BASE, + ONE, + TWO; + } + + private static final class CemVariants { + private boolean hasAdultBase; + private boolean hasAdultOne; + private boolean hasAdultTwo; + private boolean hasBabyBase; + private boolean hasBabyOne; + private boolean hasBabyTwo; + private byte[] adultBase; + private byte[] adultOne; + private byte[] adultTwo; + private byte[] babyBase; + private byte[] babyOne; + private byte[] babyTwo; + + private void mark(NonGeneratedCemPackProvider.CemVariantKind kind, boolean baby, byte[] bytes) { + if (baby) { + switch (kind) { + case BASE: + this.hasBabyBase = true; + this.babyBase = bytes; + break; + case ONE: + this.hasBabyOne = true; + this.babyOne = bytes; + break; + case TWO: + this.hasBabyTwo = true; + this.babyTwo = bytes; + } + } else { + switch (kind) { + case BASE: + this.hasAdultBase = true; + this.adultBase = bytes; + break; + case ONE: + this.hasAdultOne = true; + this.adultOne = bytes; + break; + case TWO: + this.hasAdultTwo = true; + this.adultTwo = bytes; + } + } + } + + private boolean hasAdultPrimary() { + return this.hasAdultOne || this.hasAdultBase; + } + + private boolean hasBabyPrimary() { + return this.hasBabyOne || this.hasBabyBase; + } + + private byte[] adultPrimaryBytes() { + return this.adultOne != null ? this.adultOne : this.adultBase; + } + + private byte[] babyPrimaryBytes() { + return this.babyOne != null ? this.babyOne : this.babyBase; + } + + private boolean needsGeneratedProperties() { + return this.hasAdultModelRules() || this.hasBabyModelRules(); + } + + private boolean hasAdultModelRules() { + return this.hasAdultBase || this.hasAdultOne || this.hasAdultTwo; + } + + private boolean hasBabyModelRules() { + return this.hasBabyBase || this.hasBabyOne || this.hasBabyTwo; + } + } + + private record PackScan(Set explicitProperties, Map variants) { + private static final NonGeneratedCemPackProvider.PackScan EMPTY = new NonGeneratedCemPackProvider.PackScan(Set.of(), Map.of()); + + private boolean isEmpty() { + return this.explicitProperties.isEmpty() && this.variants.isEmpty(); + } + } + + private static final class Resolution { + private final NonGeneratedCemPackProvider.ResolutionKind kind; + private final NonGeneratedCemPackProvider.CemVariants variants; + + private Resolution(NonGeneratedCemPackProvider.ResolutionKind kind, NonGeneratedCemPackProvider.CemVariants variants) { + this.kind = kind; + this.variants = variants; + } + + private static NonGeneratedCemPackProvider.Resolution explicit() { + return new NonGeneratedCemPackProvider.Resolution(NonGeneratedCemPackProvider.ResolutionKind.EXPLICIT, null); + } + + private static NonGeneratedCemPackProvider.Resolution generated(NonGeneratedCemPackProvider.CemVariants variants) { + return new NonGeneratedCemPackProvider.Resolution(NonGeneratedCemPackProvider.ResolutionKind.GENERATED, variants); + } + } + + private enum ResolutionKind { + EXPLICIT, + GENERATED; + } + + private static final class ScanSource { + private final NonGeneratedCemPackProvider.ScanSourceKind kind; + private final String debugName; + private final Path path; + private final NonPackRootResolver.ResolvedRoot packRoot; + + private ScanSource(NonGeneratedCemPackProvider.ScanSourceKind kind, String debugName, Path path, NonPackRootResolver.ResolvedRoot packRoot) { + this.kind = kind; + this.debugName = debugName; + this.path = path; + this.packRoot = packRoot; + } + + private static NonGeneratedCemPackProvider.ScanSource packRoot(String debugName, NonPackRootResolver.ResolvedRoot packRoot) { + return new NonGeneratedCemPackProvider.ScanSource(NonGeneratedCemPackProvider.ScanSourceKind.PACK_ROOT, debugName, null, packRoot); + } + + private static NonGeneratedCemPackProvider.ScanSource cemRoot(String debugName, Path path) { + return new NonGeneratedCemPackProvider.ScanSource(NonGeneratedCemPackProvider.ScanSourceKind.CEM_ROOT, debugName, path, null); + } + } + + private enum ScanSourceKind { + PACK_ROOT, + CEM_ROOT; + } +} diff --git a/src/main/java/com/nonid/pack/NonGeneratedInMemoryPack.java b/src/main/java/com/nonid/pack/NonGeneratedInMemoryPack.java new file mode 100644 index 0000000..77cdda0 --- /dev/null +++ b/src/main/java/com/nonid/pack/NonGeneratedInMemoryPack.java @@ -0,0 +1,138 @@ +package com.nonid.pack; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.Map.Entry; +import net.minecraft.resource.AbstractFileResourcePack; +import net.minecraft.resource.InputSupplier; +import net.minecraft.resource.PackVersion; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.ResourcePackInfo; +import net.minecraft.resource.ResourcePackPosition; +import net.minecraft.resource.ResourcePackProfile; +import net.minecraft.resource.ResourcePackSource; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.ResourcePack.ResultConsumer; +import net.minecraft.resource.ResourcePackProfile.InsertionPosition; +import net.minecraft.resource.ResourcePackProfile.Metadata; +import net.minecraft.resource.ResourcePackProfile.PackFactory; +import net.minecraft.resource.metadata.PackResourceMetadata; +import net.minecraft.resource.metadata.ResourceMetadataSerializer; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.dynamic.Range; + +final class NonGeneratedInMemoryPack { + private static final ResourcePackSource NON_SOURCE = ResourcePackSource.create(name -> Text.literal("[NoN] ").append(name), true); + private static final ResourcePackPosition ALWAYS_ENABLED_PINNED_TOP = new ResourcePackPosition(true, InsertionPosition.TOP, true); + + private NonGeneratedInMemoryPack() { + } + + static ResourcePackProfile createProfile(String id, Text displayName, Text description, ResourceType type, Map resources) { + if (resources != null && !resources.isEmpty()) { + ResourcePackInfo info = new ResourcePackInfo(id, displayName, NON_SOURCE, Optional.empty()); + return ResourcePackProfile.create( + info, new NonGeneratedInMemoryPack.GeneratedPackFactory(type, description, resources), type, ALWAYS_ENABLED_PINNED_TOP + ); + } else { + return null; + } + } + + private static final class GeneratedPackFactory implements PackFactory { + private final ResourceType type; + private final Text description; + private final Map resources; + + private GeneratedPackFactory(ResourceType type, Text description, Map resources) { + this.type = type; + this.description = description; + this.resources = Map.copyOf(resources); + } + + public ResourcePack open(ResourcePackInfo info) { + return new NonGeneratedInMemoryPack.GeneratedResourcePack(info, this.type, this.description, this.resources); + } + + public ResourcePack openWithOverlays(ResourcePackInfo info, Metadata metadata) { + return this.open(info); + } + } + + private static final class GeneratedResourcePack extends AbstractFileResourcePack { + private final ResourceType type; + private final Map resources; + private final Set namespaces; + private final PackResourceMetadata metadata; + private final byte[] packMcmeta; + + private GeneratedResourcePack(ResourcePackInfo info, ResourceType type, Text description, Map resources) { + super(info); + this.type = type; + this.resources = resources; + this.namespaces = namespaces(resources); + this.metadata = new PackResourceMetadata(description, new Range(PackVersion.of(75))); + this.packMcmeta = ("{\"pack\":{\"description\":\"" + escapeJson(description.getString()) + "\",\"pack_format\":75}}").getBytes(StandardCharsets.UTF_8); + } + + public InputSupplier openRoot(String... segments) { + return segments.length == 1 && "pack.mcmeta".equals(segments[0]) ? () -> new ByteArrayInputStream(this.packMcmeta) : null; + } + + public InputSupplier open(ResourceType type, Identifier id) { + if (type != this.type) { + return null; + } + + byte[] bytes = this.resources.get(id); + return bytes == null ? null : () -> new ByteArrayInputStream(bytes); + } + + public void findResources(ResourceType type, String namespace, String prefix, ResultConsumer consumer) { + if (type == this.type && consumer != null) { + for (Entry entry : this.resources.entrySet()) { + Identifier id = entry.getKey(); + if (id.getNamespace().equals(namespace) && id.getPath().startsWith(prefix)) { + byte[] bytes = entry.getValue(); + consumer.accept(id, (InputSupplier)() -> new ByteArrayInputStream(bytes)); + } + } + } + } + + public Set getNamespaces(ResourceType type) { + return type == this.type ? this.namespaces : Set.of(); + } + + public T parseMetadata(ResourceMetadataSerializer metadataSerializer) { + if (metadataSerializer == PackResourceMetadata.getSerializerFor(this.type)) { + return (T)this.metadata; + } else { + return (T)(metadataSerializer == PackResourceMetadata.DESCRIPTION_SERIALIZER ? this.metadata.description() : null); + } + } + + public void close() { + } + + private static Set namespaces(Map resources) { + LinkedHashSet out = new LinkedHashSet<>(); + + for (Identifier id : resources.keySet()) { + out.add(id.getNamespace()); + } + + return out.isEmpty() ? Set.of() : Set.copyOf(out); + } + + private static String escapeJson(String value) { + return value != null && !value.isEmpty() ? value.replace("\\", "\\\\").replace("\"", "\\\"") : ""; + } + } +} diff --git a/src/main/java/com/nonid/pack/NonPackRootResolver.java b/src/main/java/com/nonid/pack/NonPackRootResolver.java new file mode 100644 index 0000000..36bc9e2 --- /dev/null +++ b/src/main/java/com/nonid/pack/NonPackRootResolver.java @@ -0,0 +1,237 @@ +package com.nonid.pack; + +import com.nonid.NeedsOfNature; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.jetbrains.annotations.Nullable; + +public final class NonPackRootResolver { + private static final Set WARNED_NESTED_PACKS = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_INVALID_PACKS = ConcurrentHashMap.newKeySet(); + + private NonPackRootResolver() { + } + + public static boolean isSupportedExternalPackPath(Path path) { + if (path == null) { + return false; + } + + if (Files.isDirectory(path)) { + return true; + } + + Path fileName = path.getFileName(); + return fileName == null ? false : fileName.toString().toLowerCase(Locale.ROOT).endsWith(".zip"); + } + + public static Optional resolve(Path path) { + return resolve(path, true); + } + + public static Optional resolve(Path path, boolean warn) { + if (path == null || !Files.exists(path)) { + return Optional.empty(); + } + + if (Files.isDirectory(path)) { + return resolveDirectory(path, warn); + } + + Path fileName = path.getFileName(); + String lowerName = fileName == null ? "" : fileName.toString().toLowerCase(Locale.ROOT); + return !lowerName.endsWith(".zip") ? Optional.empty() : resolveZip(path, warn); + } + + private static Optional resolveDirectory(Path path, boolean warn) { + if (isPackRootDirectory(path)) { + return Optional.of(NonPackRootResolver.ResolvedRoot.directory(path, path, "", null)); + } + + LinkedHashSet candidates = new LinkedHashSet<>(); + + try (Stream entries = Files.list(path)) { + entries.filter(x$0 -> Files.isDirectory(x$0)).filter(NonPackRootResolver::isPackRootDirectory).forEach(candidates::add); + } catch (IOException e) { + if (warn) { + warnInvalid(path, "failed to inspect directory pack root"); + } + + return Optional.empty(); + } + + if (candidates.size() == 1) { + Path nestedRoot = candidates.iterator().next(); + String folder = fileName(nestedRoot); + if (warn) { + warnNested(path, folder); + } + + return Optional.of(NonPackRootResolver.ResolvedRoot.directory(path, nestedRoot, "", folder)); + } else { + if (warn) { + if (candidates.size() > 1) { + warnInvalid(path, "contains multiple one-level nested pack roots"); + } else { + warnInvalid(path, "does not contain assets/, data/, or pack.mcmeta at the root or inside one wrapper folder"); + } + } + + return Optional.empty(); + } + } + + private static Optional resolveZip(Path path, boolean warn) { + try (ZipFile zip = new ZipFile(path.toFile())) { + boolean rootMatches = false; + LinkedHashSet candidates = new LinkedHashSet<>(); + Enumeration entries = zip.entries(); + + while (entries.hasMoreElements()) { + String normalized = normalizeZipEntryName(entries.nextElement().getName()); + if (normalized != null && !normalized.isBlank()) { + if (isPackRootEntry(normalized)) { + rootMatches = true; + break; + } + + int slash = normalized.indexOf(47); + if (slash > 0 && slash < normalized.length() - 1) { + String top = normalized.substring(0, slash); + String nested = normalized.substring(slash + 1); + if (!isIgnoredTopLevelFolder(top) && isPackRootEntry(nested)) { + candidates.add(top); + } + } + } + } + + if (rootMatches) { + return Optional.of(NonPackRootResolver.ResolvedRoot.zip(path, "", null)); + } + + if (candidates.size() == 1) { + String folder = candidates.iterator().next(); + if (warn) { + warnNested(path, folder); + } + + return Optional.of(NonPackRootResolver.ResolvedRoot.zip(path, folder + "/", folder)); + } + + if (warn) { + if (candidates.size() > 1) { + warnInvalid(path, "contains multiple one-level nested pack roots"); + } else { + warnInvalid(path, "does not contain assets/, data/, or pack.mcmeta at the root or inside one wrapper folder"); + } + } + } catch (IOException e) { + if (warn) { + warnInvalid(path, "failed to inspect zip pack root"); + } + } + + return Optional.empty(); + } + + private static boolean isPackRootDirectory(Path path) { + return Files.isRegularFile(path.resolve("pack.mcmeta")) || Files.isDirectory(path.resolve("assets")) || Files.isDirectory(path.resolve("data")); + } + + private static boolean isPackRootEntry(String entryPath) { + return entryPath != null && !entryPath.isBlank() + ? "pack.mcmeta".equals(entryPath) || entryPath.startsWith("assets/") || entryPath.startsWith("data/") + : false; + } + + private static boolean isIgnoredTopLevelFolder(String folder) { + return "__MACOSX".equals(folder) || folder.startsWith("."); + } + + @Nullable + public static String normalizeZipEntryName(@Nullable String raw) { + if (raw == null) { + return null; + } + + String normalized = raw.replace('\\', '/'); + + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + + return !normalized.contains("../") && !normalized.equals("..") && !normalized.startsWith("../") ? normalized : null; + } + + private static void warnNested(Path path, String folder) { + String key = normalizedKey(path) + "|" + folder; + if (WARNED_NESTED_PACKS.add(key)) { + NeedsOfNature.logSetupWarning( + "[NoN] External pack '{}' has its pack contents inside the folder '{}'. NoN will load it, but released packs should place assets/, data/, and pack.mcmeta directly at the pack root.", + fileName(path), + folder + ); + } + } + + private static void warnInvalid(Path path, String reason) { + String key = normalizedKey(path) + "|" + reason; + if (WARNED_INVALID_PACKS.add(key)) { + NeedsOfNature.logSetupWarning("[NoN] Ignoring external NoN pack '{}': {}.", fileName(path), reason); + } + } + + private static String normalizedKey(Path path) { + return path == null ? "" : path.toAbsolutePath().normalize().toString(); + } + + private static String fileName(Path path) { + return path != null && path.getFileName() != null ? path.getFileName().toString() : String.valueOf(path); + } + + public record ResolvedRoot(Path sourcePath, Path rootPath, String zipPrefix, @Nullable String wrapperFolder) { + static NonPackRootResolver.ResolvedRoot directory(Path sourcePath, Path rootPath, String zipPrefix, @Nullable String wrapperFolder) { + return new NonPackRootResolver.ResolvedRoot(sourcePath, rootPath, zipPrefix, wrapperFolder); + } + + static NonPackRootResolver.ResolvedRoot zip(Path sourcePath, String zipPrefix, @Nullable String wrapperFolder) { + return new NonPackRootResolver.ResolvedRoot(sourcePath, sourcePath, zipPrefix == null ? "" : zipPrefix, wrapperFolder); + } + + public boolean isZip() { + return Files.isRegularFile(this.sourcePath); + } + + public boolean isNested() { + return this.wrapperFolder != null && !this.wrapperFolder.isBlank(); + } + + public String prefixPath(String relativePath) { + String relative = relativePath == null ? "" : relativePath.replace('\\', '/'); + return this.zipPrefix != null && !this.zipPrefix.isBlank() ? this.zipPrefix + relative : relative; + } + + @Nullable + public String stripZipPrefix(String entryPath) { + String normalized = NonPackRootResolver.normalizeZipEntryName(entryPath); + if (normalized == null) { + return null; + } else if (this.zipPrefix == null || this.zipPrefix.isBlank()) { + return normalized; + } else { + return !normalized.startsWith(this.zipPrefix) ? null : normalized.substring(this.zipPrefix.length()); + } + } + } +} diff --git a/src/main/java/com/nonid/pack/NonPrefixedZipResourcePack.java b/src/main/java/com/nonid/pack/NonPrefixedZipResourcePack.java new file mode 100644 index 0000000..a13d2b9 --- /dev/null +++ b/src/main/java/com/nonid/pack/NonPrefixedZipResourcePack.java @@ -0,0 +1,164 @@ +package com.nonid.pack; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import net.minecraft.resource.AbstractFileResourcePack; +import net.minecraft.resource.InputSupplier; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.ResourcePackInfo; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.ResourcePack.ResultConsumer; +import net.minecraft.resource.ResourcePackProfile.Metadata; +import net.minecraft.resource.ResourcePackProfile.PackFactory; +import net.minecraft.util.Identifier; + +final class NonPrefixedZipResourcePack extends AbstractFileResourcePack { + private final ZipFile zipFile; + private final String rootPrefix; + + private NonPrefixedZipResourcePack(ResourcePackInfo info, Path zipPath, String rootPrefix) throws IOException { + super(info); + this.zipFile = new ZipFile(zipPath.toFile()); + this.rootPrefix = rootPrefix != null && !rootPrefix.isBlank() ? rootPrefix : ""; + } + + public InputSupplier openRoot(String... segments) { + String relative = joinRootSegments(segments); + if (relative == null) { + return null; + } + + ZipEntry entry = this.zipFile.getEntry(this.rootPrefix + relative); + return entry != null && !entry.isDirectory() ? InputSupplier.create(this.zipFile, entry) : null; + } + + public InputSupplier open(ResourceType type, Identifier id) { + if (type != null && id != null) { + String path = this.rootPrefix + type.getDirectory() + "/" + id.getNamespace() + "/" + id.getPath(); + ZipEntry entry = this.zipFile.getEntry(path); + return entry != null && !entry.isDirectory() ? InputSupplier.create(this.zipFile, entry) : null; + } else { + return null; + } + } + + public void findResources(ResourceType type, String namespace, String prefix, ResultConsumer consumer) { + if (type != null && namespace != null && prefix != null && consumer != null) { + String scanPrefix = this.rootPrefix + type.getDirectory() + "/" + namespace + "/" + prefix; + Enumeration entries = this.zipFile.entries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry != null && !entry.isDirectory()) { + String normalized = NonPackRootResolver.normalizeZipEntryName(entry.getName()); + if (normalized != null && normalized.startsWith(scanPrefix)) { + String idPath = normalized.substring((this.rootPrefix + type.getDirectory() + "/" + namespace + "/").length()); + if (!idPath.isBlank()) { + Identifier id = Identifier.tryParse(namespace, idPath); + if (id != null) { + consumer.accept(id, InputSupplier.create(this.zipFile, entry)); + } + } + } + } + } + } + } + + public Set getNamespaces(ResourceType type) { + if (type == null) { + return Set.of(); + } + + String typePrefix = this.rootPrefix + type.getDirectory() + "/"; + LinkedHashSet namespaces = new LinkedHashSet<>(); + Enumeration entries = this.zipFile.entries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry != null && !entry.isDirectory()) { + String normalized = NonPackRootResolver.normalizeZipEntryName(entry.getName()); + if (normalized != null && normalized.startsWith(typePrefix)) { + int namespaceStart = typePrefix.length(); + int namespaceEnd = normalized.indexOf(47, namespaceStart); + if (namespaceEnd > namespaceStart) { + String namespace = normalized.substring(namespaceStart, namespaceEnd); + if (Identifier.isNamespaceValid(namespace)) { + namespaces.add(namespace); + } + } + } + } + } + + return Set.copyOf(namespaces); + } + + public void close() { + try { + this.zipFile.close(); + } catch (IOException var2) { + } + } + + private static String joinRootSegments(String... segments) { + if (segments != null && segments.length != 0) { + StringBuilder out = new StringBuilder(); + String[] var2 = segments; + int var3 = var2.length; + int var4 = 0; + + while (var4 < var3) { + String segment = var2[var4]; + if (segment != null && !segment.isBlank()) { + String normalized = segment.replace('\\', '/'); + if (!normalized.contains("/") && !normalized.equals(".") && !normalized.equals("..")) { + if (!out.isEmpty()) { + out.append('/'); + } + + out.append(normalized); + var4++; + continue; + } + + return null; + } + + return null; + } + + return out.toString(); + } else { + return ""; + } + } + + static final class Factory implements PackFactory { + private final Path zipPath; + private final String rootPrefix; + + Factory(Path zipPath, String rootPrefix) { + this.zipPath = zipPath; + this.rootPrefix = rootPrefix; + } + + public ResourcePack open(ResourcePackInfo info) { + try { + return new NonPrefixedZipResourcePack(info, this.zipPath, this.rootPrefix); + } catch (IOException e) { + throw new RuntimeException("Failed to open prefixed NoN pack " + this.zipPath, e); + } + } + + public ResourcePack openWithOverlays(ResourcePackInfo info, Metadata metadata) { + return this.open(info); + } + } +} diff --git a/src/main/java/com/nonid/particle/NonParticles.java b/src/main/java/com/nonid/particle/NonParticles.java new file mode 100644 index 0000000..c022b53 --- /dev/null +++ b/src/main/java/com/nonid/particle/NonParticles.java @@ -0,0 +1,81 @@ +package com.nonid.particle; + +import com.nonid.NeedsOfNature; +import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.particle.ParticleType; +import net.minecraft.particle.SimpleParticleType; +import net.minecraft.particle.TintedParticleEffect; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonParticles { + public static final Identifier LIQUID_PARTICLE_ID = NeedsOfNature.id("liquid_particle"); + public static final Identifier LIQUID_PARTICLE_FALLING_ID = NeedsOfNature.id("liquid_particle_falling"); + public static final Identifier LIQUID_PARTICLE_PUDDLE_ID = NeedsOfNature.id("liquid_particle_puddle"); + public static final Identifier LIQUID_PARTICLE_WATER_ID = NeedsOfNature.id("liquid_particle_water"); + public static final ParticleType LIQUID_PARTICLE = registerTinted(LIQUID_PARTICLE_ID); + public static final ParticleType LIQUID_PARTICLE_FALLING = registerTinted(LIQUID_PARTICLE_FALLING_ID); + public static final ParticleType LIQUID_PARTICLE_PUDDLE = registerTinted(LIQUID_PARTICLE_PUDDLE_ID); + public static final ParticleType LIQUID_PARTICLE_WATER = registerTinted(LIQUID_PARTICLE_WATER_ID); + public static final SimpleParticleType SMALLHEART = register(NeedsOfNature.id("smallheart")); + public static final SimpleParticleType RIPPED_FABRIC = register(NeedsOfNature.id("ripped_fabric")); + + private NonParticles() { + } + + public static void registerAll() { + } + + public static boolean isLiquidParticleId(@Nullable Identifier id) { + return LIQUID_PARTICLE_ID.equals(id) + || LIQUID_PARTICLE_FALLING_ID.equals(id) + || LIQUID_PARTICLE_PUDDLE_ID.equals(id) + || LIQUID_PARTICLE_WATER_ID.equals(id); + } + + @Nullable + public static ParticleEffect createLiquidEffect(@Nullable Identifier id, int rgb) { + if (LIQUID_PARTICLE_ID.equals(id)) { + return liquid(rgb); + } else if (LIQUID_PARTICLE_FALLING_ID.equals(id)) { + return liquidFalling(rgb); + } else if (LIQUID_PARTICLE_PUDDLE_ID.equals(id)) { + return liquidPuddle(rgb); + } else { + return LIQUID_PARTICLE_WATER_ID.equals(id) ? liquidWater(rgb) : null; + } + } + + public static TintedParticleEffect liquid(int rgb) { + return tinted(LIQUID_PARTICLE, rgb); + } + + public static TintedParticleEffect liquidFalling(int rgb) { + return tinted(LIQUID_PARTICLE_FALLING, rgb); + } + + public static TintedParticleEffect liquidPuddle(int rgb) { + return tinted(LIQUID_PARTICLE_PUDDLE, rgb); + } + + public static TintedParticleEffect liquidWater(int rgb) { + return tinted(LIQUID_PARTICLE_WATER, rgb); + } + + private static TintedParticleEffect tinted(ParticleType type, int rgb) { + return TintedParticleEffect.create(type, 0xFF000000 | rgb & 16777215); + } + + private static SimpleParticleType register(Identifier id) { + return (SimpleParticleType)Registry.register(Registries.PARTICLE_TYPE, id, FabricParticleTypes.simple()); + } + + private static ParticleType registerTinted(Identifier id) { + return (ParticleType)Registry.register( + Registries.PARTICLE_TYPE, id, FabricParticleTypes.complex(TintedParticleEffect::createCodec, TintedParticleEffect::createPacketCodec) + ); + } +} diff --git a/src/main/java/com/nonid/potion/NonBrewingRecipes.java b/src/main/java/com/nonid/potion/NonBrewingRecipes.java new file mode 100644 index 0000000..e5d9d0f --- /dev/null +++ b/src/main/java/com/nonid/potion/NonBrewingRecipes.java @@ -0,0 +1,55 @@ +package com.nonid.potion; + +import java.util.LinkedHashSet; +import java.util.Set; +import net.fabricmc.fabric.api.registry.FabricBrewingRecipeRegistryBuilder; +import net.fabricmc.fabric.api.registry.FabricBrewingRecipeRegistryBuilder.BuildCallback; +import net.minecraft.item.Item; +import net.minecraft.item.Items; +import net.minecraft.potion.Potion; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.Identifier; + +public final class NonBrewingRecipes { + private static final TagKey EGGS_TAG = TagKey.of(RegistryKeys.ITEM, Identifier.of("c", "eggs")); + + private NonBrewingRecipes() { + } + + public static void registerAll() { + FabricBrewingRecipeRegistryBuilder.BUILD.register((BuildCallback)builder -> { + RegistryEntry liquid = Registries.POTION.getEntry(NonPotions.LIQUID); + RegistryEntry fertileNectar = Registries.POTION.getEntry(NonPotions.FERTILE_NECTAR); + RegistryEntry energy = Registries.POTION.getEntry(NonPotions.ENERGY); + RegistryEntry energyStrong = Registries.POTION.getEntry(NonPotions.ENERGY_II); + RegistryEntry energyLong = Registries.POTION.getEntry(NonPotions.ENERGY_LONG); + RegistryEntry relief = Registries.POTION.getEntry(NonPotions.ENERGY_RELIEF); + RegistryEntry reliefStrong = Registries.POTION.getEntry(NonPotions.ENERGY_RELIEF_II); + RegistryEntry reliefLong = Registries.POTION.getEntry(NonPotions.ENERGY_RELIEF_LONG); + RegistryEntry fertility = Registries.POTION.getEntry(NonPotions.FERTILITY_INCREASER); + RegistryEntry fertilityStrong = Registries.POTION.getEntry(NonPotions.FERTILITY_INCREASER_II); + RegistryEntry fertilityLong = Registries.POTION.getEntry(NonPotions.FERTILITY_INCREASER_LONG); + builder.registerPotionRecipe(liquid, Items.CRIMSON_FUNGUS, energy); + builder.registerPotionRecipe(liquid, Items.WARPED_FUNGUS, relief); + builder.registerPotionRecipe(energy, Items.GLOWSTONE_DUST, energyStrong); + builder.registerPotionRecipe(energy, Items.REDSTONE, energyLong); + builder.registerPotionRecipe(relief, Items.GLOWSTONE_DUST, reliefStrong); + builder.registerPotionRecipe(relief, Items.REDSTONE, reliefLong); + Set eggItems = new LinkedHashSet<>(); + Registries.ITEM.iterateEntries(EGGS_TAG).forEach(entry -> eggItems.add((Item)entry.value())); + if (eggItems.isEmpty()) { + eggItems.add(Items.EGG); + } + + for (Item egg : eggItems) { + builder.registerPotionRecipe(fertileNectar, egg, fertility); + } + + builder.registerPotionRecipe(fertility, Items.GLOWSTONE_DUST, fertilityStrong); + builder.registerPotionRecipe(fertility, Items.REDSTONE, fertilityLong); + }); + } +} diff --git a/src/main/java/com/nonid/potion/NonPotions.java b/src/main/java/com/nonid/potion/NonPotions.java new file mode 100644 index 0000000..92d313b --- /dev/null +++ b/src/main/java/com/nonid/potion/NonPotions.java @@ -0,0 +1,150 @@ +package com.nonid.potion; + +import com.nonid.NeedsOfNature; +import com.nonid.effect.NonStatusEffects; +import java.util.Optional; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.ConsumableComponent; +import net.minecraft.component.type.ConsumableComponents; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.potion.Potion; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public final class NonPotions { + public static final String LIQUID_ENTITY_DATA_KEY = "liquid_entity"; + private static final ConsumableComponent HONEY_BOTTLE_DRINK_CONSUMABLE = ConsumableComponents.drink().sound(SoundEvents.ITEM_HONEY_BOTTLE_DRINK).build(); + private static final int ENERGY_BASE_DURATION_TICKS = 800; + private static final int ENERGY_STRONG_DURATION_TICKS = 400; + private static final int ENERGY_LONG_DURATION_TICKS = 1600; + private static final int FERTILITY_BASE_DURATION_TICKS = 9600; + private static final int FERTILITY_STRONG_DURATION_TICKS = 4800; + private static final int FERTILITY_LONG_DURATION_TICKS = 19200; + public static final Potion LIQUID = register("liquid", new Potion("needsofnature.liquid", new StatusEffectInstance[0])); + public static final Potion ENERGY = register( + "energy", + new Potion("needsofnature.energy", new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGY), 800)}) + ); + public static final Potion ENERGY_II = register( + "energy_ii", + new Potion( + "needsofnature.energy_ii", new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGY), 400, 1)} + ) + ); + public static final Potion ENERGY_LONG = register( + "energy_long", + new Potion( + "needsofnature.energy_long", new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGY), 1600)} + ) + ); + public static final Potion ENERGY_RELIEF = register( + "energy_relief", + new Potion( + "needsofnature.energy_relief", + new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGY_RELIEF), 800)} + ) + ); + public static final Potion ENERGY_RELIEF_II = register( + "energy_relief_ii", + new Potion( + "needsofnature.energy_relief_ii", + new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGY_RELIEF), 400, 1)} + ) + ); + public static final Potion ENERGY_RELIEF_LONG = register( + "energy_relief_long", + new Potion( + "needsofnature.energy_relief_long", + new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.ENERGY_RELIEF), 1600)} + ) + ); + public static final Potion FERTILE_NECTAR = register("fertile_nectar", new Potion("needsofnature.fertile_nectar", new StatusEffectInstance[0])); + public static final Potion FERTILITY_INCREASER = register( + "fertility_increaser", + new Potion( + "needsofnature.fertility_increaser", + new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FERTILITY_INCREASER), 9600)} + ) + ); + public static final Potion FERTILITY_INCREASER_II = register( + "fertility_increaser_ii", + new Potion( + "needsofnature.fertility_increaser_ii", + new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FERTILITY_INCREASER), 4800, 1)} + ) + ); + public static final Potion FERTILITY_INCREASER_LONG = register( + "fertility_increaser_long", + new Potion( + "needsofnature.fertility_increaser_long", + new StatusEffectInstance[]{new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(NonStatusEffects.FERTILITY_INCREASER), 19200)} + ) + ); + + private NonPotions() { + } + + public static void registerAll() { + } + + public static boolean isLiquidPotion(Optional> potionEntry) { + return potionEntry != null && potionEntry.isPresent() && potionEntry.get().value() == LIQUID; + } + + public static boolean isLiquidBottle(ItemStack stack) { + if (stack != null && !stack.isEmpty() && stack.isOf(Items.POTION)) { + PotionContentsComponent contents = (PotionContentsComponent)stack.getOrDefault(DataComponentTypes.POTION_CONTENTS, PotionContentsComponent.DEFAULT); + return isLiquidPotion(contents.potion()); + } else { + return false; + } + } + + public static void applyHoneyBottleDrinkSound(ItemStack stack) { + if (stack != null && !stack.isEmpty()) { + stack.set(DataComponentTypes.CONSUMABLE, HONEY_BOTTLE_DRINK_CONSUMABLE); + } + } + + public static void setLiquidBottleEntityTypeId(ItemStack stack, @Nullable Identifier entityTypeId) { + if (stack != null && !stack.isEmpty()) { + NbtComponent.set(DataComponentTypes.CUSTOM_DATA, stack, nbt -> { + String value = entityTypeId == null ? "" : entityTypeId.toString(); + nbt.putString("liquid_entity", value); + }); + } + } + + @Nullable + public static Identifier getLiquidBottleEntityTypeId(ItemStack stack) { + if (!isLiquidBottle(stack)) { + return null; + } + + NbtComponent custom = (NbtComponent)stack.get(DataComponentTypes.CUSTOM_DATA); + if (custom != null && !custom.isEmpty()) { + NbtCompound nbt = custom.copyNbt(); + if (!nbt.contains("liquid_entity")) { + return null; + } + + String raw = nbt.getString("liquid_entity", ""); + return raw != null && !raw.isBlank() ? Identifier.tryParse(raw) : null; + } else { + return null; + } + } + + private static Potion register(String path, Potion potion) { + return (Potion)Registry.register(Registries.POTION, NeedsOfNature.id(path), potion); + } +} diff --git a/src/main/java/com/nonid/recipe/AbstractEnergyAdjustRecipe.java b/src/main/java/com/nonid/recipe/AbstractEnergyAdjustRecipe.java new file mode 100644 index 0000000..58ade02 --- /dev/null +++ b/src/main/java/com/nonid/recipe/AbstractEnergyAdjustRecipe.java @@ -0,0 +1,140 @@ +package com.nonid.recipe; + +import com.nonid.NeedsOfNature; +import com.nonid.potion.NonPotions; +import java.util.List; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.potion.Potion; +import net.minecraft.recipe.CraftingRecipe; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.IngredientPlacement; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.book.CraftingRecipeCategory; +import net.minecraft.recipe.display.RecipeDisplay; +import net.minecraft.recipe.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.recipe.display.SlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.ItemSlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.StackSlotDisplay; +import net.minecraft.recipe.input.CraftingRecipeInput; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryWrapper.WrapperLookup; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; + +abstract class AbstractEnergyAdjustRecipe implements CraftingRecipe { + private final CraftingRecipeCategory category; + private IngredientPlacement ingredientPlacement; + + protected AbstractEnergyAdjustRecipe(CraftingRecipeCategory category) { + this.category = category; + } + + public boolean matches(CraftingRecipeInput input, World world) { + int liquid = 0; + int honeycomb = 0; + int sugar = 0; + int flowerMix = 0; + int fungus = 0; + int nonEmpty = 0; + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (!stack.isEmpty()) { + nonEmpty++; + if (NonPotions.isLiquidBottle(stack)) { + liquid++; + } else if (stack.isOf(Items.HONEYCOMB)) { + honeycomb++; + } else if (stack.isOf(Items.SUGAR)) { + sugar++; + } else if (stack.isOf(NeedsOfNature.FLOWER_MIX)) { + flowerMix++; + } else { + if (!this.isRequiredFungus(stack)) { + return false; + } + + fungus++; + } + } + } + + return nonEmpty == 5 && liquid == 1 && honeycomb == 1 && sugar == 1 && flowerMix == 1 && fungus == 1; + } + + public ItemStack craft(CraftingRecipeInput input, WrapperLookup registries) { + return this.getResultStack().copy(); + } + + public CraftingRecipeCategory getCategory() { + return this.category; + } + + public IngredientPlacement getIngredientPlacement() { + if (this.ingredientPlacement == null) { + this.ingredientPlacement = IngredientPlacement.forShapeless( + List.of( + NonRecipeIngredients.liquidBottle(), + Ingredient.ofItem(Items.HONEYCOMB), + Ingredient.ofItem(Items.SUGAR), + Ingredient.ofItem(NeedsOfNature.FLOWER_MIX), + Ingredient.ofItem(this.getRequiredFungusItem()) + ) + ); + } + + return this.ingredientPlacement; + } + + public List getDisplays() { + SlotDisplay liquidDisplay = new StackSlotDisplay(createLiquidDisplayStack()); + SlotDisplay honeycombDisplay = Ingredient.ofItem(Items.HONEYCOMB).toDisplay(); + SlotDisplay sugarDisplay = Ingredient.ofItem(Items.SUGAR).toDisplay(); + SlotDisplay flowerMixDisplay = Ingredient.ofItem(NeedsOfNature.FLOWER_MIX).toDisplay(); + SlotDisplay fungusDisplay = Ingredient.ofItem(this.getRequiredFungusItem()).toDisplay(); + return List.of( + new ShapelessCraftingRecipeDisplay( + List.of(liquidDisplay, honeycombDisplay, sugarDisplay, flowerMixDisplay, fungusDisplay), + new StackSlotDisplay(this.getResultStack()), + new ItemSlotDisplay(Items.CRAFTING_TABLE) + ) + ); + } + + public DefaultedList getRecipeRemainders(CraftingRecipeInput input) { + DefaultedList remainders = DefaultedList.ofSize(input.size(), ItemStack.EMPTY); + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (!stack.isEmpty()) { + if (NonPotions.isLiquidBottle(stack)) { + remainders.set(i, new ItemStack(Items.GLASS_BOTTLE)); + } else { + remainders.set(i, stack.getItem().getRecipeRemainder()); + } + } + } + + return remainders; + } + + private static ItemStack createLiquidDisplayStack() { + RegistryEntry liquidPotion = Registries.POTION.getEntry(NonPotions.LIQUID); + ItemStack stack = new ItemStack(Items.POTION); + stack.set(DataComponentTypes.POTION_CONTENTS, new PotionContentsComponent(liquidPotion)); + return stack; + } + + protected abstract Item getRequiredFungusItem(); + + protected abstract boolean isRequiredFungus(ItemStack var1); + + protected abstract ItemStack getResultStack(); + + public abstract RecipeSerializer getSerializer(); +} diff --git a/src/main/java/com/nonid/recipe/EnergyAugmenterRecipe.java b/src/main/java/com/nonid/recipe/EnergyAugmenterRecipe.java new file mode 100644 index 0000000..02738d3 --- /dev/null +++ b/src/main/java/com/nonid/recipe/EnergyAugmenterRecipe.java @@ -0,0 +1,34 @@ +package com.nonid.recipe; + +import com.nonid.NeedsOfNature; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.book.CraftingRecipeCategory; + +public final class EnergyAugmenterRecipe extends AbstractEnergyAdjustRecipe { + public EnergyAugmenterRecipe(CraftingRecipeCategory category) { + super(category); + } + + @Override + protected boolean isRequiredFungus(ItemStack stack) { + return stack.isOf(Items.CRIMSON_FUNGUS); + } + + @Override + protected Item getRequiredFungusItem() { + return Items.CRIMSON_FUNGUS; + } + + @Override + protected ItemStack getResultStack() { + return new ItemStack(NeedsOfNature.ENERGY_AUGMENTER); + } + + @Override + public RecipeSerializer getSerializer() { + return NonRecipeSerializers.ENERGY_AUGMENTER_RECIPE; + } +} diff --git a/src/main/java/com/nonid/recipe/EnergyDiminisherRecipe.java b/src/main/java/com/nonid/recipe/EnergyDiminisherRecipe.java new file mode 100644 index 0000000..86505be --- /dev/null +++ b/src/main/java/com/nonid/recipe/EnergyDiminisherRecipe.java @@ -0,0 +1,34 @@ +package com.nonid.recipe; + +import com.nonid.NeedsOfNature; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.book.CraftingRecipeCategory; + +public final class EnergyDiminisherRecipe extends AbstractEnergyAdjustRecipe { + public EnergyDiminisherRecipe(CraftingRecipeCategory category) { + super(category); + } + + @Override + protected boolean isRequiredFungus(ItemStack stack) { + return stack.isOf(Items.WARPED_FUNGUS); + } + + @Override + protected Item getRequiredFungusItem() { + return Items.WARPED_FUNGUS; + } + + @Override + protected ItemStack getResultStack() { + return new ItemStack(NeedsOfNature.ENERGY_DIMINISHER); + } + + @Override + public RecipeSerializer getSerializer() { + return NonRecipeSerializers.ENERGY_DIMINISHER_RECIPE; + } +} diff --git a/src/main/java/com/nonid/recipe/EnergyStabilizerRecipe.java b/src/main/java/com/nonid/recipe/EnergyStabilizerRecipe.java new file mode 100644 index 0000000..c578855 --- /dev/null +++ b/src/main/java/com/nonid/recipe/EnergyStabilizerRecipe.java @@ -0,0 +1,160 @@ +package com.nonid.recipe; + +import com.nonid.NeedsOfNature; +import com.nonid.item.EnergyStabilizerItem; +import com.nonid.potion.NonPotions; +import java.util.List; +import java.util.Optional; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.potion.Potion; +import net.minecraft.recipe.CraftingRecipe; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.IngredientPlacement; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.book.CraftingRecipeCategory; +import net.minecraft.recipe.display.RecipeDisplay; +import net.minecraft.recipe.display.ShapedCraftingRecipeDisplay; +import net.minecraft.recipe.display.SlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.ItemSlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.StackSlotDisplay; +import net.minecraft.recipe.input.CraftingRecipeInput; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryWrapper.WrapperLookup; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public final class EnergyStabilizerRecipe implements CraftingRecipe { + private final CraftingRecipeCategory category; + private IngredientPlacement ingredientPlacement; + + public EnergyStabilizerRecipe(CraftingRecipeCategory category) { + this.category = category; + } + + public boolean matches(CraftingRecipeInput input, World world) { + return findTargetEntity(input) != null; + } + + public ItemStack craft(CraftingRecipeInput input, WrapperLookup registries) { + Identifier target = findTargetEntity(input); + return target == null ? ItemStack.EMPTY : EnergyStabilizerItem.createStackForEntity(target); + } + + public CraftingRecipeCategory getCategory() { + return this.category; + } + + public RecipeSerializer getSerializer() { + return NonRecipeSerializers.ENERGY_STABILIZER_RECIPE; + } + + public IngredientPlacement getIngredientPlacement() { + if (this.ingredientPlacement == null) { + Ingredient liquidBottle = NonRecipeIngredients.liquidBottle(); + this.ingredientPlacement = IngredientPlacement.forMultipleSlots( + List.of( + Optional.of(liquidBottle), + Optional.of(Ingredient.ofItem(Items.WARPED_FUNGUS)), + Optional.of(liquidBottle), + Optional.of(Ingredient.ofItem(NeedsOfNature.ENERGY_DIMINISHER)), + Optional.of(Ingredient.ofItem(Items.GLOW_BERRIES)), + Optional.of(Ingredient.ofItem(NeedsOfNature.ENERGY_DIMINISHER)), + Optional.of(liquidBottle), + Optional.of(Ingredient.ofItem(Items.WARPED_FUNGUS)), + Optional.of(liquidBottle) + ) + ); + } + + return this.ingredientPlacement; + } + + public List getDisplays() { + SlotDisplay liquidDisplay = new StackSlotDisplay(createLiquidDisplayStack()); + SlotDisplay fungusDisplay = Ingredient.ofItem(Items.WARPED_FUNGUS).toDisplay(); + SlotDisplay diminisherDisplay = Ingredient.ofItem(NeedsOfNature.ENERGY_DIMINISHER).toDisplay(); + SlotDisplay glowBerryDisplay = Ingredient.ofItem(Items.GLOW_BERRIES).toDisplay(); + SlotDisplay resultDisplay = new StackSlotDisplay(EnergyStabilizerItem.createStackForEntity(null)); + return List.of( + new ShapedCraftingRecipeDisplay( + 3, + 3, + List.of( + liquidDisplay, fungusDisplay, liquidDisplay, diminisherDisplay, glowBerryDisplay, diminisherDisplay, liquidDisplay, fungusDisplay, liquidDisplay + ), + resultDisplay, + new ItemSlotDisplay(Items.CRAFTING_TABLE) + ) + ); + } + + public DefaultedList getRecipeRemainders(CraftingRecipeInput input) { + DefaultedList remainders = DefaultedList.ofSize(input.size(), ItemStack.EMPTY); + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (!stack.isEmpty()) { + if (isCorner(i, input.getWidth()) && NonPotions.isLiquidBottle(stack)) { + remainders.set(i, new ItemStack(Items.GLASS_BOTTLE)); + } else { + remainders.set(i, stack.getItem().getRecipeRemainder()); + } + } + } + + return remainders; + } + + @Nullable + private static Identifier findTargetEntity(CraftingRecipeInput input) { + if (input.getWidth() == 3 && input.getHeight() == 3) { + int[] corners = new int[]{0, 2, 6, 8}; + Identifier entityTypeId = null; + + for (int corner : corners) { + ItemStack liquidStack = input.getStackInSlot(corner); + Identifier current = NonPotions.getLiquidBottleEntityTypeId(liquidStack); + if (current == null) { + return null; + } + + if (entityTypeId == null) { + entityTypeId = current; + } else if (!entityTypeId.equals(current)) { + return null; + } + } + + if (!input.getStackInSlot(1).isOf(Items.WARPED_FUNGUS)) { + return null; + } else if (!input.getStackInSlot(7).isOf(Items.WARPED_FUNGUS)) { + return null; + } else if (!input.getStackInSlot(3).isOf(NeedsOfNature.ENERGY_DIMINISHER)) { + return null; + } else if (!input.getStackInSlot(5).isOf(NeedsOfNature.ENERGY_DIMINISHER)) { + return null; + } else { + return !input.getStackInSlot(4).isOf(Items.GLOW_BERRIES) ? null : entityTypeId; + } + } else { + return null; + } + } + + private static boolean isCorner(int slot, int width) { + return width != 3 ? false : slot == 0 || slot == 2 || slot == 6 || slot == 8; + } + + private static ItemStack createLiquidDisplayStack() { + RegistryEntry liquidPotion = Registries.POTION.getEntry(NonPotions.LIQUID); + ItemStack stack = new ItemStack(Items.POTION); + stack.set(DataComponentTypes.POTION_CONTENTS, new PotionContentsComponent(liquidPotion)); + return stack; + } +} diff --git a/src/main/java/com/nonid/recipe/FertileNectarRecipe.java b/src/main/java/com/nonid/recipe/FertileNectarRecipe.java new file mode 100644 index 0000000..89a3327 --- /dev/null +++ b/src/main/java/com/nonid/recipe/FertileNectarRecipe.java @@ -0,0 +1,114 @@ +package com.nonid.recipe; + +import com.nonid.potion.NonPotions; +import java.util.List; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.FoodComponent; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.component.type.FoodComponent.Builder; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.potion.Potion; +import net.minecraft.recipe.CraftingRecipe; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.IngredientPlacement; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.book.CraftingRecipeCategory; +import net.minecraft.recipe.display.RecipeDisplay; +import net.minecraft.recipe.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.recipe.display.SlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.ItemSlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.StackSlotDisplay; +import net.minecraft.recipe.input.CraftingRecipeInput; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryWrapper.WrapperLookup; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; + +public final class FertileNectarRecipe implements CraftingRecipe { + private static final FoodComponent FERTILE_NECTAR_FOOD = new Builder().nutrition(7).saturationModifier(0.0F).alwaysEdible().build(); + private final CraftingRecipeCategory category; + private IngredientPlacement ingredientPlacement; + + public FertileNectarRecipe(CraftingRecipeCategory category) { + this.category = category; + } + + public boolean matches(CraftingRecipeInput input, World world) { + int liquid = 0; + int honeyBottle = 0; + int nonEmpty = 0; + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (!stack.isEmpty()) { + nonEmpty++; + if (NonPotions.isLiquidBottle(stack)) { + liquid++; + } else { + if (!stack.isOf(Items.HONEY_BOTTLE)) { + return false; + } + + honeyBottle++; + } + } + } + + return nonEmpty == 2 && liquid == 1 && honeyBottle == 1; + } + + public ItemStack craft(CraftingRecipeInput input, WrapperLookup registries) { + return createPotionStack(NonPotions.FERTILE_NECTAR); + } + + public CraftingRecipeCategory getCategory() { + return this.category; + } + + public IngredientPlacement getIngredientPlacement() { + if (this.ingredientPlacement == null) { + this.ingredientPlacement = IngredientPlacement.forShapeless(List.of(NonRecipeIngredients.liquidBottle(), Ingredient.ofItem(Items.HONEY_BOTTLE))); + } + + return this.ingredientPlacement; + } + + public List getDisplays() { + SlotDisplay liquidDisplay = new StackSlotDisplay(createPotionStack(NonPotions.LIQUID)); + SlotDisplay honeyDisplay = Ingredient.ofItem(Items.HONEY_BOTTLE).toDisplay(); + SlotDisplay resultDisplay = new StackSlotDisplay(createPotionStack(NonPotions.FERTILE_NECTAR)); + return List.of(new ShapelessCraftingRecipeDisplay(List.of(liquidDisplay, honeyDisplay), resultDisplay, new ItemSlotDisplay(Items.CRAFTING_TABLE))); + } + + public DefaultedList getRecipeRemainders(CraftingRecipeInput input) { + DefaultedList remainders = DefaultedList.ofSize(input.size(), ItemStack.EMPTY); + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (!stack.isEmpty() && !NonPotions.isLiquidBottle(stack)) { + remainders.set(i, stack.getItem().getRecipeRemainder()); + } + } + + return remainders; + } + + public RecipeSerializer getSerializer() { + return NonRecipeSerializers.FERTILE_NECTAR_RECIPE; + } + + private static ItemStack createPotionStack(Potion potion) { + RegistryEntry potionEntry = Registries.POTION.getEntry(potion); + ItemStack stack = new ItemStack(Items.POTION); + stack.set(DataComponentTypes.POTION_CONTENTS, new PotionContentsComponent(potionEntry)); + if (potion == NonPotions.FERTILE_NECTAR) { + stack.set(DataComponentTypes.FOOD, FERTILE_NECTAR_FOOD); + stack.set(DataComponentTypes.MAX_STACK_SIZE, 16); + NonPotions.applyHoneyBottleDrinkSound(stack); + } + + return stack; + } +} diff --git a/src/main/java/com/nonid/recipe/FillAdvancedStraponRecipe.java b/src/main/java/com/nonid/recipe/FillAdvancedStraponRecipe.java new file mode 100644 index 0000000..137e012 --- /dev/null +++ b/src/main/java/com/nonid/recipe/FillAdvancedStraponRecipe.java @@ -0,0 +1,130 @@ +package com.nonid.recipe; + +import com.nonid.NeedsOfNature; +import com.nonid.data.NonAccessoryLiquidFilling; +import com.nonid.potion.NonPotions; +import java.util.List; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.CraftingRecipe; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.IngredientPlacement; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.book.CraftingRecipeCategory; +import net.minecraft.recipe.display.RecipeDisplay; +import net.minecraft.recipe.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.recipe.display.SlotDisplay.ItemSlotDisplay; +import net.minecraft.recipe.input.CraftingRecipeInput; +import net.minecraft.registry.Registries; +import net.minecraft.registry.RegistryWrapper.WrapperLookup; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; + +public final class FillAdvancedStraponRecipe implements CraftingRecipe { + private static final Identifier ADVANCED_STRAPON_ID = NeedsOfNature.id("strapon_advanced"); + private final CraftingRecipeCategory category; + private IngredientPlacement ingredientPlacement; + + public FillAdvancedStraponRecipe(CraftingRecipeCategory category) { + this.category = category; + } + + public boolean matches(CraftingRecipeInput input, World world) { + ItemStack strapon = ItemStack.EMPTY; + ItemStack liquidBottle = ItemStack.EMPTY; + int nonEmpty = 0; + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (!stack.isEmpty()) { + nonEmpty++; + if (isAdvancedStrapon(stack)) { + if (!strapon.isEmpty()) { + return false; + } + + strapon = stack; + } else { + if (!NonPotions.isLiquidBottle(stack)) { + return false; + } + + if (!liquidBottle.isEmpty()) { + return false; + } + + liquidBottle = stack; + } + } + } + + return nonEmpty == 2 && NonAccessoryLiquidFilling.canFill(strapon, liquidBottle); + } + + public ItemStack craft(CraftingRecipeInput input, WrapperLookup registries) { + ItemStack strapon = ItemStack.EMPTY; + ItemStack liquidBottle = ItemStack.EMPTY; + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (isAdvancedStrapon(stack)) { + strapon = stack; + } else if (NonPotions.isLiquidBottle(stack)) { + liquidBottle = stack; + } + } + + return NonAccessoryLiquidFilling.createFilledCopy(strapon, liquidBottle); + } + + public CraftingRecipeCategory getCategory() { + return this.category; + } + + public IngredientPlacement getIngredientPlacement() { + if (this.ingredientPlacement == null) { + this.ingredientPlacement = IngredientPlacement.forShapeless( + List.of(Ingredient.ofItem(resolveAdvancedStraponItem()), NonRecipeIngredients.liquidBottle()) + ); + } + + return this.ingredientPlacement; + } + + public List getDisplays() { + return List.of( + new ShapelessCraftingRecipeDisplay( + List.of(new ItemSlotDisplay(resolveAdvancedStraponItem()), NonRecipeIngredients.liquidBottle().toDisplay()), + new ItemSlotDisplay(resolveAdvancedStraponItem()), + new ItemSlotDisplay(Items.CRAFTING_TABLE) + ) + ); + } + + public DefaultedList getRecipeRemainders(CraftingRecipeInput input) { + DefaultedList remainders = DefaultedList.ofSize(input.size(), ItemStack.EMPTY); + + for (int i = 0; i < input.size(); i++) { + ItemStack stack = input.getStackInSlot(i); + if (NonPotions.isLiquidBottle(stack)) { + remainders.set(i, new ItemStack(Items.GLASS_BOTTLE)); + } + } + + return remainders; + } + + public RecipeSerializer getSerializer() { + return NonRecipeSerializers.FILL_ADVANCED_STRAPON_RECIPE; + } + + private static boolean isAdvancedStrapon(ItemStack stack) { + return stack != null && !stack.isEmpty() && ADVANCED_STRAPON_ID.equals(Registries.ITEM.getId(stack.getItem())); + } + + private static Item resolveAdvancedStraponItem() { + return (Item)Registries.ITEM.get(ADVANCED_STRAPON_ID); + } +} diff --git a/src/main/java/com/nonid/recipe/NonRecipeIngredients.java b/src/main/java/com/nonid/recipe/NonRecipeIngredients.java new file mode 100644 index 0000000..fbfcc2a --- /dev/null +++ b/src/main/java/com/nonid/recipe/NonRecipeIngredients.java @@ -0,0 +1,84 @@ +package com.nonid.recipe; + +import com.mojang.serialization.MapCodec; +import com.nonid.NeedsOfNature; +import com.nonid.potion.NonPotions; +import java.util.stream.Stream; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.PotionContentsComponent; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.potion.Potion; +import net.minecraft.recipe.Ingredient; +import net.minecraft.recipe.display.SlotDisplay; +import net.minecraft.recipe.display.SlotDisplay.StackSlotDisplay; +import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.util.Identifier; + +public final class NonRecipeIngredients { + private NonRecipeIngredients() { + } + + public static void registerAll() { + CustomIngredientSerializer.register(NonRecipeIngredients.LiquidBottleIngredient.SERIALIZER); + } + + public static Ingredient liquidBottle() { + return NonRecipeIngredients.LiquidBottleIngredient.INSTANCE.toVanilla(); + } + + private enum LiquidBottleIngredient implements CustomIngredient { + INSTANCE; + + private static final NonRecipeIngredients.LiquidBottleIngredient.Serializer SERIALIZER = new NonRecipeIngredients.LiquidBottleIngredient.Serializer(); + + public boolean test(ItemStack stack) { + return NonPotions.isLiquidBottle(stack); + } + + public Stream> getMatchingItems() { + return Stream.of(Registries.ITEM.getEntry(Items.POTION)); + } + + public boolean requiresTesting() { + return true; + } + + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + public SlotDisplay toDisplay() { + RegistryEntry liquidPotion = Registries.POTION.getEntry(NonPotions.LIQUID); + ItemStack stack = new ItemStack(Items.POTION); + stack.set(DataComponentTypes.POTION_CONTENTS, new PotionContentsComponent(liquidPotion)); + return new StackSlotDisplay(stack); + } + + private static final class Serializer implements CustomIngredientSerializer { + private static final Identifier ID = NeedsOfNature.id("liquid_bottle"); + private static final MapCodec CODEC = MapCodec.unit(NonRecipeIngredients.LiquidBottleIngredient.INSTANCE); + private static final PacketCodec PACKET_CODEC = PacketCodec.unit( + NonRecipeIngredients.LiquidBottleIngredient.INSTANCE + ); + + public Identifier getIdentifier() { + return ID; + } + + public MapCodec getCodec() { + return CODEC; + } + + public PacketCodec getPacketCodec() { + return PACKET_CODEC; + } + } + } +} diff --git a/src/main/java/com/nonid/recipe/NonRecipeSerializers.java b/src/main/java/com/nonid/recipe/NonRecipeSerializers.java new file mode 100644 index 0000000..6829499 --- /dev/null +++ b/src/main/java/com/nonid/recipe/NonRecipeSerializers.java @@ -0,0 +1,38 @@ +package com.nonid.recipe; + +import com.nonid.NeedsOfNature; +import net.fabricmc.fabric.api.recipe.v1.sync.RecipeSynchronization; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.SpecialCraftingRecipe.SpecialRecipeSerializer; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; + +public final class NonRecipeSerializers { + public static final RecipeSerializer ENERGY_AUGMENTER_RECIPE = (RecipeSerializer)Registry.register( + Registries.RECIPE_SERIALIZER, NeedsOfNature.id("crafting_energy_augmenter"), new SpecialRecipeSerializer(EnergyAugmenterRecipe::new) + ); + public static final RecipeSerializer ENERGY_DIMINISHER_RECIPE = (RecipeSerializer)Registry.register( + Registries.RECIPE_SERIALIZER, NeedsOfNature.id("crafting_energy_diminisher"), new SpecialRecipeSerializer(EnergyDiminisherRecipe::new) + ); + public static final RecipeSerializer ENERGY_STABILIZER_RECIPE = (RecipeSerializer)Registry.register( + Registries.RECIPE_SERIALIZER, NeedsOfNature.id("crafting_energy_stabilizer"), new SpecialRecipeSerializer(EnergyStabilizerRecipe::new) + ); + public static final RecipeSerializer FERTILE_NECTAR_RECIPE = (RecipeSerializer)Registry.register( + Registries.RECIPE_SERIALIZER, NeedsOfNature.id("crafting_fertile_nectar"), new SpecialRecipeSerializer(FertileNectarRecipe::new) + ); + public static final RecipeSerializer FILL_ADVANCED_STRAPON_RECIPE = (RecipeSerializer)Registry.register( + Registries.RECIPE_SERIALIZER, NeedsOfNature.id("crafting_fill_advanced_strapon"), new SpecialRecipeSerializer(FillAdvancedStraponRecipe::new) + ); + + private NonRecipeSerializers() { + } + + public static void registerAll() { + NonRecipeIngredients.registerAll(); + RecipeSynchronization.synchronizeRecipeSerializer(ENERGY_AUGMENTER_RECIPE); + RecipeSynchronization.synchronizeRecipeSerializer(ENERGY_DIMINISHER_RECIPE); + RecipeSynchronization.synchronizeRecipeSerializer(ENERGY_STABILIZER_RECIPE); + RecipeSynchronization.synchronizeRecipeSerializer(FERTILE_NECTAR_RECIPE); + RecipeSynchronization.synchronizeRecipeSerializer(FILL_ADVANCED_STRAPON_RECIPE); + } +} diff --git a/src/main/java/com/nonid/sound/NonSoundEvents.java b/src/main/java/com/nonid/sound/NonSoundEvents.java new file mode 100644 index 0000000..7530ad0 --- /dev/null +++ b/src/main/java/com/nonid/sound/NonSoundEvents.java @@ -0,0 +1,54 @@ +package com.nonid.sound; + +import com.nonid.NeedsOfNature; +import java.util.Locale; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; + +public final class NonSoundEvents { + public static final SoundEvent DEBUG_STAFF_DISAPPEAR = SoundEvent.of(NeedsOfNature.id("a")); + private static final int DRY_IMPACT_COUNT = 11; + private static final int WET_IMPACT_COUNT = 16; + private static final int WET_COUNT = 12; + private static final int SHOT_IN_COUNT = 3; + private static final int SHOT_OUT_COUNT = 6; + private static final int MOTION_COUNT = 10; + private static final int RETRACT_COUNT = 3; + private static final int RIP_COUNT = 3; + + private NonSoundEvents() { + } + + public static void registerAll() { + registerSeries("impactdry", 11); + registerSeries("impactwet", 16); + registerSeries("wet", 12); + registerSeries("shot_in", 3); + registerSeries("shot_out", 6); + registerSeries("motion", 10); + registerSeries("retract", 3); + registerSeries("rip", 3); + register("a", DEBUG_STAFF_DISAPPEAR); + register("funkytown"); + } + + private static void registerSeries(String prefix, int count) { + for (int i = 1; i <= count; i++) { + String name = prefix + String.format(Locale.ROOT, "%02d", i); + register(name); + } + } + + private static void register(String path) { + register(path, SoundEvent.of(NeedsOfNature.id(path))); + } + + private static void register(String path, SoundEvent soundEvent) { + Identifier id = NeedsOfNature.id(path); + if (!Registries.SOUND_EVENT.containsId(id)) { + Registry.register(Registries.SOUND_EVENT, id, soundEvent); + } + } +} diff --git a/src/main/resources/assets/animationframework/geckolib/models/entity/missingmodel.geo.json b/src/main/resources/assets/animationframework/geckolib/models/entity/missingmodel.geo.json new file mode 100644 index 0000000..11ad5b1 --- /dev/null +++ b/src/main/resources/assets/animationframework/geckolib/models/entity/missingmodel.geo.json @@ -0,0 +1,79 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.unknown", + "texture_width": 16, + "texture_height": 16, + "visible_bounds_width": 3, + "visible_bounds_height": 2.5, + "visible_bounds_offset": [0, 0.75, 0] + }, + "bones": [ + { + "name": "bone", + "pivot": [-7.5, 0, 0] + }, + { + "name": "e", + "parent": "bone", + "pivot": [-8.5, 0, 0], + "cubes": [ + {"origin": [-8.5, 0, -2], "size": [2, 2, 4], "uv": [0, 0]}, + {"origin": [-8.5, 8, -2], "size": [2, 2, 4], "uv": [0, 0]}, + {"origin": [-8.5, 4, -2], "size": [2, 2, 4], "uv": [0, 0]}, + {"origin": [-9.5, 0, -2], "size": [1, 10, 4], "uv": [0, 0]} + ] + }, + { + "name": "r", + "parent": "bone", + "pivot": [-4.5, 9, 0], + "cubes": [ + {"origin": [-4.5, 8, -2], "size": [1.5, 2, 4], "uv": [0, 0]}, + {"origin": [-4.5, 4, -2], "size": [1.5, 2, 4], "uv": [0, 0]}, + {"origin": [-4.5, 8, -2], "size": [4, 1, 4], "pivot": [-3.5, 8, 0], "rotation": [0, 0, 90], "uv": [0, 0]}, + {"origin": [-3, 5, -2], "size": [4, 1, 4], "pivot": [-4, 5, 0], "rotation": [0, 0, 85], "uv": [0, 0]}, + {"origin": [-5.5, 0, -2], "size": [1, 10, 4], "uv": [0, 0]} + ] + }, + { + "name": "r2", + "parent": "bone", + "pivot": [-0.5, 9, 0], + "cubes": [ + {"origin": [-0.5, 8, -2], "size": [1, 2, 4], "uv": [0, 0]}, + {"origin": [-0.5, 4, -2], "size": [1, 2, 4], "uv": [0, 0]}, + {"origin": [-0.5, 8, -2], "size": [4, 1, 4], "pivot": [0.5, 8, 0], "rotation": [0, 0, 90], "uv": [0, 0]}, + {"origin": [1, 5, -2], "size": [4, 1, 4], "pivot": [0, 5, 0], "rotation": [0, 0, 85], "uv": [0, 0]}, + {"origin": [-1.5, 0, -2], "size": [1, 10, 4], "uv": [0, 0]} + ] + }, + { + "name": "r3", + "parent": "bone", + "pivot": [7.5, 9, 0], + "cubes": [ + {"origin": [7.5, 8, -2], "size": [1, 2, 4], "uv": [0, 0]}, + {"origin": [7.5, 4, -2], "size": [1, 2, 4], "uv": [0, 0]}, + {"origin": [7.5, 8, -2], "size": [4, 1, 4], "pivot": [8.5, 8, 0], "rotation": [0, 0, 90], "uv": [0, 0]}, + {"origin": [9, 5, -2], "size": [4, 1, 4], "pivot": [8, 5, 0], "rotation": [0, 0, 85], "uv": [0, 0]}, + {"origin": [6.5, 0, -2], "size": [1, 10, 4], "uv": [0, 0]} + ] + }, + { + "name": "o", + "parent": "bone", + "pivot": [3.5, 0, 0], + "cubes": [ + {"origin": [3, 0, -2], "size": [2, 1, 4], "uv": [0, 0]}, + {"origin": [3, 9, -2], "size": [2, 1, 4], "uv": [0, 0]}, + {"origin": [4.5, 1, -2], "size": [1, 8, 4], "uv": [1, 0]}, + {"origin": [2.5, 1, -2], "size": [1, 8, 4], "uv": [0, 0]} + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/animationframework/textures/missingmodel.png b/src/main/resources/assets/animationframework/textures/missingmodel.png new file mode 100644 index 0000000000000000000000000000000000000000..bbb93f47cd6dc38554763d2cc1d192ac52a50f23 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|oCO|{#S9GG!XV7ZFl&wkP*B{{ u#W6%nAyy9f z5`jz>&vo;vaJ2Y-V=W zJHNWSsyI}4Pxs6wIp#^FYV}Oj_xrxD-mBN@yBzj>jb;Fvb0-mr&^nC#S#-qET0{(@ zOg0NjX=bNqKq&weVfNk?b9d0gdxvu=&Zl9{hP4UKC49K^pFlt);*sqj-k@ECD1(*^ zPz>S>A{ym12d5WNN)e|HP+Z-49e_*oYk+5O&}L)s768xJzK8b~=Pb@!oJ#@$molqo z0FXc+0BMQBtRu9pp(Pu-7E}?Fy$dQ`-FcnW#uBTICH}Irj+Tgx!CRbfHaOpGa5H|p zSg$2QOKqgCM57RB9id&2`Pf_ots{(?LUcr&IFu6FaSNq{Sv?C%5i47Xvw}1 zd&8KysTu$ePu$|%_K)cGyI7a7-TwsZ6k69x3h2!9%=;WXc&Y(KRV|6Iv{HxU96Lu?(x)Y9(n(&}~S+adjQSP4@;e)i-mymR-HeZlvJaq;}( zvuAmC`LFctAloNmCOZQm1jQ-OGC*pGcOzm-BZLePF({>3fAD(%UO022ko@AxNdO+~ z>;iD)-~m<}OUxflS$pXe>o;!lE!QJKd|Y2NN% z;f?#Ra&hG(T7;U>#l|^!c$VOu2+Q+zuB~se^U(0(vuA04=U*tLP@+c)h!_+Bq#Jia zD3n3_P=s!Dt^j1fs^U=&I&L|M{VX^uE&xA$?ribj4?Z6qRz=vmATUf4zJ2I8+GK^t)cEtgKLQl7w$7n5*>d2o zvYg1}bEoNKA^%m@6FV-GKt6i_NtKzMEI4*__k35?Qd@jytMUYsCGAxae_ zQs%N1s7F>*`pf4|Py8phN(cbnY+Yk{vCf@)pB5x?BJ;D;mBl%D*>dn4v`psF<1Uxi zo(9s1brRTAH*9zNw7Y%o-21e6x6>Q&;iDEX%I`cB<#)e6^Y>yvIGA}V0_;3Ati5!K zcb2Y?2zc-L$>NWB(As5ru|7fOqtDtC%=fb#6yP_T7mKXOv**^G?Se#Z#nZs{CN>-= zQ?~rzIXk))e|7r@#bHdc_mS3E=UJYw^YQInSgG!lRSSp5b7|p+JZSCi3)t@V0g5o> z7U@wn6s0P_`&pg|&8H5J2uwv0-PqENCu{eY*)UD{+wJ$6GKMK*h_o&WmKIeZkrSIg zn$mn?sUT9E0q-5&J1)%aufjrB2XYlQHn-T=+~Rz*!7#0!?(y=RgC99~pg4^4N5dKe z;GJd0c(jJ&3l09U_r+fSZ+i!T3#Xsq(c^9fB=Z-u3XA<@Pa_-TrJ{5^Z4@9 zukfd>*D4!y`a=MIaq72-(!BcKFFD_Aus4jEj;dhJd#v*nQsc_xnUE2%Mfck4chS1W z+L50lD$1G?O{>=px8>uBk;mOua$o#A%T^@>CcQb8#j zx)_9>?H0`ogtjXW!*paS7gf1U`yvYp*d%!56_jd`_a5tX*0OoL7n}_W %s.", + "debug.needsofnature.energy_locked_attack_cooldown": "%s is on attack cooldown; energy is locked to 0.", + "debug.needsofnature.attack_blocked_pregnancy_friendly": "Blocked direct attack %s -> %s (pregnancy-friendly mob).", + "debug.needsofnature.attack_blocked_gathering_pair": "Blocked direct attack %s -> %s (both actors are pending/active).", + "debug.needsofnature.attack_blocked_energy_busy": "Blocked direct attack %s -> %s (mob has energy > 70 while player is animation-busy).", + "debug.needsofnature.no_defeat_tag_animation": "No defeat-tag animation matched for player; skipping defeated queue.", + "debug.needsofnature.missing_entity_bottle_fallback": "Missing bottle mapping for %s liquid; using mixed bottle.", + "debug.needsofnature.missing_birth_animation": "No birth animation found for %s; pregnancy was cleared.", + "debug.needsofnature.missing_birth_cue": "Birth animation %s has no 'birth' cue; using stop fallback.", + "debug.needsofnature.birth_fallback_spawn": "Birth fallback spawn triggered on stop (%s).", + "debug.needsofnature.missing_pregnancy_entity_type": "Missing entity type %s for pregnancy spawn.", + "item.needsofnature.entity_liquid_bottle": "Bottle of %s Liquid", + "item.minecraft.potion.effect.needsofnature.liquid": "Bottle of Liquid", + "stat.needsofnature.animations_started_total": "NoN: Animations Started", + "stat.needsofnature.peaked_animations_total": "NoN: Peaked Animations", + "stat.needsofnature.liquid_gained_ml_total": "NoN: Liquid Gained (ml)", + "stat.needsofnature.multi_actor_animations_total": "NoN: 3+ Actor Animations", + "stat.needsofnature.water_animations_total": "NoN: Water Animations", + "stat.needsofnature.attack_animations_started_total": "NoN: Attack Animations Started", + "stat.needsofnature.attack_escapes_success_total": "NoN: Attack Escapes", + "stat.needsofnature.m_injector_peaks_ml_total": "NoN: Injector Type M Peaks (ml)", + "stat.needsofnature.manual_peaks_total": "NoN: Manual Peaks", + "stat.needsofnature.bottles_filled_total": "NoN: Bottles Filled", + "stat.needsofnature.pregnancies_started_total": "NoN: Pregnancies Started", + "stat.needsofnature.pregnancies_completed_total": "NoN: Pregnancies Completed", + "stat.needsofnature.offspring_spawned_total": "NoN: Offspring Spawned", + "advancements.needsofnature.root.title": "Needs of Nature", + "advancements.needsofnature.root.description": "Explore the systems of Needs of Nature.", + "advancements.needsofnature.natural_curiosity.title": "Natural Curiosity", + "advancements.needsofnature.natural_curiosity.description": "Reach the peak stage of any NoN animation.", + "advancements.needsofnature.filled_to_the_brim.title": "Filled to the Brim", + "advancements.needsofnature.filled_to_the_brim.description": "Completely fill your liquid tank.", + "advancements.needsofnature.favouritism.title": "Favouritism", + "advancements.needsofnature.favouritism.description": "Fill your tank completely with liquid from only one entity type.", + "advancements.needsofnature.first_pregnancy.title": "First Pregnancy", + "advancements.needsofnature.first_pregnancy.description": "Begin your first pregnancy.", + "advancements.needsofnature.mother_nature.title": "Mother Nature", + "advancements.needsofnature.mother_nature.description": "Complete 25 pregnancies.", + "advancements.needsofnature.messed_up.title": "Messed Up", + "advancements.needsofnature.messed_up.description": "Reach maximum mess of every type at the same time.", + "advancements.needsofnature.group_activity.title": "Group Activity", + "advancements.needsofnature.group_activity.description": "Take part in an animation with four or more actors.", + "advancements.needsofnature.tossed_around.title": "Tossed Around", + "advancements.needsofnature.tossed_around.description": "Get thrown into five defeated animations in a row.", + "advancements.needsofnature.collectors_habit.title": "Collector's Habit", + "advancements.needsofnature.collectors_habit.description": "Fill 25 bottles from your liquid tank.", + "advancements.needsofnature.field_research.title": "Field Research", + "advancements.needsofnature.field_research.description": "Gain liquid from five different entity types in one in-game day.", + "advancements.needsofnature.bad_idea_beacon.title": "Bad Idea Beacon", + "advancements.needsofnature.bad_idea_beacon.description": "Have full player energy while your skin is fully destroyed.", + "advancements.needsofnature.productive_pregnancy.title": "Productive Pregnancy", + "advancements.needsofnature.productive_pregnancy.description": "Complete a pregnancy that produces more than one offspring or egg.", + "advancements.needsofnature.udderly_unfortunate.title": "Udderly Unfortunate", + "advancements.needsofnature.udderly_unfortunate.description": "Drink milk while pregnant.", + "advancements.needsofnature.emergency_snack.title": "Emergency Snack", + "advancements.needsofnature.emergency_snack.description": "Receive an injector type M hunger reward while hungry, or drink an entity liquid bottle.", + "advancements.needsofnature.stable_diet.title": "Stable Diet", + "advancements.needsofnature.stable_diet.description": "Trigger Emergency Snack three times in a row without eating or drinking anything else.", + "advancements.needsofnature.acquire_hardware.title": "Acquire Hardware", + "advancements.needsofnature.acquire_hardware.description": "Start a manual peak animation with a held item.", + "gui.needsofnature.player_profiles.title": "Player Interaction Profiles", + "gui.needsofnature.player_profiles.tooltip": "Set your default and per-player animation request preferences.", + "gui.needsofnature.player_profiles.default": "Default for other players", + "gui.needsofnature.player_profiles.player_name": "Player name", + "gui.needsofnature.player_profiles.player_name_hint": "Known server player name", + "gui.needsofnature.player_profiles.apply": "Apply", + "gui.needsofnature.player_profiles.unknown_player": "That player is not known to this server yet.", + "gui.needsofnature.player_profiles.allowed_profiles": "Allowed Profiles", + "gui.needsofnature.player_profiles.allowed_profiles_hint": "At least one profile must remain available.", + "gui.needsofnature.player_profiles.allowed": "%s: Allowed", + "gui.needsofnature.player_profiles.disallowed": "%s: Disabled", + "gui.needsofnature.player_profiles.mode.deny_all": "No requests or attacks", + "gui.needsofnature.player_profiles.mode.requests_only": "Requests, no attacks", + "gui.needsofnature.player_profiles.mode.requests_and_attacks": "Requests and attacks", + "gui.needsofnature.player_profiles.mode.auto_accept": "Auto-accept requests", + "gui.needsofnature.player_request.select_title": "Select Animation", + "gui.needsofnature.player_request.select_for": "Request with %s", + "gui.needsofnature.player_request.select_join_title": "Select Join Animation", + "gui.needsofnature.player_request.select_join_for": "Select how to join %s", + "gui.needsofnature.player_request.section.available": "Available animations", + "gui.needsofnature.player_request.section.unavailable": "Unavailable animations", + "gui.needsofnature.player_request.role_title": "Select Your Role", + "gui.needsofnature.player_request.role_option": "Your role: %s (%s)", + "gui.needsofnature.player_request.role_activity.active": "Active", + "gui.needsofnature.player_request.role_activity.passive": "Passive", + "gui.needsofnature.player_request.roles": "%s role options", + "gui.needsofnature.player_request.consent_title": "Animation Request", + "gui.needsofnature.player_request.accept": "Accept", + "gui.needsofnature.player_request.reject": "Reject", + "gui.needsofnature.player_request.start_prompt": "Requests animation: %s", + "gui.needsofnature.player_request.join_prompt": "Requests to join with: %s", + "gui.needsofnature.player_request.waiting_title": "Request Sent", + "gui.needsofnature.player_request.waiting_description": "Waiting for all players to accept...", + "gui.needsofnature.player_request.waiting": "Waiting for player consent.", + "gui.needsofnature.player_request.started": "Animation started.", + "gui.needsofnature.player_request.rejected": "The animation request was rejected.", + "gui.needsofnature.player_request.cancelled": "The animation request was cancelled.", + "gui.needsofnature.player_request.timeout": "The animation request timed out.", + "gui.needsofnature.player_request.out_of_range": "The players moved too far apart.", + "gui.needsofnature.player_request.damaged": "The animation request was cancelled by damage.", + "gui.needsofnature.player_request.disconnected": "A participating player disconnected.", + "gui.needsofnature.player_request.player_busy": "A player is already busy with another animation.", + "gui.needsofnature.player_request.recipient_busy": "A requested player already has another pending request.", + "gui.needsofnature.player_request.not_accepting": "Player is not accepting requests.", + "gui.needsofnature.player_request.energy_low": "You need at least %s energy to request an animation.", + "gui.needsofnature.player_request.already_pending": "You already have a pending animation request.", + "gui.needsofnature.player_request.no_match": "No eligible animation matches these actors.", + "gui.needsofnature.player_request.block_requirement_unavailable": "Required blocks unavailable", + "gui.needsofnature.player_request.block_requirement_summary": "Requires: %s", + "gui.needsofnature.player_request.block_requirement_tooltip": "This animation cannot start here because its block requirement is unavailable: %s", + "gui.needsofnature.player_request.animation_changed": "The selected animation is no longer eligible.", + "gui.needsofnature.player_request.protected": "An equipped protector blocked the animation.", + "gui.needsofnature.player_request.invalid": "The animation request is no longer valid.", +"key.categories.needsofnature.animation": "NeedsOfNature Animation Debug", + "key.category.needsofnature.animation": "NeedsOfNature Animation Debug", + "key.needsofnature.animation.select_actor": "Select actor (debug)", + "key.needsofnature.animation.select_self": "Select self (debug)", + "key.needsofnature.animation.start_debug_animation": "Start animation (debug)", + "key.needsofnature.animation.stop_all_debug": "Stop all animations (debug)", + "key.needsofnature.animation.stop_instance_debug": "Stop animation instance (debug)", + "key.needsofnature.animation.next_stage_debug": "Next stage (debug)", + "config.needsofnature.animation.title": "NeedsOfNature Animations", + "config.needsofnature.animation.force_vanilla_entity_textures": "Force vanilla entity textures (ignore resource packs)", + "config.needsofnature.animation.block_search_radius": "Block search radius", + "config.needsofnature.animation.auto_switch_third_person": "Auto switch to third person on animation start", + "config.needsofnature.animation.debug_menu": "Debug options", + "config.needsofnature.animation.open": "Open", + "config.needsofnature.animation.debug_title": "NeedsOfNature Animation Debug", + "config.needsofnature.animation.debug_chat_mode": "Debug chat", + "config.needsofnature.animation.debug_chat_mode.all": "All", + "config.needsofnature.animation.debug_chat_mode.setup_warnings_errors": "warnings, setup, errors", + "config.needsofnature.animation.debug_chat_mode.setup_errors": "setup, errors", + "config.needsofnature.animation.debug_chat_mode.errors_only": "errors", + "config.needsofnature.animation.reset_icon": "<", + "key.needsofnature.animation.speed_up_debug": "Speed up animation (debug)", + + "key.needsofnature.animation.speed_down_debug": "Slow down animation (debug)", + "debug.needsofnature.animation.stop_all_broadcast_queued": "[NoN Animation] Stop-all broadcast queued.", + "debug.needsofnature.animation.server_eval": "[NoN Animation] Server eval: actors=%s chosen=%s keys=%s", + "debug.needsofnature.animation.start_received": "[NoN Animation] START %s actors=%s keys=%s", + "debug.needsofnature.animation.stop_all_received": "[NoN Animation] STOP ALL", + "debug.needsofnature.animation.stop_instance_received": "[NoN Animation] STOP instance", + "debug.needsofnature.animation.server_cannot_receive_stop_all": "[NoN Animation] Server can't receive stop-all packets (mod not installed server-side).", + "debug.needsofnature.animation.stop_all_requested": "[NoN Animation] Stop-all requested.", + "debug.needsofnature.animation.no_active_instance_to_stop": "[NoN Animation] No active instance to stop.", + "debug.needsofnature.animation.server_cannot_receive_stop_instance": "[NoN Animation] Server can't receive stop-instance packets (mod not installed server-side).", + "debug.needsofnature.animation.stop_instance_requested": "[NoN Animation] Stop instance requested.", + "debug.needsofnature.animation.no_entity_targeted": "[NoN Animation] No entity targeted.", + "debug.needsofnature.animation.target_not_living": "[NoN Animation] Target is not a living entity.", + "debug.needsofnature.animation.target_not_mob": "[NoN Animation] Target is not a mob.", + "debug.needsofnature.animation.animation_deselected": "[NoN Animation] Animation deselected.", + "debug.needsofnature.animation.animation_selected_add_actors_for_join": "[NoN Animation] Animation selected. Add extra actors then press start to join.", + "debug.needsofnature.animation.entity_deselected_actor_count": "[NoN Animation] entity:%s deselected, actors %s", + "debug.needsofnature.animation.entity_selected_actor_count": "[NoN Animation] entity:%s selected, actors %s", + "debug.needsofnature.animation.self_deselected_actor_count": "[NoN Animation] self deselected, actors %s", + "debug.needsofnature.animation.self_selected_actor_count": "[NoN Animation] self selected, actors %s", + "debug.needsofnature.animation.no_actors_selected": "[NoN Animation] No actors selected.", + "debug.needsofnature.animation.no_extra_actors_for_join": "[NoN Animation] No extra actors selected for join.", + "debug.needsofnature.animation.server_cannot_receive_join": "[NoN Animation] Server can't receive join packets (mod not installed server-side).", + "debug.needsofnature.animation.join_requested_instance_extras": "[NoN Animation] Join requested. instance=%s extras=%s", + "debug.needsofnature.animation.server_cannot_receive_debug_packets": "[NoN Animation] Server can't receive debug packets (mod not installed server-side).", + "debug.needsofnature.animation.start_requested_actor_count": "[NoN Animation] Start requested (H). actors=%s", + "debug.needsofnature.animation.no_active_instance_to_advance": "[NoN Animation] No active instance to advance.", + "debug.needsofnature.animation.next_stage_requested_server": "[NoN Animation] Next stage requested (server).", + "debug.needsofnature.animation.next_stage_requested_local": "[NoN Animation] Next stage requested (local only).", + "debug.needsofnature.animation.no_active_instance_to_adjust_speed": "[NoN Animation] No active instance to adjust speed.", + "debug.needsofnature.animation.speed_change_requested": "[NoN Animation] Speed change requested (%s).", + "debug.needsofnature.animation.server_cannot_receive_speed": "[NoN Animation] Server can't receive speed packets (mod missing server-side).", + "debug.needsofnature.animation.stage_sequence_ended_server_stop_unavailable": "[NoN Animation] Stage sequence ended locally; server stop unavailable.", + "debug.needsofnature.animation.queue_enqueue_failed_exactly_one_player": "[NoN Animation] Queue enqueue failed: exactly one player actor is required.", + "debug.needsofnature.animation.queue_enqueue_failed_full": "[NoN Animation] Queue enqueue failed: queue is full (cap=%s).", + "debug.needsofnature.animation.queue_enqueue_failed_player_missing": "[NoN Animation] Queue enqueue failed: player actor is missing.", + "debug.needsofnature.animation.queue_enqueue_denied_mob_already_queued": "[NoN Animation] Queue enqueue denied: %s is already queued for another player.", + "debug.needsofnature.animation.queued_at_position": "[NoN Animation] Queued %s at position %s.", + "debug.needsofnature.animation.queue_cleared_for_player": "[NoN Animation] Queue cleared for player.", + "debug.needsofnature.animation.queue_cleared_for_active_instance": "[NoN Animation] Queue cleared for active instance.", + "debug.needsofnature.animation.matched_unpathable_actor": "[NoN Animation] Matched %s but an actor can't pathfind (not a mob/player).", + "debug.needsofnature.animation.matched_multiple_players_not_supported": "[NoN Animation] Matched %s but multiple players are not supported yet.", + "debug.needsofnature.animation.matched_multiplayer_direct_start": "[NoN Animation] Matched %s with actors %s -> direct multiplayer debug start.", + "debug.needsofnature.animation.start_cancelled_block_and_water_requirement": "[NoN Animation] Start cancelled: block_requirements cannot be combined with water requirement.", + "debug.needsofnature.animation.start_cancelled_no_valid_wall": "[NoN Animation] Start cancelled: no valid wall placement found for %s (actors: %s).", + "debug.needsofnature.animation.start_cancelled_no_valid_center_support": "[NoN Animation] Start cancelled: no valid center support placement found for %s (actors: %s).", + "debug.needsofnature.animation.start_cancelled_players_too_far_from_requester": "[NoN Animation] Start cancelled: selected players must be within %s blocks of requester.", + "debug.needsofnature.animation.join_failed_no_active_instance": "[NoN Animation] Join failed: no such active instance.", + "debug.needsofnature.animation.join_denied_stage_disallows": "[NoN Animation] Join denied: current stage does not allow joins.", + "debug.needsofnature.animation.join_failed_players_not_supported": "[NoN Animation] Join failed: player actors not supported for joins.", + "debug.needsofnature.animation.join_failed_no_expanded_definition": "[NoN Animation] Join failed: no definition for expanded actor set.", + "debug.needsofnature.animation.no_active_instance_to_adjust": "[NoN Animation] No active instance to adjust.", + "debug.needsofnature.animation.speed_unchanged": "[NoN Animation] Speed unchanged.", + "debug.needsofnature.animation.speed_set": "[NoN Animation] Speed set to %s", + "debug.needsofnature.animation.no_such_active_instance": "[NoN Animation] No such active instance.", + "debug.needsofnature.animation.instance_in_different_world": "[NoN Animation] Instance is in a different world; can't stop from here.", + "debug.needsofnature.animation.broadcasting_stop": "[NoN Animation] Broadcasting STOP", + "debug.needsofnature.animation.queue_continued_for_player": "[NoN Animation] Queue continued for player.", + "debug.needsofnature.animation.queue_cleared": "[NoN Animation] Queue cleared.", + "debug.needsofnature.animation.start_cancelled_water_requirement_not_met": "[NoN Animation] Start cancelled: water requirement not met.", + "debug.needsofnature.animation.start_cancelled_not_allowed_in_water": "[NoN Animation] Start cancelled: animation not allowed in water.", + "debug.needsofnature.animation.start_cancelled_not_enough_space": "[NoN Animation] Start cancelled: not enough space for %s (actors: %s).", + "debug.needsofnature.animation.actors_immediate_broadcast_start": "[NoN Animation] Immediate start. Broadcasting START", + "debug.needsofnature.animation.queue_skipped_player_unavailable": "[NoN Animation] Queue skipped for %s (actors: %s): player unavailable.", + "debug.needsofnature.animation.queue_skipped_missing_non_player_actor": "[NoN Animation] Queue skipped for %s (actors: %s): missing non-player actor.", + "debug.needsofnature.animation.queue_skipped_no_actors_resolved": "[NoN Animation] Queue skipped for %s (actors: %s): no actors resolved.", + "debug.needsofnature.animation.queue_skipped_missing_definition": "[NoN Animation] Queue skipped for %s (actors: %s): missing definition.", + "debug.needsofnature.animation.queue_skipped_requirements_not_met": "[NoN Animation] Queue skipped for %s (actors: %s): requirements not met.", + "debug.needsofnature.animation.queue_skipped_start_failed": "[NoN Animation] Queue skipped for %s (actors: %s): start failed.", + "debug.needsofnature.animation.queued_animation_started": "[NoN Animation] Queued animation started: %s (actors: %s)", + "config.jade.plugin_needsofnature.gender": "Mob gender", + "config.needsofnature.server_settings.no_permission": "You do not have permission to change server settings.", + "config.needsofnature.server_settings.stale": "Server settings changed while this screen was open. Reopen it and try again.", + "config.needsofnature.server_settings.invalid": "The server rejected the settings update." +} diff --git a/src/main/resources/assets/needsofnature/lang/zh_cn.json b/src/main/resources/assets/needsofnature/lang/zh_cn.json new file mode 100644 index 0000000..def4651 --- /dev/null +++ b/src/main/resources/assets/needsofnature/lang/zh_cn.json @@ -0,0 +1,901 @@ +{ + "item.needsofnature.debug_staff": "自然调试法杖", + "item.needsofnature.flower_mix": "花粉混合物", + "item.needsofnature.energy_augmenter": "性欲增幅剂", + "item.needsofnature.energy_diminisher": "性欲抑制剂", + "item.needsofnature.energy_stabilizer": "性欲锁定器", + "item.needsofnature.iron_plug": "铁制肛塞", + "item.needsofnature.protector_v": "阴道防护器", + "item.needsofnature.protector_a": "肛门防护器", + "item.needsofnature.protector_av": "阴道/肛门防护器", + "item.needsofnature.diaphragm": "避孕隔膜", + "item.needsofnature.strapon": "穿戴式假阳具", + "item.needsofnature.strapon_advanced": "高级穿戴式假阳具", + "item.needsofnature.accessory.tooltip.liquid_decay_reduced": "体液流失 %s", + "item.needsofnature.accessory.tooltip.liquid_decay_increased": "体液流失 %s", + "item.needsofnature.accessory.tooltip.equalize_liquid_decay_context": "潜行/水中不再额外增加体液流失", + "item.needsofnature.accessory.tooltip.inactive_tank_slot": "不在体液储槽内", + "item.needsofnature.accessory.tooltip.player_energy_gain.increased": "性欲增长 %s", + "item.needsofnature.accessory.tooltip.player_energy_gain.reduced": "性欲增长 %s", + "item.needsofnature.accessory.tooltip.liquid_capacity.increased": "体液容量 %s ml", + "item.needsofnature.accessory.tooltip.liquid_capacity.reduced": "体液容量 %s ml", + "item.needsofnature.accessory.tooltip.liquid_gain.increased": "体液增加 %s", + "item.needsofnature.accessory.tooltip.liquid_gain.reduced": "体液增加 %s", + "item.needsofnature.accessory.tooltip.filled_effect.increased": "灌满惩罚 %s", + "item.needsofnature.accessory.tooltip.filled_effect.reduced": "灌满惩罚 %s", + "item.needsofnature.accessory.tooltip.pregnancy_chance.increased": "受孕概率 %s", + "item.needsofnature.accessory.tooltip.pregnancy_chance.reduced": "受孕概率 %s", + "item.needsofnature.accessory.tooltip.pregnancy_duration.increased": "妊娠时长 %s", + "item.needsofnature.accessory.tooltip.pregnancy_duration.reduced": "妊娠时长 %s", + "item.needsofnature.accessory.tooltip.mess_gain.increased": "污渍增加 %s", + "item.needsofnature.accessory.tooltip.mess_gain.reduced": "污渍增加 %s", + "item.needsofnature.accessory.tooltip.destroyed_skin_damage.increased": "皮肤破损 %s", + "item.needsofnature.accessory.tooltip.destroyed_skin_damage.reduced": "皮肤破损 %s", + "item.needsofnature.accessory.tooltip.attack_escape_hits.increased": "挣脱次数 %s", + "item.needsofnature.accessory.tooltip.attack_escape_hits.reduced": "挣脱次数 %s", + "item.needsofnature.accessory.tooltip.attack_escape_damage.increased": "挣脱伤害 %s", + "item.needsofnature.accessory.tooltip.attack_escape_damage.reduced": "挣脱伤害 %s", + "item.needsofnature.accessory.tooltip.player_energy_aura.increased": "性欲光环 %s", + "item.needsofnature.accessory.tooltip.player_energy_aura.reduced": "性欲光环 %s", + "item.needsofnature.accessory.tooltip.near_animation_mob_energy.increased": "附近动画生物性欲 %s", + "item.needsofnature.accessory.tooltip.near_animation_mob_energy.reduced": "附近动画生物性欲 %s", + "item.needsofnature.accessory.tooltip.blocks_injector_v": "阻止 V 类动画", + "item.needsofnature.accessory.tooltip.blocks_injector_a": "阻止 A 类动画", + "item.needsofnature.accessory.tooltip.blocks_injector_av": "阻止 A/V 类动画", + "item.needsofnature.accessory.tooltip.occupies_av": "占用 A 和 V 槽位", + "item.needsofnature.accessory.tooltip.ignore_injector_slot_effect_suppression": "匹配注入动画期间保持生效", + "item.needsofnature.accessory.tooltip.blocks_pregnancy": "阻止怀孕", + "item.needsofnature.accessory.tooltip.male_roles": "允许女性玩家扮演男性动画角色", + "item.needsofnature.accessory.tooltip.no_injected_liquid": "高潮时不产生体液", + "item.needsofnature.accessory.tooltip.stored_liquid.empty": "空(容量 %s ml)", + "item.needsofnature.accessory.tooltip.stored_liquid.filled": "%s ml 的 %s", + "item.needsofnature.horse_liquid_collector": "马精收集器", + "item.needsofnature.entity_energy_stabilizer": "%s 性欲锁定器", + "item.needsofnature.mixed_liquid_bottle": "混合体液瓶", + "entity.needsofnature.horse_liquid_collector": "马精收集器", + "itemGroup.needsofnature.main": "自然需求", + "trinkets.slot.legs.v": "V", + "trinkets.slot.legs.a": "A", + "trinkets.slot.legs.d": "D", + "key.categories.needsofnature": "自然需求", + "key.categories.needsofnature.main": "自然需求", + "key.category.needsofnature.main": "自然需求", + "key.needsofnature.start_manual_peak": "开始手动高潮", + "gui.needsofnature.repair_skin": "修复破损皮肤", + "gui.needsofnature.repair_skin.needs_workbench": "附近需要织布机", + "gui.needsofnature.trinket_slot.requires_female": "需要女性性别", + "gui.needsofnature.trinket_slot.requires_male": "需要男性性别", + "gui.needsofnature.trinket_slot.unavailable": "不可用", + "config.needsofnature.categories_title": "自然需求配置", + "config.needsofnature.category.toggles": "开关", + "config.needsofnature.category.player_preferences": "玩家偏好", + "config.needsofnature.category.settings": "设置", + "config.needsofnature.category.gameplay": "玩法", + "config.needsofnature.category.vanilla_overrides": "原版覆盖", + "config.needsofnature.category.liquid": "体液", + "config.needsofnature.category.pregnancy": "怀孕", + "config.needsofnature.category.interface": "界面", + "config.needsofnature.category.intiface": "Buttplug.io", + "config.needsofnature.category.debug_advanced": "调试与高级", + "config.needsofnature.toggles_title": "自然需求开关", + "config.needsofnature.player_preferences_title": "自然需求玩家偏好", + "config.needsofnature.settings_title": "自然需求设置", + "config.needsofnature.gameplay_title": "自然需求玩法", + "config.needsofnature.pregnancy_title": "自然需求怀孕", + "config.needsofnature.liquid_settings_title": "自然需求体液", + "config.needsofnature.debug_advanced_title": "自然需求调试与高级", + "config.needsofnature.loaded_animations_title": "已加载动画", + "config.needsofnature.loaded_animations": "已加载动画", + "config.needsofnature.loaded_animations.load_order": "加载顺序...", + "config.needsofnature.loaded_animations.load_order_title": "动画包加载顺序", + "config.needsofnature.loaded_animations.load_order.summary": "顶部 = 最高优先级。外部 NoN 包覆盖内置内容。", + "config.needsofnature.loaded_animations.load_order.read_only": "连接远程服务器时只读。", + "config.needsofnature.loaded_animations.load_order.base_non": "内置 NeedsOfNature 内容(锁定)", + "config.needsofnature.loaded_animations_search": "按 ID/实体筛选", + "config.needsofnature.loaded_animations_summary": "已加载:%s | 显示:%s", + "config.needsofnature.loaded_animations.expand_hint": "缺失详情", + "config.needsofnature.loaded_animations.copied_id": "已复制动画 ID", + "config.needsofnature.loaded_animations.copied_missing": "已复制缺失详情", + "config.needsofnature.loaded_animations.status.ok": "正常", + "config.needsofnature.loaded_animations.status.partial": "部分", + "config.needsofnature.loaded_animations.status.missing": "缺失", + "config.needsofnature.ui_title": "自然需求界面", + "config.needsofnature.vanilla_override_title": "自然需求原版覆盖", + "config.needsofnature.initial_energy_max": "初始性欲上限(0-200):", + "config.needsofnature.energy_gain_rate": "性欲增长率(0.1-10 倍):", + "config.needsofnature.near_animation_energy_gain_mult": "动画附近性欲增长倍率(1-10 倍):", + "config.needsofnature.player_energy_aura_mult_low": "玩家性欲光环倍率(100-149):", + "config.needsofnature.player_energy_aura_mult_mid": "玩家性欲光环倍率(150-199):", + "config.needsofnature.player_energy_aura_mult_high": "玩家性欲光环倍率(200):", + "config.needsofnature.destroyed_skin_aura_mult_stage1": "破损皮肤光环倍率(第 1 阶段):", + "config.needsofnature.destroyed_skin_aura_mult_stage2": "破损皮肤光环倍率(第 2 阶段):", + "config.needsofnature.destroyed_skin_aura_mult_stage3": "破损皮肤光环倍率(第 3 阶段):", + "config.needsofnature.destroyed_skin_aura_mult_stage4": "破损皮肤光环倍率(第 4 阶段):", + "config.needsofnature.normal_damage_destroyed_skin_chance_percent": "普通伤害破损皮肤概率:", + "config.needsofnature.player_energy_aura_radius_min": "性欲 100 时的光环半径:", + "config.needsofnature.player_energy_aura_radius_max": "性欲 200 时的光环半径:", + "config.needsofnature.player_energy_aura_pulse_ticks": "性欲光环脉冲间隔(tick):", + "config.needsofnature.scan_budget": "扫描预算/每 tick(1-512):", + "config.needsofnature.debug_chat_mode": "调试聊天模式:", + "config.needsofnature.debug_spin_mode": "调试安全模式:", + "config.needsofnature.debug_spin_mode.value": "%s", + "config.needsofnature.convert_male_v_to_a": "男性专属 V 注入转为 A:", + "config.needsofnature.destroyed_skin_tint": "破损皮肤色调:", + "config.needsofnature.destroyed_skin_tint.pick": "选取破损皮肤色调...", + "config.needsofnature.destroyed_skin_tint.pick_title": "选取破损皮肤色调", + "config.needsofnature.destroyed_skin_tint.use": "使用颜色", + "config.needsofnature.destroyed_skin_tint.auto": "自动", + "config.needsofnature.destroyed_skin_tint.no_skin": "无法加载玩家皮肤。", + "config.needsofnature.loop_seconds": "普通阶段秒数(5-300):", + "config.needsofnature.peak_loop_seconds": "高潮阶段秒数(1-300):", + "config.needsofnature.gender_male": "全局雄性生成概率:", + "config.needsofnature.gender_female": "全局雌性生成概率:", + "config.needsofnature.gender_both": "全局双性别生成概率:", + "config.needsofnature.allow_player_gender_change_anytime": "允许随时更改性别:", + "config.needsofnature.require_player_gender_selection_on_join": "首次加入要求选择性别:", + "config.needsofnature.allowed_starting_genders": "允许的初始性别:", + "config.needsofnature.allowed_genders.all": "全部", + "config.needsofnature.allowed_genders.male_female": "男性或女性", + "config.needsofnature.allowed_genders.male": "仅男性", + "config.needsofnature.allowed_genders.female": "仅女性", + "config.needsofnature.allowed_genders.both": "仅男+女", + "config.needsofnature.allowed_genders.male_both": "男性或男+女", + "config.needsofnature.allowed_genders.female_both": "女性或男+女", + "config.needsofnature.toggle_label.enabled": "%s:开", + "config.needsofnature.toggle_label.disabled": "%s:关", + "config.needsofnature.player_gender": "玩家性别:", + "config.needsofnature.player_gender.male": "男性", + "config.needsofnature.player_gender.female": "女性", + "config.needsofnature.player_gender.both": "男+女", + "config.needsofnature.sync_female_gender_mod": "同步 Female Gender Mod:", + "config.needsofnature.open_female_gender_preview": "打开 Female Gender Mod 设置", + "config.needsofnature.female_gender.customization": "自定义", + "config.needsofnature.female_gender.physics": "胸部物理", + "config.needsofnature.female_gender.breast_size": "胸围:", + "config.needsofnature.female_gender.separation": "间隔:", + "config.needsofnature.female_gender.height": "高度:", + "config.needsofnature.female_gender.depth": "深度:", + "config.needsofnature.female_gender.rotation": "旋转:", + "config.needsofnature.female_gender.breast_physics": "胸部物理:", + "config.needsofnature.female_gender.dual_physics": "双胸物理:", + "config.needsofnature.female_gender.intensity": "强度:", + "config.needsofnature.female_gender.momentum": "动量:", + "config.needsofnature.female_gender.preview_unavailable": "预览不可用", + "config.needsofnature.armor_shed_effect": "护甲脱落效果:", + "config.needsofnature.action_sound_volume": "动作音效音量:", + "config.needsofnature.action_sound_volume.value": "%s%%", + "config.needsofnature.intiface_title": "自然需求 Buttplug.io", + "config.needsofnature.intiface_toy_title": "玩具设置", + "config.needsofnature.intiface_vibrator_title": "振动器设置", + "config.needsofnature.intiface_stroker_oscillator_title": "抽插器 / 振荡器设置", + "config.needsofnature.intiface.libs.install_title": "安装 Buttplug.io 库", + "config.needsofnature.intiface_enabled": "启用 Buttplug.io:", + "config.needsofnature.intiface_server_url": "服务器 URL:", + "config.needsofnature.intiface_auto_connect": "加入世界自动连接:", + "config.needsofnature.intiface_stop_on_disconnect": "断开时停止:", + "config.needsofnature.intiface_animation_baseline": "动画基线:", + "config.needsofnature.intiface_energized_level": "兴奋 %s 基础/脉冲:", + "config.needsofnature.intiface_reactiveimpact": "普通冲击:", + "config.needsofnature.intiface_peak_reactiveimpact": "高潮冲击:", + "config.needsofnature.intiface_cooldown": "脉冲冷却(毫秒):", + "config.needsofnature.intiface_connect_scan": "连接 / 扫描:", + "config.needsofnature.intiface_test_pulse": "测试脉冲:", + "config.needsofnature.intiface_toy_settings": "玩具设置", + "config.needsofnature.intiface_vibrator_settings": "振动器设置", + "config.needsofnature.intiface_stroker_oscillator_settings": "抽插器 / 振荡器设置", + "config.needsofnature.intiface_oscillator_regular_speed": "普通动画速度:", + "config.needsofnature.intiface_oscillator_peak_speed": "高潮阶段速度:", + "config.needsofnature.intiface_stroker_max_distance": "最大距离:", + "config.needsofnature.intiface_stroker_min_distance": "最小距离:", + "config.needsofnature.intiface_stroker_regular_duration": "普通移动时长:", + "config.needsofnature.intiface_stroker_peak_duration": "高潮移动时长:", + "config.needsofnature.intiface_connect": "连接", + "config.needsofnature.intiface_scan": "扫描", + "config.needsofnature.intiface_test": "测试", + "config.needsofnature.intiface_disconnect": "断开", + "config.needsofnature.intiface.libs.install": "安装库", + "config.needsofnature.intiface.libs.download": "下载", + "config.needsofnature.intiface.libs.redownload": "重新下载库", + "config.needsofnature.intiface.libs.restart_required": "需要重启", + "config.needsofnature.intiface.libs.install_line_1": "Buttplug.io 支持需要 NoN 未附带的可选 Java 库。", + "config.needsofnature.intiface.libs.install_line_2": "下载大小:%s(来自 Maven Central)。需要重启 Minecraft。", + "config.needsofnature.intiface.libs.install_line_3": "没有服务器设置控制此项;只影响你的本地客户端。", + "config.needsofnature.intiface.status.disabled": "已禁用", + "config.needsofnature.intiface.status.unavailable": "不可用", + "config.needsofnature.intiface.status.disconnected": "未连接", + "config.needsofnature.intiface.status.connecting": "连接中", + "config.needsofnature.intiface.status.connected": "已连接(%s 台设备)", + "config.needsofnature.intiface.status.scanning": "扫描中(%s 台设备)", + "config.needsofnature.intiface.status.error": "错误(%s 台设备)", + "config.needsofnature.intiface.status.device_added": "设备已添加:%s", + "config.needsofnature.intiface.status.device_removed": "设备已移除", + "config.needsofnature.intiface.status.scan_finished": "扫描完成", + "config.needsofnature.intiface.status.no_devices": "未连接设备", + "config.needsofnature.intiface.status.no_vibrate_devices": "没有支持振动的设备", + "config.needsofnature.intiface.status.stopped": "已停止所有设备", + "config.needsofnature.intiface.libs.status.available": "库已就绪", + "config.needsofnature.intiface.libs.status.missing": "库缺失(%s 下载)", + "config.needsofnature.intiface.libs.status.outdated": "库已过时;需要重新下载", + "config.needsofnature.intiface.libs.status.installing": "安装中:%s", + "config.needsofnature.intiface.libs.status.building": "构建库包", + "config.needsofnature.intiface.libs.status.restart_required": "已安装,重启 Minecraft 以激活", + "config.needsofnature.intiface.libs.status.failed": "安装失败:%s", + "config.needsofnature.male_male_chance": "男+男概率(0-100):", + "config.needsofnature.female_female_chance": "女+女概率(0-100):", + "config.needsofnature.entity_entity_chance": "生物+生物概率(0-100):", + "config.needsofnature.multi_actor_join_chance": "加入概率(3+ 演员)(0-100):", + "config.needsofnature.section.world": "世界", + "config.needsofnature.section.energy": "性欲", + "config.needsofnature.section.animations": "动画", + "config.needsofnature.section.difficulty": "难度", + "config.needsofnature.section.interface": "界面", + "config.needsofnature.section.player_identity": "玩家身份", + "config.needsofnature.section.local_visuals": "本地外观", + "config.needsofnature.section.intiface_connection": "Buttplug.io 连接", + "config.needsofnature.section.intiface_libraries": "Buttplug.io 库", + "config.needsofnature.section.intiface_baseline": "动画基线", + "config.needsofnature.section.intiface_energized": "兴奋心跳", + "config.needsofnature.section.intiface_reactiveimpact": "反应冲击", + "config.needsofnature.section.intiface_controls": "连接控制", + "config.needsofnature.section.intiface_toy_settings": "玩具设置", + "config.needsofnature.section.intiface_oscillator": "振荡器", + "config.needsofnature.section.intiface_stroker": "抽插器", + "config.needsofnature.section.mob_energy": "生物性欲", + "config.needsofnature.section.gender_spawn_chances": "性别生成概率", + "config.needsofnature.section.player_gender_rules": "玩家性别规则", + "config.needsofnature.section.energy_gain": "性欲增长", + "config.needsofnature.section.player_energy_aura": "玩家性欲光环", + "config.needsofnature.section.destroyed_skin": "破损皮肤", + "config.needsofnature.section.aura_range_timing": "光环范围与时机", + "config.needsofnature.section.animation_pacing": "动画节奏", + "config.needsofnature.section.match_chances": "匹配概率", + "config.needsofnature.section.attack_escape": "攻击挣脱", + "config.needsofnature.section.attack_rules": "攻击规则", + "config.needsofnature.section.last_defeated": "最后落败", + "config.needsofnature.section.core_systems": "核心系统", + "config.needsofnature.section.vanilla_integration": "原版集成", + "config.needsofnature.section.breeding": "繁殖", + "config.needsofnature.section.milking": "挤奶", + "config.needsofnature.section.pregnancy_timing": "怀孕时机", + "config.needsofnature.section.pregnancy_behavior": "怀孕行为", + "config.needsofnature.section.liquid_tank": "体液储槽", + "config.needsofnature.section.filled_penalties": "灌满惩罚", + "config.needsofnature.section.runtime_budget": "运行时预算", + "config.needsofnature.section.debug_feedback": "调试反馈", + "config.needsofnature.section.animation_data": "动画数据", + "config.needsofnature.toggle.pregnancy": "怀孕开关:", + "config.needsofnature.toggle.liquid_tank": "体液储槽开关:", + "config.needsofnature.toggle.mess_system": "污渍系统开关:", + "config.needsofnature.toggle.destroyed_skin_system": "破损皮肤系统开关:", + "config.needsofnature.toggle.vanilla_overrides": "原版覆盖开关:", + "config.needsofnature.use_animation_breeding": "使用动画繁殖覆盖:", + "config.needsofnature.require_male_female_breeding": "繁殖要求雄+雌:", + "config.needsofnature.female_cow_only_milking": "仅雌牛挤奶:", + "config.needsofnature.attack_hits": "攻击挣脱次数(1-50):", + "config.needsofnature.attack_decay": "挣脱衰减/秒(0-5):", + "config.needsofnature.attack_escape_invulnerability_ticks": "挣脱后无敌 tick(0-1200):", + "config.needsofnature.post_escape_gather_max_mobs": "挣脱后追赶者(0-64):", + "config.needsofnature.attack_outcome_failsafe_threshold": "攻击失败保险次数(1-50):", + "config.needsofnature.attack_creative_players": "允许攻击创造模式玩家:", + "config.needsofnature.last_defeated_enabled": "最后落败救援:", + "config.needsofnature.last_defeated_energy_threshold": "最后落败性欲(0-200):", + "config.needsofnature.last_defeated_search_radius": "最后落败半径(1-32):", + "config.needsofnature.last_defeated_cooldown_seconds": "最后落败冷却秒数:", + "config.needsofnature.pregnancy_chance_percent": "受孕概率(0-100):", + "config.needsofnature.pregnancy_duration_minutes": "妊娠时长(分钟):", + "config.needsofnature.pregnancy_egg_hatch_minutes": "怀孕蛋孵化时间(分钟):", + "config.needsofnature.pregnancy_auto_tame_mobs": "自动驯服可驯服后代:", + "config.needsofnature.pregnancy_friendly_mobs_ignore_players": "敌对后代忽略玩家:", + "config.needsofnature.offspring_count_button": "实体怀孕档案...", + "config.needsofnature.liquid_tank_capacity": "体液储槽容量(ml):", + "config.needsofnature.liquid_decay": "体液流失/秒(0-20):", + "config.needsofnature.peak_xp_per_ml": "高潮每 ml 经验:", + "config.needsofnature.liquid_puddle_despawn_seconds": "体液水洼消失秒数(1-300):", + "config.needsofnature.filled_stage_one_speed": "灌满速度倍率(第 1 阶段):", + "config.needsofnature.filled_stage_two_speed": "灌满速度倍率(第 2 阶段):", + "config.needsofnature.filled_stage_three_speed": "灌满速度倍率(第 3 阶段):", + "config.needsofnature.filled_stage_one_jump": "灌满跳跃倍率(第 1 阶段):", + "config.needsofnature.filled_stage_two_jump": "灌满跳跃倍率(第 2 阶段):", + "config.needsofnature.filled_stage_three_jump": "灌满跳跃倍率(第 3 阶段):", + "config.needsofnature.gender_spawn_button": "性别生成...", + "config.needsofnature.gender_spawn_title": "各实体的性别生成概率", + "config.needsofnature.gender_spawn_add": "添加入口", + "config.needsofnature.gender_spawn_reset": "重置", + "config.needsofnature.gender_spawn_reset_title": "重置性别生成概率?", + "config.needsofnature.gender_spawn_reset_body": "这将用 NoNpack 默认值替换实体行。", + "config.needsofnature.gender_spawn_column.entity": "实体", + "config.needsofnature.gender_spawn_column.male": "雄性", + "config.needsofnature.gender_spawn_column.female": "雌性", + "config.needsofnature.gender_spawn_column.both": "双性", + "config.needsofnature.gender_spawn_invalid_total": "总计必须为 100", + "config.needsofnature.liquid_gain_button": "体液增加...", + "config.needsofnature.liquid_gain_title": "各实体的体液增加", + "config.needsofnature.liquid_gain_add": "添加入口", + "config.needsofnature.liquid_gain_reset": "重置", + "config.needsofnature.liquid_gain_reset_title": "重置体液增加?", + "config.needsofnature.liquid_gain_reset_body": "这将用默认值替换所有入口。", + "config.needsofnature.liquid_gain_column.entity": "实体", + "config.needsofnature.liquid_gain_column.ml": "ml", + "config.needsofnature.liquid_gain_column.color": "颜色", + "config.needsofnature.offspring_count_title": "各实体的怀孕档案", + "config.needsofnature.offspring_count_add": "添加入口", + "config.needsofnature.offspring_count_reset": "重置", + "config.needsofnature.offspring_count_reset_title": "重置怀孕档案?", + "config.needsofnature.offspring_count_reset_body": "这将用 NoNpack 默认值替换实体行。", + "config.needsofnature.offspring_count_column.entity": "实体", + "config.needsofnature.entity_profile_column.chance": "概率", + "config.needsofnature.offspring_count_column.min": "最小", + "config.needsofnature.offspring_count_column.max": "最大", + "config.needsofnature.entity_profile_column.birth_entity": "出生实体", + "config.needsofnature.entity_profile_column.birth_mode": "模式", + "config.needsofnature.apply": "应用", + "config.needsofnature.done": "完成", + "config.needsofnature.done_unsaved": "不保存离开", + "config.needsofnature.reset_icon": "<", + "config.needsofnature.remove_icon": "x", + "config.needsofnature.debug_chat_mode.all": "全部", + "config.needsofnature.debug_chat_mode.setup_warnings_errors": "警告、设置、错误", + "config.needsofnature.debug_chat_mode.setup_errors": "设置 + 错误", + "config.needsofnature.debug_chat_mode.errors_only": "仅错误", + "config.needsofnature.ui_energy_x": "性欲条 X:", + "config.needsofnature.ui_energy_y": "性欲条 Y:", + "config.needsofnature.ui_attack_x": "攻击条 X:", + "config.needsofnature.ui_attack_y": "攻击条 Y:", + "config.needsofnature.ui_prompt_x": "攻击提示 X:", + "config.needsofnature.ui_prompt_y": "攻击提示 Y:", + "config.needsofnature.ui_liquid_x": "体液储槽 X:", + "config.needsofnature.ui_liquid_y": "体液储槽 Y:", + "config.needsofnature.ui_energy": "循环进度", + "config.needsofnature.ui_energy_heart": "性欲心", + "config.needsofnature.ui_attack": "攻击条", + "config.needsofnature.ui_prompt": "攻击提示", + "config.needsofnature.ui_liquid": "体液储槽", + "config.needsofnature.ui_drag_help": "拖拽 HUD 元素移动。方向键微调选中元素。", + "config.needsofnature.ui_selected": "已选中:%s", + "config.needsofnature.ui_selected_hidden": "已选中:%s(隐藏)", + "config.needsofnature.ui_hidden_label": "%s(隐藏)", + "config.needsofnature.ui_hide_selected": "隐藏选中", + "config.needsofnature.ui_show_selected": "显示选中", + "config.needsofnature.ui_reset_selected": "重置选中", + "config.needsofnature.ui_reset_all": "全部重置", + "config.needsofnature.tooltip.ui_toolbar_visibility": "显示或隐藏界面编辑器按钮。", + "config.needsofnature.tooltip.ui_toggle_selected_visibility": "显示或隐藏当前选中的 HUD 元素。", + "config.needsofnature.tooltip.category.toggles": "主要 NON 系统的主开关。", + "config.needsofnature.tooltip.category.player_preferences": "客户端持有的玩家偏好,如玩家性别和破损皮肤色调。", + "config.needsofnature.tooltip.category.settings": "玩法、覆盖、体液、怀孕、调试和高级设置。", + "config.needsofnature.tooltip.category.gameplay": "性欲、匹配和挣脱行为设置。", + "config.needsofnature.tooltip.category.vanilla_overrides": "覆盖原版繁殖和挤奶行为。", + "config.needsofnature.tooltip.category.liquid": "体液增加、流失、装瓶和灌满效果设置。", + "config.needsofnature.tooltip.category.pregnancy": "受孕概率、时长和蛋孵化设置。", + "config.needsofnature.tooltip.category.interface": "移动 NON HUD 元素以获得更好的对齐。", + "config.needsofnature.tooltip.category.intiface": "可选的本机 Buttplug.io 玩具控制集成,用于你的动画事件。", + "config.needsofnature.tooltip.category.debug_advanced": "调试输出和面向性能的高级选项。", + "config.needsofnature.tooltip.category.disabled_in_toggles": "已在开关中禁用。", + "config.needsofnature.tooltip.toggle.pregnancy": "全局启用或禁用怀孕机制。", + "config.needsofnature.tooltip.toggle.liquid_tank": "全局启用或禁用玩家体液储槽机制。", + "config.needsofnature.tooltip.toggle.mess_system": "全局启用或禁用污渍增加、清洁和可见污渍覆盖。", + "config.needsofnature.tooltip.toggle.destroyed_skin_system": "全局启用或禁用破损皮肤伤害、修复、手动剪毛和可见破损皮肤覆盖。", + "config.needsofnature.tooltip.toggle.vanilla_overrides": "全局启用或禁用所有 NON 原版覆盖机制。", + "config.needsofnature.tooltip.initial_energy_max": "生物首次初始化时分配的最大随机性欲。", + "config.needsofnature.tooltip.energy_gain_rate": "自然生物性欲随时间增长的全局倍率。", + "config.needsofnature.tooltip.near_animation_energy_gain_mult": "生物看到附近活跃动画时获得的额外性欲增长倍率。", + "config.needsofnature.tooltip.player_energy_aura_mult_low": "当玩家性欲为 100-149 时,附近符合条件的生物获得性欲,如同其自然增长率乘以该数值。", + "config.needsofnature.tooltip.player_energy_aura_mult_mid": "当玩家性欲为 150-199 时,附近符合条件的生物获得性欲,如同其自然增长率乘以该数值。", + "config.needsofnature.tooltip.player_energy_aura_mult_high": "当玩家性欲为 200 时,附近符合条件的生物获得性欲,如同其自然增长率乘以该数值。", + "config.needsofnature.tooltip.destroyed_skin_aura_mult_stage1": "玩家处于破损皮肤第 1 阶段时施加到性欲光环上的额外倍率。", + "config.needsofnature.tooltip.destroyed_skin_aura_mult_stage2": "玩家处于破损皮肤第 2 阶段时施加到性欲光环上的额外倍率。", + "config.needsofnature.tooltip.destroyed_skin_aura_mult_stage3": "玩家处于破损皮肤第 3 阶段时施加到性欲光环上的额外倍率。", + "config.needsofnature.tooltip.destroyed_skin_aura_mult_stage4": "玩家处于破损皮肤第 4 阶段时施加到性欲光环上的额外倍率。", + "config.needsofnature.tooltip.normal_damage_destroyed_skin_chance_percent": "非动画玩家伤害增加一次皮肤破损的百分比概率。", + "config.needsofnature.tooltip.player_energy_aura_radius_min": "性欲恰好为 100 时玩家性欲光环使用的方块半径。", + "config.needsofnature.tooltip.player_energy_aura_radius_max": "性欲为 200 时玩家性欲光环使用的方块半径。", + "config.needsofnature.tooltip.player_energy_aura_pulse_ticks": "玩家性欲光环施加额外生物性欲的频率,以 tick 计。", + "config.needsofnature.tooltip.loop_seconds": "循环非高潮阶段自动推进前的默认秒数。阶段时长倍率按阶段缩放此值。", + "config.needsofnature.tooltip.peak_loop_seconds": "循环高潮阶段自动推进前的默认秒数。阶段时长倍率按阶段缩放此值。", + "config.needsofnature.tooltip.gender_male": "新初始化生物只获得雄性性别标签的默认百分比概率。全局男/女/双值必须合计为 100。", + "config.needsofnature.tooltip.gender_female": "新初始化生物只获得雌性性别标签的默认百分比概率。全局男/女/双值必须合计为 100。", + "config.needsofnature.tooltip.gender_both": "新初始化生物同时获得两种性别标签的默认百分比概率。全局男/女/双值必须合计为 100。", + "config.needsofnature.tooltip.allow_player_gender_change_anytime": "禁用后,玩家无法在性别选定或分配后自由更改其 NON 性别。", + "config.needsofnature.tooltip.require_player_gender_selection_on_join": "启用后,没有保存 NON 性别的玩家必须在加入时选择一个。", + "config.needsofnature.tooltip.allowed_starting_genders": "控制首次加入性别选择界面中可用的性别。", + "config.needsofnature.tooltip.player_gender": "你的性别,用于动画资格和玩家 .m/.f 模型选择。", + "config.needsofnature.tooltip.player_gender.locked": "此服务器禁用了性别更改。", + "config.needsofnature.tooltip.sync_female_gender_mod": "保持 Female Gender Mod 的本地玩家性别与你的 NON 玩家性别一致。男+女同步为其他。", + "config.needsofnature.tooltip.destroyed_skin_female_gender_overrides": "根据你的破损皮肤阶段写入 Female Gender Mod 身体设置。关闭后停止后续写入并保留最近写入的值。", + "config.needsofnature.tooltip.destroyed_skin_female_gender_profiles": "配置 NON 为破损阶段 0-1 和 2-4 写入的 Female Gender Mod 身体设置。", + "config.needsofnature.tooltip.open_female_gender_preview": "打开 Female Gender Mod 自己的设置界面。", + "config.needsofnature.tooltip.intiface_enabled": "通过 Buttplug.io 启用本地玩具控制。默认禁用。", + "config.needsofnature.tooltip.intiface_server_url": "本地 Buttplug.io 服务器的 WebSocket URL,通常为 ws://127.0.0.1:12345。", + "config.needsofnature.tooltip.intiface_auto_connect": "加入世界时自动连接 Buttplug.io。", + "config.needsofnature.tooltip.intiface_stop_on_disconnect": "离开世界时向已连接设备发送停止命令。", + "config.needsofnature.tooltip.intiface_animation_baseline": "处于动画中时的低振动。反应冲击会暂时超过它。设为 0% 以禁用。", + "config.needsofnature.tooltip.intiface_energized": "兴奋激活时的基础振动,加上与心跳同步的高潮峰值强度。左滑块为基础,右滑块为脉冲峰值。", + "config.needsofnature.tooltip.intiface_reactiveimpact": "普通反应冲击提示的强度滑块和毫秒时长。", + "config.needsofnature.tooltip.intiface_peak_reactiveimpact": "高潮反应冲击提示的强度滑块和毫秒时长。", + "config.needsofnature.tooltip.intiface_cooldown": "脉冲之间的最小毫秒数。设为 0 以允许每个匹配提示。", + "config.needsofnature.tooltip.intiface_status": "当前本地 Buttplug.io 连接状态和设备数量。", + "config.needsofnature.tooltip.intiface_libraries_status": "可选的 Buttplug.io 支持库是否已安装并加载。", + "config.needsofnature.tooltip.intiface_libraries_install": "从 Maven Central 下载经过验证的库,并在 mods 文件夹中创建生成的库 jar。需要重启。", + "config.needsofnature.tooltip.intiface_connect_scan": "连接 Buttplug.io 并扫描设备。", + "config.needsofnature.tooltip.intiface_test_pulse": "使用配置的普通冲击配置发送手动测试脉冲。", + "config.needsofnature.tooltip.intiface_toy_settings": "打开振动器、抽插器和振荡器的玩具响应设置。", + "config.needsofnature.tooltip.intiface_vibrator_settings": "配置振动器基线和反应冲击脉冲。", + "config.needsofnature.tooltip.intiface_stroker_oscillator_settings": "配置抽插器和振荡器响应。", + "config.needsofnature.tooltip.intiface_oscillator_regular_speed": "处于普通动画阶段时的振荡器速度。设为 0% 以禁用。", + "config.needsofnature.tooltip.intiface_oscillator_peak_speed": "处于高潮动画阶段时的振荡器速度。设为 0% 以禁用。", + "config.needsofnature.tooltip.intiface_stroker_max_distance": "反应冲击提示时立即使用的抽插器位置。", + "config.needsofnature.tooltip.intiface_stroker_min_distance": "下一个反应冲击提示前抽插器瞄准的位置。", + "config.needsofnature.tooltip.intiface_stroker_regular_duration": "普通反应冲击提示时抽插器 API 移动到最大或最小位置使用的毫秒数。设为 0ms 以禁用。", + "config.needsofnature.tooltip.intiface_stroker_peak_duration": "高潮反应冲击提示时抽插器 API 移动到最大或最小位置使用的毫秒数。设为 0ms 以禁用。", + "config.needsofnature.tooltip.female_gender.range": "Female Gender Mod 值范围:%s。", + "config.needsofnature.tooltip.female_gender.breast_physics": "为此破损阶段档案启用或禁用 Female Gender Mod 胸部物理。", + "config.needsofnature.tooltip.female_gender.dual_physics": "开:使用独立左右物理。关:写入 Female Gender Mod 的单胸设置。", + "config.needsofnature.tooltip.armor_shed_effect": "客户端视觉效果,进入新动画时显示装备的护甲飞落。", + "config.needsofnature.tooltip.action_sound_volume": "缩放由动画音效提示解析出的 NoN 动作音效。Minecraft 的常规音效分类滑块仍然生效。", + "config.needsofnature.tooltip.male_male_chance": "同性雄性配对继续进行的掷骰概率。", + "config.needsofnature.tooltip.female_female_chance": "同性雌性配对继续进行的掷骰概率。", + "config.needsofnature.tooltip.entity_entity_chance": "任何生物+生物动画尝试在匹配前进行的掷骰概率。", + "config.needsofnature.tooltip.multi_actor_join_chance": "总演员数将达到 3+ 时加入的掷骰概率。", + "config.needsofnature.tooltip.use_animation_breeding": "启用后,处于恋爱状态的动物会先尝试通过 NON 动画繁殖,然后再生成幼崽。", + "config.needsofnature.tooltip.require_male_female_breeding": "启用后,动画繁殖只允许一雄一雌双亲。", + "config.needsofnature.tooltip.require_male_female_breeding.disabled_animation_breeding": "已禁用,因为动画繁殖已关闭。", + "config.needsofnature.tooltip.female_cow_only_milking": "启用后,只有雌性(或双性别)奶牛可以用桶挤奶。", + "config.needsofnature.tooltip.attack_hits": "挣脱攻击动画所需的基础 A/D 命中次数。", + "config.needsofnature.tooltip.attack_decay": "未按键时挣脱进度每秒衰减的速度。", + "config.needsofnature.tooltip.attack_escape_invulnerability_ticks": "挣脱攻击动画后玩家免伤的时间。20 tick = 1 秒。", + "config.needsofnature.tooltip.post_escape_gather_max_mobs": "成功挣脱攻击后立即开始追赶的附近 70+ 性欲生物的最大数量。设为 0 以禁用。", + "config.needsofnature.tooltip.attack_outcome_failsafe_threshold": "在 10 分钟内生物可以挣脱或高潮多少次攻击动画,超过后其性欲在保险冷却期间锁定为 0。", + "config.needsofnature.tooltip.attack_creative_players": "启用后,生物可以攻击并对创造模式玩家发起攻击动画。", + "config.needsofnature.tooltip.last_defeated_enabled": "启用后,致命生物伤害可以让玩家保留半颗心并开始落败动画,而不是死亡。", + "config.needsofnature.tooltip.last_defeated_energy_threshold": "触发最后落败救援所需的最低生物性欲。", + "config.needsofnature.tooltip.last_defeated_search_radius": "造成伤害的生物无法触发有效救援时使用的附近生物搜索半径。", + "config.needsofnature.tooltip.last_defeated_cooldown_seconds": "一次救援后,同一玩家再次被救援前的冷却时间。", + "config.needsofnature.tooltip.pregnancy_chance_percent": "达到高潮的玩家动画开始怀孕的概率。", + "config.needsofnature.tooltip.pregnancy_duration_minutes": "怀孕持续多少现实分钟后才可孵化。", + "config.needsofnature.tooltip.pregnancy_egg_hatch_minutes": "怀孕蛋实体的默认孵化时间(分钟)。", + "config.needsofnature.tooltip.pregnancy_auto_tame_mobs": "启用后,可驯服的怀孕后代会自动被生成它们的玩家驯服或绑定。", + "config.needsofnature.tooltip.pregnancy_friendly_mobs_ignore_players": "启用后,敌对的怀孕后代被阻止以任何玩家为目标。", + "config.needsofnature.tooltip.offspring_count_button": "打开按实体的怀孕档案编辑器。", + "config.needsofnature.tooltip.liquid_tank_capacity": "玩家储槽可容纳的体液最大量。", + "config.needsofnature.tooltip.liquid_decay": "每秒从玩家体液储槽中流失的毫升数。", + "config.needsofnature.tooltip.peak_xp_per_ml": "达到高潮动画后每个符合 XP 条件的 ml 生成的体验。默认为 0.0667,等于每 15 ml 1 XP。", + "config.needsofnature.tooltip.liquid_puddle_despawn_seconds": "生成的体液水洼粒子的寿命(秒)。", + "config.needsofnature.tooltip.filled_stage_one_speed": "处于灌满第 1 阶段时的移动速度倍率。", + "config.needsofnature.tooltip.filled_stage_two_speed": "处于灌满第 2 阶段时的移动速度倍率。", + "config.needsofnature.tooltip.filled_stage_three_speed": "处于灌满第 3 阶段时的移动速度倍率。", + "config.needsofnature.tooltip.filled_stage_one_jump": "处于灌满第 1 阶段时的跳跃倍率。", + "config.needsofnature.tooltip.filled_stage_two_jump": "处于灌满第 2 阶段时的跳跃倍率。", + "config.needsofnature.tooltip.filled_stage_three_jump": "处于灌满第 3 阶段时的跳跃倍率。", + "config.needsofnature.tooltip.liquid_gain_button": "打开按实体的体液增加和颜色覆盖编辑器。", + "config.needsofnature.tooltip.gender_spawn_button": "打开按实体的性别生成概率编辑器。现有生物保持其当前性别。", + "config.needsofnature.tooltip.scan_budget": "每个世界 tick 的最大生物扫描操作数,超过后跳过额外扫描。", + "config.needsofnature.tooltip.debug_chat_mode": "控制显示哪些 NON 调试聊天分类。", + "config.needsofnature.tooltip.debug_spin_mode": "客户端视觉覆盖:用简单的旋转调试片段替换动画。", + "config.needsofnature.tooltip.convert_male_v_to_a": "启用后,男性专属玩家可以使用 V 注入动画,并将这些高潮按 A 处理储槽和污渍行为。", + "config.needsofnature.tooltip.destroyed_skin_tint": "用于生成的破损皮肤回退的十六进制 RGB 色调。留空以从你的皮肤自动检测。", + "config.needsofnature.tooltip.destroyed_skin_tint.pick": "打开你的完整皮肤纹理并点击像素以使用其颜色。", + "config.needsofnature.tooltip.destroyed_skin_tint.custom_skin": "已禁用,因为存在 config/needsofnature/destroyed_skin.png。自定义破损皮肤忽略生成的皮肤色调。", + "config.needsofnature.tooltip.destroyed_skin_tint.disabled_system": "已禁用,因为破损皮肤系统在开关中已关闭。", + "config.needsofnature.tooltip.destroyed_skin_tint.default_skin": "已禁用,因为选定的默认皮肤有自己的破损皮肤纹理。默认破损皮肤忽略生成的皮肤色调。", + "config.needsofnature.tooltip.destroyed_skin_system.disabled_setting": "已禁用,因为破损皮肤系统在开关中已关闭。", + "config.needsofnature.tooltip.destroyed_skin_tint.auto": "清除手动色调,让 NON 再次从你的皮肤自动检测。", + "config.needsofnature.tooltip.loaded_animations": "查看所有已加载的动画定义和运行时 GeckoLib 资源状态。", + "config.needsofnature.tooltip.loaded_animations.load_order": "重新排序外部 NoN 包。在世界中更改会自动重新加载。", + "config.needsofnature.tooltip.loaded_animations.load_order.reset": "将外部 NoN 包顺序重置为默认字母顺序。", + "config.needsofnature.tooltip.liquid_gain_add": "添加新的实体体液行。", + "config.needsofnature.tooltip.liquid_gain_reset": "将所有体液增加/颜色行重置为默认值。", + "config.needsofnature.tooltip.gender_spawn_add": "添加新的实体性别生成行。", + "config.needsofnature.tooltip.gender_spawn_reset": "将实体性别生成行重置为 NoNpack 默认值。", + "config.needsofnature.tooltip.offspring_count_add": "添加新的实体怀孕档案行。", + "config.needsofnature.tooltip.offspring_count_reset": "将怀孕档案行重置为 NoNpack 默认值。", + "config.needsofnature.tooltip.gender_spawn_entry_entity": "实体 ID,例如:minecraft:wolf", + "config.needsofnature.tooltip.gender_spawn_entry_male": "雄性专属生成百分比。除非男+女+双合计恰好为 100,否则忽略该行。", + "config.needsofnature.tooltip.gender_spawn_entry_female": "雌性专属生成百分比。除非男+女+双合计恰好为 100,否则忽略该行。", + "config.needsofnature.tooltip.gender_spawn_entry_both": "双性别生成百分比。除非男+女+双合计恰好为 100,否则忽略该行。", + "config.needsofnature.tooltip.liquid_gain_entry_entity": "实体 ID,例如:minecraft:wolf", + "config.needsofnature.tooltip.liquid_gain_entry_ml": "此实体用作注入方时给予的体液毫升数。", + "config.needsofnature.tooltip.liquid_gain_entry_color": "可选的实体覆盖颜色,十六进制(#RRGGBB)。", + "config.needsofnature.tooltip.offspring_count_entry_entity": "实体 ID,例如:minecraft:wolf", + "config.needsofnature.tooltip.entity_profile_entry_chance": "此实体的受孕概率覆盖。留空以使用显示的默认/全局值。", + "config.needsofnature.tooltip.offspring_count_entry_min": "最小后代数量。数值限制在 1-16。", + "config.needsofnature.tooltip.offspring_count_entry_max": "最大后代数量。低于最小的数值会提升到最小。", + "config.needsofnature.tooltip.entity_profile_entry_birth_entity": "从此怀孕生成的实体 ID。留空使用来源实体。", + "config.needsofnature.tooltip.entity_profile_entry_birth_mode": "出生模式:蛋或直接。", + "config.needsofnature.tooltip.apply": "应用更改而不离开此界面。", + "config.needsofnature.tooltip.done": "关闭此界面并返回。", + "config.needsofnature.tooltip.done_save": "保存更改并返回。", + "config.needsofnature.tooltip.done_unsaved": "不保存未保存的更改就离开。", + "config.needsofnature.tooltip.reset": "将此值重置为默认值。", + "config.needsofnature.tooltip.remove": "移除此行。", + "screen.needsofnature.warning.proceed": "我了解,继续", + "screen.needsofnature.warning.proceed_countdown": "等待 %s 秒...", + "screen.needsofnature.warning.close_game": "关闭游戏", + "screen.needsofnature.warning.dont_show_again": "不再显示此警告", + "screen.needsofnature.warning.pack": "来自 NoNpack:%s", + "effect.needsofnature.energy": "性欲", + "effect.needsofnature.energy_relief": "性欲缓解", + "effect.needsofnature.energized": "兴奋", + "effect.needsofnature.fertility_increaser": "受孕提升", + "effect.needsofnature.filled": "被灌满", + "effect.needsofnature.pregnant": "怀孕", + "item.minecraft.potion.effect.needsofnature.energy": "性欲药水", + "item.minecraft.splash_potion.effect.needsofnature.energy": "喷溅型性欲药水", + "item.minecraft.lingering_potion.effect.needsofnature.energy": "滞留型性欲药水", + "item.minecraft.tipped_arrow.effect.needsofnature.energy": "性欲之箭", + "item.minecraft.potion.effect.needsofnature.energy_ii": "性欲药水 II", + "item.minecraft.splash_potion.effect.needsofnature.energy_ii": "喷溅型性欲药水 II", + "item.minecraft.lingering_potion.effect.needsofnature.energy_ii": "滞留型性欲药水 II", + "item.minecraft.tipped_arrow.effect.needsofnature.energy_ii": "性欲之箭 II", + "item.minecraft.potion.effect.needsofnature.energy_long": "性欲药水", + "item.minecraft.splash_potion.effect.needsofnature.energy_long": "喷溅型性欲药水", + "item.minecraft.lingering_potion.effect.needsofnature.energy_long": "滞留型性欲药水", + "item.minecraft.tipped_arrow.effect.needsofnature.energy_long": "性欲之箭", + "item.minecraft.potion.effect.needsofnature.energy_relief": "性欲缓解药水", + "item.minecraft.splash_potion.effect.needsofnature.energy_relief": "喷溅型性欲缓解药水", + "item.minecraft.lingering_potion.effect.needsofnature.energy_relief": "滞留型性欲缓解药水", + "item.minecraft.tipped_arrow.effect.needsofnature.energy_relief": "性欲缓解之箭", + "item.minecraft.potion.effect.needsofnature.energy_relief_ii": "性欲缓解药水 II", + "item.minecraft.splash_potion.effect.needsofnature.energy_relief_ii": "喷溅型性欲缓解药水 II", + "item.minecraft.lingering_potion.effect.needsofnature.energy_relief_ii": "滞留型性欲缓解药水 II", + "item.minecraft.tipped_arrow.effect.needsofnature.energy_relief_ii": "性欲缓解之箭 II", + "item.minecraft.potion.effect.needsofnature.energy_relief_long": "性欲缓解药水", + "item.minecraft.splash_potion.effect.needsofnature.energy_relief_long": "喷溅型性欲缓解药水", + "item.minecraft.lingering_potion.effect.needsofnature.energy_relief_long": "滞留型性欲缓解药水", + "item.minecraft.tipped_arrow.effect.needsofnature.energy_relief_long": "性欲缓解之箭", + "item.minecraft.potion.effect.needsofnature.fertile_nectar": "易孕花蜜", + "item.minecraft.splash_potion.effect.needsofnature.fertile_nectar": "喷溅型易孕花蜜", + "item.minecraft.lingering_potion.effect.needsofnature.fertile_nectar": "滞留型易孕花蜜", + "item.minecraft.tipped_arrow.effect.needsofnature.fertile_nectar": "易孕花蜜之箭", + "item.minecraft.potion.effect.needsofnature.fertility_increaser": "受孕提升药水", + "item.minecraft.splash_potion.effect.needsofnature.fertility_increaser": "喷溅型受孕提升药水", + "item.minecraft.lingering_potion.effect.needsofnature.fertility_increaser": "滞留型受孕提升药水", + "item.minecraft.tipped_arrow.effect.needsofnature.fertility_increaser": "受孕提升之箭", + "item.minecraft.potion.effect.needsofnature.fertility_increaser_ii": "受孕提升药水 II", + "item.minecraft.splash_potion.effect.needsofnature.fertility_increaser_ii": "喷溅型受孕提升药水 II", + "item.minecraft.lingering_potion.effect.needsofnature.fertility_increaser_ii": "滞留型受孕提升药水 II", + "item.minecraft.tipped_arrow.effect.needsofnature.fertility_increaser_ii": "受孕提升之箭 II", + "item.minecraft.potion.effect.needsofnature.fertility_increaser_long": "受孕提升药水", + "item.minecraft.splash_potion.effect.needsofnature.fertility_increaser_long": "喷溅型受孕提升药水", + "item.minecraft.lingering_potion.effect.needsofnature.fertility_increaser_long": "滞留型受孕提升药水", + "item.minecraft.tipped_arrow.effect.needsofnature.fertility_increaser_long": "受孕提升之箭", + "screen.needsofnature.gender_selection.title": "选择你的 NON 性别", + "screen.needsofnature.gender_selection.permanent": "此服务器之后不允许自由更改性别。", + "screen.needsofnature.gender_selection.changeable": "此选择之后可以在玩家偏好中更改。", + "screen.needsofnature.gender_selection.subtitle": "选择 NON 应如何对待你的玩家。", + "screen.needsofnature.gender_selection.option.male": "♂ 男性", + "screen.needsofnature.gender_selection.option.female": "♀ 女性", + "screen.needsofnature.gender_selection.option.both": "♂♀ 男+女", + "screen.needsofnature.gender_selection.current": "%s(当前)", + "debug.needsofnature.prefix": "[NoN] %s", + "debug.needsofnature.join_stage_disallows": "当前动画阶段此时不接受加入。", + "debug.needsofnature.mob_wants_join": "生物 %s 想加入你的动画(性欲 %s,动画 %s)。", + "debug.needsofnature.join_request_accepted": "生物 %s 的加入请求已被接受(动画 %s)。", + "debug.needsofnature.join_request_denied": "生物 %s 的加入请求已被拒绝。", + "debug.needsofnature.starting_animation_with_mob_200": "开始与生物 %s 的动画(200 性欲)。", + "debug.needsofnature.gathering_with_mob": "正在与生物 %s 聚集。", + "debug.needsofnature.mob_pair_attempt": "%s 试图与 %s 聚集(性欲 %s/%s,动画 %s)。", + "debug.needsofnature.mob_pair_gathering": "已排队聚集:%s 与 %s(动画 %s)。", + "debug.needsofnature.post_escape_gather_started": "%s 只附近生物在你挣脱后开始追赶。", + "debug.needsofnature.gather_cancelled": "聚集已取消:%s(%s)。", + "debug.needsofnature.mob_join_no_expanded_match": "%s 无法加入:没有匹配的扩展动画。", + "debug.needsofnature.roll_result": "掷骰 %s(%s%%)→ %s。", + "debug.needsofnature.energy_locked_attack_cooldown": "%s 处于攻击冷却;性欲锁定为 0。", + "debug.needsofnature.attack_blocked_pregnancy_friendly": "阻止直接攻击 %s -> %s(怀孕友好生物)。", + "debug.needsofnature.attack_blocked_gathering_pair": "阻止直接攻击 %s -> %s(双方都在待定/活跃中)。", + "debug.needsofnature.attack_blocked_energy_busy": "阻止直接攻击 %s -> %s(生物性欲 > 70 而玩家动画忙)。", + "debug.needsofnature.no_defeat_tag_animation": "没有匹配玩家的落败标签动画;跳过落败队列。", + "debug.needsofnature.missing_entity_bottle_fallback": "缺少 %s 体液的装瓶映射;使用混合体液瓶。", + "debug.needsofnature.missing_birth_animation": "未找到 %s 的分娩动画;怀孕已清除。", + "debug.needsofnature.missing_birth_cue": "分娩动画 %s 没有“birth”提示;使用停止回退。", + "debug.needsofnature.birth_fallback_spawn": "停止时触发了分娩回退生成(%s)。", + "debug.needsofnature.missing_pregnancy_entity_type": "怀孕生成缺少实体类型 %s。", + "item.needsofnature.entity_liquid_bottle": "%s 体液瓶", + "item.minecraft.potion.effect.needsofnature.liquid": "体液瓶", + "stat.needsofnature.animations_started_total": "NoN:开始的动画", + "stat.needsofnature.peaked_animations_total": "NoN:达到高潮的动画", + "stat.needsofnature.liquid_gained_ml_total": "NoN:获得的体液(ml)", + "stat.needsofnature.multi_actor_animations_total": "NoN:3+ 演员动画", + "stat.needsofnature.water_animations_total": "NoN:水中动画", + "stat.needsofnature.attack_animations_started_total": "NoN:开始的攻击动画", + "stat.needsofnature.attack_escapes_success_total": "NoN:攻击挣脱", + "stat.needsofnature.m_injector_peaks_ml_total": "NoN:M 类注入高潮(ml)", + "stat.needsofnature.manual_peaks_total": "NoN:手动高潮", + "stat.needsofnature.bottles_filled_total": "NoN:装满的瓶子", + "stat.needsofnature.pregnancies_started_total": "NoN:开始的怀孕", + "stat.needsofnature.pregnancies_completed_total": "NoN:完成的怀孕", + "stat.needsofnature.offspring_spawned_total": "NoN:生成的后代", + "advancements.needsofnature.root.title": "自然需求", + "advancements.needsofnature.root.description": "探索自然需求系统的方方面面。", + "advancements.needsofnature.natural_curiosity.title": "自然的好奇", + "advancements.needsofnature.natural_curiosity.description": "达到任意自然需求动画的高潮阶段。", + "advancements.needsofnature.filled_to_the_brim.title": "满到溢出", + "advancements.needsofnature.filled_to_the_brim.description": "完全填满你的体液储槽。", + "advancements.needsofnature.favouritism.title": "专情", + "advancements.needsofnature.favouritism.description": "仅用同一种实体的体液完全填满储槽。", + "advancements.needsofnature.first_pregnancy.title": "第一次怀孕", + "advancements.needsofnature.first_pregnancy.description": "开始你的第一次怀孕。", + "advancements.needsofnature.mother_nature.title": "自然之母", + "advancements.needsofnature.mother_nature.description": "完成 25 次怀孕。", + "advancements.needsofnature.messed_up.title": "一团糟", + "advancements.needsofnature.messed_up.description": "同时达到每种污渍的最大值。", + "advancements.needsofnature.group_activity.title": "团体活动", + "advancements.needsofnature.group_activity.description": "参与由四名或更多演员组成的动画。", + "advancements.needsofnature.tossed_around.title": "被甩来甩去", + "advancements.needsofnature.tossed_around.description": "连续被投入五次落败动画。", + "advancements.needsofnature.collectors_habit.title": "收集者的习惯", + "advancements.needsofnature.collectors_habit.description": "从体液储槽装满 25 个瓶子。", + "advancements.needsofnature.field_research.title": "实地研究", + "advancements.needsofnature.field_research.description": "在同一个游戏日内从五种不同实体获得体液。", + "advancements.needsofnature.bad_idea_beacon.title": "坏主意信标", + "advancements.needsofnature.bad_idea_beacon.description": "在皮肤完全破损时拥有满性欲。", + "advancements.needsofnature.productive_pregnancy.title": "多产怀孕", + "advancements.needsofnature.productive_pregnancy.description": "完成一次产生多于一个后代或蛋的怀孕。", + "advancements.needsofnature.udderly_unfortunate.title": "牛生不幸", + "advancements.needsofnature.udderly_unfortunate.description": "怀孕时喝牛奶。", + "advancements.needsofnature.emergency_snack.title": "应急零食", + "advancements.needsofnature.emergency_snack.description": "饥饿时获得 M 类注入饥饿奖励,或喝下实体体液瓶。", + "advancements.needsofnature.stable_diet.title": "稳定饮食", + "advancements.needsofnature.stable_diet.description": "连续三次触发应急零食,期间不吃不喝其他东西。", + "advancements.needsofnature.acquire_hardware.title": "获得硬件", + "advancements.needsofnature.acquire_hardware.description": "手持物品开始手动高潮动画。", + "gui.needsofnature.repair_skin.needs_loom": "附近需要织布机", + "gui.needsofnature.repair_skin.needs_loom_or_crafting_table": "附近需要织布机或工作台", + "gui.needsofnature.pregnancy_cycle.tooltip": "第 %s/%s 天\n%s", + "gui.needsofnature.player_profiles.title": "玩家交互配置", + "gui.needsofnature.player_profiles.tooltip": "设置你的默认动画请求偏好及针对每位玩家的偏好。", + "gui.needsofnature.player_profiles.default": "其他玩家的默认设置", + "gui.needsofnature.player_profiles.player_name": "玩家名称", + "gui.needsofnature.player_profiles.player_name_hint": "服务器已知的玩家名称", + "gui.needsofnature.player_profiles.apply": "应用", + "gui.needsofnature.player_profiles.unknown_player": "该玩家尚未被此服务器识别。", + "gui.needsofnature.player_profiles.allowed_profiles": "允许的配置", + "gui.needsofnature.player_profiles.allowed_profiles_hint": "至少必须保留一个配置。", + "gui.needsofnature.player_profiles.allowed": "%s:已允许", + "gui.needsofnature.player_profiles.disallowed": "%s:已禁用", + "gui.needsofnature.player_profiles.mode.deny_all": "不接受请求或攻击", + "gui.needsofnature.player_profiles.mode.requests_only": "接受请求,不接受攻击", + "gui.needsofnature.player_profiles.mode.requests_and_attacks": "接受请求与攻击", + "gui.needsofnature.player_profiles.mode.auto_accept": "自动接受请求", + "gui.needsofnature.player_request.select_title": "选择动画", + "gui.needsofnature.player_request.select_for": "请求 %s", + "gui.needsofnature.player_request.select_join_title": "选择加入动画", + "gui.needsofnature.player_request.select_join_for": "选择如何加入 %s", + "gui.needsofnature.player_request.section.available": "可用动画", + "gui.needsofnature.player_request.section.unavailable": "不可用动画", + "gui.needsofnature.player_request.role_title": "选择你的角色", + "gui.needsofnature.player_request.role_option": "你的角色:%s(%s)", + "gui.needsofnature.player_request.role_activity.active": "主动", + "gui.needsofnature.player_request.role_activity.passive": "被动", + "gui.needsofnature.player_request.roles": "%s 个角色选项", + "gui.needsofnature.player_request.consent_title": "动画请求", + "gui.needsofnature.player_request.accept": "接受", + "gui.needsofnature.player_request.reject": "拒绝", + "gui.needsofnature.player_request.start_prompt": "请求动画:%s", + "gui.needsofnature.player_request.join_prompt": "请求加入:%s", + "gui.needsofnature.player_request.waiting_title": "请求已发送", + "gui.needsofnature.player_request.waiting_description": "等待所有玩家接受...", + "gui.needsofnature.player_request.waiting": "等待玩家同意。", + "gui.needsofnature.player_request.started": "动画已开始。", + "gui.needsofnature.player_request.rejected": "动画请求已被拒绝。", + "gui.needsofnature.player_request.cancelled": "动画请求已取消。", + "gui.needsofnature.player_request.timeout": "动画请求已超时。", + "gui.needsofnature.player_request.out_of_range": "玩家距离过远。", + "gui.needsofnature.player_request.damaged": "动画请求因受到伤害而取消。", + "gui.needsofnature.player_request.disconnected": "有玩家断开连接。", + "gui.needsofnature.player_request.player_busy": "玩家正在进行其他动画。", + "gui.needsofnature.player_request.recipient_busy": "被请求的玩家已有其他待处理请求。", + "gui.needsofnature.player_request.not_accepting": "玩家当前不接受请求。", + "gui.needsofnature.player_request.energy_low": "请求动画至少需要 %s 点性欲。", + "gui.needsofnature.player_request.already_pending": "你已有待处理的动画请求。", + "gui.needsofnature.player_request.no_match": "没有符合条件的动画匹配这些角色。", + "gui.needsofnature.player_request.block_requirement_unavailable": "所需方块不可用", + "gui.needsofnature.player_request.block_requirement_summary": "需要:%s", + "gui.needsofnature.player_request.block_requirement_tooltip": "此动画无法在此开始,因为其方块条件不可用:%s", + "gui.needsofnature.player_request.animation_changed": "所选动画已不再符合条件。", + "gui.needsofnature.player_request.protected": "已装备的保护器阻止了该动画。", + "gui.needsofnature.player_request.invalid": "动画请求已失效。", + "config.needsofnature.category.credits": "鸣谢", + "config.needsofnature.credits_title": "Needs Of Nature 鸣谢", + "config.needsofnature.credits_intro": "Needs Of Nature 的诞生离不开:", + "config.needsofnature.credits.resource_credits": "资源鸣谢", + "config.needsofnature.credits.project_links": "项目链接", + "config.needsofnature.credits.category_value": "分类:%s", + "config.needsofnature.credits.creator_value": "作者:%s", + "config.needsofnature.credits.source_value": "来源:%s", + "config.needsofnature.credits.license_value": "许可:%s", + "config.needsofnature.credits.license_modified_value": "许可:%s | 已修改", + "config.needsofnature.credits.assets_value": "资源:%s", + "config.needsofnature.credits.modified": "已修改", + "config.needsofnature.credits.source": "来源", + "config.needsofnature.credits.license": "许可", + "config.needsofnature.loaded_animations.load_order.move_up": "上移数据包", + "config.needsofnature.loaded_animations.load_order.move_down": "下移数据包", + "config.needsofnature.loaded_animations.disabled_count": "%s 个已关闭", + "config.needsofnature.loaded_animations.details": "详情", + "config.needsofnature.destroyed_skin_female_gender_overrides": "用受损皮肤配置覆盖 FGM 设置:", + "config.needsofnature.destroyed_skin_female_gender_profiles": "胸部设置...", + "config.needsofnature.destroyed_skin_female_gender_profiles_title": "胸部设置", + "config.needsofnature.destroyed_skin_female_gender_profile.low": "受损阶段 0-1", + "config.needsofnature.destroyed_skin_female_gender_profile.high": "受损阶段 2-4", + "config.needsofnature.open_non_player_preferences": "打开 NoN 玩家偏好", + "config.needsofnature.intiface_latency_compensation": "延迟补偿:", + "config.needsofnature.section.intiface_timing": "时机", + "config.needsofnature.section.player_requests": "玩家请求", + "config.needsofnature.section.player_attack_balance": "玩家攻击平衡", + "config.needsofnature.section.mount_protection": "坐骑保护", + "config.needsofnature.section.liquid_based_pregnancy": "基于液体的怀孕", + "config.needsofnature.section.ripped_skin_repair": "破损皮肤修复", + "config.needsofnature.section.death_persistence": "死亡后保留", + "config.needsofnature.section.animation_runtime": "动画行为", + "config.needsofnature.keep_mess_after_death": "死亡后保留污渍:", + "config.needsofnature.keep_ripped_skin_after_death": "死亡后保留破损皮肤:", + "config.needsofnature.keep_liquid_tank_after_death": "死亡后保留液体槽:", + "config.needsofnature.keep_pregnancy_after_death": "死亡后继续怀孕:", + "config.needsofnature.allow_crafting_table_skin_repair": "允许工作台修复:", + "config.needsofnature.player_request_min_energy": "请求动画所需最低性欲(0-200):", + "config.needsofnature.attack_escape_damage_difficulty_percent": "逃脱生命值惩罚(0-200%):", + "config.needsofnature.pvp_full_health_escape_bonus_percent": "满血逃脱加成(0-200%):", + "config.needsofnature.pvp_critical_health_mash_strength_percent": "残血挣扎强度(0-200%):", + "config.needsofnature.pvp_max_heat_escape_strength_percent": "最大热度逃脱强度(0-200%):", + "config.needsofnature.pvp_zero_heat_attack_strength_percent": "零热度攻击强度(0-200%):", + "config.needsofnature.attack_escape_invulnerability_seconds": "逃脱伤害无敌时间(秒)(0-10):", + "config.needsofnature.attack_escape_animation_protection_seconds": "逃脱后动画保护时间(秒)(0-10):", + "config.needsofnature.block_animations_while_riding_living_entities": "保护生物坐骑:", + "config.needsofnature.block_animations_while_riding_vehicles": "保护载具:", + "config.needsofnature.liquid_based_pregnancy": "基于液体的怀孕:", + "config.needsofnature.pregnancy_risky_start_day": "危险阶段开始(第 1-28 天):", + "config.needsofnature.pregnancy_ovulation_end_day": "排卵期结束(第 3-30 天):", + "config.needsofnature.pregnancy_safe_day_chance_percent": "安全日概率倍率(%):", + "config.needsofnature.pregnancy_risky_chance_percent": "危险日概率倍率(%):", + "config.needsofnature.pregnancy_ovulation_chance_percent": "排卵日概率倍率(%):", + "config.needsofnature.pregnancy_cycle.safe": "安全日", + "config.needsofnature.pregnancy_cycle.normal": "普通日", + "config.needsofnature.pregnancy_cycle.risky": "危险日", + "config.needsofnature.pregnancy_cycle.ovulation": "排卵期", + "config.needsofnature.entity_profile_column.egg": "蛋", + "config.needsofnature.entity_profile_egg_button": "蛋", + "config.needsofnature.egg_profile_title": "蛋设置", + "config.needsofnature.egg_profile_start_size": "初始尺寸", + "config.needsofnature.egg_profile_end_size": "孵化尺寸", + "config.needsofnature.egg_profile_texture": "纹理", + "config.needsofnature.egg_profile_health": "生命值", + "config.needsofnature.reset": "重置", + "config.needsofnature.tooltip.category.credits": "查看项目贡献者、资源鸣谢与项目链接。", + "config.needsofnature.tooltip.female_gender.breast_texture_editor": "打开女性性别模组共享的胸部纹理编辑器。", + "config.needsofnature.tooltip.wildfire_gender_controlled_by_non": "在 Needs Of Nature 玩家偏好中更改性别。", + "config.needsofnature.tooltip.wildfire_personalization_controlled_by_non": "在 Needs Of Nature 中更改胸部设置。", + "config.needsofnature.tooltip.open_non_player_preferences": "打开 Needs Of Nature 玩家偏好。", + "config.needsofnature.tooltip.intiface_latency_compensation": "提前这么多毫秒发送可预测的动画和心跳玩具指令。动画开始无法在收到前预判;停止和断开指令仍即时发送。", + "config.needsofnature.tooltip.female_gender.breast_size_extended": "超过 100% 的值在某些条件下可能看起来破损或被裁剪。", + "config.needsofnature.tooltip.keep_mess_after_death": "启用后,玩家污渍数据在死亡和重生后保留。", + "config.needsofnature.tooltip.keep_ripped_skin_after_death": "启用后,破损皮肤的伤害和阶段在死亡和重生后保留。", + "config.needsofnature.tooltip.keep_liquid_tank_after_death": "启用后,液体量和成分在死亡和重生后保留。", + "config.needsofnature.tooltip.keep_pregnancy_after_death": "启用后,怀孕状态在死亡后保留并在重生后继续。", + "config.needsofnature.tooltip.allow_crafting_table_skin_repair": "启用后,工作台也可修复破损衣物。织布机始终可用。", + "config.needsofnature.tooltip.player_request_min_energy": "请求新动画或加入现有动画所需的最低玩家性欲。", + "config.needsofnature.tooltip.attack_escape_damage_difficulty_percent": "低生命值对怪物的逃脱进度有多大的削弱作用。0% 禁用生命值惩罚,100% 为正常,200% 更严苛。", + "config.needsofnature.tooltip.pvp_full_health_escape_bonus_percent": "在 75% 与满血之间获得额外的 PvP 逃脱进度。30% 时,满血防守方的挣扎强度为 1.30 倍。", + "config.needsofnature.tooltip.pvp_critical_health_mash_strength_percent": "PvP 零生命值时的挣扎强度,在 25% 生命值时恢复为正常强度。对攻击方和防守方同样适用。", + "config.needsofnature.tooltip.pvp_max_heat_escape_strength_percent": "PvP 防守方在最大性欲时的挣扎强度。性欲低于 60% 时无逃脱惩罚。", + "config.needsofnature.tooltip.pvp_zero_heat_attack_strength_percent": "PvP 攻击方在零性欲时的挣扎强度,在最大性欲时恢复为正常强度。", + "config.needsofnature.tooltip.attack_escape_invulnerability_seconds": "施加逃脱伤害后,玩家免受普通伤害的时间。", + "config.needsofnature.tooltip.attack_escape_animation_protection_seconds": "逃脱攻击动画后,阻止新动画开始的时间。", + "config.needsofnature.tooltip.block_animations_while_riding_living_entities": "启用后,玩家骑乘生物坐骑(如马、猪或炽足兽)时,跳过自动的 NoN 生物-玩家动画开始。", + "config.needsofnature.tooltip.block_animations_while_riding_vehicles": "启用后,玩家乘坐非生物载具(如船或矿车)时,跳过自动的 NoN 生物-玩家动画开始。", + "config.needsofnature.tooltip.liquid_based_pregnancy": "使用存储的液体和玩家的 30 天周期,而不是在峰值时直接判定怀孕。混合液体随机使用最近三位非玩家供体之一。", + "config.needsofnature.tooltip.pregnancy_risky_start_day": "30 天怀孕周期的第一个危险日。", + "config.needsofnature.tooltip.pregnancy_ovulation_end_day": "最后一个排卵日。排卵日始终包含这一天及前一天。", + "config.needsofnature.tooltip.pregnancy_safe_day_chance_percent": "安全阶段的怀孕概率倍率。10% 等于 0.1 倍。", + "config.needsofnature.tooltip.pregnancy_risky_chance_percent": "危险阶段的怀孕概率倍率。300% 等于 3 倍。", + "config.needsofnature.tooltip.pregnancy_ovulation_chance_percent": "两天排卵期的怀孕概率倍率。500% 等于 5 倍。", + "config.needsofnature.tooltip.animation.auto_switch_third_person": "本地玩家处于动画中时,临时切换到第三人称视角。", + "config.needsofnature.tooltip.loaded_animations.load_order.move_up": "将此数据包上移一位。", + "config.needsofnature.tooltip.loaded_animations.load_order.move_down": "将此数据包下移一位。", + "config.needsofnature.tooltip.entity_profile_entry_egg": "打开此源实体的蛋设置。", + "config.needsofnature.tooltip.egg_profile_start_size": "生成时蛋的渲染与碰撞箱比例。", + "config.needsofnature.tooltip.egg_profile_end_size": "孵化时蛋的渲染与碰撞箱比例。", + "config.needsofnature.tooltip.egg_profile_texture": "可选完整纹理 ID,例如 needsofnature:textures/entity/pregnancy_egg/default.png。", + "config.needsofnature.tooltip.egg_profile_health": "蛋的生命值(以 Minecraft 原始生命值计)。", + "config.needsofnature.tooltip.egg_profile_reset": "将这些蛋字段重置为当前 NoN 数据包的默认值。", + "debug.needsofnature.player_join_accepted": "已加入动画(动画 %s)。", + "debug.needsofnature.player_join_failed": "无法加入动画(动画 %s)。", + "debug.needsofnature.player_join_no_expanded_match": "无法加入:没有匹配的扩展动画。", + "debug.needsofnature.player_join_denied_busy": "无法加入:你已处于动画中。", + "debug.needsofnature.mounted_animation_blocked": "动画已跳过:你正骑乘 %s。", + "debug.needsofnature.animation.stop_all_broadcast_queued": "[NoN 动画] 已排队广播停止全部。", + "debug.needsofnature.animation.server_eval": "[NoN 动画] 服务器评估:角色=%s 选择=%s 键=%s", + "debug.needsofnature.animation.start_received": "[NoN 动画] 开始 %s 角色=%s 键=%s", + "debug.needsofnature.animation.stop_all_received": "[NoN 动画] 停止全部", + "debug.needsofnature.animation.stop_instance_received": "[NoN 动画] 停止实例", + "debug.needsofnature.animation.server_cannot_receive_stop_all": "[NoN 动画] 服务器无法接收停止全部数据包(服务端未安装本模组)。", + "debug.needsofnature.animation.stop_all_requested": "[NoN 动画] 已请求停止全部。", + "debug.needsofnature.animation.no_active_instance_to_stop": "[NoN 动画] 没有可停止的活动实例。", + "debug.needsofnature.animation.server_cannot_receive_stop_instance": "[NoN 动画] 服务器无法接收停止实例数据包(服务端未安装本模组)。", + "debug.needsofnature.animation.stop_instance_requested": "[NoN 动画] 已请求停止实例。", + "debug.needsofnature.animation.no_entity_targeted": "[NoN 动画] 未选中实体。", + "debug.needsofnature.animation.target_not_living": "[NoN 动画] 目标不是生物实体。", + "debug.needsofnature.animation.target_not_mob": "[NoN 动画] 目标不是怪物。", + "debug.needsofnature.animation.animation_deselected": "[NoN 动画] 已取消选择动画。", + "debug.needsofnature.animation.animation_selected_add_actors_for_join": "[NoN 动画] 已选择动画。添加额外角色后按开始即可加入。", + "debug.needsofnature.animation.entity_deselected_actor_count": "[NoN 动画] 实体:%s 已取消选择,角色 %s", + "debug.needsofnature.animation.entity_selected_actor_count": "[NoN 动画] 实体:%s 已选择,角色 %s", + "debug.needsofnature.animation.self_deselected_actor_count": "[NoN 动画] 已取消选择自己,角色 %s", + "debug.needsofnature.animation.self_selected_actor_count": "[NoN 动画] 已选择自己,角色 %s", + "debug.needsofnature.animation.no_actors_selected": "[NoN 动画] 未选择角色。", + "debug.needsofnature.animation.no_extra_actors_for_join": "[NoN 动画] 未选择用于加入的额外角色。", + "debug.needsofnature.animation.server_cannot_receive_join": "[NoN 动画] 服务器无法接收加入数据包(服务端未安装本模组)。", + "debug.needsofnature.animation.join_requested_instance_extras": "[NoN 动画] 已请求加入。实例=%s 额外角色=%s", + "debug.needsofnature.animation.server_cannot_receive_debug_packets": "[NoN 动画] 服务器无法接收调试数据包(服务端未安装本模组)。", + "debug.needsofnature.animation.start_requested_actor_count": "[NoN 动画] 已请求开始(H)。角色=%s", + "debug.needsofnature.animation.no_active_instance_to_advance": "[NoN 动画] 没有可推进的活动实例。", + "debug.needsofnature.animation.next_stage_requested_server": "[NoN 动画] 已请求下一阶段(服务器)。", + "debug.needsofnature.animation.next_stage_requested_local": "[NoN 动画] 已请求下一阶段(仅本地)。", + "debug.needsofnature.animation.no_active_instance_to_adjust_speed": "[NoN 动画] 没有可调整速度的活动实例。", + "debug.needsofnature.animation.speed_change_requested": "[NoN 动画] 已请求速度更改(%s)。", + "debug.needsofnature.animation.server_cannot_receive_speed": "[NoN 动画] 服务器无法接收速度数据包(服务端未安装本模组)。", + "debug.needsofnature.animation.stage_sequence_ended_server_stop_unavailable": "[NoN 动画] 阶段序列已在本地结束;服务器停止不可用。", + "debug.needsofnature.animation.queue_enqueue_failed_exactly_one_player": "[NoN 动画] 排队失败:必须恰好有一位玩家角色。", + "debug.needsofnature.animation.queue_enqueue_failed_full": "[NoN 动画] 排队失败:队列已满(上限=%s)。", + "debug.needsofnature.animation.queue_enqueue_failed_player_missing": "[NoN 动画] 排队失败:缺少玩家角色。", + "debug.needsofnature.animation.queue_enqueue_denied_mob_already_queued": "[NoN 动画] 排队被拒绝:%s 已为另一位玩家排队。", + "debug.needsofnature.animation.queued_at_position": "[NoN 动画] %s 已排队至第 %s 位。", + "debug.needsofnature.animation.queue_cleared_for_player": "[NoN 动画] 玩家的队列已清空。", + "debug.needsofnature.animation.queue_cleared_for_active_instance": "[NoN 动画] 活动实例的队列已清空。", + "debug.needsofnature.animation.matched_unpathable_actor": "[NoN 动画] 匹配到 %s,但角色无法寻路(不是怪物/玩家)。", + "debug.needsofnature.animation.matched_multiple_players_not_supported": "[NoN 动画] 匹配到 %s,但暂不支持多位玩家。", + "debug.needsofnature.animation.matched_multiplayer_direct_start": "[NoN 动画] 匹配到 %s 与角色 %s -> 直接多人调试开始。", + "debug.needsofnature.animation.start_cancelled_block_and_water_requirement": "[NoN 动画] 开始取消:方块条件不能与水中条件同时使用。", + "debug.needsofnature.animation.start_cancelled_no_valid_wall": "[NoN 动画] 开始取消:找不到有效的墙体放置位置(%s,角色:%s)。", + "debug.needsofnature.animation.start_cancelled_no_valid_center_support": "[NoN 动画] 开始取消:找不到有效的中心支撑位置(%s,角色:%s)。", + "debug.needsofnature.animation.start_cancelled_players_too_far_from_requester": "[NoN 动画] 开始取消:所选玩家必须位于请求者 %s 格范围内。", + "debug.needsofnature.animation.join_failed_no_active_instance": "[NoN 动画] 加入失败:没有此活动实例。", + "debug.needsofnature.animation.join_denied_stage_disallows": "[NoN 动画] 加入被拒绝:当前阶段不允许加入。", + "debug.needsofnature.animation.join_failed_players_not_supported": "[NoN 动画] 加入失败:加入不支持玩家角色。", + "debug.needsofnature.animation.join_failed_no_expanded_definition": "[NoN 动画] 加入失败:扩展角色集没有对应定义。", + "debug.needsofnature.animation.no_active_instance_to_adjust": "[NoN 动画] 没有可调整的活动实例。", + "debug.needsofnature.animation.speed_unchanged": "[NoN 动画] 速度未改变。", + "debug.needsofnature.animation.speed_set": "[NoN 动画] 速度已设为 %s", + "debug.needsofnature.animation.no_such_active_instance": "[NoN 动画] 没有此活动实例。", + "debug.needsofnature.animation.instance_in_different_world": "[NoN 动画] 实例位于不同世界;无法从此处停止。", + "debug.needsofnature.animation.broadcasting_stop": "[NoN 动画] 正在广播停止", + "debug.needsofnature.animation.queue_continued_for_player": "[NoN 动画] 玩家的队列已继续。", + "debug.needsofnature.animation.queue_cleared": "[NoN 动画] 队列已清空。", + "debug.needsofnature.animation.start_cancelled_water_requirement_not_met": "[NoN 动画] 开始取消:未满足水中条件。", + "debug.needsofnature.animation.start_cancelled_not_allowed_in_water": "[NoN 动画] 开始取消:动画不允许在水中。", + "debug.needsofnature.animation.start_cancelled_not_enough_space": "[NoN 动画] 开始取消:%s 的空间不足(角色:%s)。", + "debug.needsofnature.animation.actors_immediate_broadcast_start": "[NoN 动画] 立即开始。正在广播开始", + "debug.needsofnature.animation.queue_skipped_player_unavailable": "[NoN 动画] %s 的队列已跳过(角色:%s):玩家不可用。", + "debug.needsofnature.animation.queue_skipped_missing_non_player_actor": "[NoN 动画] %s 的队列已跳过(角色:%s):缺少非玩家角色。", + "debug.needsofnature.animation.queue_skipped_no_actors_resolved": "[NoN 动画] %s 的队列已跳过(角色:%s):未解析出角色。", + "debug.needsofnature.animation.queue_skipped_missing_definition": "[NoN 动画] %s 的队列已跳过(角色:%s):缺少定义。", + "debug.needsofnature.animation.queue_skipped_requirements_not_met": "[NoN 动画] %s 的队列已跳过(角色:%s):未满足条件。", + "debug.needsofnature.animation.queue_skipped_start_failed": "[NoN 动画] %s 的队列已跳过(角色:%s):开始失败。", + "debug.needsofnature.animation.queued_animation_started": "[NoN 动画] 已排队的动画开始:%s(角色:%s)", + "key.categories.needsofnature.animation": "NeedsOfNature 动画调试", + "key.category.needsofnature.animation": "NeedsOfNature 动画调试", + "key.needsofnature.animation.select_actor": "选择角色(调试)", + "key.needsofnature.animation.select_self": "选择自己(调试)", + "key.needsofnature.animation.start_debug_animation": "开始动画(调试)", + "key.needsofnature.animation.stop_all_debug": "停止全部动画(调试)", + "key.needsofnature.animation.stop_instance_debug": "停止动画实例(调试)", + "key.needsofnature.animation.next_stage_debug": "下一阶段(调试)", + "key.needsofnature.animation.speed_up_debug": "加速动画(调试)", + "key.needsofnature.animation.speed_down_debug": "减速动画(调试)", + "config.needsofnature.animation.title": "NeedsOfNature 动画", + "config.needsofnature.animation.force_vanilla_entity_textures": "强制使用原版实体纹理(忽略资源包)", + "config.needsofnature.animation.block_search_radius": "方块搜索半径", + "config.needsofnature.animation.auto_switch_third_person": "动画开始时自动切换到第三人称", + "config.needsofnature.animation.debug_menu": "调试选项", + "config.needsofnature.animation.open": "打开", + "config.needsofnature.animation.debug_title": "NeedsOfNature 动画调试", + "config.needsofnature.animation.debug_chat_mode": "调试聊天", + "config.needsofnature.animation.debug_chat_mode.all": "全部", + "config.needsofnature.animation.debug_chat_mode.setup_warnings_errors": "警告、设置、错误", + "config.needsofnature.animation.debug_chat_mode.setup_errors": "设置、错误", + "config.needsofnature.animation.debug_chat_mode.errors_only": "仅错误", + "config.needsofnature.animation.reset_icon": "<", + "config.jade.plugin_needsofnature.gender": "生物性别", + "config.needsofnature.server_settings.no_permission": "你没有更改服务器设置的权限。", + "config.needsofnature.server_settings.stale": "此界面打开时服务器设置已更改。请重新打开并重试。", + "config.needsofnature.server_settings.invalid": "服务器拒绝了设置更新。" +} \ No newline at end of file diff --git a/src/main/resources/assets/needsofnature/models/item/energy_augmenter.json b/src/main/resources/assets/needsofnature/models/item/energy_augmenter.json new file mode 100644 index 0000000..a2e7f37 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_augmenter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_augmenter" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_diminisher.json b/src/main/resources/assets/needsofnature/models/item/energy_diminisher.json new file mode 100644 index 0000000..2a6b6d6 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_diminisher.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_diminisher" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_lingering_potion.json b/src/main/resources/assets/needsofnature/models/item/energy_lingering_potion.json new file mode 100644 index 0000000..7665662 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_lingering_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_potion_overlay", + "layer1": "minecraft:item/lingering_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_potion.json b/src/main/resources/assets/needsofnature/models/item/energy_potion.json new file mode 100644 index 0000000..fc0ccbd --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_potion_overlay", + "layer1": "minecraft:item/potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_relief_lingering_potion.json b/src/main/resources/assets/needsofnature/models/item/energy_relief_lingering_potion.json new file mode 100644 index 0000000..1f217bc --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_relief_lingering_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_relief_potion_overlay", + "layer1": "minecraft:item/lingering_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_relief_potion.json b/src/main/resources/assets/needsofnature/models/item/energy_relief_potion.json new file mode 100644 index 0000000..49fa5ae --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_relief_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_relief_potion_overlay", + "layer1": "minecraft:item/potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_relief_splash_potion.json b/src/main/resources/assets/needsofnature/models/item/energy_relief_splash_potion.json new file mode 100644 index 0000000..6b3d107 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_relief_splash_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_relief_potion_overlay", + "layer1": "minecraft:item/splash_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_relief_tipped_arrow.json b/src/main/resources/assets/needsofnature/models/item/energy_relief_tipped_arrow.json new file mode 100644 index 0000000..60ad37c --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_relief_tipped_arrow.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_relief_tipped_arrow_head", + "layer1": "minecraft:item/tipped_arrow_base" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_splash_potion.json b/src/main/resources/assets/needsofnature/models/item/energy_splash_potion.json new file mode 100644 index 0000000..c66e258 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_splash_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_potion_overlay", + "layer1": "minecraft:item/splash_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_stabilizer.json b/src/main/resources/assets/needsofnature/models/item/energy_stabilizer.json new file mode 100644 index 0000000..2915378 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_stabilizer.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_stabilizer" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/energy_tipped_arrow.json b/src/main/resources/assets/needsofnature/models/item/energy_tipped_arrow.json new file mode 100644 index 0000000..e5d4e02 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/energy_tipped_arrow.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/energy_tipped_arrow_head", + "layer1": "minecraft:item/tipped_arrow_base" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/fertile_nectar_lingering_potion.json b/src/main/resources/assets/needsofnature/models/item/fertile_nectar_lingering_potion.json new file mode 100644 index 0000000..6e4e49a --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/fertile_nectar_lingering_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/fertile_nectar_overlay", + "layer1": "minecraft:item/lingering_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/fertile_nectar_potion.json b/src/main/resources/assets/needsofnature/models/item/fertile_nectar_potion.json new file mode 100644 index 0000000..a291bd8 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/fertile_nectar_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/fertile_nectar_overlay", + "layer1": "minecraft:item/potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/fertile_nectar_splash_potion.json b/src/main/resources/assets/needsofnature/models/item/fertile_nectar_splash_potion.json new file mode 100644 index 0000000..42d1736 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/fertile_nectar_splash_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/fertile_nectar_overlay", + "layer1": "minecraft:item/splash_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/fertility_lingering_potion.json b/src/main/resources/assets/needsofnature/models/item/fertility_lingering_potion.json new file mode 100644 index 0000000..0d5a65d --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/fertility_lingering_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/fertility_potion_overlay", + "layer1": "minecraft:item/lingering_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/fertility_potion.json b/src/main/resources/assets/needsofnature/models/item/fertility_potion.json new file mode 100644 index 0000000..a68a691 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/fertility_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/fertility_potion_overlay", + "layer1": "minecraft:item/potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/fertility_splash_potion.json b/src/main/resources/assets/needsofnature/models/item/fertility_splash_potion.json new file mode 100644 index 0000000..527189b --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/fertility_splash_potion.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/fertility_potion_overlay", + "layer1": "minecraft:item/splash_potion" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/flower_mix.json b/src/main/resources/assets/needsofnature/models/item/flower_mix.json new file mode 100644 index 0000000..0b1b1cc --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/flower_mix.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "needsofnature:item/flower_mix" + } +} diff --git a/src/main/resources/assets/needsofnature/models/item/horse_liquid_collector.json b/src/main/resources/assets/needsofnature/models/item/horse_liquid_collector.json new file mode 100644 index 0000000..e4a4763 --- /dev/null +++ b/src/main/resources/assets/needsofnature/models/item/horse_liquid_collector.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:block/barrel_side", + "particle": "minecraft:block/barrel_side" + }, + "display": { + "thirdperson_righthand": { + "translation": [0, -3.25, 0], + "scale": [0.6, 0.6, 0.6] + }, + "thirdperson_lefthand": { + "translation": [0, -3.5, 0], + "scale": [0.6, 0.6, 0.6] + }, + "firstperson_righthand": { + "translation": [0, -5, 2.5], + "scale": [0.6, 0.6, 0.6] + }, + "firstperson_lefthand": { + "translation": [0, -5, 2.5], + "scale": [0.6, 0.6, 0.6] + }, + "ground": { + "translation": [0, -2, 0], + "scale": [0.6, 0.6, 0.6] + }, + "gui": { + "rotation": [23, 140, 0], + "translation": [0, -6.5, 0], + "scale": [0.6, 0.6, 0.6] + }, + "fixed": { + "rotation": [0, -90, 0], + "translation": [0, -8.25, 0], + "scale": [0.6, 0.6, 0.6] + }, + "on_shelf": { + "rotation": [0, -180, 0], + "translation": [0, -2, 0] + } + } +} diff --git a/src/main/resources/assets/needsofnature/particles/liquid_particle.json b/src/main/resources/assets/needsofnature/particles/liquid_particle.json new file mode 100644 index 0000000..4869817 --- /dev/null +++ b/src/main/resources/assets/needsofnature/particles/liquid_particle.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "needsofnature:liquid_drip_hang" + ] +} diff --git a/src/main/resources/assets/needsofnature/particles/liquid_particle_falling.json b/src/main/resources/assets/needsofnature/particles/liquid_particle_falling.json new file mode 100644 index 0000000..155f302 --- /dev/null +++ b/src/main/resources/assets/needsofnature/particles/liquid_particle_falling.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "needsofnature:liquid_drip_fall" + ] +} diff --git a/src/main/resources/assets/needsofnature/particles/liquid_particle_puddle.json b/src/main/resources/assets/needsofnature/particles/liquid_particle_puddle.json new file mode 100644 index 0000000..40a56c0 --- /dev/null +++ b/src/main/resources/assets/needsofnature/particles/liquid_particle_puddle.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "needsofnature:liquid_puddle" + ] +} diff --git a/src/main/resources/assets/needsofnature/particles/liquid_particle_water.json b/src/main/resources/assets/needsofnature/particles/liquid_particle_water.json new file mode 100644 index 0000000..1182831 --- /dev/null +++ b/src/main/resources/assets/needsofnature/particles/liquid_particle_water.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "needsofnature:liquid_drip_water" + ] +} diff --git a/src/main/resources/assets/needsofnature/particles/ripped_fabric.json b/src/main/resources/assets/needsofnature/particles/ripped_fabric.json new file mode 100644 index 0000000..03952fc --- /dev/null +++ b/src/main/resources/assets/needsofnature/particles/ripped_fabric.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "needsofnature:ripped_fabric" + ] +} diff --git a/src/main/resources/assets/needsofnature/particles/smallheart.json b/src/main/resources/assets/needsofnature/particles/smallheart.json new file mode 100644 index 0000000..2611174 --- /dev/null +++ b/src/main/resources/assets/needsofnature/particles/smallheart.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "needsofnature:smallheart" + ] +} diff --git a/src/main/resources/assets/needsofnature/sounds.json b/src/main/resources/assets/needsofnature/sounds.json new file mode 100644 index 0000000..74dbd7b --- /dev/null +++ b/src/main/resources/assets/needsofnature/sounds.json @@ -0,0 +1,208 @@ +{ + "funkytown": { + "sounds": [ + { + "name": "needsofnature:funkytown", + "stream": true + } + ] + }, + "rip01": { + "sounds": ["needsofnature:rip01"] + }, + "rip02": { + "sounds": ["needsofnature:rip02"] + }, + "rip03": { + "sounds": ["needsofnature:rip03"] + }, + "shot_in01": { + "sounds": ["needsofnature:actionsounds/shots/shot_in01"] + }, + "pop": { + "sounds": ["needsofnature:pop"] + }, + "a": { + "sounds": ["needsofnature:a"] + }, + "shot_in02": { + "sounds": ["needsofnature:actionsounds/shots/shot_in02"] + }, + "shot_in03": { + "sounds": ["needsofnature:actionsounds/shots/shot_in03"] + }, + "shot_out01": { + "sounds": ["needsofnature:actionsounds/shots/shot_out01"] + }, + "shot_out02": { + "sounds": ["needsofnature:actionsounds/shots/shot_out02"] + }, + "shot_out03": { + "sounds": ["needsofnature:actionsounds/shots/shot_out03"] + }, + "shot_out04": { + "sounds": ["needsofnature:actionsounds/shots/shot_out04"] + }, + "shot_out05": { + "sounds": ["needsofnature:actionsounds/shots/shot_out05"] + }, + "shot_out06": { + "sounds": ["needsofnature:actionsounds/shots/shot_out06"] + }, + "motion01": { + "sounds": ["needsofnature:actionsounds/motions/motion01"] + }, + "motion02": { + "sounds": ["needsofnature:actionsounds/motions/motion02"] + }, + "motion03": { + "sounds": ["needsofnature:actionsounds/motions/motion03"] + }, + "motion04": { + "sounds": ["needsofnature:actionsounds/motions/motion04"] + }, + "motion05": { + "sounds": ["needsofnature:actionsounds/motions/motion05"] + }, + "motion06": { + "sounds": ["needsofnature:actionsounds/motions/motion06"] + }, + "motion07": { + "sounds": ["needsofnature:actionsounds/motions/motion07"] + }, + "motion08": { + "sounds": ["needsofnature:actionsounds/motions/motion08"] + }, + "motion09": { + "sounds": ["needsofnature:actionsounds/motions/motion09"] + }, + "motion10": { + "sounds": ["needsofnature:actionsounds/motions/motion10"] + }, + "retract01": { + "sounds": ["needsofnature:actionsounds/retract/retract01"] + }, + "retract02": { + "sounds": ["needsofnature:actionsounds/retract/retract02"] + }, + "retract03": { + "sounds": ["needsofnature:actionsounds/retract/retract03"] + }, + "impactdry01": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry01"] + }, + "impactdry02": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry02"] + }, + "impactdry03": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry03"] + }, + "impactdry04": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry04"] + }, + "impactdry05": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry05"] + }, + "impactdry06": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry06"] + }, + "impactdry07": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry07"] + }, + "impactdry08": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry08"] + }, + "impactdry09": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry09"] + }, + "impactdry10": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry10"] + }, + "impactdry11": { + "sounds": ["needsofnature:actionsounds/dryimpacts/impactdry11"] + }, + "impactwet01": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet01"] + }, + "impactwet02": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet02"] + }, + "impactwet03": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet03"] + }, + "impactwet04": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet04"] + }, + "impactwet05": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet05"] + }, + "impactwet06": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet06"] + }, + "impactwet07": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet07"] + }, + "impactwet08": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet08"] + }, + "impactwet09": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet09"] + }, + "impactwet10": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet10"] + }, + "impactwet11": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet11"] + }, + "impactwet12": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet12"] + }, + "impactwet13": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet13"] + }, + "impactwet14": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet14"] + }, + "impactwet15": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet15"] + }, + "impactwet16": { + "sounds": ["needsofnature:actionsounds/wetimpacts/impactwet16"] + }, + "wet01": { + "sounds": ["needsofnature:actionsounds/wet/wet01"] + }, + "wet02": { + "sounds": ["needsofnature:actionsounds/wet/wet02"] + }, + "wet03": { + "sounds": ["needsofnature:actionsounds/wet/wet03"] + }, + "wet04": { + "sounds": ["needsofnature:actionsounds/wet/wet04"] + }, + "wet05": { + "sounds": ["needsofnature:actionsounds/wet/wet05"] + }, + "wet06": { + "sounds": ["needsofnature:actionsounds/wet/wet06"] + }, + "wet07": { + "sounds": ["needsofnature:actionsounds/wet/wet07"] + }, + "wet08": { + "sounds": ["needsofnature:actionsounds/wet/wet08"] + }, + "wet09": { + "sounds": ["needsofnature:actionsounds/wet/wet09"] + }, + "wet10": { + "sounds": ["needsofnature:actionsounds/wet/wet10"] + }, + "wet11": { + "sounds": ["needsofnature:actionsounds/wet/wet11"] + }, + "wet12": { + "sounds": ["needsofnature:actionsounds/wet/wet12"] + } +} diff --git a/src/main/resources/assets/needsofnature/sounds/a.ogg b/src/main/resources/assets/needsofnature/sounds/a.ogg new file mode 100644 index 0000000000000000000000000000000000000000..7754c5f392a3edfc242a959de6c82f5d29c5dcc0 GIT binary patch literal 14014 zcmaiabzD@<*Z6F(G%Vc+2-4k1EZx#A5{e)#B}lE52uevwORFHA3M?U@beDv*N{EGm z!0+Poe82De{`I?`*?Z@nIdkUBsW~%u4V;}#037^#5rrGGpI6uD_8-AmVF6w~j<V^9KM}8Q9-vGtA(;=YOyBp64xXBj0<;l+XUxbr10m8B(ZK&mrKZq>PZXl#r;1 z$T>P2^mFpO?sdb-M*(WQ4mFC3OPsf|x!-d9ry~sSpC^ZksyPf`0O0hc=hGDdf$`6jU!7pTPA@Z1|sG6Uxw?kfjUN5v@OMoXXqDQ2eHVP@p^Dtsvq<=D!SS27z81Ar4To&D2o?%H@nve6j==qSz#E(10WW3JQ7R3VDdojI~6d9{@DQh`R4K^V?+R zw>8Yf1f*7YkRkv9gr^l(td+2?mBMM2!u7XoK6oxLQ2V!t{q3#=fT0nI`fY0bzx{3i zKu{nPS7I4g;u5zNn?Mj1QvnB90Fcly5~?QhnyG3dre-sZ9t;1g*rYo#{Ou0O5IuhD z<}*~R2!a9iR}T;ySc*829_)XFjHTBhDnW``*A4;{a=*7cz=Zq%@>1? zdF1e(mtcI$g-wIPkx}fU^5M1!A!B5Y%BQhym*P}80plU>6j_fjtXb)PIzp}^J#6xo0(rr(6D=V_z z+}f?O!B~g4B-NPcul(2`*{h%!ZX_9^5gA`ajcz6bihlHUHY1b}gswIOou~fWbEj11be0YV`>TvoRW5BWVvKDW?fZ zTN5c;Gjk7%bPtvg~QP0|5a@9|4xw-0J)TlyeS;KDQhwboZ1+dem;OfoqJ^5 zDo?_iObVxV3fHX^=b-fRCs`k#O&?B8V@`ErsPb33Sdw$;GIxt|s{05Gj7$78$1y4eNT3#e z{{)SH6#xLq+iL28F=l;At|?h`<_>ICqiAMw-CBpMio8i0Ew z#zef)sgA?ETS><6WH*@|<7CTnt`E!dHcR{2`J%SsxK`-6b@y|s_JrO!LsM!-X8INl9wz=CHdN{}G-gxj z>L&gWl~6l>r@0xmF@JTlG^g_+)0}MYI9W77T|CSJLYMt)Z8Q9BEpOT!Mo;+r+om}! zTR2%AI@$J3`!65rO*HDyO!-fk`7clVZ#?l>_yu+IH%pJUgYageKLmL$(&ZVM`7fs0 z+CkmuJnf|C+dbv0JgzS&)>d64(QL04uw9U9ZP(BAU)*L={U9A9F;VXU(OdMhwaf5d zbotzBXliC_;WT$>X7Mu=V&cQk&}DkFnL|kAF;nTe4u_#7d&FsGjnNPXTbUlVmSJn# zf7-?T{B5AVHG}Ohoj*T#pA49aBL7R*`@{Do0gMIhHyS3I)7TNya_& zQG>F)48F!Hyw9SIjpOMflVmS(`CifCd0Jwqqx&>!T7|bJ+SuB?aw^L@UNUOjnx~|} zP$&Lb38Rghq{c(#{+!1tvfL%9e;Wp6`Enzn5DEo4YXROjma^PjkqJ8Z>?J8WdR!&+ zs(dwRsg6*?thIZ~n@nGxk~b+j?p!67HhQv+^u7t_gLD5KyrjX{*e%W17$W6Fro#u3 z($NuxxPnNLspvqYOmrZw9Qk>tEOlIKAX2)b(}u?JP=l_HC`8J$W@B2}MX;FiIuBPb z?e&+H4UDS1{|r7wd%d#ld~mMb6rD%sgFkA4!r_&gjV$YmmN&C*9xXqzd`F)(Sexjz zoO9KJXXU%s7C8#d{EeZ`!^#G0n}@RZ{~bK80V3sA^Al=-Na^ZGHqyH#NdC;SegQSO zxz=oE`R*zIYmv9W6<u(kfAKKdK~}EG^KUusq#PcK5|k8ATI;6yqII;NHOx9zr}pBybjsav~@+zbk=3aZfV^r zWM`RmZVC2U`VO}pCK;*od-XD&?witc>21cZPM?FcD2 z2l7K`+7LD~v^eu{8UKZmsSt48_z$ck_2EBwge9pd|KdS;gxFRA)E9yo>-_CYa9;cP zUl{LsX8X65m+QZ^k|?we&p93>TeJ#v`&Y{Ufu;NxW^DW)7^JFy@luVop%j!^1~OH8 zWo`yDT8AP+I#%po`^5l2-D7U@AXj-%)cF}ZfoSCfQPp>BrMxCY?9uGLaqRG? zZ_IVyKnpt>2?!t&h+qsw5P*aXt%$G8jq$FBvRjoKFeCx07eNVDgRzbB=TNe^b_UllT;9rEfgYA5WtYt!npgx<{4z>h9gEFtD<) zOovqnAYGB<5o8AD6O)RNmI!|az!dJ{x!uCP(~I;2%^x(WSKg6a1H1h@UedRfM7Wa! z2UqmTIR`(?neE=Z8LeJcAfe-!2dD3W;kcO%(M4h~^#^<8E05EIel?;|!ko~!E9O=rsKQKgL z60wbRm^t>T!mRYG+#(AafgK*_;h1 z@KY{Z5{9J{P6*bmzed=z;XrdM`sp5nF(_}kfdFQX#EEgv zc(_S7mg~WC;ymjQ2gJSZbU$bR<~w06`UWjOv?|^r$9=>8t5a!)@x72xx29R&oTQ)n ziLY8oy)KMqG{a~l1AZtKp0MmYT?+}-rA#jGvg4gFXt%yoFRXb#vwK+F2Ryw?cZ)C# zUCs}h0DxF6@On^mBjoN@?XLb*1Jjy>2kLkDYevW*ico>POx?O$6=)J9?qoc!?Cs~> zWm}p?dGO1X2DsKU;Ea;9)Axh`aup9X-P)!Gu>Rq0as>2v(`qoKToZCZdzRBA(YYk)Ym+lXZ3O}Pm@d`J!$d^YgcK!9SygIPsE%N7iA-yRiO+$pc zG^|1cwj$`D-5HZpTPT-$C%*tM!$E+ZM4w-r8l+fL;@%A%3V{&qAei>Y-?N@17%nDb zfx#V*N2AvpQ#J8cvX*vwKdA8--rMt3ql$jIkY2rogq=;8eMWiHFO0LgPj`JoeCyGb%~iBsz*gnvZ25cY{= zm@|3jPc(#VNRCV`fPhh#nS=|*Z!QUx$}Dp*cnA7QQGj1fW~!?(&%nTwL?YnytuI>& zm*@L?WE5lAsl&M#v)MxW`Gz$EZS(Cjn!KO^h50U(O6DR;&wfqXvaVT)4?zoZ)DoD@ zO1m7|;XzUne39ThL4m&dCC1`p(j8^he0dqzwL3%Ic9i##f*&VlTrOwu83vyR*h8gK zM9#h=>~5Ms6i7;azAG9*gakNo!wqSP1!^pxBsth0SjZnwbAx${C?}g}GD=ZfGbQ|< zaDOHT1`d7IZl;PUMg*S}v(QPzKL&I=6 zDye0yTBHt*&7)1#D~~SLTOh+6d(?@K673aI$ad==)WE?cBJBRqiXD73`h+3lLU6lo zLpc;gu{|g9G}x1{eC(CZFOli-1bk=!SnMBt4^(`?MzouQ0N=MS6KAIBX|sixgoRRr zexslV{YeOgc|8G~UZflAn_{ugz26Ay#nTd2(ozve4S4;ZDZ12Zx}$yche{qhmVKhR zfw17RdZ#R(et7CO7jPptYhoMbZ0v}N(cLU`5M|o4gB23u5Z5lT^oE8@KM))-uygXW z^E&4acuP;g-2LGJ(&Z$fDUQ~N zct5j$BpxW;WnR#?Q(pL9VwFkcyY|RS6rTOgf%gmI_oTR4D$st3^leiQvF_1Vhs{-X zjTehu6A1Y;{{Src*(pjy@ir{g;1Ujn(?IlZnB=o*fE77<)#MrZQMREk;_;1C=+D{-$mz%Op7VWPtBA zU3stvip}UT46A)d{OUT0W7d&47v{@d1XPAai4+ChD)@>&naLD8=u#+7c)mRd zf3^5Uj`rc(MCX%Yr)7nWyK;xZhzp ze@ZUqgm=sfMHl<930cg(EY-@VC%tzRIW5muzEOr1W2qo(xS?%00=OQ_qfDdql{9WS zX4s8crw3to6nYszFE=%Qe?^@)qfkjquSfuXF?tKdw#74ZH~p-o`${f}TMY=M;Km(9>1G-*HDfGRQHu&#YK*du7O-w**067e})3J=TWQHbHnejqlx z;ger_0UT{@R>DyE-Ov|TwS{&Br=u77# z%JtY|lX;&MH_H1c+g{``9w;U~+925e$aCY`g)WaSG^_MbEcx4uJR=55yHy^`C3`O$ zx@iD7TI0p-t?|NoS=|A;%S*t8ogP}2xs^+P%N!YkOphwyeSDK<@9-uXt7QU@4Y6@S zPsEKd>ndS->ptEDFvCI#37gKzpA(;G*}kofat?C5DogB$LW>ah)&yctw_0gAbvAdH z@vbYAW&IdEY)uhZ>~1Jf2=n%yv37I8>5anLX@_u4#hqhf;Vo-Ti|-#BQd*p^7C5_X zDtl3K>@l;8H6fTK@jTx-TW@BHf;{5M#Qgm1X=!ROI;+zV0&d+6}J{2O9djlg^b2qgStCEt{73Q z!ci^HeIvH-;f0BPK#Kf`x=kWpYJedJteebg7tRDL_=QLO(g%q1PeiVsZ(sy8=s+^p zV%3Xb6==8f5;+RP#hU|yR~_sk8?FiVjw5r8b@h95_HR9v`WCrh{Q=;Yj74a@13Z9Q=hhX;h^ID0DW{ZULiW$l5I%>HV0IFamREgSHozg3XB+ z1H35??gRJ6IUt5DFv|TG8S${|c0}Odb{lU`ccFS;B15}Bjh`CXNm#=tmC|}blBD~p zzuM$(vxt$z%Hn?Bxny`|Oan@V_=s!f9|NKt^uutzQat%Yn4BeRLGHKyCczoIz>$Ks z2kyUm1#mqmZWt#%lrQTp$GrOKN9+H``xQpaJhbPA>SnO>AQPceB{Nyy4@ zJNkTw^rvGAc<)KDu6S7Az3Y~kn{+esCJ0a%so+T;Q#o@?12X3YDv2bb%k?BgM~Mk*!Jn~ZC>SvkB;h8u|jm|;KGG!E1y+!hIU0cDsbvYCG+1m;;L zh8YJ-4a}TjoF1H?HU}2a7OmHgt>guhPd-tBa3fOO>q9^wO5bSPz(7I5CZa(R(@_@x z{3q_ubtFzU{Rte)tZaa%6{IanQ5rj(S3gR1%r}%D zeBt33-7gcNygy5+Fwpz4v?2!R2!ACFpxupzQ5*;xtBdL zM*}4Le2Ci3`2o_Wb=7s8kD0%Si|z_ZZN%=yXJnmL_NFD@g=65e3XM{f7{|pK+9~rMUnkLtzHl$wd}a`I|EamT2P^Ie zk;;oW#|ZJ~QB?>Go;zkT_EM>6t!MkMsOg#VG6@wn1IIcd0I5@LnbI>yV0VMbdO_0S zk6Yrz>?7>i4{HEKMhFWOg|`DdilUJs;MLYxWX!AYE{j-CAk&Tnr9t7;0^ZwAOq>L4 zSI7^~t}@OxjqtHxRgT-Wf#Zd|4*c*lI^k=hE5}m7=HcIh7l;)ifF*9tK>N9^Y_ zdWlV41pCePOF@owmhL?}{qAVvay?2UQ2M$@iv2S^=>fE=l=6W6@1)YT7iF zu6dox#v&Wwgt=|nkKW(CIavz}h?m1s8GCb2nLPCpIN)bjtyj@NWIDXolzZj#5%KYO;{OkI>Huxqgj6Eti za37d^NPt!w&WJ7MrjWywA`JVP`;eHxe5vR*J`3rGx|wq@?Ch`G2=fsZno<|0Vvf@q z7Kgb?y2`SHjXl`C@M+Mj6=;#w^&l%AHJgR!f9T9(zMQRuv{k>T5p5N zH3zTSC+tPv-{h{kG2HEXBvN(flFpS3Iw6Beo+o+%smAw&F`NZM8S4V0{a&L7mOn0C zI&C2NAPfuq+~N6hPh|dwYB}dk>Dy3<{hVXr5(t8$9^gTHXIva!1;~aPN#iI%ig^-j>j)Rb04$N zSJDuPkg?%eC#Dvz7}A?^S6(@(a)X1a%M2{SeJQUN!_2;f7ST6$U3^MqL~_c8QF}DJ z9+rtmOS%Vxc-$6-{YtTuX3cxXW$W4h{jZ18cW}dA{!ti`6%gzXB(s_FIx3&dCN$#xwDGV~21j^@FL3vB^@yt%4XAy}jHqcQ zBzim5_Ks{Cg_yw4(cqGhT%L=Tce{_~W$zMbuW$eHNVZI#SeAM}ZqsL#Pjb4WTNRa| z%SNj|xXc6g$sVmMDyv7ptRNs zTZz+)p)L@)=?lIDtb=|Ziz~@g^@(#Lx;c20X+u=-mCwSSV5Q=_II#MzAb&6G59`JE z`Ga_XQ~t8MrbgGTKswXaeJTvnXR6bSv01F-x@Mq#bc0?I$_IY&;l%Ht{a}(wfhJqH z%_9|nvL8)=Ej0{QE5TQ$!)rp1dM5*pzPY+I3U{mGzu#cB5C4HTdRZF0(4v)sV=lNa zSkhjE%j~{O8+}N>Hua^hL7a~Oy;F+^GCS0g+|6JFM^$ueKbk%g0(+V4ob^7#OF=IX zCX{qo?Gmq7cwEKGmNg_#n{ItCBVdqZ82U|3k#uispqd6oLX!rI10Z7Rkp%P*2nGQa+?lt2IdYmhbPvfh?nBQkwq&l4>qrd}bW|+e1er9=R|rAW$k#`Q^A4 zgZ;3NH@D!9um%N8EQGnCZ5BJFZ%3}v!!N`S?@c%2g0x~&(BnB7L9Fu{Y{*udn4t%GhDe=HY^Y#BgCU_DGm!`Mbs3)v81dw z`Rjv;DC>+17`S9Nv`!e@4+V%rvncscUpGK`fm#$4+vS0VAD*p!9PE8IvkhyDrn9ze z2<;8F_c^;#$`UUZue{!^cJR+`?G`wgCv>&{^~u!NyGrwb+_LF6H75SVBJeFS4WNHd zh(4l|O8SIFL5@2F1s9*w;(?}z8@+9d^ZN4GJ%btSv7HT=rGQmm7B8pkbb>N+(=|!S zx6UUN5rzkSaK7e*dLppg*Gj_{-}Q(CWbWn)IQx_?<7tiDS_Sh zOB1>K98^=6nh-(?HtlQz%|GnkXB=wnj?i>BoZBE~X-FHsvk3g+4dtVSc%XCX4BA+X zPb^4%#Xk>5zXf;0XVckZ_;c^jUkCQt%K>18WWN>cDOm^GGnb%DMC$c7mTo#!U0OPH zMSeGCJ(1?ND%Xu2P-wU{f|*y=y5(7h2yI09w>}#C_T@9xbU~5C(0Vn@`yG$OHB-Gm z4>+lo~)6o5R&u5nwN}^^nKJSA7J4!=xWz|ZT zB6{5-S`nME2_P+HS0Q(UWdtmsg6(l+A=EKRmFRy-{ zRHf6Z^7x>5m19D@0qb`+dcHJV!9*xU&uKFDv34QHO+E2rs@?nJNw!m=S42XRlQGgE zG3oBYG&hvmh{CvT!D6LW`g);q^m|9WL8h)tnkTq)x>6E-aBrT+;Gt9gzLkQ68zoPOuVj`vE8JZ8x zU_Pur?&yA8`P`W1cHA_h_{AAqqhBqopp6~8HlEJv68Syxb%Od@V&&-ln=lRW5d((& zB?H_dBW5B{M~t;^KMu;M);lvmPQ2Zj?MZ676z`q$LmRfy8D5-dxFq(P1hwEhG?aY5 z#JUe0DwyD5%~@c$LxF~*LCx zG!nwEy81Wg{;?Vor`C+WNt5KKg8!`SmS6YK`tOyw3}ajBiOuq+QB|(`Z>xOk<)O#0 zOB1hO;c9lJd_54S4V)h`fW&rHyV9a~$@};ElX3jeS8Y2b#LgxtXUesnL^42}c^(x| z%#VwMZQ@VR$%FhG_yhZ1G+}5P6%1RoWk3XOEUsvQffCO%cu$1gdnL6#o1O%ovP;zW zR+-utgAIk%M&z^8!|Ep0e}{H0^=+uqvfJqfzg^r_9UE1BwRHh`@}l*y%AdJ}Z#0{y zFJP7Ay>kb?#Pv^t_7CBIzda+dC>XRs1mOJJGXi>!5uAln<~_+TDap;v%PLII%FfJq z@+1fH@}9q5p4$OOR_km^^g}4Si9py969LZ01Rz8z z2u7ezC@mi4eA%o`OwE1Y+{XTri|ZAjhw9|n^HrG`Cl6a9+x}&bh4qJa%f?{<$ALD{ zqgcj~MDGA;N0WomrW%x;tR;8n=v<*;WWb5NWs7(pq}WwTKAfDa(@g`?n;&|6DSY8V$k4TqBvc;{v9BYi|1GdvBGO zaz*GqJz<(Hsx5O^$kWVV)ku7y7L$d0t#kpP?V=Ss z^}3Qo>N&tHhsOW`jyfbZj!2Z?!)ogNh3fcsb9yZT@`fkd%MyC!%{{L!3pXh5PkvHm zTaMo}J5dt6tNo!On^`Y~9(fdpS9I6L;9z*_NaAAL5r^;Dq*i9NP|uDC?ay4#4pnPAclnl;A9Ok?P`GfN5=&e*gMrnNIVteqvxu6b6W@QO!Q-Xox0^s_gMn*D%N>_HUd0j(eTXmLSYYK<{IC&s0XB}T7<3w~I%?$|a)!M` zpIABmH4V1WTL=z120!E0$rE+$D<1CyT)m(e z56)Iq!@oTcaOdlyH&OJ;~Mx&H{zekzZ zzm5KriD)1F?OpkUAKI=QSHH?oO=|q#}O!^bG-ut;8d}HgSQu0~b+K z0$j=T?*N?cPVYXlxL2D`;#$)RaIa7NCJ^L|JVGT%BS4!$v?=@~$mHjZpStrJMc6Ep z&{BkBfleW(!rkCm4r-B*jn|se@A4?Ne>3F6$Z`dS+uN$|Fu%a>=1GWoEWvMf*$&DX z)}h?rWxMLN@G|xMuTAT%+X{+MME;_85p13Sch6yo_ITDJ3?URorsH(*zgR1rF3ZnNkwx__pY9OoAZ@b{Oz|0yTQVyye_2^aEC^wEAN^_ z`6HK}ex3-$dk6Uq!5IrE*zoP~&!RgOWe+rrd117h&)-Pw4V~tpmfnh)v2p-` zNkQdR%TE*588UMFzifyA~jVb#~>@&uynfqF8d{NOje+Z6LDLKL9zP#%UU8=6+J zI~;>&>E6O7E7s*;8n|69&^mF7=&d`fkm<6|e03Ku{UTJT5IxlKQ|o$8FWxA}n1f%6 zs1rSrM(;72(5akII>Z*2@y;x^lmb`TOcF*{ZACFKg@paS| z9rm8w1UL7i>HP>GXh+1c!}CzEQ?lLjST&4cREJ~DeeeSz6hYo6f@Xz$38g=mpXo1$ zbg0%t+Voc-#fD0<;N;<$K?12p?id1;Vc-sIr#|$^-ojaQ-*Ejv1J)>EYt(iNN|~coqc+IkLHe_scA8!$Eky3LnU?hr%(9 zFSkH-C=;lnq256n|0*!MlpJv_^X?vrW~4-Fm!jMFEsa@;PdE15QmdZx9SV0UMP#ql z;8S0j`>x*bdVnPU5!|A8i$e6*Pr%y9`MnJI1)Ytz9X;WldbN}B>Sy5Sm;Gm;@l!S! z6bCc9;|j8ovGxG$M%v_~%`R@=^#k7fzmedsE+zIP-DbA%gcE81sf|f5liZG)XsRyc z#CZ;X5%c>Gn?qdfVLSu{-0U^)ilMnJAbCY?@`Bg$9YJ<`46brBXqDab!(@>&b%YCJh7^o9k_fO7 z5pevXsD+E*v1Kxbr+zl4==pi{jT&Wt8b3maWjf0x%l=;V2Kc^Zi13iy!(JYb zUt%;IMz4La0Z*K;WJ?Xm9bFK3&~9xN;;b8dwI(|doM;oq)`;U=?$Q@26L$ivdAFC= z;!L{$ZT4xmwNZNIta?h-8!xvX&O|*Qv9u&}8*=9L zJ}IyM^vTsnJJx+bc-S~hQ^C1A;wPt79C^x^8%$Gw{f{>pAE)Ux)9}j(d_qid5B|3V R*su^jLh5qP#)gXo_tDC`K}?%mQwv3>lVqzWZtH96%|Zj2scB+`t-Xi)V%%rldw2ZgD5(#2WP6WI$$LP z>|(zMlZGHGI$8!M9SOQ`QOq{X#bTzGc?Ca3%eoDn>S6s%khgcovx;)gju~u2uj4Aq zhJKOJXiEsO~&z_A;=z&k{w0018a~i2bi{gEV~hw=Lj%` z!Xp3voKoTiD1x~VkIq_Z;~%Y6b;&}soc4R&%ioU8z1*5m&#sBHnRLuPo zKK8(Gq4$vT3n}O%jv5Vt5n=K+#($@i$H%wf1RKwAc9X=Z`^iRT!uS-W=4c3F!<8uh zvr|HO8O4I!Eb|WRr7r7E3q@De(z9c>wgv8_jRz>UumvbSBb_Q?)e!eY3#G$>TZCLP z2`5~RDBx6dc#eabkupfq3z!|aiy$gKcHDE5yK6V}ebny0KzZq!g4Snk>1|_2@uB!7 z#s-{E7`>cEy_YaHk~B7QCd4rPpTPPnav%^iY-LPxY1#PJ;?%v4n#&IUGjfh1TF;`| z&ziW`n|StHbiX9*TOjo5xp)ygz4fVm`UwLDbnnlo1Ge-53VkS%e!r0(bU7)Y@k78Y zht1eX!bg##2oYlfe#W$8yHEb2hu>ngry2ax6bGlP4}Lc_uE?`bXp(%DPyP zbulArI)j7F%#X_CNv@W~bkxi?{nz$aLP9U1>^8Yw z1Ef7w$P0p!@oo4!NM?^)JKwnnuX%ynMl=)V^}FFL^N}_d zTQ469N>-myMjCCs;wfGfdVmilAdw!hNbws^atBmz&-?lm zN+ad|>sU&D(&Kmv)sKFEkrH5VB4{M-@tdT_NCoGSp#(q;9j1p~P8h>R@uC=I6%rm+ zA`wd(Va|&ux^{o8pY?IY-5w25IInyL=YKog}I@;A3iz=7Q+%WIF zUE5JVKil+aLjuN&lJFCkI^hTOhz35(Els(?qSqbuAy#d*4a>e$pS85 zRhXJfB2}NuC3h9)DOja)NN%{sT#^Sk6^T@rMh=X@4;PbTY{97jt6Wlyr93dmLz(~& zT%|l!wbVD7Q7s3;O@&BzQk~QwIu1Xzjucg`5H6M(1aW|H?`T`mFhfBIinSZ&u}Ia! zgXA-S5EgrVi1*F>pPJJcmhxd*wL-Xg-ML&KeCk19Hj&#;1FUk%*kwO$<>ySb2af^a8yNKqRj$K4E zG1J2lhof{kXqZZSh&Y^Vn5bbYG;lY!l=(*Ea6k)7PGW z2rAe)aG*J59x;V-)FFFhIgm??gwwi+Pf-Mr8BuH_ zmkh)KmBdVKU^|(wg1`-&Lj!2U*e1dOtmJ`Cq-vRebPYy0OsldT0U(x6@uEq95Z!$h zf*!6^hiX;UyL@Jb)HElAE0st_12bmh(-E{R{B*IC22NH0Y8(+#QF?%%tfKzN1_baa z2L$3YO3@XwIDZ!=e~&c(A0@<~&RXjW{?>BjdX43(e7COhGc4vqkzTO;`BBaOclG{n zqoC+&0J)Dl$i7)i9r{Cky|3$b+`3g33LW7(pwMe604G-4z)r_K6|jMON}WhVtSLYV z4hP@^BAOdWDrAw#=BC1YMd}y~Bv}I9>ZZbBG6P82p&@J-REWCgum>j`4m@oMLXHGJ zY1y{vWUxebk5+{j^pin+BEzac00l;aqeolfq{FmqDX>4t5W}?7w!?v|phs~>pyWNZ zN7K&NKmpi1RcG{n8gjZp=HJFe0O#Z z6HeQ@mYhX&t)uTTk|BqJwdSEp4qoo8 z+tib_&zxyEz3Qp;&6U)L31~K26_X)@A###a_2Rx7U%l_jEzKP+LY?IcFbvuVLAk5Z z#>PA%N9VgWIqNAuIaXefRS$yVN%1Hy$O*x9!e#^l_N*v3Fa1^&n*YT-$r20j%!7wU#~aSrkzZ*Z~9vF_RGdP|4&Ok!#!v|{xpncmwByKy2Us6 zs4P?0NRKEK{rpKXZh_KEM7tE5z0EAv2?7?(@TJF1R6beUV?? zL!a5bX}0;g-zLlUx2?xV0DY!qh-imRN_TYxq ziyH?F$%f`5n^Jx?Lq0#`r1thHJre%bc})cM^N?Wiz-erZ*NXav-*TU$7Q(d_7cPl* zd@WvB2|fA#`jg_sW6&2}tMqRCZdIy(%H!2&FS*0f?LUz_sVAc<)P9et2-AgjZAV1j z?&qYd9lzW}STK3Jf5>+{DkO2Q&F{P3&e_P9oQ&j^WAA7^Z4 z{;s7%1vMnzxocnE`JeQK<2xU-RSr)q6_!MeBlJSv^{4D+!LHj5{PKd~kYQXWu=s}5 zejuhM?wgzVdC2g5+8beCo|JuSL(k)~xSFfeep|HzuA}N*v-TJ?O7ZJC2X$6==Be4Q zcG`Opf9qlE%Lvw9R%0vlyYH(!^;atEevQ-ZsV*K8a!GJ$F`gwE1+~?#$nDU+>0K^S{{J@AUIv z?gCb z*(m2H87FGySJ)jydjF89QTlx!d)_+TZiz0v9~?vsC%kCdU`{!HIP(}aLv7oiM~5o-$5+&Um5t{M!w$|5<2gwkUpaW&y{mGZ za|-B+89whT~}-J_5PIT_RTN~@2PO|n|xgD((S29?)&bcxiQ~a; zH^T2(zGmbcI1(jtTi|>b!Vu(}&$^IHYG*nxpVo$iDyOB4zkzBa}A;Daak-k9gI`CRr=3!Tq|F)>hGb8wf^U3!5IcM@( z5h3VevH8+U@wv{JXC-rHCqrTbJVg&~&+I%FYsq|(DY%x`;{GzW(tK6P|-Td>$TE_v{{{Zv4J|X}B literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry02.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry02.ogg new file mode 100644 index 0000000000000000000000000000000000000000..0bbdb3bb4213d9f8b872b42059c64b8d667a49e4 GIT binary patch literal 5973 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 Pp90ji^$NN4XbSW{vr%_O literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry04.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry04.ogg new file mode 100644 index 0000000000000000000000000000000000000000..bb18579d4faf3f69a42856ba5192fbc071fe80cc GIT binary patch literal 6507 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 BmU#dG literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry06.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry06.ogg new file mode 100644 index 0000000000000000000000000000000000000000..611fc57206a54184cbfd9df3168ca5aa7dd0df7e GIT binary patch literal 6448 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 PBrmq-7Kxn}&w~C3joJ7% literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry07.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry07.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e1fd3ba08a83e1278ef9d51ef6288b6efaa84523 GIT binary patch literal 6189 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 TG`4AM->v8TEsw)A)r9^B@=sM` literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry08.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry08.ogg new file mode 100644 index 0000000000000000000000000000000000000000..264e0f657070c86dd5d89cbc4d8523a3c4fc1d00 GIT binary patch literal 6189 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 K&Tcy61p6Ot1bBS_ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry09.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry09.ogg new file mode 100644 index 0000000000000000000000000000000000000000..354fc966b63ec0b71f2207e1759b0e013ea1d99c GIT binary patch literal 6253 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 aYkr=*QYu{v-}Z@752q&uJ)j9eQvU-Alb$&M literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry10.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry10.ogg new file mode 100644 index 0000000000000000000000000000000000000000..cff9f1d20471ccf6cdc29cc7422c1618841c4456 GIT binary patch literal 6164 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry11.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/dryimpacts/impactdry11.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a3a0cb0b02e8be1a590a1d2bc97c6ff723832a0e GIT binary patch literal 5979 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion02.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion02.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b9aa8f15eba61ef90d39c91da4802c4fabd5db34 GIT binary patch literal 6874 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion03.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion03.ogg new file mode 100644 index 0000000000000000000000000000000000000000..0c08b4dfaa4eaead9647370a6a4a830ac65dc4f4 GIT binary patch literal 5735 zcmai13p|wB`+r3kq=`wRrW%)tccL0b8ReI27=&TmZ5rgR%%E*sr3@iSW3midx4}$C z5prpF6$ay0Bct1fT2$z=sdlT)|IBE2`|tn%+28v)@A;hfJm);md7kI{JnwVd!oxkG zWzgs6lI7AMu)%v+Zi8IXK{_pxA)S)5tNpy>u$*krBIhRE`PU=eDW%lCrUxT;F8;5l zPkxEfWPzJJTq*(X`^b~ zC?>lQM!iGoeozehmN7({DgN9cWO`u1S!@)wLyW zdQ^WsX7HB2+pk6*nr635?LEC*so+Tr@<|FCOG+F|O7<&A3usC9Ye@^ZlNR(YZ9h8w zbNelM{GGJPRL9CerFx|N$8fps+VV%Fa(QNY?J_7-Y7#7hr3}AYS-k9;#^M zdZt&AK`XDINan$qA??S&m$0a?yR`5ohcV=&#^ElFj%XAq33y^Kr#(}wF4W!+)6dOwJxMFV&r*ZNN=9DbpMaK}@6! z#ea4(C@-P7l9h!Q8D1GM={J&gwVgv2vzI%OwrT``Vk0wv;xkRt7Z`OU$7pVo#D6oF zO(DbFrHBGf#fIiuD{9MvB)xzxf~!GPeCfEC`opW%-}<5rzruKE;6S@&V(e*ZN%f-o zB*prDmw5kbTF{-u@v-Fbu`~PDr2i9Ge?<-if`%!JNj5E;&{Lec*9NiV;6EcL9@TRW z(|1naxk2AmY&7^Q(d{+ymZrT2(baQx(5=;pBU)je+k-~T!bYfJqe)?Ro5KRGCi^vi z4w$8|86QjhB66f5qD#to?8s91J90`5a~_%J=sB_U{aBlhQZno{ew!b3Bfvyi4h#Y&m{uhzcLBQC8XlgKWeZ8ck z)1p75#Isk$?a@Cw=dBlTaJW4Ser5IDaEdM=X0Ckk;Izj}7Jr6vq30-LVkW*i&-4Fp$!tNy*aP!1YuyH=x6vkC)ggh>;5Igl%veC|2 zC6yW>nF6p0QUb4P+0ES5a#kn^qN!0-;50U62%gTRhzL^{3Yk=0>^VeIUe6Q}6iZT( zFc;%F1ovQ=1J+Z;sW+_+Aqd+c2VNUPVyHcYG=L@!=?~`-ol!VvP5WU~*qEm4*m>e0 z(agh(+OJ6+^`hR_a-}xoTs2)?7rdyN_SDtXk$hKbQ|vG??5-E}XD#YzbJ%^YV?Ozb zV8Ss{;P@OhLN1esPY;< z$B@tG@tb4$b%KEMC;W20s3DwRS1W3`bg?dG7EtSI_zgyUzR|^o+7EoA+2$I4n?TfX zxv1{qZ1W4%PNUhjL;SkBiw$#9YMWqI7(d&#?b|B2zv|+%HeOp>+-zI?Kyxgng8%GC zeE*HhqJ|f;yArn! z%n4s_-o4*Eha1iTC*=!@_!nmz;x>2I&f0iTlY`EOjqeEZTN5|FBPU?&OgaBq8@~TX zAc*ilco5UR4HI;KJ0O%a2vn0pdfy?2qwfpjKUh3|n^Nag2Td75kG-iwaqjBMUSthkk0&SXL}Dj<#^!GIl=8UaqQ5ak(bEad13v4B;dRJcsW zQ~?{XDokaO$%6AN%0O|RlvQdD*$HlDkzK&4$mIGoihm42QcQ_41E&J4SmYREp?`o& zQzFt|Rdy;tlXrBKKnR56xM*i`eUnyn9ASDbIZ_}M&g<3+$N|DVqs^)$QBp!ctfhp* zAPXe7DQ5s7B=(0<&e0ctYEFwX7D{LWsc?M#c@_{pea}Ce#1_*4D;6ai>}U{cC(>_8L? zu^{g#YID)4H_?@-5Tq^_4gA7Ig#g>48R|B8?T{gBLzwAK#=~hyvJ;LA3S1QosOMsC zvVAe&KudWfSdFGeNXI64nc!D3UC0K!6TqT^L;wYD_ke$tpz)5CvLH=j;*0}OI3VaM z@COy_95~RNERW!zTqVc~T><2h!jZHA(i033WJU~A9WN|SRku-I4UPf+RRbi@A#DHw z)O%&ApyRT|QUZni6bt5HZi1A+rl>#={t&2$E_;MBPM#|TIdd6wM4B_nB&9O;9$1KF zvnW6uP>DxZ1Gek-mJ-$x7>wY(M~? zLO>wRV5D6!Bj@kJa11X;BPHNuTx&C%J*oAS zHVTLq0LXpWK~_JhC_#%#>%1Mz;kBwpQXP@%pwR0q04G*l&rFA(R5F2k%52FbLmWT} z3>j>rjFbGWMjZv3CERCqJR_&Wp3kbsi-^Fjxbl^f7%#?92tDl zvdz*dU`h8KS}mbcOabwU4ygqJt+sDQXZDe zwq+G1i{wxr=BV9`9%U1_C{IfClVV!7hg(Sqnb29w_9h#iJ1M5uHYE*!8zoj5+J@9I zJz*t>LiYkeOa&M!z1(q=5U8N=i-w7cno19oA&!)Xkj!4Sa~ZyB8k+nQ}(!xUV0raBI1au(0Q9a2_|eYHQj19(6F$e zf(qaV+zU>87GSXFM(yzL*(+A+%+EqcdONNzrf>P9O!GVH)+dF~!U9?2u0<|F|Je@t zwHE6m9uf&eK_#W{5>@PW>zFU6B>L@0`_Ue`g#|lWDNp0%cnmy&tIZxCwAp*%YI!na z@!ai_U8FPXY!8oLL9YKem(aD%5NiBRwNk~rsvTd66Ah2uyIIHSD9`lMetq)S%7vOA z)?;3MtL3G93X0aUBUc?yW^BKW{Lj^iu0PiFwXcp@&s}Y3`=h+y9SHMu7n^t>xFa@Q zJ@7`OS!|Nwqs5QcavT4%@&4@h2J*@CPh-{`Yg(f}Peib>Cq8UM5EW*uu9FpM29Dla z_V3<^NWHM5Rau!e*g=n+P}|}DvHrYMAb34(c@x?m*AJx{DKRrl=VxA^BAI*9jwXFHjaf7wKr{5gX32pdc z?qh#4TD;-Xr$@hVR8NV3*`N(s0y)BLG+`ybGDq`Bfq!H+s%t2&c;vB%WWN+7Cq#R z3#>EF^-pUm-pi~P#eCA6C3H4BXV-7xCoW$I54?ui8n)wGbDV=__S3A&6PH(BUm>zs z3DG9LHEQCiys@=HY0ZK2ehZ$Rw)jov3)dHUCtp{uhO(FckuhUDM=x_q z?mS68wlJR;c`)E(>D6I}KfHTiUVgW57++z%$vh${dnVu-izoaF5%?<0qqNJZ#Vh)S zMorhk3A6m1XzP+!&0qN>y-Lowo7~!fKk7S?*vMCL|21Z+XI8;vkAmQ(?(W=l_p5b_ zk1KKsf!}<_}IPVFs>kpL`xv=IOo^jWCeKj6~k*GgBJD zJt{L?k-RYvGJ2JOu9zZu!V!|nWma7ssMdilYii%6@^s_7wQpPk>|0tqBl>?=Sx%f+ zS?4a+mki7}faehI8xLtK`;V07$$KQ6h?g?aPhNFgdK3_L+ zF)+|fJk^)Z%tzQweO!A`Y(jb7`@##+;rMmfbAgyr6nIrgPtIpVwp)pDQ@{Q+BVa*x zZ&Zv9ty-48(@N{G(#xpt1kPnwFOiRAeTaV#8f&_Hz#SQ!&#yi-9eA^%|NNYla#OT3(0FUpPD5^;*wV&Tev-mf@IO zed{&(uW8hd;m*XKP|lfq8BDzd!L{+J$35>yLSH8D2tB~NDiXdI+u;e2*JM_)Ckkcv zI#|gu+$^O%VcN&e>}ZIf=tu7n`h(@Vjo4X;A9rBB!TYDtiucd1M^4FQ|6I3XS%-4r zr(GvpT!b%4xcPuSDtX&s{fQEGOrn z?S2&}T-*5{v&u<3#geG;|8$)^IMo~f;HR#(+>M0&yY+V4eAs;S4@HYT?C5><@eXg> zcIuBjdGmev&QsRaa{A1Wz6Nz(m!D$X`xoIG+b|zIcGD#UcAxISH_ZLHKRZ1?eEe0e>flgA`Nxr3y=S@+V%EtS z1oJGOmvj&oY?p}cyZy{cYe&7g+Jg%2+}oV{1Eyk^LEp2b!kW^Fxgry-^(>j*C5h|Iq!-$fs1x_T702A4V^ECx1Ho^}dmTqcQ!f{bm!@ z2AntD?)D7+SW^Demh52GG|oyi?41QK)?~cjta{q9>Q|OVV55)~OG&x(>C>|^M@x6n z!kE2R%Kqb<-ub95mJqk{7|=`lW77AmS2sE!wd0adXxQr)UYd#7!|zo^D81HG@G(Vf V2>C7RM#t-KHdOHgS3Xz<{SQ51n27)Y literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion04.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion04.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e10e3b6f4ee9c553c537283f15cc76ae54f13cb7 GIT binary patch literal 7203 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion06.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion06.ogg new file mode 100644 index 0000000000000000000000000000000000000000..df6146bc11259d7a0ecdd2a699c0fac4837da628 GIT binary patch literal 7406 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion07.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion07.ogg new file mode 100644 index 0000000000000000000000000000000000000000..504ab0d63fb731205960ebf8ce56f4e27f7991ac GIT binary patch literal 8961 zcmaiY2|SeF_y04Ioh(BmQ;pqNVi-cnF2?+HUeb*vV_BAnwv^ZkDQ-{0@`{XMTc_jR9p?mg$8bKd7X=Z>kHn>la*`0F{Ovp=|2 z;Ru5qgM|17IJpPXrXV_1e=WHT*&958n9_Fs>!IzWQHJqcRL)bo|KIb5Zl949NVj(P zyL7@dzzyN$?qs>g9$|oxmqW?R$;&AqBuu@1-2DRZfq0)FB#k%Z;Qm&mmbN7XU;<}Q zMu@hEzEm6la00+Z5hO>ts{xWoD3Z9EP0*)}+T`N13Gr<(=Oh&6?+$@>;ROJCAVPvU z<^GI;N3V(-Ogtr^P{rLu`2m74!R(_7^Q$#6XP?r?rJg>$NKqzsn}Yyo7Y%*EXegG} zImHRaz`!mR9?&89ATNMLvaIkHi+l$n(nx;3B;7=HzU+RA>Q{-!eOjzCj|VVDs3A+| z>ShLq>w�vw44Y%zJtuz_VyE@VjtoG11t^AxpCZKvw%|A%F-_P2lVus6j2Xs}|nV zFJihZZN{V6ub^vgVT=QxU4itQ0Sgh`ahbhO(y>TZW@<5BmoW3mhJxVc7G9FtVnx+G~=FyI{?t98d&MJ z0QA{FbbH`wu$>Lkc|$PjSv5NJw-ab?2LKHeyv-l}2DAoN=3RiIRiN5%pz$zh3Yta! z`*U*-FQ6iV`L3BB5ljlHj(cW_0NKKdn6%mUC4md#ilz`beK{*VDjYe}h2_xPcQ4~D za>X0Vvvc=6=+(>r1@tAd^KM(=-M*Z_UIVrq;{NC=cP=BTd;px|pUvZ-@H>L*+y`

V-cYya=Q8D-~q$(EI#qhu3{b7RxD4h5hEOR}=_Z;X_GYnWR=A?a?dX%KyjH-LkyDn+#&tDyReM4`P{VSW! zg_Cbx3jKXsqF2XFMdQBreOZ%B7j_-pbgO3j7AH+%gPCn^Zz-OhSSPg>e%`Z)xD`H--4 zNc{HaV|Ared-M3;u|G2?lZ$A#nj|gEZ57Iq&Pc~@_MYiU zKIpE`ZFP(LiH2JH?V!9v#vbx>uUO?f)Sx{FvSxEnj%{~;Cg2qM^yIs~$|PFZ*Q

(H|3U8R z5b(&+lemtjhb0#7N5<2lv%;mu z?p_E3K^)z>uf zHuCBg4U);$4UR$Ec@pV$R1EE-hb)B*Q#I@werH2r#X=_ zniqzJo|mEFM|`H1>~hkru(#j-HXGS%Ab8BO(%U&uF^A{g-CkPm@ekTHKMQ_^K3{s# zbk^7#&#>m2{WE9`@28ED?z3$4p#$ z%CYuCRUfD2V)6sNcOYq3w8tvElV)H6<`QQ;nlEHZXPQ>QB>YW|Eah36WW@XCm%Ez& zcC}L?cjo(*ggL1Ji!WZbkCdeA$aj@*z1CZe3Ug0bnz9Tbw2u-VEm9C_@Brxl0X4H? A)&Kwi literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet01.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet01.ogg new file mode 100644 index 0000000000000000000000000000000000000000..41a5e0b568e53e3e8b4b2827d960456a8eeed232 GIT binary patch literal 6042 zcmai23p~^7|Npuh(%eU@Ga}ofnoBk1Bq1A$F~z7%%VmhUluAb#$2~C)Gv%_;+$y2y zD5=dQk=sz6=*TUVPAQ$P|7X)Vr~mo?U%%h?_3U{)-{JYO;P30(3tk86El+9EKA1Lh)%Ovkp{(v)L>+XP4FNg|bYmXXyI-w#ugU z{X}Qnkf(obi@fiN$R7MO=UWGp=z#)fv6a_72)9)fvrop9=Q9ASCACmcJWvzbc~+Bf zS@Z5?8N%UPz^g|?l zYwzR67R61XI!X#EUX9P2z?2&3mHv(|m0>1TOG5|6CPBfn4KUedgz`hdwLyiAfmMy6 zck4{<)~QIA0H4Z=PXiSPvCIEYJLHu|`|r&rumcO(09&?4AloAh?Ma613=;*3g*zbN zQyY?rfMKyS(xN?bD`-2T>^fP__ZFO&|K$X++absXhZIC0JApMwrUMMiJ(1SKkuJl) z6k?0~=jW6JFF+CfqCY#Gzfj`AkRII$;7d$bfuOkHK0C69pu*-Yjbhac6>9kXU`}_Mqbk((1M7nN z*$Vv8y`pit-&8^8E z?yqf4c59}@I!Y*6bl7s(>oCwYK zv&hb~hE8>cF8wC=UR&&bW6`H!?`GlRuI1aO6+NI$bKmGYU`ZPw(}rSbqm4Am)i|%l zZvnFuHe!ezh$7&aGzbRlRpa558~ z%E+<}v)-$Sn=+Iu!ONH2K`<-e<|_DQ22oktjjgX`<8Q0H87HB&?3@!aj5cMWmbI1p z5GFS(g0v?Kxj|4ot^;=m!RR4$7TfjUU`4DByfH7QpMWvVLzrWX%GLcRIA#5WPA&`K zlv>19_fM$-n;-@7Q!crmr6pyf2|-j9k`gA7Me4y!Ws$mYlSmQ~U%_$j!7Hw0cH!iU z65tpf(!B@Mj%0RkpX4Om-;RbLRI3#D*`h5D+Cy+-Xgu7oH^;(B)5uA~zE_hrtl=`8 zYjMxQ(#?Z>SA#s{L4K(1LT)s2(Qt8D@E~j0leNeLc`oFJ&|VALs0aBsZSqhf?Vwx6MUmaeG%pXbR}9T-k-SF~=LD$k zE~8pxawB>4Z74Y}?nxBccMolJk?f^?k}{n5x|sj#+cu5n)Zw#i&`cx7#ERo!zjwYg?a7%|tp;Y2y+Zh6(SW`1*X*j#f&cVj5B ztn%3n?A^9YU3K$w&Fkx<_1(zPzp;EL_5eLX_a8Y&Sxl@8-Ey<$V;5l1{~q6a7}*52 zT?tzO=7ew7Zu8rm$qQhElUCLgR#wf`g{{3^J!k7ij`Pi>jcxMv(g_>elt~%RD6M?f zjJptH&8E2rdk8RIy_f)4O&4=jZHYXt zrW4WBvl{@ptbe;BpR4Xp;5PtPJg&AA5!a|m3B!qyUr&jPFp60(WJn%rzsHz!BTDj}0dz%*tPox!Py#LI~!?+~1bLkh73rvj|9i6N#! zZ;Eq6G~8QRa;lmJPkL~T5C}KoA)JVp8?@onPU!YCN1j60vmgS;gF@X|l zEn-I!Yea*j3_$P?Z5d)8pZ`m9YOtwLM6D4E$6n6O2EwNvc&Fo8{Zzmzo0JZAP(qr@ z^MV0^O-gH+dJq|6`EJm=S}el}d?6ywG`;l%StniwvWbV$ip42J9Jbq&Nj-!hIHMbn zh1quFjrj~`TMUNWyPn0$6$aKrl9VSgsr?wia<(fnHLPQ z15qqQfxIKH%R?mGM{qU$;RZYe@C#4VAJ`TVscMVW_3zn^#xPxpSWF_ENHF4o0#^wM z>ba2nti4EZprt%=K!vJ;OF~6?m|@kJ&O~GWDPYl>XaEJCpxZmRrhddmu_jSu=41q* zaH!@^zBi~~pMwKEm*f#6C`$pdL6iZx_y9Py8$W}z0GScVRK*H8397b=THqMqTh)yR z9o!ZWK)qLz06HG4UrZnoXHj4d!;2CVSR^G-K8J&f=#tyP$kS(wLC%zhV#PU=h*v0K zIbeh+RyGNU11j+uD!_IEPcebz{Tv0L5n>qw2e1-)?;zF)_R?$hc_M1H-&YS0Nzar;*~06Pfu>Ve-!i_Wvls2X&U3C-_Givs z{h#XnKaD8#8UVTPJILmyk^=NqVTI?;%@{pp6S0nPRZ!^N&Ic!!U&&0u%y5~&JtaGc zc(f5f2?hh;10tH`O)Q8ck+4R*JaOt6^d_1D-U>!M5h)l*u~OvK4~j*NYE9_4Z!{&Lx`wpmLhLuXgn+& zig&yek(A55($+UF@wGlEwrwTW7GDfVz!$}0wCLLr0jk6p(PsiTEX^^q{+@$dO)o(U z0BU+^9m~~L0xiW7Fo#(QK!%o#jKB!CRKQ8V$W#ZV91Cz>nq#R08ItmjMG_+tm_|&H zKvgn=>5I)JS**XbS6mAMh{}kw@ah@EvMz%VizjU|u7MpyC|(7EB1VGeFrp5WfWypl zVrDVm)OBj4RQ%4%G)G+lVu*#~X}Xi>GaPEV+wLNNBJOrI%ae#cd%B-d-4N3aZj>lR zXaii|{FIH9CZYofV#;Em;!CafaXzxLPu0zoRFvA8k^Mn35S-beayEIdin>PSsZy#H zwAbfAheEtcMn-*_@{C!lobRJ(6cZ(@pDd8l%!*IY2s`@wvfbBi!L00gs!JDODQFc0 zWiLY+7_jk~s%MsG!OyB?tLJF20DK{6Tq=G!6mKY(oSb|pv+VYSjG^5@Cil)$StAKq z+$kz9T~%ZyW61?s7FxMP7GIFXVlw0&${}zTmgcxk7M5$)qD`WW8O4qcO}12HbKVVGL3dyn zgEF6uo`23m*r7B+nJ_Cb<-@hIV|F3)kv|+BxuT%8uTj@c>-LH4hdQ>7u_DmrDw{op>*NNeZKr9|M@S+UtOd? z&u8;@pJY&F4Bj2rKo2_`tbMoPcJRxuMGhnHj=kt^8GaRN`FiVxzRH)q=g+P0SMnvi z-GPI+ZTIx{+j4p5q%vmLvIF#tzgckx;B|GoDXb- z%2M=T&)09-g8YD1a-SkCTB*8nf{1rSw-ea*6Tt|*6O%-_0H4zkZH=yto#1p^bd>^g zS=A#)kE81uijls2dWq2b-1JVFgR@G3ZgrRRIHf^mi?VQBy0wu@sKbI~`b{tM&$ooT zRXQK1)7K6Dv8<0W8#1}TYSz6O5MM7dBfBV_8S$cY##1&jE6>?2XX^NohTQz}5T0r3 zjS;=A;GnL5jw4dKcf*)fjz<=TvRT9x9q+sgu6DOQJsLD^_&((Ny~pD~{6ey&`DI;B zRyR}efZ@W_n}$*%A!g3vs{g%<)Rv0CUAet!sVn7~FYJnlc^-I$FTLk%nyDWQExpN-<9)CRI%3|=c3 zKT@$N_Ue>!;wrNDfuc>T_8)us?5-Tq_Zf>z(K|2mz{tbFL5EtBHgYTK^be1N$5r@#xT0@3}|dehK->(svl z-m$kWkNPRxiTj1YnXGGg|JQiM-`##URvo!0c?o`-is-*lzX?oK<~YO)4AC9 z5pM<(7R|ptST*vA7xI2Xq2<*D*L`7w#$iu=x?ieZ*)*Wm>P;_nYl)+p(P7FCEswR2 zr(SMt#Izh;jcw4yH>t0upVzI*YTxtecGiRY-Q6JtyWGwoPfQ;_?$Ftv=9WQ=FozmC zbfrdrX8Y$m8~F6RNPlGVCwyBD&e_}6OGp2J0psN)B_NMPqdSD}6rN9}P|PNhHp7k- zG%Liu`t-g?o3_Sk!t{*7szb=(`i2|D`)m{ZU*tpnsM)5O`UCc-j=fJU=-`~tI)8KX zz!jIBU$*RD=TmUfEcymVe*KD%LHnF&spZn9>YXjnsV@}0{Z9rXTORe!4dp?j$G6yA z{3U9uo0nhS?SQ07W4$k}K{D4D85H-@P`ii3X8uv@eT?J4h8zbB?8)jiV5Yil3xDK*gXdRMXT zwBCWv<8^X;nN>7r|Fo-ev5A@P!gaIpxEi8*w&rP!YmD(p6{MNTndP)8MhZ(OC{(rQ zS5Lj}u9q}Dw1bbTnICoiTDD^gz9p|NZr(}rkF%ro(7b0!`!IC+^sl+~z8amw;b_qZ zX_0PH8A+b4@@D7hLoIIAa|AiRL%vdoXRVC7=S@xb4KXI-hkrwlzw)V=nl*)p?6`hNgLksWjZ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet02.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet02.ogg new file mode 100644 index 0000000000000000000000000000000000000000..bed753ddc368d7cf5a8e0817b178e71cd2f6a0db GIT binary patch literal 6049 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 SPew)-=S1Zg(z28K5-nfnNCZZ}8@7IJE5>ZU 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 I&R^<(0H%q6zW@LL literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet07.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet07.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a77e8b4ef3079f61acb66ba86bc811b71218a466 GIT binary patch literal 5985 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 LdFt8x8t8uj1Y_I1 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet09.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet09.ogg new file mode 100644 index 0000000000000000000000000000000000000000..4b2eaeafa14053bb1b0721a3037e85c6c9185872 GIT binary patch literal 5633 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 aS6^>ybZv($;LXPZHrk~fR!8bVD*ppWxp;m6 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet10.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet10.ogg new file mode 100644 index 0000000000000000000000000000000000000000..69294ce63a56df03cb8ead18cd7a2dce0b49344e GIT binary patch literal 6201 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 MA01-%Yj1=82hQbB1poj5 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet12.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet12.ogg new file mode 100644 index 0000000000000000000000000000000000000000..09e9074908d573e5b60cc94455780915bbf027e9 GIT binary patch literal 5933 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 as`|NoW4~CDWx^ic^Qg?V4SW6r7y2Jp*G~rk literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet14.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet14.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a480773f6a601f05d55e99a6c6f6d776d9fbd11b GIT binary patch literal 6439 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 E03lNFvH$=8 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet15.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet15.ogg new file mode 100644 index 0000000000000000000000000000000000000000..ef788ac7cf473ca5f36803c4d4c725cd691901ea GIT binary patch literal 6617 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet16.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wetimpacts/impactwet16.ogg new file mode 100644 index 0000000000000000000000000000000000000000..4154c5ae2408bc5a6ecd491f161043f5c8bec109 GIT binary patch literal 6747 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/funkytown.ogg b/src/main/resources/assets/needsofnature/sounds/funkytown.ogg new file mode 100644 index 0000000000000000000000000000000000000000..6562b0482be6737fee438271c7b21c37ebefb716 GIT binary patch literal 1847419 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

Oze82NOjU|qBu2SnJ{wacmW&8v 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

XV%9a3Nfp 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

bNXJ}YIitTGj1_%0*sx;S2*^9Xe46o8};30Mtp z=JGJBl!m{VyhN}teD7$iLt4)}6j0zg_Z(u#dWA5di69j$1S;2XL?;N7puy4<6W0t< z(4z+ky8i_&K9g-Qg>xcBg6vxr3a}gl6d>uZ0~*$VsT%NegY(&m#o1eK0+DCQ7J@?p zx>#p@|7g$b3EU{_0Dpug2nT~Px* zb-0v&qHQeW$L4D|f-H2htpkARK}tUN#=MdLtM`$6?cYkn)_fAvh5R&~8w<)!Pkv6K zB&8rD2SI>|j2K7`88!|O=;~+!H_=@s>2)G5!@&W{&81%HBtiLvXL zPfQ(yXX3!?6=l!)=B*Jx-?;Tp&eEcHI~c<7e;tcX2A==c(hbN}cP;Xfl)Dvz!Q21y z=Lm!#KhiwHjUY#+laM1gkQ@v~Bpm};lp%;@7WM}5ns0~9;Ek$>C?mq-z~xc3M|Kqw z0fEoQZ3mp(Ag+{dqXzplNC7#nq09trWXJoz8&rI4P*Dm)HJw}=aBad9+xBC9!-awg zY_2{iNPZ(EftCTSEksIhKzt_*OV?Un;dDRP51a>ZT`^2RPxC4H)bhqO_tQDUI+q}T z_3gy9fjE(f>z?}g2WkR@!QRJMJphoi+NJ_(EfD)%i4blsff*%k=)^8HDn^(eqAM+?l(C{thU2{Qor_EYxZChaXR6Iud`gT|;bY{f-^RYPFgfqo7^9(09kzAOMm9!x_J zP)hYL(r~H-4@iQdWn}}{fIih2Xvx8_p1ihPHv>3Y

dD_{ec$U7FLaF=A9!e8LK8z*_d?oHfEkzgG8ZBj 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

  • 4xFhsvIidu4(Fh|WvUx2j>d7y}MUXoJ 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 z

    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�cY8hAM?kVyQF>*2Au=GKYrXBJ2hI;hMBn0MhUyNOuhasSxy(`l0}&sr=*(E ze4Ew&O>DALy!G)VW7L(@hqc;#Xnh^e-=szp;gGv8nwP9uk3)4|U4ogg-;|PV4SU+f z&G02ubZaGf$eRy6d^=eAguDf6wzoz>H9Z261vcl7l{FB=UFZ&ZK3)r&$~iP&yF2vu z$oO6H=eC;*-q_7N>zMN5V#dA70w9Q|Zt5GwmqIf6Q#jNEXbI?mINm*>9i}z@|Hekz z)&Fl#!W66kUKUpLNy;hRy(2Fz54ooe0o|^;WYJ`J7Wsh^M3ZbLe^*8M-hJ{_GO+o> zuYmD3Wrd)8;E?Q2)IAp=Z2&0eS3p;;V)gFdOv}Uw#dZhe^$_ZMs*miZZa^Yg0-}%-z+HYrnI%@dIZMNBSg*}8VI21%$ zv-+#w{fr{Std!jWv;-{y_4k<_*J4inx;?U^FRf0``wiVLYdM+P0%uhf>vxm4Zuq&2 zvd))JKs9AK%vi%T%c(PkmQ|}aENltZr9M4xJZdy-$GgVIF}DwK_Qf+`Nb?G>U%@?i z9B8NnRDnIcD63SZL>-gYpUz|Q>G7?Z*VOV))K)i+;)rc`484$2nmz3X-{{PaM8boIHO?_fw3m$Qn~34gyRmuP8eD&fSc_{OyMhN*G@Oq`p}S_!Ey)K6fz0 zMx>fjo93TG;+OM_N5w|RuN|eyRdh{nXZTM!uYvBwf?Y<)p_Bl122E9N4e4dXfKf?@ z?}P%gyKDWJ0&rByvZ}%YsUi3Fy-?W}9I%ffva4(*#w%1hWzD5eBJ71<8?}}~D4*t; ztJPRTrB@5RjZ4l|iYm$-DaKZ9kSogP&@>m-$-qhZv?g8CvKFzJWd1`W`5$Vzc6l57 z{U#X{%@!1t-u!Uuxw!BK#mk-k$N26c!2eicGJ;t)*|mn5oZ|8$JDXcbZ)n z4X>| ze=C|q=sIk0R3l!l;ul;3Ycms(X0o)#Ar+nTsd@_@kIy}60C`?GgB8K^$q?ey#!=(!u%k|F`ZQtmuV(hA zYc9J?4PLXDEo|HIBYdP_g=6fnlG~lRhOJ8~I|Br(5dTO;#=^z_!*11`GCS^`{ zR@1uo&s*h`P_3XA<0S=vAg&`y&9G(6uKii71XpWT{(9$guh;CZbSK236hNr0j2D16 zxLyIY!mYl%y%$ws0HpMBCw4yXrxo%@Oi|freyx}+l$K#_MuH9}FQ4|~(*n}{WCu^h!3o1jxwE>Y8Ltr1bi>sUD| zWk?aTAxYsQXM3bj4E)BAeR4!L!yXEMhs{P(lUke6Ea?$a5oGLUUAz+f^~9S@9jj=A zMPmLJCfEfbg#iGkW7Zc07?_Xu4x!;=K;!WTD)-9tCUbgVZ+Aen{9D=ip}FH9^TXW* zgV-U_RFAcPpNtx1LhWNyVCBI&=svEf|@otXTs09)K-`Ii5PV! z?Jwc27_I`X@PpOv8VIamfXj4`<*WEhfDOt zmWsQbD!*n_b`5t1xK9ap6zoyz*|s$Vx#OL`N84k@S-H|YAiD|g6|UsVq~!Lf-@m_6 zr5tixg*sJ&fCi`q?@PV|9P-dV*SN9GKfNNhQV&Sfq0rNe1~>L|)IP=6H;My8Q(I#f9>(;eHquo?WxDpOvtkJ-erRYT{wUEP(q7% zK<+V7z`0QZ!flP3fEHck9R~I9t$*3P7=qjwKjE*mX9)*SBkB6Ng*0G*78Ry7q@T4g z=3|Ih_#edyJjRtxIKRC@Z%6|+8R-cmjGD!N06^jT#aJ{@Mgf|>wY31CP~-4tm;Te4 z$vu)d|0i4Lc%zOUjd+5@K{Q(>X-$HK_!AhS1ss^&9!wl70Q=!~lpg}-R*Y^@M~gI2 zYsI(7`Ty#D0>C82LS=M;TNKxLXr?OeBmfLSi~y$O%sEhbZl&&0V4p&6^H&x>k|}uB=-~T&jx!b zu{&$t{8NU!HbSi;dI2I+b7#H{dHyg~wI zP{rfGU!s>yotj+*nTQSLW?rzqX%-i#yTmi0!0>&|+L)|QFPwP?g))Gyjp7=bT8~p~ z$E1~v=A{HWQ@jvQj^ZlpZO1jc;(`}pg~gl038y_x_JJ1uDIso>@(K6!ryk^(D{2chZRruR}Ko0!hKrm@5`6*TSc|4&0xGW z8=?{F?VA0B3GJP3X7&wY7hAH~RaSDo&`NE_37pVf&H+sG9DpGz_ZC5&XAFhSv+wg} zQ+{O2+|2su?=jfUT6JFVU&HBa32$DP)Ixee;4bg90NZZJPij9=UY1}D*r`Y!Mm@)C zg&r7>H_^KM=>XM{weiF96L!;X2WQi0>0n>nsFSupZZpi!S9)?>fkhGJy{_0ZhDqvc zMXGL8=SL42PNH}c?p{_Cr7SM{+e>oJaMhcTnAk!7iKG?1L5V{>BhKkTL3M@tXA5sA zsrsV!t>FHXrGFGQ4SQM9xc`j=r-&|WRy-ODYU=7@6pM_K>UeyYqC2zE#=#hr%8vHc z{Yn7bw{C>ew7=;8rT6k5bsWCG_0QO1?!eBA5}BXG_qxhDXYq5UJ!{Qj+@cVEt>to#W6Rc3O2`GNPec*qbA6 ztAa8XmqDn(k%X(x`1*sa4IkxpVS`^N|MACrcO4V+S5H8gQpDUgj@jHcX0vmjH?Y@h z4O0ffKbgi;$p~8&gEQ4#!}s!Mt54@rVnpq|Cc-FVixk9icseIPUvj1Zz5`~e2;BqY zK61>fBV=?l5sFv)Ww zAtDJr7pxsmK7A3*JmhM_i42gU)5bBVZ>Hm?{>yPSmq1t6P(MWBbFV?%hy?JKa?|lw zqGr7A@>#fS2esDU$$8R-w)?@4Ipx(!(!q%oNrP#{pYEk#kRXT#)B($9*uC-BGOL-R zrT(BYn5i9O_B(~z3jN$`949j=3n}+~(!}q5YF3iV_5*UjsIXXkG4U6>k-c@+HXWtEPdX7iONf zf$W2Z|8GS)uHpgGGxj~%t0{sz)FQ)2gO@mfMV46B7bzXqQbU}dc)Q;O*;uFmtJJar zb@W=MBGztYR_&_@_{i#_d&kiz;|tEew4Z3L5-B&IJ`yF>8&_Y`@MU8o&rA-7MR(Pu zt;_9Tqh*4jZg*5PH@mvv0)hu=Oy)P-c|1N%^7Q}>6uD-P2XC7a<2p?ZsfA;BoN`4K z*)8iWRFV`UH08y}?j_Inn3J0GI*|We-{F}=+a8macQmuDe8ASks`ze~pI~6PDLkI(g0kO{3Q&oSntZLH_qLL96APKohBV@Mw)+aNYwt}Ac3 zV?)Mv{uzUrMFTy-_a<)t^{aU>+UZ5U0Z#ix^lh#R{{cdc5K9kVjI@rYBr%E%)4Wiv zg*DJ=r3eXeMg5*Sz%%$wQ&WC0DWRv{A(Kbb3;zi^1;TM z;6nW+U}ZrDfe3*AkGt8Je!f*(vag{ihu?PH#A?~khj1GX-Z^-^9e49gIo?L5j^^zr zP`a?oA6=d>295W{ik27)WK?MkqF1lOd*jM`Q1Ecod2Dx9MO_QJW=Wt>vzUh1y}s`( zYH;hUE3oQfgB^zTFoD{G(}n60t(&V9Hkh%ssuRz_V~M`mlM94mO_adyd*6X~ZueN< z7ytl}MkQTo_>k(dciE*pdI}&6Q-|P9Scye@O(^VX57uu_WGd>%{#gh)bd`Ir2GGkF z-|8b*3bQ*ZGSK<)u~a2la&y8%bb8 zVucvmp8!M7mCAQ@;WIbR92*XFzMZEMh84`bW0&y_BC-2ULOda~so0@6lDgm#JycL9G^&5!L9+dvS2-%}P6d^&AHZdP^W8a|Wk$QYS)A zAsv7e>Unwa*x0S{Cr7Kw3#Q)`3YLptH)D^a$p40BSq3}@Y1e-60HEgt?kfCzUOlFJ zHQJk*(sr!SmPD&L4jX$)MgJDcq~!)|IVi7AtN|M)Vp?O8tcinF|+d@Gm{sX*oMo`(4@U!!K_FyzGNk1bJe5pE)+95Lt;m*Sn)+cyHy^D1 zm)Y-_W2hZz;CRCMD)^6E^mtFjAxj6SZ@8$wp#X5-=|h~~t*OEk3euvZVWhP>xuF+u zA>hk{cUOHyvkW)pQ~!ubGJqod4y>dAJ%J|=V1hfRJ$F^SFJo!0= zOI;(#Nb`GU$=9QQ|C*z}qHk=sol@OkWhe>vs3Q{9kzXN*ECHQr7PPnDzm^9eeH%q3dj+Gb5l7APtZ)rPR;#G3;d-48|fdSxZ46zzG z-t(M+`l-=~-@L9mLd}&!MIF>02|CMMJL-=F`!qs2rf(Nt<)knx@Yuc0wNSmK@?MLF z8}5xG9XfO68I~KINX>WPm!64>j#9l6zNG*X@2|~2>>2@vuWOC-C;kg1Dh1QtOj?=D z8~ivtwixBI=@y{IA00V zzVDepT}98G*iERVo9l)JOA>EU}7b?x3_spKB zH?52csAaOai~hN6&RRMCBcFzys`v;9;F$@qt$z}lm0Y)R@mBc8?4za-xF~thMUk)8 zv!m3AtR6A|_=z6o60M;{pY_*I)%J~|XmdW=Y2pvaIBwgnr{;Dd%y}Q{k*I-P3;(sO zZzyhy&Y1>!X*f@;B;~)zql4jivEsp~J9iZPkkj}2Kh6yB0OQpv27RcGm$T6xbACu? z``MF;=lciwZ92*Y3Oy39tnN&1#a;yfqj%DACq54n95ooZiVx$w37*hh~zA2?(K04rTF z7I5KBaCi`jmX&0C!@&J!$`FQOvE-{V(5g|81t#VIsjSlR(${V7DNSwx&dsCWW1wA0 zh(fW`ntoqXQ55-I98be2y2r5+{G-Z%DQ3YF99N;p%bi>i%FH-nNd=1Yp88%OmY>*j%L?2@P*f;zL$mgZTBh zhu)_;r`&Xw3Wd=CjWCXbRytF_fWEJZ&VluAzpFWV1l7uuX3^G2pKZpxmUU_C-?WZWOhzbl zJWyxnWQbSfd><3wZYNjElP9}^P0{vT;2V0^av&pGp=mBpTHPM$MQ3B`#-Rkl)24|9 zbkx!th52cv1A)IfC0~zKieGmgNbdXDPz?2L0p<%2`_8!&P!ISd?^{?vd~o7oKp)<8 z!N1B2Gv^vB=VPVfi%^f)z%6a12j3YB>tCOpx6sZ@xDfo3>Ydft7Mytjh6CODiF^mm zPGX1X_#}eb&OEM`Ki|N$oQxb*=O%dMuI@T3xi!=?B3fPXlp))lW#>w9?>_|&Qlf80 z(hs1XIP@#Q`pTrK#w1H?!PMR@;QXNT0p&Zk-YX*Qldr50e{^1sB-)QZa*L-W-VQ8N zr(SaI^Aa7OYt~78EdaFTLd9XCphfX^KzGXB??XCaT%?EwMEJahnnuph1@i8cMQ1$5 zgQ~q&w3vl*z-(PiD(8~x+eZH#;Dw)NRN63Po!WuZ{Xv$r*rL2RKH(1l9H^9WQAF*U z+u4q{Q)|7abfB>7n;A_R61%K#N#GCu5^x#6)5CHjBeJP-F=-w$Y~^U z6W~(j{W8ROHIp^?>;@PqxI%FSSS)u=3j4?#VaB>pFxqR+vmMw^Cd%C<+9Vn4?|nJC z3oHfevqaV($9IUdc=`58pAa9?Ed6t5>hFFz&~%Qsssz0gfYcRwpb|iop1E2JbFX)? zlimv4v3$o?QNlO&`?AryiXRUpQ9L2qlsim56ky?^3>Rj9KD=@ELKC$0MPI(G1>~Tt zTunCo*9PPk44V;TU-Yfkqjja7*j@u*4BPb7HgHzWq`uu4kTp8CY@xG%3-gVuvbUwq zrxr<>ye0bH<3@4nlc`@0v^n~~b#;J!4*&zLcBM11c#*qQ4p%2OjPe?Ze>eT*zd1h}3 zx{Y+-t2wsZ*G+6pV9QC=c&&nKqg{JJCvSeyO30vJ>>1G0Mcacl0Zl*S;~aYXPty;N z?}&3Hq04!u1ORG*hd}#%%IiQ9*hTpt0T#=DjU_X)P6xx1R#Z;a>{~x=JE^dF_7Y({ z*-|Noi(qNt;hl;vf9F*G(cr=L#Vj%*?v41u6_6;C7SdLet(zTE?(^~CS|X*0JDD$*&#m}}A%>LVaeVU(& zawfj>KEbb5Py>hTmgZFaU$}s_Sf3>ZRZPygqj|6!ZdHWxj$|FbE_RiHN95h@2Hti@ zQ!yBq&xCA8qDA{rO25APPG-HDmyaq~fW*7f&Imv2dtt2$^_pBOE!ohY z3K2`4HL*Y1eUlVwXTdzf%w6c5W7BFO7g2%?^_1VAe310^E1rF~R@8KruUMDD)P+L{ zcl;$uH%rH7;LlD4zg-N!M^LQyb`>dHwR5zhlXt5^u!N}7Vw?L9rOm(MO<21g`t3}w8!YK&zG*MF1qE40YxGSKiQ>n^t6#VmkTACygYTN{%=NJ3<2DJJun`hQ#m%757N(gMuE8(`H1hJ62 zI&>8d>m>rxRccKB&!fm8F~z9y?;)m+xBl0mJ)#%Me&31J85S=zw%;rZ?TUR(0bn$! zxb{q%6any7b4Z9_XJ^@4(_&7qGdFQ7e^@=TRgapJuaZ+|gi*}lWCo3Nd8n_whF>#z z@^XyQiV0X;n0nv7RPcEHiR(i5M6~Nc0$pIJZj!h>pvwHT0Fyb^~lLc5qfg!EbI8d_SX zg(ur~yF%N#H?1rvLh-LPbUbbA6v*z^`Rq1TZVQkD!%HrluOe(wrQtp2_DIg9XF(yy z&qG5_PL~>Q(kaKc@(SBc$i6nz0TIR~`0c!IVsQGm2Hq8&@7T2%+I1b5&PFg#&I!>0 z!~jI>^Fzj0Ous5*&=AwT55-RIS#u{f^uOb&S_gY*FF8m8;GPVYy+F12-ixE%roZdj zV_4I~YpV5HIj(2r-gWYlL0&)BW*(kR-L9jkMXMM**gUo^9QF0d;*ha6qjqNlE+@NH zFTMB~Kje10uk5Jz)Yw+eUbM8T7PD9AMWlhre~Rh+(dO!q`R10iMXHcWyz^j0TTnpe zYOl4KvjZ0SbCvXJfSb01OSMJ5mvdoK1p`U0f~-y%^#v>DZDKM6H5ET{Pbx$X9gUT_IH>Qu@b9Vrv$!hxmtiLv4TiR1hc_M*8>0IX zy5KD}=p%heTeH>q^N40O)Xv&=uzegimW2Kbo7I{pOgu71Wy`@(A|QAadLpB13kDrJ|CrUI**QlRC6K;+Yf6_K4nLZ zyzFXF(g=vtcpLY?Q&g2uWdRp(6ozd-6b!2L76de|iRvJ<8Wq>6b5=XGBNN6bXkfBV z-#hfx782FBh$o@d&KU>y_3wUEO4^$a9UU!wQ>Pi_iJhH!_|Ra1oywI(?06M_9W5YS z0=&gEI$LWWK(upavWkr?(=TgSHUF{KP)u1q)eS3Lg#ovfd=F;s;wApPQ#jk0gb0ky9JaT{H@HT= zlWa9Hww)ol_}0#741l0BN71ji_O&9FCHPAG_?t%tPWr2Rx_o zL20gKe~)A``~iGQ$F(OG6x0|x;7{O0;YkLH1`tFyx;>(J%DdxFta5KKrDYpmW0$qQ z*+$KIWpg7&=}TLcqSh>Lw^q6K!v_z>T;9*-gs=%Es3OZQ%)*%YAIaaNTYNX*4lp@T zyaCKZLnca$oP;tey9A4d#%1d11Y5veJTQ6b2e10~Oq*svi+JgXx;A&PGmr|HW=deo zZBJc$C-;e+sl85KD|NdxjD$qLE>iBW9vm)d7pKmyH}n~OV!Sw`fGIJuRca3S=tVxO z_j+>Nny=P%BP&?l5MSrk6;xj~8FJZYjpUEtQDzKedgm=6rH{`rd~6mbYn^g2nQgve zVP9bMnby~)t)J3)J@EQwDJGYRO(J`~WSc}JBr+FZ#oUjc%p(y5j{ktMYj|ImO`OoV(e`l9W&OUnq z{Y8dA)b!9qf;WXJ(m31yiK*ae@Mtsrthh7oA`g%f!Gq+FHg(Pu%9|IiUSd3DU-+_{ijYl zJX*{FPOEBMX+8l+)f5bbnV)-CFU8~$>ulf(<$AcNp+$q;M_!t+TR92iRBwOIvzk2A z3a@ll?iHIlO#WoS(qS9o=)74aXF|G~yFCE`C)CXSj;hqa9eFuU0nES0pNej=KELGV z7`e=>k0>qEn&bd-B+r;FoOu13rXRnG{*U#zt$hi`1~d)`vw2g3aktd~U@f*fs6DkMka0{Nx~@AK%L`J#u1+U*4Ht zOQ1a7ks0juT0Oc(Old}vS7NzqhXp+`n5l}I1VW&ps@3g;suYqG_&0u@Cs{gbm#(v7 zy$KKQIL9`&X`G4Yk%u0=>rihZul(u0%*_jjbJht^f@uuZ>@cfK3JwBI`lH`dysc?0 z0oW;@OoWw6e+xe1paUGqJqYkN9GDNXq5}lBXC4r_e5m|Bl$Z?TzfwsK?U>rjbJ0t` zx%MZdP=GxQIE^u$y z^DVq;xzJ@w436upRr?XZj)^XMHzafj`iL?sDCDK{S1-^~nU4miwry*KucUE^f9)C{@Ou=dl=ypAWOjo27U_<=%X&+$zvDcM+n(-F|4tkp)}f%0JW zUEyOKdH#l9PI?b8#=?J-<>b6a)%&p9A373g6@J}7nJ%5<>Mhrw@LDydMGD5u5}#8w zYA`nvk}r9PV+L)nvI1U`KODPy1XM1^jJJAdZ6*Lm?0czHTJ9nOSw_DpzP4t3S`q(1 z3BU?&7zgrwuae(Yn&7t>w?TX@Hs@VQJf``r@_tE9F8t@Mgo{Mmv1ir{;$?MyjAM}J z1@ThrkfP2;}6LrZ~zFq%V57Q7z?ew{hNti^=s3Lg&6Ad&^}qYYQsbJo8aF#D z;6&o_-_#qb)p>r)QnCNqz7V_*N@Obz>;wggAw)ukuM?m5-1UtA}BiWt}cP1Ze(n*9hn=5$6nFUJcZYckL1OPOI~>&A!EmftSuUpI;td%SANh%$YER`phLLUbQNx3{?*pd*g1x(e zYAgddlne%g=Hm`$j#L4_Sx7b|zX;fhfR)^G2|1Z0ev$k>ioWCd(9;EFh_Liru1^tw zB^7&n?6yhZ1OMv(rU?_&XWVlemH8Bv1Heo)$nxFHrYCN66_0HXf=U1uTk7U=2bISb z1D}3jXt`hpbbC2e0GyJs5=;+jy1U{eC6szEf3<#M{_@72>6pKm>ZavERITr7Z18F# ztofsVf9l$tDcyt(I8*iCd)2t{+xG963FS6 z3+^IbB~P2qS)G;rVs*r41;K0!Fz*j`{2!l`^KBA9Z4?Hh$F6auj@iTUFBJQ_Kd`-CEyK{)inM;^y z8^qApkQEXGoqLfTxA`b~w8WJkk!5bHLG0Tl+dps9fNG3G^D2qe$B%L?KRI{0!ZA4O zgS+vg1@?x7Nn;Ol>6Kw`oB63ce?ERIBr(o3G`>uSgR+e5Z>DdkopvVQwAPT?i04Z^xv0`_;$#Xqx<(eIh~WEe+jy}=M4AMggpj}zxMp1Jm4 z@#AWV7MlkOiKuP8wdbLVBI5V-wre#(-o)TzNBjkK_QEKFOj%fwEhl5nBeg-UUdYjH zYOo!jIk0kDlZZW#Wl@0hnrJ3TJVL@z7|mp;Ksm}4hqR76k9XgKuVrB%njKx(I`=ty z&xMq^J{{rnh5ZaAl*qYvT_|C>V|2bzx+{L3H*k*I-rMogM zd-}5SA;)8(JsTj4S`F5Cv!3Gphv;a)PdjNm(@QbsvhNIiSwHqMzXg+9PT9s|B(lhE z!KDbxuz0SJGX4nGraxb;i%Tzh`zJL_1+84ZfRhOhR-11f6RV$MJE%4I%3>{R-EBc_ z#uzi}QM4B4;`(iL^eP);*tU%AxGNyhPDx$4NU;vfr;2Eu*)$a9%E>e6o>D*c)h92i z1nJeTGx1?2Qh_fNA^_pV9i96!!<&~v1X~0Ioj@J+<;#>HbR&DTUoZ9f^TbQ|H+E+d z#___OYJD%nCo+#Rekc@a_8zy%*M&*Tt2~Q2s8f>CbvkUab8EG(ls|5?S<4I`!3u`m z^`+-vb)aZ;+v`Xb0555jLhBz`cQ>ya3NsF|AxV~3wXhzZxbNsdWkx>Xk5AX_hkVFl7C5kS>AbK6C+HQ0!H(6D2J;3J>64) zAJIB9DkChq_wmK)LRzm|FpgAsH_q^Jm!ZGQJsVzt`((%+aZnLJ!RIh-PDhl*9d*Dg9F7~BY=qpyqCdfz-42cRhWD4Y8)15> zPaz|ySx4KwwTwcI^RnbKDy$yUcX8!*+I4f}6giYe zegIg3r`@y?cL8>R9E!y>fCu1mOF!qWzZ$o{`z@>!-^W0;sDGyNlQ+G9cW<1QS!&7) zNpIgz4Q9h9Sr0<)oT>bZEO!I%v(!UJQ*LI?v!X4q|@Umq0silwnsRl{nO_qIS8x!@^la*$@8c=7l|Ew2}6A z^{(5Ka$;5=cR0WQ#cW5=1kRWBW7XWJvyrsG~-;fEG=(<{Y6XQ%_> z>Cy?7tWlNsOOWg}u`8)~B8q)1z5IR6iY>F9fhW?J@|yhh9>=a&Zb}LY9w8yMA+2`| zT7Y|jw|>|r`nJr2srv*47C`7yF4=KfrPhuu|KSGFuZ^c5kTnIW$HbKjbuWr=uz(C? z5A4Mv23yNOv8%uD>bIxfCn5e*ArLl#!U3_UbrnZ6zQgq&w^u*qtNa9Mxg(2P=Mkm9EO)wWt44cKTBn89Y90CasXuk<%N)16g z?5d9}c9&PgUq8h4P7T44sAk)+kg#SH63SgDWkN<$Q@D#}s}ESBUOfA`_Ax$$g_C0Z zt8DVb%UHsW^E1x8jA3&^&1d^^5~)e)NlZCQaaphme5T;=3ZS00e!It=*c<2-Wjdxo z;HtF@7pb5DTosZv;4dOqGCE$>v17i694qz|SkU9LO}`mgkDH&qordV0TlEJ6etF_r zy=O{rpd2&T%E+VjQ7@f#%>B7}mZA7 z-Nala15Bl2ONf`@b@f}rTd{G&tE8|$s0v&{n8_7GGJ;MK7p7V5+pIf%sumItwbWeV zCLde>`0jCld9!+Nt~Vq{m_e_Tdi-!(tIB+5N9nqgRmko|7t-7IqRx7Yc}_S&%|%~cYuC? zpLZWp6?Gadt)a4!zB{N5FQ_P2%6nzP{$zH5D%1yQZ5EoY)HO%f4Vv1_-Za497eKTXM$Q|H?B+d`!8O;aT3}z& zS>XR4a-p^SUz6$ofHy3_VmFl~I8l`RDCI+H8Cmb1`h5u5`Y;Yt|LLEif`XEoik6m& z=7axvXECLltFT6!H~UxrOEe*dXhvZ!affh&gVsXo{x1_0T`Z^C>6Cvq7yzo24=@id zrBX?U#8OwT+Vnqn8@5b&Th);LcdjqL>+GQic_hHq)~gtge1YuiE_ul5`KX*<4%fH& zuofBm#75su8%xx%_!Xz4t*dEkwaF>ouHr#i7F@bI>9`DEBm6hptSU=uc}O~eqP$Oy z2uHl*QO$w}q#tP6a`^8?tC||Ltf2F*V6Tc!+axi_F1$2Iu(iPRw3@W}sXl@b>}xZ~ zu0_uu1Na7%f`3U*X*MH_)K=ct%R*q|}p#=27 z-vO1i4;QMRuAM3KI<67tIaItPRM*r`z5QDxwyBxs$UeWpgk&%zbXOC?t>6d^wwo`d z0Lma2{j)0nNGWT-K2n46v-Ei{Vdls8vG!=J|2JgD#{re8V7VvLiz8I-kr`dITBV}1 zeQ%(~N?+)~Kqzo#kgYd|H@xfLSAAtIvG{aYa%81bYA6}{$b-2oe?eP!IsfkgM6e5G znlvQ6cc|&wxNTlXjf-_r{8&kQV7BHh122?n>)PJ?dB|K~uJSVFie3TsT%FHJw0R!zZo@VQRcM7v&|dM_Rm`#L`K__R)uFa$oD9vc)kFPp!&Pn3*yk?F+O+JF`HRW z2x<)!8Rf}nPWXxnEx)%%AtXb?HJ$fP!=6THRhJ4D#`|jcCWTz*YYAJJtcVr}?eCSD zMuqg6Sbw0N9vPF4IUe0C6V_-oMsRR4jf%;iq7;XP#R@j~b6)1}Dl!5}#J3J^I`4uY z=bdUYfT1Ld%EL|fT}Ad^+P{~mW#6UJ zw09Lxuj8c`Z?^~gb!anH62MU@<<56?bg4&2kAGeR>miE5+YPdujw5-S>E<@-DzhV< zHMNTD$7PI%3qQ4D1n_t(0AdL|1OSg!W>ZK#W$ou<^-*doHc?Od;oswyw6|!ksRjs= zj?^dbACci>xXnj?yV^ft-v3;uc}qZ;%Y}26R?1&@N&GM{NF@ZLH3E3wp?U0yivFv0J|$Ib$QC6f*)J$p>Z z>G0ng(VAv58ARUp1O)q?j@CE_Hywd+;7j|cY0mQ_UNigo>wJTx-G@6dRPor2Nw%WT zy*dr6HKO_0X%{s3bTpZ`x=|y&pn+sOGyhju-jh8-3d3(rZ{TgitOSt!(kl?Rmona* z=wT(#uQ<5kEM7`RkkFkXXwhaHp-x<=Kp~E?oi)w{pES-I1h1Bp9&HQcd@eA_Ogi71 zQQb#d=1>^{Pjih}v{_}X`EN|HG%+kahVf{-4ZauaJNW3Idfy4@o>JL`2a9Py@O``#4}OrYBBR=o0)-niuQ+3E=0raMa}ws`&?7Yo}sp&ZZs;s2?CLH<#GT5Fh>#FE{e(ErR2o_4TN zgv~;pYE<_$bOk-Q<*csbb-Rg&*e%SrGC3BALTPT>r((_&{wdlFj!{IDl|AF1>&X&X z1Fke2@wBxOw{R+#4*jub)%Gn4+;HPNaMy?kawz*aVF8?QHSU<|jS4YT)HX-wJ2k4f zehu*fMtcpq|E zpvrn7PgNrrA=% z;0JD{h@nzCN7l`IhIte~I~{W}&>jHe--DY;z>a*WGJ&xVNm|H8QKI9<*EW%o$wHgn zQ|4cr%2!tFzGu!bDqo=xnyj>4a>LPU%+$l_gT#d&seip2S27q+83Znyr<*sb&0Z#i z3|FxIWHjKQ;ZymP>J--*5d#FQ&0Et<4H}N|v_Q&C`oRie?LTqhINU6F6lqXYq}6HX z79XnA?4;xri$%`|E0K!0@{v1I%2Tbb4VEXNu|$0D-%L4&0W(tKLuz1+CfYurUH||D z>ITxG)xeb3DL3G+^Cc<>h$t;Sqp{(i!4tWv>`W{p^vvF3nGx*o;Ueru)K9>5P6jwG zr*1c3{vWvu+~OL64)MJ_@lUsn)#CS)*W^t+G(vmo{svgY9mpdczuvmA`k8w$!5JXvvR?l8VGmGA>5y+G~7H_y~^Og z3S%jactAmYy)bR-zH65M50^UwyWO!>q_v4BZN9|!Lmf@NA{`%oPx2b*J#LO^hJ4JA zx>GX6Ya{#jj^+r-iT8iQwCfufV(h5YLIHMPZ~Xf@l&&A~vkP$86y}l%M=o4p&arb?(oUDpo8bFekh4-I5Mt=11E6qq}w*w`6=c|J$q;LnjG17%qB0 zf<$!dvcRP;1C~-?jzjg#`nnRp)PRPtXc7C=hf_Fdv+jv?Wer8f73e4dSWmDmSUbo- z$S|)br1ozySAwBlab#n+w zjLtpF-CPd-x~oBocOO>@hEUY1aVw_E=Wx~qL44={N|7tMmoLM7u!$xkGlum|uUn7B zq^<1T^j)J&~0I8B^q41KO#jy z6u_Ob5G(LU_X7$|qjP6$*j7WV%U_~aMUc${gcw08|H&Y)VpK`C(He~b@a?j!5T^n0 zVzRlhH}>d0sgm3UY2E&BgfN4j$pBmGx!Rb*jROtC(e)$}*`LTLGBi|C9?inp?`jhV znlpH^m0F6-z$yHo(QdD__=|JvJURU!^K{>BY-D6427@WW$Ii|oJ-yUG9H5Bf=MLnZMZCUM=j;|J@Xz%<5bce`y zL4n+0GU6&svI+eB{XyJ4ipEMvE(q982_XH-yCkJ7+U@l4K?yXnne$hodAefFk&A-K z^h3uYQ}_i6x~TJV@7+;KBPVDa=fOvwcJIe=nPNF$6YD2c+_8%vSCt`k`)eO!S}Mr{ z8?OLEW`}2L52UY5$z)F-9ZOC=vUyyv|@Oq_NX#Fq#Z z=cVsN#p5{^)suCG!E*H?P~$2>ffc-aB6Li$3QGNPCXemRA?>C>=0vv7K7q{^L75G2&un2vP5{+LUJ($>uBDY=z7 zOoCUn9Pgwe_$;IgYTJyS0SN-%*{5)OlLF=+slS&Hp?Go751RYu)RtNcoy}o7y8N)k zQrul!Q)7~2j8QA zF(f{vVXPhPro_n>a@7}v2%WDurbNyC^q@1ttf8dQ)k3O5AQx^ zL?7Q2d0;VnO7u8x_Eln@T$wsSBi>Yl@k$y^$0amaazZ?!jM<@a`wBXfVA=U)*NLP; z9skix?_S8Rx=8NwTGhY(i<3wZATu0?=cluWA2!~}bi0NJ&~fM5f1lakj$)&w++QO4 zO2;w+)6HT-F!g+b1@tsJdXi|(u|1uj52!L9i3ggbo;;aLkKV0cJwhBnBQoLOyG_UN zC$l-3Le?k^LLuZydpSw&@GWF`I54U;EL_8K<{m1Z^Ih+JFq1N~hkv%s%^a%umhZIY zwRrUqgj)F5lElmYx$P$Tg-93ISe`KQM32Ooy0ht5K3b#U9Rr$L*4{jay3`IyH4PIb zv13_2r+vl9r z`Q4$|%D>-;JVUDP(^O3E6(uhSTY@#WIi=b@2wxPj8kww`P<4GVRk+<>+d9l%KyHf{ z#)K4a8X1CJ0z*Pr>XNY-qujxOxQez4BIDxV`-|Dvvj<2%CKofSu@o)&{^Dy?|M_wU zw7q)?9IVIM$EFvkHhPx_>G9Bc0cc(Q}Ulg z!%#_J(pAES_H?%{z~HRu8H`&vG$HEzxm>=s7FlO|k%f!A(L$KJPZ808ZcS5AQc<3b zwlJ}vabdrR6v_T_Dw-9%j+Hc=bynJCmR1!Y9SRDq`hj~Hw61>3y!|GaoYggWq#ezj zg511&gWz*fNE(EoO08JBI*(eHCH#aw8A3VVur-ZBp&s{7N0cAhSZX;i39Z6gro8O* z`ffF_3CP}^BDscb(WCBw(W9WEt}!#$uxu77pVmvSr^%x?N#y8bt|nmyk}=%ZnDbUU~! zDq@-T&WA<}|K|q#6?EzTCE_{#@sjR_Kc z5_m0p1-h1|@2b|kJ@Bx)+mHF%h(96j!xD2`XMZD|A?kgI3eC1mYWIt@jx3$sKaL1I zRsoO7N0oWs_dkN4qKATJviIMvUg@G%8>PGKf3+c2o4F1I`<;$BQFLJy5h9t~$wPEO z7Z!-ezOh#{D=@0Lm{H>+h*cd&{S6RfaU;`{{jh z6a)b!TvAd}v_FngOY|@og)R%<(Sg@fV^OydCiuQX@}$F7X}N;+P1xK>;$%lqnes&y z`N~jq;5>tT{nKjKcAUk%h^NuTwQQpQq&MaT%eqB zGZ~)h-hm2jD(d)i8+smaX4uDy(GFX=TF2Rv&8>1B*IR5ff!js(Du=Gb94H(xiiCKr z66WhQwVR}1z!k)`hUop42x`5t`y~65EfnJ-*vPy5rbMqqL`zo;q|M#HqGgt> zcHA9i!UK>c$U5JDu9WyyayFfB?TuVC!^PpIoUuM;Y*jr|XK2<~CT}yO|Lq(jW3Kz1 zMR%eglMgn?`j#n+7YTC3NKiG>h$}TV;ASSGEmWq?!-Y!{=9dl@hkU=K8I72Af`#*> zxbdRbn z^XlJr2c-!R2MNEq{i&s_n=5{=mBs~J3B=yMBSK;??B?J|Ro3I>Hty=iQ?xWSTq1Xi z3VAg;uL+n-RyW&VA3(P?Vg}qG(ktJTlIMNUt?>>$mXsLfztIlMG&7ex?HX}U?*HXlW*#q z@V>Zk=oj7&vmI_J|K!`N?oOsFYZGfDUhc?*Nt?rl+S3=2i5K=J^32uVqv*Z$Wv!9X z=5?1@c`u!LH2*cS^GGasCi^@#bL?a#1#!9&+_@POAsar_-XL9FowTrUBipKYHeI>$ z{t*2#VgYgbuWcr0@v1(grqONF;F>-Q?bbtjYBkz9P-*QDpFL)NH9eOdY~Uh!YU2G4 zzRkxXeNrBwXRs7$mCt&T zy>6 z)>E$PSWH*a3G2lycK$flo%TK5(PUU%DoPrZV(I_Ef@)lPnSK?dwUT$1I~~#Gw2Cof z@OwGSX=O9}#b9SW(ypB+k2c3^^@#h8jwFcou|C-BZF$FYfA#;%*>=y-@|un5G*Cc=9$do5#D6jQm!0 zca43ij02n#D~BmtTWYrG(RmkPW8C&C`=7i6t=gFb+`mvMPSr6#P@Xg1YZU9fr}gG5 zNc44#M+0c8vQZM|_*P=uT)0ZX`+!~S)R!$U8*MphPT6_g?(Fc>H5}sXnk-fGP>O+U zlE9*5to`PswZB#KG5vu-^|Hfn3^6S5dP1%d8e4zR&cMZW=}#~&b_PC(IMoT7@NE~m z&ni8W)ppS%?h)a;iO@f}QJrQrnoeSgL57C^z3htGj~FabY;E8aF)_LrVifV|NGL5w z{FP2b?))4Z|1osZ<%cN0-nnnNRS-6aJoe}xqb;Eji9e;=95;f3Q7k3X2#ovs{P?C` zNR$ACfmPu)EYbt6KHawTXWH6J=N%HM#w6cKhZelJlsOMn**htP$k?p3wdu2jn+s;k z2A+eIMmH@wRN*ln0BaVdcxSMKcYQW~L7ijut} z137&YPB<$nWUN1%Skoj`oRpX20GM2)iH!hXwarvQ2@yZ|;gLxy*9zYc{`smGmJAP0f| zR5oThG zni0|dySx2+&@29>DC(e3tV#Y(-k8qq#fEKGsBA^)MMXLKU*N@g@VoX~mQDAj^RSH$ zs}LwRri}O*l)R*7Givwmc+%!zqNToa1b6MX=S5LU;P^_A>&_l6EARs$FbRiSD zd$=_@zlWA@of-*am@V?%FLzAZpI?7D#0xRiHachtClzL!`-d#Wr%PcJ*X?=WSK7Bbs6}g2kSulJ53ZP4*-d|bOW7TP?w`i}6Xe#(8 z>4cmOB9oIb5Cst_B6F@7%qFF5V2bj}|$VH>vBmI&W)t(y5`MYY=*LsF7!QdZx7-sk1@{u{;}bh#aXM_w_JY~Sd-%x+aI+G73g|> zq4B*Ubzq^m*cHEML9QD?AXqsT?yd%D>As4@0an5}dT}N+r9V7s8Ycp5o$Dq+N6<&l z0pm81@BFF}&L4P2h?fdQ#y`ouTC*C{6o!QqAybbLneBziVLZVXy|#AnRsJ+Vjd(4I zkCv4TA3fguww(NY-WNjtMIqd$-9H|+dAD;av;6|%P=9)ufXq8n= zLMI8GbGK1eV#X%i{*#NeEiR;bPcANGK{_fpwluu^)=uSNLkPj}1flP(Vff}?!(OK! z=2lv$9W{_uE)jtU` zNrLlPcJ<|M#xtV?wVf!C%P^!7uZn!rvi5@Ud_DG@-JR|+&Ix7}8B5eVDM z#s!3Ie-La{HeTBf>e5UGjf&bQppyEkiJJK(*&GA#47Z;>MoN=J7L`;_a+ujj?(Uq-2~O~{`6tew>}<{p%|VT+*$QFfwHV$wZ}c(aw9V@A7to8* z9qt({2*i`}n4F8K{&5i_g&~zg?#@tQr>DXe_ZP7{R=G(5BHXygf{EV9hX z9&&@J-v41^$}CT9+P+nEo`DJh(yrIqF)->(YA+$&fMsW2(OjSd=^s><}c&28yT< zGJINPBuAU0v&b745A+zP`Q%Is1x|36|_eYat z34rAaZiDsPBCZIbc|R{L&Fy3-rUQ+0&L=mIBezdVv5h-~xXR>8lmi4$<#6M&PO}H< z*NiPJe296p+}s5Q!hP$$hnLdp|1NH|RfFS3l(`P1pvIwEGw)Qffs3Y#?SVgw6U?b0 zy1#b~k~K||Pe&jR{WdE0QevJub_F{E5xmRnwNbZ)mFY>&!XCC*XhYC)QpeS_h56Cn zWkcty<8m5?v5)c+iEUw;=?JR-ZBB{&KaIf*t1(=xj7c!dDJWsoF`^hytp6Jf8-^T1 zfvt^wfFZp(Zdv##pJOZ=i7aB3XD0H4julQ*N`iuTE9m3y4rIj>xcUc|xaKO`yQpqR zi&$rq5e;|n<2^ScZ-cri1KdE~>i+jP$qXh7FR>UR_?kmbdoG&X>F#3RMf5YeBLv*G zsdlj0Ke;tFVUyP>o%GA4|5zQewu!R1G6E%3d{Is{V3m-==e*E{KgW8QCNSt#@U{8z z?`j>lLZjuoIU5Y>AbGj#YHbS@l)UF24;P6+*VI6QU$#K00fwdMVh-(>AM$YQA8u0t zwqpi<%0+K$mKh!>6+L^mQJDT&rGBP7GvKq$RzuC7_}y<>UAJl9FWgUtb6l5g>MFE2 z^u5@3wHmGV_$&N^_N}Tr`t(NBiV^+aejfCSAmp@yy>DBZqoy{yO#j{Z7qvkweH6nm zcL>3X5*rrGl)QVn@ip9KWc#RlsioUuU!RFnuCeRRbY%2V;C9fy{sU-mQju@imG7c- zIs8^SZvD*ABA~h1YkR4arFXfj5*%gGxWJNUg?3VQxV5;?h?!~*Wl+%GP+5;epBk3$ zT_Eb@Tu?Mt+9bcz74P;X1$?YcrYB2e>1SK*Bq;T8_>XAe%jlY`>otY#%la{dr96zG zKbMMBUO8KYL2J*jjNqb2$2?;8&PfejN6^j$rQxho;i&lzWw1(Xh9kEM$Lx z{Cnaal%%)`5I$6H$(SX2bh&*7yc2BRLQZ&E8*b=gk44_BGdWf?A6P zq1jHVqW=t=`u>zrjsGm%KbJJ?ApkD5ez;3e)}{*Mvt@nD7!t%;Kdh@dwao2p38lB0 z0FyFSSRe|PTYhT8B?R5&d=(TVq7S$9#<(6V_ob(&610Ub*Nr<(Wvw><(vO(xjf<#c zJ{5s=Ro}@KsWK1JD5P}XHs4K7YohYOQ%*|fL5ZE=1Vg+PTXRgwBh z<__xGCFl1oHL*~9IsC_^BvK0~Jh32Tdu9A*xcVX+;80S*p5Nw=E^{v&)QTe$IK-}e z+$Y!I<|#f8<-y89;rKRvf*Br6Lhj-TQv@H)|f5ebeB|V1Vm=oX^}OCv!Et3Be4oYzXP8MI6`ql&;`UZniX@#UE`FAt z+XbPHS7HDfgL#U;v@JEtpD5mphN8R8K3w_CB&RR`WnbAcGz#1d9b}Bj6VWvYD!Rc2 z$fZx5uACc7uv8}_tGmFchk9_g{^iy6VA$|)eNE8fqj*g6C#GoS;iX?if-bM+h>kXx zf*Z$@D%W?-te49<9zylLga#He^`(85uVX5bPv}bjH@gDmW@^Kh_B>>B9Xe3?y}_SL zw{aI0$icK=c2Gphgen@k_V@)IdQ@>P0ci8|19o5^vsmI`2{sM&c ziHuNz3-~pS=)IATMOhS%{X#D5ERTuRS65|T>))k8I6H3mKZE0gu|kpsatJA!9>A%4 zE2e27xJ)EL3i}eMaUFSZLuEnQ$3P$<3@Scdh=9`L0N^kPnD19t_Is}^VK=4jWEi2X zh;$Utkh3&)YYmAEoy-S%7poM)I6;r4c8K!Uu?JyTD@lju)U&RK8$){@im!hiD->up zF?eQ23mc5)vr5ncra?qKF>XqN;m^a4@H`}@Cr8cG>_t5~2B5nK!`nUs;rA%rE(MEH zakHdL_`*~U;Kq87@ax}sM*HQ<>~`3XOPYH$-1YEmDjVKbD>i#IY6W8G#E1aUZh+d)eT5U2BTy6UN(C7Pe_{vMQw8O1zL$|GX5T0{38 zkGoF0tipG?wm{&!sP1$JV0mCV$p z-g93Ie0-Ml6~;z((Iac-iWehJKU_?5w&K%ogjOr|+R;5hXJhDdD*J?4LMS7W+A zJnzp7T=R8LZ5F2)%duSXj%QGm1_TyF5YT0!qDHov_X{fO{5#CcMvtwEnv&*ccoFW) zn>!vgGlm^>Vy&Tga>yBw&W|K^HQTac$@-egwa2tmi{)V_3Ashi3c!2jqVoX!`;MO` zY3Tp>u4N2m?o<`Vw%D!eIa6UHfa`k?ALK)w>&S}N8lyOx8W^pgv~8=L=7jCy)A``^ z6bDejn0J1+{;{nV50o7I7_yKuw%S4(RD48lZR&BttiE?M*HhT-c~Al0;ENv2 zEbIs$DXPuUKN~Oy{ffV|R|9FzyiRGtom)r6J+_*iTI#r3J4aw{`v(Lo!`M78?YSlI zxD(L6k?@pAXmi`uyDso98X^-#eR9{keeO(6gErG0&TzG^iB=YxB7|ITs#+c;-91=u zT}nm2+jF>nY&)i5NoG(*Af6O=?p8KGEi%WDD*aZ!L3H0~sbeI&oj<0%;qalUprFvZ zHX$pkby{9d|D&}}NZ@bu?~;|PyNpRT0LUF)AHsKh{$gc6R;qKf>rE4cSFVcgyBg z`3AQpcw)cjCS2hhZXv{S>@fkp>$Fp-3L||c(&RH1mF*)NwzcI%6Eu?RGJ6!H|4qIf z-Km)z;5K4(|K5cq%|7Y9`NVMoEdB-Hav@9Cf$F30&m zkTn%9J2OXBc~H`-6-WFb23tzHPdrFs`r{wFUhF!0Ll@L;<&aypchzeIO9z1nI_?3v zSc4BQAQzaP>?TQ(UT)u{`Fq#Dc;L<^Ntn~_YSZF`=)upba-Z(puwpHXp02=8^6ztoc>aI_;pn!&!9OQG|u3LqVt`h=$QKe z%W(82_&G$K21+Lq`-wN?quG(<^PKO5SbG-m*kauA7ah#DB;_d|kak7qaq~49{5D+h zlVB7NMn@nyC+!RBoj&ecf4*u=5b|W>lBCtjASCFzd91yK*UnJ|vX;kcBA8)jopf|z z`#>iWFC^9gZ+qf&qMLG1Q0ICM|c5b?xtyuSe}@ zk_`dK`1L6n6%z}F=FlT<6C)9v0s?=ktKx(A5+xf2XqD!$sx^dftbq_;3 z9%B4;T{e><{99p<8U*t2_sQK0)N{1>o$~~3mDS__=5bPWdp;eWlQK+p4wk4yTTGoGoNUnt+E$a$o^c8VHnv?eKt&;X5S@UuM^ zjn=k0tx!y7Ahx~m@sZ@JkubXmvJaj#f&c!g|Ke9LHj=!eU2*F3uLBqC`F`T~P%|ct zFpDN!l=t(>oEc8-J!D(;9?bZ`_$1SR17cM}399z9Py!ccBeLBox3=92q4~Y~?=za? zz&@8a{;fd|{{v;lvSs$}Grfn3d$|vV#|BZU$`Gs6MNybfwO_8_B3y5yW%tb@s;0U< zfy)eHwP{+_AEX(2J!B9-AkPc*+~_821Ly%@nW*Z|qiUX+=Snavb{GH*hgP-QOGf&q zQt`g3V@6Mkoc5|mzO6FWJ7~N)S0oS6v2P?8)-N<fvm$RMEP9mlTgAU#v8Uomnb4ka z)?`(z~~z(jJXOIzeC* zvu|(m)Gr#3P@UAIzAi3rJGEikD%3{~fI=I-EpLAW)ZXILV*l1<|Rc4>hFxCs#c zr7|V_AC!>2_q`Meu5D(+sW)peQH$1JBy!oe=&$1wA-YP7OIuvHAj`*NrDYDeB=t1K zZRTg6H!f?uc(!Uv)E$kB#RLmhy!s@DF4R+2P+D6qj$VjT==Mvt%4y7`;G=PV|rwCOrfvC6T^1@_^95@k@5y=_dP5!+fZ!UysEJ7r%#eF=#fc8`sO5v<-Z&8ht$ep{-hGIi&1a#^I%(Ys9M4tIIiYJS_|{8bL}SCV zSCmPp3A~s~b=TmZ_>{r-yMD)=>_xQtf%>w$u7+SREwiY<#aF86ASOH*ugzV-KM?~X ziPm`2r5ut{Xr$bc-rdb*mek(Tst~qxHE{@zJU?7^Us}xp_WwqWYG9Ak|41Ny9Uu;S zgLF93^zeFU6yvDnsmZga^D))xeZ^5+9DN)%Q19^-hxg^0H z5)nL%r2(V>qY~!7k_> zT2i*jO}~?-H~VoevLX=jh7i&Uwjfb@Nx-@p_P%7b4d>4nL+o9)(#Pqs&ewP-#>WKM3B=p$Rhoah1a>So zrT<3!7xbBl^zxttZ&G0XdQ2}*iFsa7XG{-h?C}yIQd-w){goQ_E1jVFJ|&W2B=xEB z1Ad^<8Mab=d`hhCb*xtzRTdpa?nCn7FK8uU0nYmPHQVigjdhra^hs}|8GGX^T}E;} zX`<5P-=-wk)W8MciFmY-Kgh+JhYNf6-}tkrg*0Z}iOq%jC#U{zBvbixZzbax>zT^fPqOUZIUe@bzekCyKmgOpCqt)e0S_oBKmcs6 zodqY^Ht}CuN5Q;H%wHJC0jn~hunIn~9F||+MHu3CAL!K~VF{^rzMi0cTK=_ke*PY^ zB12hx^Kx}J#XozX;TRX1>Mr;iueV2Mxdp`I(_AR<$7)$&cZlwv>PuM~SsFkv)D}k@ z&@LX)wlyM$UHg54@c`j2b7^~Cr*8^Vtk+PVY9(%>R506aUp2i;2FLC(htLcmQp|v@U(vZBeXHf z-)(ZN+B1~8T6~k5k=rGPqQ-*?&k!<5Ezf#T1CxmNF$@UlwXa7cFQXWn+%IR+&i zK7|;#9o_sE$u^8~@Z;=U4!e#_%D`sHs8twswz*KpQ#l550r@*E8J|HfJiMyKHdGWX zKE5_yUv0KQ?%ML7bw!oSjwq_feC)G@$rvEnV{)*1)|kV>^Nuk>)vp(R-H$-Tp9U+oci&w4 z`lHMO(o95a0NIWSMfQfo9ws}|U+tKxlZdl%z1t{@%68$B=HO9R^Am=;IUs?_m{^&F zgH7TWfx`wJS*1*c+m@w(ePjmwxlz`Zhf~gO-pTUxIW+F2)xpX8GG-1TM|a@?_^4K` zf3PKY4$9iTG$KNhGTBFP?Izqaco%vRBCo@r+*-Ie3zg|l&P`55%N*ru`)of{N~%X7 zw=71&SB`6tqrrEfqEbk)c-mVeKir}dda6oI#=Lsh(^~?5WODa~yJ%J2s3Y6kI%7`Q zM&dvf6^f-?jRlM%Oau2{Qpa@2Cl@5dYO3|8C}49DoKjUL%NBgSZb@#{aKhR0BZ{qM z8Ly17#5o7%^n)VZ9q%?i@m(2Q?sj`m16&v7Xxkb9g$Y{Tjai=(JhcgihjVLIG5=#_ikt|S(hv3(0 zsIL_KpkC$9|M&o}b|zaOm+`^=t72iOPB7ZsHRQH!ZmbF=v8YJDOYiYzEPp23E$Qlw zW8Fh#=b>72iJzC8DIK#p4^6Wo&IBYeT2a^ti{{-~suOX$2;*v=*NPD9>BdVOs~ZYN zW&xR=)s@m8*&%vUJqIz{MP2sC8&Qbf7gYY=vk+T0P-z7SI5DbMa4K7bFx>3x4t~#QFtp31QIz0VF}}$M>-(*P z2aRBnlUYUZ8Oz!42l0(T=dd;NSu-2^l|=D?QF^d_#B!BkD&~bD^Xi;KI77VX5jKfE zwq04WrB5dnu#E?-EM@llJTReZ{uhfw7;05Ew{KSBS@kQJ;GGpCj7+*pm+kbrf}EE! zT7wMO5mhZUPFQc#mZD1=O8w#)M>h5gw7%pkf)EXHfmjHYrPbP+~cx`x^ zc3Q&AT5J6KAIOO``#Bxw6%LY8xaD3Wsvbk&IU#zPT{6BG!tt`B=1!oNth7^yHCuJh zV>K4i`!Lzaz$RCHgG3O}!c6^mLmPH)W#1f4?kOU>nRf>Ew$Oj2sK+hqPMR}!b;TKg z)9Uqw9hh}*52nt4*s*#<4`8_{Kt2GBVWL*GyZJwFYEM1bZsRFjc3|BrbtnkaPLwJ= zvux}Nn&Za9f8o^T2kKetP7bd(Gck|(nMj-`G3_NI#;O{5Ldp!_n_?-K+deHF#@{g) z-vjd(XB2z{ig&HEfzh)sY(E?bBFyRH@b}4;I`D1B$uf-tqb`=Qg~Al^?*QUIoD<1s zf6&H+1qZ>sw^|m-k1ixINV8>%$Js}JkHx1eR zZjc|M1nPN%2O|!(-r@sEC(-367(oYYZ`u}kf(IcWXby0;dxk8gX$&0}81N4$9o zc3VhZ72az;Wc(d@_U4{g1A=JUq@wDlTbV+z8V!64+S76*jhyRiAAvm1h`M}x=zBLUE z+V$Z9MHDF$@s%fs`K1JT3g3=txWcCNs|>}oEO1NRBPDjvA0J&+<-Jf`oG{+q`2ha1 zVVN9x>AzY7UH{=kZRvRd1oCDH0VosrcgqZlD}iz)+(&_~stzDSt(n}26%jKZS+1r< z|K~&Ps6lqFDi!>j+>??geM8IqC&}d<|2+TK0s%ZQ`)`H!KMV^C>{2`BpS(BE)2B+< zq=goS9)sUkFd`u)Eh8lFukKgmO`jX3agXIkr+>Qs!kTL)M9gD6@=8Ya+6r~ymjpP(bA9l&RClMr6^st#h zALVTy6iJ;5#C?81&w+!?-P`x_R|p-7KsP|z>yG6~caMcIm*!G^d}_I$a;I&=8Q|#s zRC2H|<1wD&BdF};IPM2To6?Z|Z^F_>&kDQL@0T7-EHkyw+~3~Rb?QCtt9X6a6s2+4 zvXEtxC(##`HnzSg=7T*(injvEW^wG`v|8C>Tnr7vut0jErh+4DpR&z3R& z5CCQ*RB*%htLKNEhnaick}bN)HGb7HS3VKx8s;9d`$PvJ1xTGa!lw~VtZq5RnZV?? zX^`y}7qA|3P&0z5$@n$;X+~)~h5Y`!(11y^Pm657uZhAcXM&ag5r%FnVv4a`E|bDw ze>w8qqQ0u@uq|_(fnfXbwXy((D$Y|NxTNdLicAGLT*Raqe(8IS`6-`)jTE1wn66Li zE{@OW`Z`8i!MP^-JCW9jW-q2|XI0(XzIktc7Te~kR_om>tv+>BTV3G|I~xe>rmp~_ zE9!6XM$E{K6p~s-A(Tqptd?qPA2(?mkE~kG=Q5Z~EW*|c&&{C?LIDP?3`Rjgr@#Fg zOo!W!YPxBIeQ4n$9lurVqj-sJS1_T9Bao0$bbAOwNx6t{rXn9aQvm>7+3|M-{?eD@ zcc&RV_&&>ep+5_?dbk7gSvs)jz8-Q{+do8shF-6+zA?6FBdpnzCuorL$5B1?gj)C7 zWZ-i}@hBHz#$sZdZ=&^dIEyDxz*dpt#N*93qFWvzoNPA*CcuOOR%#Q~xGLT9%#nTY zR_ImvYU&$bdkQ6-_9aChdR|_e3tlib?Id^ACX(&R=#b#IY!(pth6^DN~ zNdirJH#X**ZOmJucZ)eZoEL$j>79;EI;64IZ0d;!vySBTHK%UqQ~eE-5cnQ@?I0IY$UZ2D2#uGOobCW;OB zfH%QhTs(zOWvi91+r*C|A5|*-Iok(f7b~@djR4YxRd+UFpNI2%yR)>b;ddCVaD?JT zsBFzj&4D~$WiW^ueO zbcfMe7{qng0h_Lc78L~Fe;lV|AK|^m`L76s_*vLLUQAupU-WS=`nel}2t#g7Etv>= zt=K>BR3L;~!+4v1QL?G|!ad0!_YAi-CmRHq>O6baD+$;Fu%WAy=NBoOzI#@iLi*i* zT)-oK)pXlKb*7xXCsQmgOM7TM*Y~s6F~>BztE<=DO)lhEdw~XreqRxsj%0!9xLp|^ z=3doEV~G75}xvqgu$@#8R~Hb%#_~nlnH$Abq$k?oHE;#o}CW ziGa&fWZB(we`co`HninT2C-P)ikUKtv!uwmBy`(Q^5YMCb7N?z2V)h^_hmu*&7)V< z)tieF;8FK~eBDB8Cx%f=d^sR@V5KAT+UCWCodXXyZ*y$yIcfbYGoYV*(0JgafNlcasclw>hPfnK|j#fFJ z?;o+Q1T>!400_%FI;&Tu@Z)+K@p*qCn8{b`4`0cxigqqG@8*Mz-JR1_8^*1@s;yp~ z+_#3>qy#LPuo|Yn5*OT6@6}qDh$zF~(x^{&?6v-#s6@6m&EX;4;v)Qyr>6ra^mj~} z+EE9dEjvI~i)ArpegoUa8XHU;DqtLEg|Mbd-43K8QE31GgD$Zl@m+0o|DzbF{C=wP z21fW7?{SNk$EwUir)E(1jjYH$0Mf~U(sBp6UkoY!}TA6t= zqrNiynzf1ToML*F^Yzg4GBx1?2+v^d!FAqg2IvFumw_d{o-;Y8lAov%WY!bsc_LD)h>;CUV% z5y#X9+KxQ#7t5cJoNe%=pC$SC4?H18Kijz0Jx<^om9oj-VWK6H0&hoVI~H^RwzE~`#gOqVs?)%gI4a7ad(Zyn!JGG%N|pcF6=#A zc4H9!NdSl_b`t79wQHdyoi}O0(DHy_4v)9P+R9>TiceTS8+~=^EJZ3l@T`~> ze8h^)m18!SZadCBi(IT$^B=>*k3k7Fj34qns4?eOJd40_-531)`y@%TvjLUOs!?Ru zNp5h=r&SzJlP3>HM5boH1~z!uU@F>jPxkhHy4|(oBnVjuZG^#6Mgz4_^a7MG4(9O> zoff(?=wn*vHc-3vMS;_uzIx=jtKB9hcUiMecSp_{j8b>$=_AMB>4#Yf6*>T8ZPY$J zcR_s05w4i|*&rRo3v$NVPRB<=3bVI+|Bt9M4TtiN+x|6nQbZ*CQjtPJ_9cl*NcNqS zec$&Pm5?>EuaR}E*@aPIl6@ItAKCZ8FqSdqzW&c~KgT^Uy)nlzGuQ9?JHO}oIj7R4 zPx@D9@E=B#;^3L(xU<>Gt-TCN)vn2p56fASC@_wL%$2U*>E zM=)SrtRxz;7Yhy1=_GTZYZ^L8P?ObjS zW_rplnmG;T^Pak(65GePQ`YMnrt^f<;^+cr(=M>d?u~J(zYj@|?GSbbr2*f)+UDPA zQ10EG+L{h#zy5hFMCWN!^n1^)>R(qCFxj`+N5LOqvu_SN39ixBtez%BNWoMFe+(P4 zO3hWW%*#O?>Ut6O&wLD}k*x&N?YPARP0vBixkDTK8r`vD@x`)pTOBhLCu)+GIS#|f znn3J33OxK%>IucyYIGl+HmqvXt_O0CD1#=@&;XI#YgiJvcCtSW{HNQz)SHghuu;SJ z(7Z^gx|!y>v*)OwF$1YV+1oGo!z;g4PuJE;UFs)jdQOBuW%^3sCFKE2x@O*_TBukF zrQ#@&Ivf5UtgGwNNps#YHZ2x=(&b(NMszH>H61z`y?p|DD{g#__GN{NmSQCe^ucKDKEE60Y>I0Hvk19HP|?D zV#o5V9RltKR37kyb<)5WBf{!IYY6}T_qV;$8`3Y_G8f&s3|_sB$4LOevET0xtpmb; z-Dv5aTpM4SQ*bkQ%F?L`03TY=k4^7DV5+Y@?S6%dho@H5M}YsO2FNGB%=!ZSXo9pD znyc9-u6wb&C~xftl}p~VgJ1mN|ITrU4Tn+G-JvE^=*ap4n5u6o7#qVVxbp!Bz{+(GtjcOdU9b4cmvfyual;}oZ|qU0(k(0V zo8LZL*R7So0B%f$&{e>E2ssNe{i&q*@-|44y$ZReUj(fX?QQq2tXXzd%Xoyo{+h)o z`}x1=@4$9gn84GbITnD`cC~4}o)QQ(QMhu8tc!F(x>T*x4o!`hWej&kHy^$wm*fU$ z0yKEyNn89=th8)_ialgas9d%5mA~=|OTT2zsOE<3hRVKL@ z7_aY}egOMDtvulib}})!OUF-f`}fluVoFNgDys}pLC+xbivIEs^f?^5nM&R97^aKU zi@(?!h8_yvxT~>O8?Kd<_Xymcc$A;5_V`xEzY90=&gPe7dsdv9q<5B?TmOUsf zCl$0g0xt^95}&>-qyS#Ba5esZ3IG^N(}lPs%ZH^W!K*J)vZ%zm7sf4q*yeq@ba$-i z*2WF9_IoV~Rpnj^0`eLWO0dSGo~t_bI~2eXSw-ED8IwYX9PM{+|6&IZfx;W@Rm}RHS}i@~oHrk1tSGj1yvPAIf9d zGghHJ6k}6Cu75-R@vK}Cb@%Mxy`4$fhVj?;eA6nY^u++ws>N^3gGD=R0t)nExl6S& z@H`w#6>I1%`?s<>f>VGug9r-X&DXhi{;x{fLjLAVor->k>ygX7?{l~XbHem*H;B!I zeRRf*7T<&7YPoMq5w8lFshXRejU1hBpq|Z_3SRzSdIL=B|6~PoP*z|V+HAP}NLuxg z0?C3TITZ5lw}gbGth6l1$aEvUB;6r##YJ}ui{2NPmHr?Y{GQ$68dOMZ^ERN;|3VY( zz~^l;2b8yFo}Yd$;ATvo6)-t;AM(9iXXK+Z@ZIy`-^09GjtP^%@q?HeOLvMdB|?L5 zBtN;s+g<`HxAIM!e|l@?xKdTGr2Km|Hl|i^`u!nJa_M)9h@mGM&E@`+M^XMPYKhZa zK#%?X=_bUGgpT&(HHA+(01ZdNx+eE?1WI<|)9)j9P*@ViAPLFSD#*&-9S%P#gKVDTvduOsDzA*d5*-S||0|LK(7Gn2ofVvE05#(Ou4ISJN zGDp)VQ!=fhym^l!!2IgT%FQ6`;D*#ev~iLjTOqQvtmPEVL)w=Le(;uiHCr$X?Md=g zcEi-#9SU*Z(ctFM)<569(N-!5&D^OfYw{~h~Fj7mA6gn30vshYd;Oj zIWh{(Mx4egFHG$WZwZnjOMpw9mie>C=QI zarF#j&Q_DO#d0!`P|If^YxI-dC3BYDyk$qdi}@a6to>$UE?LKb>(grT=w+tae2#o> z4DnL|#DW}nN$r>!3>b=LnmUWv<>vJMYJST+TECgOKIKheJIfjt;NH>G&T{9wpMOiG z6!4~7-(%Fi1(-yTdp)&!%p@gY^8Vov+dCQ$zkRDFiU4zd@;&XCzUD%7q3-e0R~@-{ zSZLD!u9bGwIMbMH#ZxX+1m2JOrn1AZigJ1{RDk5uk<=X6@|k~ zgc8?rB^=$oFGkd4P8wcVG*N$QBi_++T^~!<^o&Q&9djO+XCdQozy9Q+yEiwq506f_ z$C7o=gW8vJHscFI&QH_R4w8K_%?Y07DLvY&A=OjKgM&QwnTGk*}4J?{Qk8 z`@Di899)%vhU)(3ZVYJNU3YGs%tj^2{hp$2tBa*{?xF*hiUz3MuswDR)e z;gq)YKmY__p?aUXxbvdq{9RbLIdrSa(deXOj1^rdF{MEx{Ka+4-Os93Cge(I-K{U% zeArX$cBRx7-&V~^U&dcuL0!X1G1LGu;Q zY}Qt6rD%cN5Yv^CL^8-x<;5CbYjK9)eB|h(*YMN#neE_V9PZ|c?Z}N%k6@ngsdd8)6Iw1VY3t--Sx=+j@EfE?khPC1pt+s{7hm#K_Qg%A? zT9a+Ev+tdpr%5yA=Z5bcv717wid~oi6*X{=-bqAA-~L5Y-PG8hNhm@F5<`DJbSx6b z+tLeh@GdJ0|BHP-qZ|Ii&iRZ3AW$X1P8`4A1%QUe+rYxZZ0F09K{Uev-BTf80b&^Z ze2gd2NNc+z?81Lq+eQ(*svrB$`DFqf!9CAiAasnKOSmA_+0XgR>le`~94;5TS32=g zQ52B-tAXo<5q{3~?vYP)-qwZq3s*=`iMpzg2;Xn>`6Dld;PsXiF&Tgq2}Kql$Linx z4ECT&BpUcqJs0A)bvF2>Z+r5w5nSVP2`^hu>9B#IPx+nLF zn|d$hJVovq*7u*kmXS9_`Y^BwWCIEw5`!On^pb83KQaOI3q@{JaH}jYS$-i~kfn5x zJw>b*)gf-xXDD7PxH@N2XApwUvE1#RQlQS6-QOp{^C9|$H-{LH7<80gYvVgkC}iI4 zF*sMq+6Ic5?A09N-Q6xER(%cbDV4?y9rH-%YQNX#XED#S9u(RVKs=Do^s%7MfC<}@ z2kT0H8}PBfmp(l9y2J!EmcpDaqR09)+j(|TIi7ZHSKk!Ad?#`U%w%1U0=Z{y)@cr z$|7ZLMK`34?1SK(WX745Y>RK-3)!TJk0zgJO2s8hs16Yim#6H9TA3yYITPO_rz>!^ zdB?{DTP%{xJ#3rkH@EXTE^~{4y^U%9r_A$f9hYSwKsX-70pu>9&716Rq35q zW43k4;ytF$mBp*3Av-=9V+n5vmG}oiF^)<1W=&Q{8VPZfUhvkdg&dq1HQ!K)we8XUYqS9OKzY$)3#|i%lt(iQU{_1$6q{VoP5r8xt@XR?B^l`- z6Qr0P3FeE60^8ya4D!Dhq=8rp88-3?)Hr>nRCk5|Mu2kc1Sz=2*MO$!KTy9DPbL7AY&Vbi)ySwoV8mAU zNt@1!al7xQ_^U2M35nJo%S2Asq6WlY0%{9kWxrZ^hcn7X%w)o z{oqSz*;OOe{UBmO)P3}~Hrknz#d$Q62KMvRDB8$Wf^)>Gd$|zd>JJUepLT3j>z|^> zoEV&6+r8&_<(q+_f0jeXUlzKBaW&j~qWwPUp>Zr`uVfB&4a?dS>9o5u)0=msWZ+M_ z<4NkvKDzzRFuE0!GR{k~qfE%g}$*AVo;QzG}yIOYyr$?5q! z@I+A7vF!3k#%j17E9J=1&7ion-BR!PRgWs#HKc9x8w2Ynr?_~;w{?^20I%rc+i+yy zO~9qL4N;vdTIWDAV(;Eu)6)XO6m4>&1~e*yyFV7gC#p+VTvIIQG-OJ*4){G*NNbOvPzd=g4 z2rH8|wY87%KXDkMdQ@XE=lmj-sf&7U5` zv;>kzFc_x&;3 z$cGf@P~>GdFV9!R_aiw>=__}zDwWc|3X}>0GDi#c>aI7*1wOjFK*arEg28BbRlrF^ z(=47(`ePco{!0IG@Ku+rpwth3;cL)za4xbemU;sR7VcwbkJ z(MBwlD@AXPHLCGdV$E={QvOTY^Z+10g*q&|^{9!FT+;ZWOOPQ+)GtuEoqkJ=U+`s7 z&ub_U3K=fugX9GW3Ib79txvTJ%x(q1Pr%2vni|bZA)u(&{0PUJDSg=Z!F{#_KJm@; z3^I$zdb~(wm0PUIY|(H@Qrc}cz+hm;MIRV6VHo#m9ss0#U4;RL8iUZUkYRw^`X||K zmHUuOVEHFIT5S3L!^&Wc9OCD@aqy$pNf2GCjDUIk?0Hq^GE6%7L%Yj4NnT1Wxxovz zb#0^V`1<=*`hVwc#fu0_l-|tD>^N*Wk(5s*Con82ufz+3yMfKOp%Tw5p#e3;77M4l z>e9GWcXibpOGnzs&;F!-C|MqMXe$FsFu=wPA(fYO!$B8=oX0?Ki~pv^ZY86C{Bm4| zk$zs`wC0XuQ}L0-_>IR+;fkXUOHfBBn;{HPxG4nihF=MgQz7GJS_dHJCFBO>L9id( zGX^STHEfsWQ)FEwvD-7HY=SZ2B7;H&I<f!x&GdfA_PLC}fhrHbHA8eGDd2u&X;@_ zN*1&`L7=v%oJ6m5V^7zcFWQYCR=urc`A{b11#A`}yW@z%4Zk={CuQpzn!#JZ`sfVP z(_;o?RMMtmYA_FlR6yW3UTB$!q~mC+qWfQcK_&VB^aTs>t3CH5U!F&jWJr2W63>mv zypWQUCFznR2Et?FNfc7b3M8(iv=4BL<({fBy?*}YQ8dwfxF-K`%o!!Hm(w^Ctmm}+ zs77zeH2g)19;|DLdN-tQuW#uUV5+IQ8`1CGxnzfu`$Y@v!JYem`t5yDs#CfD048Sw z3sX1Brlk1u?()cF=h1l+4>6nmPvd3dOK_6`;imAq?}+41F7>3}Lk%<6^_Y9?nN>Ru z<|K_3kR)aUtwqk-O;b`a@<<}TiTGVYal;}F%4t~YG_N)AFU1FYXEm+;X@mR6(AU%Y zciLOceU5%lr=+2p0(0MJ2^J`ewM*Hzc;EJz9-7H&78?1m^ap*9`o9Z_p09hP1XY1(H$~#S_kyO=?!rrK!_wgP#Vu>Y%qV4+ZUtLvx_?xHS-= zPyHY9)7+-|pQNqGKw{L&A}k(EFfr&;oymXz93Q=gk)ErR3 zOS3!RQ0=~58|W*03Mz!ZqOA(Y=1h=+N5x^=SH0_sX=cQ7FfGQGW(UNqNH!_k``e+h zU4uNz)jxLb{R7Pw{h9q-{C67swdSpnSfv&N4gS3!WbOatI)g9C)xzNAs$}3va8mh6 zrQ|>RSVe=A6#T14viiEOLwm+u6VHEE+NX5^hp#cLyXfFn4te;NJ$vIfV-*T)CPd|K zKezlaGeKGfPN5=H9DGqTN|)7nnK3BuZJs+=?Z{Z(G|O9OW+gc|yYnJ8jdA_p1Iw|5r0tpDJH*sxbdc@ z<8smCH$$SnzkAb(p(%cYffmwOZ74xW2^lo*9(xw!-P7YKU6mImu-JYZrr6g0)p?g3 z?4xf@XWkBYNo>rTjc`oCw2BF^u$^qqPz1w{wIf^sMOmA0X3zl76ZlTc;cYR3b3Yqo z=zqBJ>1Aj3s$}$=;%fd!9nz_}o1Z0ZRK_1uD!!71u`rV5|}Fam{ZU_s20lqzrfrOqugahU+PTz_R)BydxuETbtu z1Lz2u?s0dx&b!HwcJ)Dfir{VnuegA-3j_IyA~lC0S>XpD+Zx~%;AO7lY`&NiT9bo7 zu6}(9`sH_b9(Ij4bv$C0=t#%s=gn)-CzbfP_HRHO*WVE~N(#XoAhBII= z&F}7lSL|&Ek_zwBbNuIIJl7OC`gPMgN|m3@8xDCwAFPMZmQdr;eIJX0GMT<2ZaOat z$30_n43CJQ%e8|909?b6E;S$ZO(et^4lAc;5cz{UeH6! z-lpBz-buszb15{}29nL}lU750+@v{FGvQUhDMM343T(1VXZzL|oOpw^!&gg{=2>0& z;Ubs)qu8F}m)X5SKXrs1mGQ+@lso67=|kg{JhFhq#D2>&5fxqCi=rmN+RxxBc}vs0 z`$r@BXF3Bny;!U!3W^)9n0hr&yNmeUOeR`1}^wE=`HT5CEFSSyc8xH4G5U@1ndfF zRreLx2QR<#FK{dj>NW>3v|fXOU!gZ;tC=2(Du1@bopdRH^w>`1CFF6* zYB4lQk@HGtNK&d*E(zyz!I8(2(8}S@Mp;CzpZ7!Z(}Y!Kp10ohP+jpB3^1bVH_$Ks zEwuzhCt{@|pC8_;Khv!e^ls>VkeB^4z$+-^xkTt!8q@8A3$j1dJ~~vn0mR5WL1hi) z``p=Eing)h_^v2X>E<9AnmFHoauPRv+@#FHKLq+-rf95esEYX5;%U-4Zy^OF z#u8n5NXLQnYiEJ%JzM@(p}j%R8wgGvgrYFq&*{QgV!Z!TrZ+q#cj}q4m{fHJzb#(X zkTyqV3%A#IWYk1el{n2xM{9}`Wb}02IW`R{5PA`|xes^Lv6h2Sgsu7RC4@eY)5G}1 zwbbm0Aa{J^C?ap%d|)Z~A}koY^&Gp_`z$2Da!gPMdhQ>%xh<;rz4((rp3_-R$feqS zj@w*@J35UwV})erx2DEqT1?W5@b)Q4)Wn?77v^x4fcC9wqTRWqZo<92+L zKPVw`&$VveSY3Tml9es8cczrlKPc+uzum*uH)(5e9}=@?E1L+JP@>GC0c+hTRSM8@ zspOoOfk};?VRX~Y#w0zHw^_W%eFCjyzCw1MC5T_*ADH zLiPCi()?CkpPi3y|DWSASlLfPqT^yt)!pB-*SN!y|Y0Z zk%{EPx(Kiv3**yehXt{0WAn{D1Uw?8xQbm?(Rv66l@&nzT zF0yChwuv%Y2a?2WI@{K7`Vm-TmWF*DvseRYoc5t|)2p!?+>yRT4;UMx7JB7TRrdBsax))XJFy?)kQb^QvN5@R%FJ_5x#O!qQR6QP*M`x2CX0hQi^2q}6~dga|1 zbE~lyskvxC$MBN0KSI$rO~kkDP1W%pV{y69kQ<55j2p0YmG!|q8FEaK$v8^$lL=c= z2yn6%v;vG{p2*Ux{3HjqHW5zI3ht5=-KH=<;+?mteGxOqcn=Q~@iHrMSc zf|>J)SB`SvcdKj7YkU-rW5q1xfLfFl)8qvc*tLkwwhej#qxAs;RO^6>3b`b30he22 zc;&{vLc|U(&E&jY_BEhxsFR+w<}p-)w25Dy6b@Hd117QK;hu`(04yE$ka0&LRQ|G> zn%W1l&KV=!jIBuFI)ZDABiOQ&3ooe`UR1OMR`>1DE2qL&Qa+%hB6y$F0WaN8;yE=a zfZ@5Oj_2Uo+giB(v>t3Sqj(8WFg?8w8NLcAC;-NW!Ey=AjrjY;R`LVV?)bP4fkjY# zuP<%(nS&(+L3ALUpcgNZ65u6O^yh?M_RH0~Trp)ZcJ(6}(K5sE`rV@C6DBjuoQ1_j zSzAJxM_Ss=K?OStBqJsP|YM6K~6QB&$3;?bHE=4zFB&rd|S8L1C?Cgu4Aa4WKpL&*^ zBeX&sgHsXQm-@Mn+E61z3-`cNl7(A;=1{Ucg?@x*vhJ-9~+AhS-s-d zrO<8^O$f<=h(bTYr#y5ORKP+f_9H8>L-{9DT2l}^3w!LlU2PCDNB)y4--a*2QeF-7 zD0OTNxDA?I7=gUL>D&~_%ISfc3D|ss*`dQy)!~zN{tB#oXC!N=tjt2s+kC8jijqbg{y|?Xb;`dir$t&^1V!!c zbUiPqXl%s~5tIpD{^K19n|Y*}sj#KM6Ji;oMbkXV%Miqg-B$SuS%CPkuLDnE8sz_9 zg+TwG3SkMV5LgZbJ+F{}knkLHYH-O4h%SY zTXj^6$oBN6ZOjk)*J)%gIvGq?K9Rtc2Rv-M*HT%1R(d+%nGj57&7sY{cb&Q;dwNEU zkLhXQ>={YDQtvr(X14q?RfiTNw_K1(&AjEZpn0bvB$(Z$aawya*ei4jT%f9{fdyr3 z0|twx1ZIwdEVB?TmCF;iKT2~HOeDJwwX2J-e>WpVG@d0T-=(-8SWBC|&Fzcmk_m0G zF;{j|m{}-qJ_>18QYz$p+4-ZL(VA=lLYd!5ZcWFlQV4;OF~TTvV_WOR7>(lgJD6Ro zT|L@-q1VELhZmd$9e?e72E>!(KlWsX`*9@-?AYJ>Kymdd9}p5KcW@5>=GJl}_A}m* z{}wLI=cf2w@g6$qaKf~ezUA5VVfGSV8Eo3g>+9sie}_97AZ3gY^lqsqcpy9Y2wkW= z-mf`VL^HW!BLZ0%(^S8_ZF|oOViqnw`QDD)g;7}W`d5|@XkG}#rFCE(b_GPpvTA0Y zX*y6@b3MuFmrVze`E{+VBD|0pcr z-f9uDsKdL@x1Va+l#mC=rh8p~1E!QuR=72NVZF^VX_M!1|Bvsk)`9Y7%L%)R;?W z2=st(4)yeLtPl^XmCl+^?^P=aD{uu$>HPSvVVD9p+xH2QiXr+0DJb@V@(U>p=d<6n z_=@!C^CanXQAXW&>58XMnwSx4X;8MjyqsGHIp(>Hz_fz%A83O5urTA z%Xtm4_duQP-b?(q<5Az28VwI%ZsHKI?fuPHDayB7?}g#cKSreyLRf&m)r~(t)l~3T z-hkJ*@#9SIiix`^IA?GDQ8x2dk7m*ira90@F)g4RyGjfs`*0`cdSpKJAFSvD_&kI^ zyT5&aQYFsgogFW2&o?}m?<+z~lKA2`OFut!KQZ`8wP@e#*L~aab0v#I2`h?c@KHOm zAD0)X{jQ|z05HV^iZutXJT)iUaIL^5Dg?hD`rWr^4{gRt#_b8ue0Mhh)*w3EpA}O) z;lwIEH^TSFAtHT<&5m?-u|wQu2bVw5svKfZDI?oMG!E3;oc9Hl&RRtOnRWTkOEkDU zB4zz{ZO747Erkhdgo>T#M|UC(0O0Z|hxd4>hE}00ua-=(!+Bx#jou7rlTM*gDMcqI zC#Cbiusb2w`>N{{F8s&42)fx~ea_s{zt9_HP4j^*x$U)UXiSOj!P8*e=EPXQm%(#| z9`W2z(q>0qRRX(wbv*Z4aEoWtn7Tp8m%QuJ$Uh$a-yy{L$sCRCeZL06S>14;XuRax zq&ZA6rGJA{!}+>3%1Qw*XB#iMv@EIJTfP0Wq6OipU|~Nv8YMEpsdtZm(z3sJ_bqqNPV!c*$QYQcOmSd@FjSeAx!VAERKqSSC#_49q^m+RTtu zG>BT?3-lPuzi#YUWpHw*kl6EL3L9#}2lQPq-14%l z=$|&DpS3+BnuC6A@HDMCUc3#t2)1eqhM#+pTbCT1zNM>KEb^^sZ6WS!xW`w+vyS)o zCHyXkp_uvS=yd_h|Uh_#zN6%77Wcwb3O<12jZB(fTdqQ2#hH}=Rzbq>Nuf1rcsd_zNn zUx%?a+F6p!ii-mZnocfG38%fi;7zSu=cy6hSvL2fgzvd^vcLdp-rJEUN95$o;&AvlavUyAo$Hn%_n3XEX}l zrsdC{OK1oyBSNX-KD3H_gUuF2`Pz09$`JY%p*KG$2T?4NZfVIV2WNUao(CnH$DCyz zdS}+Jx)aWRH=AT?SRJn%%TohgM@Xk#d(0q`BtK@0&7D=Qez=|`)fM{B3%A-FC;`px z=eFgzzz=#~60-q&6JlTBD45NWSuF-YA$xzJcSe5edoq{gi!nPLJePl~+l)bYt zbB}IE!Y)s`pONLS9nf(|&LlvkGMdo`vk-;_Ax-x!`Y7yZoV4wYMm$dgYGAbNFfw+4 zt_DWQ3?AiQaFECIlO*VND)rAtt8?BT8%E>{wm!O-*xxf}-P8ZvtMO2#dz_s0&mb?3 zEdTAN|Fp?#K;CMMXv}$8FvO>tQ^d-S7EkSzp2^*%z(? zDh@z8pyU!%PcCnH@A)|5jvzV4Kbo)G&SFdbnpD4|yC3jou?kSUtRNv5bqQF_xUt$W zk*S5GX~(0hp>~|QO2I>f_VJ-Hs!axg?fokm&z|yW*JiKQlUwx*<_URUe)Me+CsU&J zv9%_>>ed^R!apq+n3j^UgFq`TMD8YB^bWTX;alC5M_imOqV+To=NwJ^J}ftP?~Xj;DvYCBhfx6$)bkwn8>ZwUs ziFFLjAidZ|p=5_v>iF*UO&tB`Gi_nB_j+jYig_qP!eXL^TSJruyE8}xQ&jQ#(+z#H z?mFxPJBYQTw*5dw0`ADZuc?Fx=Mbx=Q{W;&&qJm)1G_f%CA(It-3*{*cd7H;uW9q7 zar$H2EOwTN9e;!VLb;9VJWSh;h=|OlB>V0^T#~Dpcx{U5=k5Im-ho%_>HGw z|1zLRSN1Ea{Bq-+a9)KqCf%f*)anX49i;E6xVhr@a&zrPPo;!HY+k$Jk5++$vkU!x zcnk74W~?UTll*2`E?(wLy(Q{mT|Lm;RXMsmQ^PQeD=a?nZ?SMZxyqnCT-&M#Z*V#m z<0;Uw`X`Uw{U$2=QxPCXiEaECZqzU4pS)ha^wD=cN0@J{~k>M zCyhO{*3fJ!>&Ncz)tMPsfuL{VOdd!wfP;)fo!?#p?X*5QwDM=5%g|&Oh1Wue*^r(+ zabsU3{#m_}`q}%C>2=B=(TK@ZLNn`FGL>zq#p0FXt({w^ZPE5d&k`<_^ej<^N2Rq2 zl}Bs>;pw5+8d_C<$Ycw0_9DD~ zYr+I`Q)O#lh93IuwFJ%V`8540E5%u()&QtrwJ&qdaWxN|a8`~LlU=oBDcPD)=IrhANJmCk$@0=?sOYU^u%52sg%n7uXwlw|qaIUbshtpzlptOF3HL{NEi2=m2i5 zX=0?U`0dC)q)=<{7OuP0qO$a^a*V~X3GkU!c_+D~k`OX9V!QDDXD%SAD8YXXaPa5x zzsgGO+Vk)-3AgLJ>%5T8p8DA#wkmAtMab=|>AhwRb{{Y&!hO^IYlDM**vvgm$aunq z{i^P4W-F$dz@bYtvmNR|xNC%kUgx%_+AiEKDbkqFQ&V0o4LpBE9LQYx_vHLl=5-?; zD4~tHIPNoU`Jgs)O3G2YCd(0@1ylXNwEX>6c9nMW``=`Pf!a*z9`={zU^9;(9sSeS zJjp#n-<@lGEr0|pwkO%eE?lmk3mP_8tWl}5G9JzWY^ph2-k^QBtUGYofVCy83X*2? zmboV%3Jm1aR zJL9w1`eHy0o=z%4t`CyYv(BQPk!Ib{T~^Tx5t;b3mEbo<`==bZDX70_V&vxLN+n0_ z&=pRw?r&?uQ~0xy^k4UOMV!!D{1$-)A(fZIZPdgMm&rZ2ld!LR2N+H>V%hpAewUZU zQhkM%&YxPW{`8mT`yo*>;83jX1u0Jl`0xB8nmwiR`8PqdwcvXI?nK#2D#FpI3eV4sQPzh16cy9AkY%qi+>$q^^f=oTm*Zv{3~ zRDJ9)0Wm8zHD`0bZ3UNBkH&BSoc%}|Gziz(eag}-J)O~8U&~ol`WwkzXv{XR$&nqq zp=(Kv&Z9(O%soQudghMFJ9F`QN-_Y$|6(S&K;AX@Y6=1XD{yA`X-8KgT#&vZA|@^& zuR?m+_N}xMB_bs$DJ3o^NurA{6TK%cE+r`?FQ-7d1{VRH&;)lrCWn#fr%AS!KX{O) zm4iDGTMT1>^u6}`3(i*nwRlS2t@bD-S+)$|$$$6g{35YGOjNFERrcGKl@s=rX-+@v zEy?^`Q~svG8>80-=~@phjQJPOyrjK(2V)TMfUCr5KfU#&MFD^(X?t?sZv ztI@J*K5U&Q`8dvk1%$f``KrSnF8F&Vs_ zspg~asZ;1CrrQ09q9OLk;<&M8TNxem7`kLAi>$LEVFz1~LsmN2fG{_>Qb8_NR{jg({mG4w@#Y!90~>-U^47-iHAAt)Rirr!yb4a#f{fa5%9Kk6cCd zw6#QD0sg7Ko4b2Dk^J;vWx4tLUF@6MpZ>|CVdwpYM`e#p$oG5G#Ht-)#grAuQRL&| zL+(bv2>%bUnb{+)iqq+(SR32pZ9lCO@L&qKF|wG#>Z zD=XDVE~kl;CTBaXfnHp)q78ip;>+6kUM=Cr(C?M_(oQU+iVGh00#7m?R`` zWl0hGX#ZVk#BOJGt2Tq(vw33+fPcIosAadNiSa=SzNTU4uZqz~RLAaq-o7m`z-8Yw zpD(|3tEN@};^ITq>@fr(pJwM^@INzRetHxe#R#l>L0vIb;!;SC{_|tY9>lmpl~$!E z7<$!9>k6iM7BPk#b^qM~@1P;w_&nZrQy7~8cNM|by`Vds3p{RCd=RCiZ&1DC11B{Ctj$LY6XjFvSWW1Xi8?Sp+bEhLMQhW zpny@##|FiT@m+FwoGE0lKc5ktzK~h#g5{2_bw1jmV8eUVUZ$ysbeyiUsicEXFS>8@ zB4gv#gx2($CG#d0)r{;FnW3Xb?jz7-Df2N~Dg*3a=+odF_ovLl=F>l7Ao}^kLrD4=6V-VOVgc%^a2~0dc%k@vZPKoI9xW7sP ztpA>cH2evOp|89HVar&EXuQh@Bj04g^ZqK-CJ*ih-cUVPSNm|*5#to)N)Wc;3B{CXcyXzNLuc;G0 zeaDl2Ge&E?jF+Kq`t`6a{ljM04kayTxk>$fpyGt~%as?b!wNO)z@-I@8sooP*>e1|RASm8Yd0yk}hzhUk|#(#KmeB#w+kMXV1{ zbmHjm$Mh!UrhhZwE{K75q-_3ZSgcoYeRkwyy7IZ<`NqKtA0R0pa56&e;WbrTYy6So zR}Ms@S*sn7Uy#mHx;XJ^`r7)`5|teyBa9+Ao@ZAs&d1Uo!O8SXwCf)o0u@i_c@-`V ze{GoOIFe9))&9F@wU4>s%%sQB$Ou<>aF7&5NdBP7TGR_D@L<+gs^%Gj@~^b2a}aVL zQy{=Xo{QOm&mZo(95JhehtbmU%rCQVbW2ll7)x6#>an+z!Oqxu``i-p=*`9 zEP@6`--!c|SWIlr{}6T7VNrHrw||C4Nu^T-0hN&MGC(C&LK>vII|dX5r9_lwKmiBo z?vh4YVrWoe=m7>8m|-(RJi`;wP_gd?>Muq|xk7V6(-!rfV_&SB-ja_b6 z!e*ta6+a36e9315d3SHz`4-oYB6$c9a-0=BMxv!e%b}Jd71!q&8HMgtQLT`)l+v)hB<)@JIuUnaqSfP$ZX7!&1mjj43u5`U&Rvo-cP=s z*?j&7(k>=2n8>oP%4>@TF}G!cXR&^+=^|K*zWC<1y6=kOL~x?qU+JzF*UsVeJ!oazbfW1e1#YCZ*&=R8Yg+Y-Y+(W!zZG8 z+4|@Lz`Q?@08R;urakoMvlA=X{_OGalFGNMccrtmou*C%pXB7dq`My(BZeefon>fd zt%X?WG6KWkjtjg-4XX*l!*Qgm%>{n&_dDnluhkCfF-p3TJa{7n>RnQKzRO+uWiySR*1e5$Vio>F_ObC6d`0LgV=W?_Dk za<=N%)6)49(C4<-z{eu#TL~L&xn^Az%3^Oq>Q4&f^~Qn^`C(iHciHFq&cU1g>+d#i zKBvfbeI)|8RRjipPGLn!?T|!bL|6YI*emwC9Xag{+H0+GNkh*)ks}jU2z1Ljw)wnD zF+%CSSLJrFb(+F=(zIlZA&^e*OUK)yFXm319d=l#r(CA8HjMEd4H6!5nrKWZv5H^bfL&b*Frp^s^!3kmpaGb!vkewL0JtZRg@m`$b`=Aj;~rdrCb#n7C-e` z1n86$xsjVZ=RP{oXRn6X{g!NyG0$i|wHQ{IfRFjw!ROOzbwCpacgQ{jS)KE>$@0ygS+;|xTPuN=PdBbp!i?zZ&I)YJHi+GCfv~bRKzf=~ zyQ~+rV+xuZ)bBtW*{uX5}{hJ17IU2oBvJ=upGZ9OtuCbOb&+JE+67=x(X zNt}aphM9BR8XRxu$M>jPvM;%f%EEjqm zPR$tTUcFb9sJwN7$?Xedt|)=3=6HIVmG_{7trTV3Q8?t_B1v}C$C+Hz@4y^`G}DPk ziYS7Qp>KFkw&W%rAMMU=9pT(jMuKjg*-uggQRaVgCgei}%@VReX(V2_f-eU?>|+Bn~W zQZbJS=~+f6T#6r<>Z+v!0^*v_6l+^2WOG%+u5WUKC{2R>t-1Z%ffkYXCO~BzcqBUu zu|Plu=omlhN0azUzwojC+)IUD>~D(T$<*5RF+yFgHDZ86;cM33`c}uI%S>vYFAfHj zr!8;%j$5FNm*5Rqa$*rZ5iwIyk-*t6aq9{8M1R}iY!AY*XD3`&2R?t47D3j$iAFgX zziXy_e!aixar7f*ZLdO78OWz6Ks3cCKnSn`Ee>{t$fEBr@){I|D-3!akzpMWry!>=>%#Ou4S&;r7!rx> zcYd9B3mB^+O&2&H8ax@q8*4lm#i5TTE?uRUvFnKbS)tTW6CRZ@^b38!94-hjZ$Zrc z+MMfHbX@Jne1(D4(mIB;3O%2A!z0;lBaz*|#VX8S6*Yd%;c7mv6%aBSlyXunr|~Ry zg)~XEgx&J|skuG2CNV4%M9tM?~`uElb1GSo+d86F7ICVeCqfGUR$xxrrA~w;-KT8XJSlGP;^EB%8a$U#}+qGWc68}_@RK8UY08$Jfpg@%6ax4Z&J zoVKy?V$e?0l4)j1D@qAkVM`_}voAd*J@>Y!l?q+CokJbIPEXb(aMeL8jmo5{ELN1*|y@vEQV@=f$wsKq4+6UDy*Dd4(~R?{FC)N32Z?id~*D^Ou$S9cC_Jl3icd9k*?D;;`qz!b@S+BS%-uqF!c);kUc7zdoV*@l5%JHC%m&CB#WfthV*XgXR4nBRam%KCfqjxng^VQUT1_^yh0qj` zJ9e-vQz~z%L!vpzwvst#OkKslU&j!hz3>^%>OKjQc^Uj{)2y!s(NTv>UUna2=$19! z?>%tH)-jdl7g|q6dDJM+k}hS z5GveS0lU0jd0bkDNdcuTKlVuL>z;+x7@X^a3^IE{))UTtA4ND;0)K8K2;P>9s)ehS zD*{*5cC`I90SI^AHqdTvy4&!H0?0hJ7bZFMNY(-|EaN%g9S>4SYg4tF z1C4{dj5v4E?Y_&pw|-4_)4J=?d)++YO0b~!o$i-Is*#R=aAU4-zK2_Zng~Qj#_C5> zk*CWWBXlILY|4*$rOx|XF5~9a+H?m?vVG2aHS@YW-Y%MlnW%gy*3Xhr0o*kOsGZrK z@;~53AH*w{iM8DAyFUID3`gXUE8K{r14smdBEiszN?3Cl?*m0h))|M(?uj$vLw`$k zj?S~75*a||=+%=DJ5p@`yr0Lfyx*<}cOzkd@PRt9dWL=Srym-2hU=1Nd-zN!{9#(G z;5DRRI4TwTtwfXcZuu~1jS?hPs~xG}ZI>j8bpP@qV#R{Rg`Cu-;{cy{Lxo@~jcB?~ zb6007OS1S1!`5O_=X3IZkQ?K95P?-l`XNBg4;<11Gp6$xR+=B|7JuGZ?v~T^gAb*# zB+0Xgkw=q=H}G9Dyc~vCk1{d`_^?w9<&@hxe+XYf3UwDo6}Yt& zCsW-#=FxK%(}qOqeM(_1I~Tjnuc2Xn%C^UIHV|>Cl)$y|^wzq|Qh%#RXY`v~fGh~0 z2G3{(GXBVIbx0(IHk>bGVF&VHA5Co!v^zw?oIlo{H4tQzeSSRVi4eP6;nGnKh3P0h z(|8rKx@JUTP>4XJDc-0ljCRK9W8HL3=b1%Q4w34mr0E67k zFESyK38O;#iqr_;YyX|UTXhGDU%czOGAB_&?3N$qqmG;|Pak~sH8@rJYDCnmKV#!I z|LI-#mB)I0l2be zbt?%6Lv!Ff9sbpTY_+Va6nX$<+fAmZPu5d4WtP=TsK8Ag^BogA!W0sAkMch&Xy-D; z+@_i)RZ26Su%hOu)IpR7ve-CIi~*7Vs-n(w8fqW4q1Ne%S{S_h`$sQo{CWz{8h#gj z-TO#OWDF~*o6ZZ#`0ZnqwL9hcSlv*W@eOt;lUPeNyQd5?e}`*YrNtlAUdf=Ip2zRwrDtzRU4bN5KGDeUwN+Ad66PLZLiBOS z$P1#%C|{;@ENKLelpl>P%X_5_otm?{WLx6<_A-%efkD{}!?U~{i?j?$L=2cL9qiXQ zTk}{X`bhM}C?I5A&w5T{GP8II4oeguhG#IjNl@N zx?d18hHTT_yc=gh;NJg`gBOZ#Q$*s?^3ZJezio-xk^z<<)4k2B9rTtxk@k*BC*L&I zY2=QT=ek%o;>_gO2rnGt*+Y+Co10t;(x39?-txDRuH8ai9UxgAp5LH1m{GeNGoa!g zQ&g88q0gBM;Joz0Q*%;FCPcEV{`LJxRtjvrck7kQ-oMyo1^NGsR{$WpW-pKqpRF7{ zE&PDffwQ7**q18ku$NxC1nr1_aZANhmOk~>pDen^PS4XA|Ku5SOM_ zT9DR%^)pF%odyq&1F4?yI{{2LAl$&&P;;hW<+hh!suX&Zh;;2`O*#$KzKiHXy20Pu zdMY8@yiW2U*`z$-s@Z^?E}-y@*6;FrB4jug9RI!MGiEVj+cb5qux}6KJoT>Iv)>)S zhX0G%jYncmepK94R~k9X@3Gie*wqJ$#rMcp6MiVVx=wd0^Q`qWr+}46En}@3X%PkB(o7bfAX@=P{br7b zeaLNJGGxXKxZrB092B5e!pZ^}IIb#p(mP0!O8|2Fq^>2tOPU>6Veg2zctUWUhu>!UBT{!#xP^pUqNR|Dp93p4iDO|+|rFqpQao`LXFw~*??Lp zcCcCb-9C=Ubq<)HiJj!}gfqL9-0rd2?O0CEIU7p60JjA{Ai&DyNm{q~5IkN<(fgI6pjw0%t#fzonZhCgw(8)k@gk@Ot}KEGN^_8Bs=hi*CJ-8iY8T3!bSF7gi{&;cvlzDu#+|hq!D0 zRq>we_f<#Tj4Uwtq{1Pc$)|^nqLk;z0y~?~-Y9G~B5=j!ioJ?o zFg;2ClDwmmgdP~iKCXznh8dfA@57)fIgDL(N_-q_6?Oim^`96zA`=nH9vYra)83lv zZphrosBS-Gl6OFJBSWr>OpSz|KQD?}Et1^GEaVHP)XP~FST<(p8n`bwe@~lJ8%;mX z?$!Bl!PJ03iwZ+hap}kGljl67K*B@oMV9^ z51Gq%pja!m4~e|+yD;4fm1h5ZrCT9M!#jPNU)#MwZy4u>_xc2p*Sm7Hk*g5r4l6ZW zxgYDwTX3^6X&@<4m%pKai)4fDcm+ z+Zff`;?lCps`4u8kBC-8)yAxEMSGI>CB(&ux6;y*Kf#C;YN~2P+pxkwLAe5~XS?@*Q8lNzbOO=REO<{vUb^bV)EXrPu-6sMSyR$N_p7@tuGs!^Kfs?(WG5!(jzeWU1J zliifhvWWP3G#@Ipp=d2ClCXAGc z^@Fa2|G#@xKS!$9`s7% zKeLJdg`rKZ!yRqDM>oJ`jER=IXKHlycHQDKsj|I+SY*)drdJo?5DqlAj7n?I31Uf1bs9my+@MP??tmx9cjhW@?pVsqQ7 z?zsH&p9Na18s{&p-lRtVi#;|V^oyg$D8AgQc4~+aR~?hGZCK2%d$6!q@2G?()ZVE+-xg5@tC ztK}TCAxuJVr-x;>7-%Q1d!aL|j*hO{+Yc^n?aIS0^?fn22L=Njgxy74%k2{|nyT7j z1_|LZGmQ$Hj3hmy2E4Fcr#AcBv|@i;Dn+VvrF5#>rTe|JW!4pP+D^4?%bFZkNZ5cH z=kqqzEetM)R-|HRgimmDU~{%a*;XVQp=z;jJ5X}kXRszWc0bn9-acOp=N<9q^SWGf z{Op@-X#DfnWZyps{wq8w*3emhx300*WX{=>w}>&`i~w$&K~OpQIfXbR8JV zsT;7TeKFwiXg%biU}qH|LmF|!aiiuP;6?^huFQ{8_w*C3%v9$4_BZ>LS!6O$awjR; zJC+-%_$JKu5lY2(%f`JNpEXF>V{Z&{wGsS@`C+;rUa>6pt{CZ%THPjzI9=!Zd2XP( z=R>^d*?P)W7CwZ%HNJ`HM=e!zh(zMcgnTv;rkj_!PFdAlO_Tu?CoxG0c8dq%JWw2Q zbPH6Kg7KDu4jrY;78TV9*?xcV_J|L=Sr=<*D{{fxB83mvYP_ENFOBYOp4Qy;(V(yC zXB%#VMmYHCAgNIel)9UZyR-6+D_lv}`(}r%R$5IYeNfPBKb`mNVh_uSQ4^|{FEv8q zK^p=B8XjuCBD|a%)<1e%=Bqy=MN!}GS4W4)xz8$|+fnZ>)gcX_kQ4;1wH@oerWuq^ zeB57yzi~0v^!V=cRSnyWlF>vWFMKUjTR)3uB9Cp3lt{c>y#xS; zND`*JJW>IkAY}oj&hMlVVG5}$>AS3D4}RBJ@tXSzp}yxhZ0}L^pkwGIw+^qqNk@`J z>0GP#2bmBp)B(hUzx--yU?Mw$sROy^LZ9m>SpHg2Wbt1R+ zR|ALlPgGzO*ALuVtC*XLb&yGRx*KW@Eu1y72%3?y-)w;of~1!^I) zyo}@5i3lyAQ{%zCJjBH&hL@57l=vBM4imLI+>B7!ySt}a+JUM?0`r+VzE0)sfiT3| z7k@I1Ydfw2u3L9XZ^Ao70Ao);eC)Q%(*o%yh{+K17uF=#gZO`FJz{ecRJ0MN(8wbl zUnNZ?^W(;n<^ck=pt?`VV;<6hdc%_L_H_$Wahi2st2=0g=QcIFuyWEJ5{5F;|9Cf@ zVayh9jR0)BZW~FWxQ)bK;DH- zUCx>uSu*u2leBUdPYuOL{Dv&?=x{+5NcPue0jMw8J_ntgn}XzWQ4R756_l*WEfhtVgt|KRBHn@U8o_V}XdN@ZwQ< zl)Mtu+<&plk$U@!X-kr8WOu~6p2V0e2m=tAP3DcQT8FvUa~+3$y@2LmW{c)--6+{{a!-jc4G zh+tP0MFoLXrjpOslM{Ud>fRvXnqKaOA)Mo zL_c0f_&vxm?&hh-2dw$2`}8YR8+3+`ECsZkgynsjv6#JlJ9LvqS~T5m9(_jyo~7!kp&D&)9|%BmS`Sy1XZAOlX_hkLM3bf4Mp^sbFY> z%qGH^5)tT~irq|HjsFGaVu1I^bMW-&a_mn^=5f+W#ZLOsT0ur{pMp?{ncQB+4~ybv zqA^>qi(OLNWVQuqx$2N+#MhPbs=&49Z*VU0S=+mAvAZ0H1Ma!92#Usxz>0&Q%NNrp zjUBi(GR$<$0tNy|MN)w9!JZ_BtF>z9F+SWPu>hDmzzzE)5&nT%cWKfELR8T74PM5c|RMZBj+#Mnx z^f?@?ONsXd5xo!-#0@gJxg7F-+%Pn8Ax=rgPv-YN>j7l&R(MN6}nm(@7J0(Z@WFg>~t!PK7-rfpWVa~ zJfVt&JLiY~aC+%A4$q6f6}!iqaslySYLuN$w4WNU159cpDU|6zB^V`>I=ymM{Tv46 zn)%Tu$;!y}pn^`R)ZA~ioW?!bOS3D)n%~Kn@y0gf)`M^Q(Ev~-ns>p(EBU^y4IQOqHvj4;ndEL){D zjfGt%tkOXu6<4Q*C#-X2)GXcYbN6(AimRwVanPe%NO*6)(_wKZCmn} zKWSEf?E6sgto>D#Op5{H)X-l|90U`R06w+iGXeyM%ZCNma@608a)gIO3HG;J4Rv)=uV^x%RfrntRW%ZBYZ4=~X3SMP)*QUia_x(o1K=gJ8n;VY z+j37nDsh#L*4$B8G`imrVpg`x0l3mWU|{_HJY(p$c1~9LAa7#}!<0dZk-YM94p{sawVT3XLNW^lueoLWoM` zo`c=UIcdg=Z*AdL9#FT2pb+z~C3mx65ip+MHTywFytj0cm7Zd`$E6I2Q!48hXRz3} z2}R#+zARoGn$JhpEVnavl2(?;!u6Z?12}iIl=~y$Lt_^-)bhg!1As`uRM?mQ+=pSHf%x>WQ$waJtcOB!(f8w~O# zmfVWlJeV@f$`97`XJNw5rTlaUF2O%XJvIHyDlU?z>4J>oO%`{n{Co12aM=Fb#g=!N zR&-8Z|MdE3SI$EvL-fE-u@w`KRcKAf*dIlfgMiT;IW|;-<~P4#l;pqjSEHHm#*hff zhI?sz|NPPWt?gBCFf$|TU;{JP6>F<9{41t6=BZ&-!qagi?Di$;rb|S2V{TI#d_P`A z`P=jM;4ANzCcX{vCi!msI(>WAe3yr(@|j*KaRs`S7~OSj48a2w86C4sLT*UlOxjaGf( z0r?-JgWB@{ZYW-Z8wz)|=yd_&qp2SSMMObq1zDo>Y`x$;QAsf|X$2y8{gk+bsP;jRIQn{5?3sJc58_7-b`uLKD zXVzDjVMJW`b8>AY=$s*Ql^$cuP=IvuoJG1rlmRK$acBO#?h~sbzB4JVK{NWAo^Jk7 zeq>#clMUwC`lpk4t|J}Xfr3N&zQUpXre(9>(orFF%=9jWt8^W}Q*!bBQ{OY9x#l_e zma91W^ynKS29xBn>n*f+W2Lzdx^IPhAj}o)fOQ(=c@BB4q@{dMzX~{nD#w4TGioJn zxV^yRH4?w==CRCr&LZs$sotv@_m2+kU*hI&V+eK>3U8A+6+z^(WZ!ywC1y;p^ZAlC zZtXy-`sZPQ?_kq|(R4(;E5+ow7jEQ_plqf5E4}RNfZs1e(YA)t0H$9dcs=9y1ib?d zgp_xiAfg{#B(AsY3Pyy@n{}YkZ1;PGcvhA_CPW20Zb;9WOLI0YaV{;9dUA#fO3}*P zextXa4UF4%vr)&pk$Z(7;W=*TrzYHzc;IMGANmBwt0YAxZeZ7rJ`0UK+i2)&YaBwf zM{H6wBW(MzU);MY0(LzZ>ZoP09kBu;8tGbH`^Dpld=&nX5#QcUX_2yLE*5Vx+7kLa zFB?PuIUISc&7&)`B|}DAr}gH+14e8Wr8Qx1zuJlfv^NfvlOA7_ zd~u+(_8-fs9osnV!Tj|wA~BBS{|+qQT=p`h7W%wpPXE5QyYM{TDIV<^7~bZXLbRR9 zgx{--@f^yEd;x&?LH}y|ng@tF)nyYah7Q><{uv{( zr53xsowkYn@oRU25CHZ_hgYhEgp6`FPQ1?d1Kb-At^Tw9B>K__1lEoz!2VLL8r)}W z7XTbAw}2s^OY$qq89@xq)Xn3N)Cbm#!cIVtHUN%SOSuKR!Hk56rfFa5fmWoUuj&2w zg@Wz(WUtLnL)Z{rCE9r&5cke>K^|S2``{OM%a3#Nfblr%EGq)cuQ|H>ak}BY1Viv} z=#{XiFkgF~9rBZ!==4PckPYQ^H5o^kZHfi1l?rS@i^HJbD=fcMszT96fM1W5(dX6J zD+znC&R0V(67U+z`_Zpu5?b6>qjdae7w#j!lm6HE>;a*=56OebXw9ouytx+CF6628 zcich#Klaehyh_4EBqyXcJkuLIsb%uYcPr7_H8aI@+|dWF;hN#`PK^G(PyQrFxH4>e z&pUhZ9oOv>N?`NSWOtx^(G1r>C%{u&aBY5XZ7$TSu75V_D>pnQpqmIU|wAW%RQDD-ezbh`@1A1BqEHb|Hnvcv-jE_VzPUp05WTt>RLx@%=?(xBs@1T zxFfs&Xm9RfvL+(Ly9v4pMe4!(!Ry$M%TsXs&O$=Wjx*E8Ba4^Hx=H2#t}%L_HhxjH z;IW7Z@wb9rc;6Z1tntHXTnx>d{FM?6?&7uJwCf5uCKdyom!;i?e}A&2eVzmdw*p1- zc;Aq-{tE#>*y`0?DkTt7-AQ7e@rX79i%aQyy6vF?mFLuC z*|g5qCmoZf$qQTpuH>QyOSFgB3FaghuQbkP_)klecvhv(Ej$X?A&>ksxxZVrD4tp` zgvOCMC*|22XOLGewzrV0g2dG(E zY5CmRCl9R(tsfPqd+7_+m}%_Iv_e=-$74SNnI#{62A#khQ`b%mNJZI-yMHabHi3pj zxcw37{Rc_4;suj#nn~gzf&G@2(`PRao}(9?k+8Ad%aIU| zT$Sv~7c{hsVvtma#QL8dA_BqA?te z-DKk%0sTCvW!kxXhsD-lG#0x#E)G>%!65@dv=p0dBTsl!j-VqxWke@BO({7>RXcNgJg@rXwP{u-p|FO!D9)_6&sTb;s>G2opw%R}3A@d@m z+3TZ768uMht#ri~^hNH;N7a>*k^?{mys87XV1so&c&+egEjaGp?YmLZ3A>>$1NJg| zaLzRx`I$^C6}J<-z0+=f+^x>~F5U;GBlXYih$Qde9mU>C3r*9fBR7L~a(-Fvv|Vm^ zE+2t0N)x36m|v#1g(L~s4~)j1A<$bkr~N8th=#xu@IyxNI3jL?CRhTcE4lteKoQeV z{o|^?W~b{dlYi46^Vu~cOdTEm2IzOLy8wC{XC6uFxcp>$I4!D@!9s1bDHdATZ6=xuR`yhP#)3yk6T8VlmK51fNr*r*8uKoc@S!F zb%2Hxga9qqn{Gq`Zgkdq+$Cj~sIh0irA8ZAE4BkavkJW2`9Ky8SK$fSwaRr}ir(iE zG6%ePVzR?`8KZOc`E4&(IKBB?4)o154G4=^S?Pj|D+GilF!m=-lC(rD}zko%grY)3_65tYH7<;UGJ!Yn3;PO~2>A9zgMuMf^ zy^+EU)#0)>W5PYqQsvJWZhh#lSEW}Yum*d;Ok_rtwn_yu*x1{olCy$K$XexoZXn&*U_Zi}qa zRS}s@dPcSho;3>lK;Fv3Owoq1(H=J6^}BT8pe%4FcrCT;q!GmiJ6Z{ISkaSs?aO~~ zvy)@Yg(t%5!hUK)`_#pm_j0V`t|A&Cjv*XeHcuub5zl|&#R5?A{%}Hzd9P-yw8^17Nl&#W11xn6my;bub>*KM3(09bi zX{u#odH6fU%RXVJQ?Vpm|JSG=CNx&pO3kLMsNS}T(QI8Z=8-O!VtMqQ*g7hOw~iR= zvBn`!J^8XSrk+alJt7T5zEM7+-a-p>5~ct)va93yHuSS^nycr$6O#eI;aDp2NLfNp zvtZojRBClG*rjJ$P0tz%XEnI6$>@V%RpZsD7o`I)xTyLkYr3m7fZxirI(JKf35Q^u zWnm!oO7Rzx=TAke?Y1LXTSY9D=L`Bl3`~5s&0F?qMY7E|p@2Ykz^KTHh^e&TG<`ng z!{dLD&Zm@-1)#OvHAGu>`2D}jV6LIR_0Pc2hRY)hd0HKnakfx^L2}DXW*lPPi64G4 z{NL)7X%_KjJkzO8U}XO)_jK*fZ`$l!udyC{i-uD4cvX9NIm!7Juj_OFtZ7T=`UMmCcUIYT=nsOISqFZW7_;max4tIzfcwjY9vJoden zB9@5nSpW1uEdD~Y;m)2iD1cq9(N#_A6W4WA?+$v%iie2%rY}(JBDS&S^A{Rc#cON| zU3q%@+(iuyb}&9wr7|O0#7ZJI8Ef$4TBK}@b&}~R9Kh7n>R~>?mm7Kw@%Vd^GpjLVORk>;cF+;{kK}>xHtr-GABf0ch5@2+eKC1UE_@0 z`=>PUSoL!TxxtWn|1rF9C3v0i1#AQuwGAG;YGka@QhV5oP(*xrKLLdXOjekY#Wyg$ z6}QF*cKiW#;C`#ekJZAnm4baNKD(>P8;=CwvDHa@+8K%71jP{ZJE|Q=err(AT;7A1 zDp<4&B)jewrSz#Wvgu{dUXIGCiAZAYasgAMi~p9R=dSAw3=bmX5~bfH9vyjPxnOwW zE#F>F+^YHBzqaxk;ZbMWYWgx}?>k=;ogl*(@NfG}VUw2g-oyBWR9f2QkM8-3w)C{I z{$!ltTk=NT&7a19Rz;Cnm(P{XdCjX<$30Y)N9FAP`Miv%K%Qw-?2aAO5n=B;IaPc#RX7=_zRo56XR33V)YajbWF$ zh~2YpOC}s}-=xdNEdPu1=S763_B}>i>{v>$58ie$DYH4=8*^Zr+`)?o{bDR=fR^eC zWsPY~Dqde#U^=U{^E{iZF-d4`wXZ{Ox~3XSYgq7_E#ujKeOPxzBVBT;>xah9Vk5R^ z<j9H!CfAfCjazm=&!!Nt7fcy{YNdo?M!rz?}f{-m=T%7Zw$gAo*L!33gO4*j4K zM63idp0EFO<6&YB0*w}n^7ipv&PiF+3{RUBUpo*-4sM*_?TbSOhlDk5ls@q@>&zn^ zp0V|FwtYzY|JDt>X5d8iKa2)jaNY2PQ@#BrQE9HSW0Xik6sSlh@<_@n5v4PUFXiOa zR350Qfpp1dATg>yMtz~3%2M{_JI0`PWkLRy(15jq=NK*{Yd(gPH*w256yMGOYKEM6 zSq)fUrte4{#ls@`B>U1o0Rui%xeaUe?Wq=&pWd} z;vWCYLS}Gyo5Uxj7oTrj_Lp_cp&C>Jh(u6*V&+TwXJVvjlYc_ku_aEu3}0SZQQj7} zPR!{TUMZ^XKOM==$w6tTew{kmTp4rFx5)A4^y41Q4n<@qqk4n!!Ej|~#>$efEReR2 zm+O8+^Y{+Jd6H_#qE(*{amWJmj^l0d`5H*yS{~kFI<}b33Z81lyy+wOU=G|+4gHOw z#DEsj@;HoSNIdS!_nM@xqlUm{qIaMMY-(}W22^Y#g0Kt3z&Dz~N9#UHsDgf|o*`g~ z%%32yxDE!cmFB0;ZWctcBwq6V+F4Iz=&|N2Zn3Z8nq4Yw7yoeLag#awNcc zdl3@He(;-SMbf5A_g_#FlV(C$+a_n)zg)VxLf%5hZ6eAHd!!-$b#* z#D^{}kVrcVDHNo0YJIP{gXYO=KYj_Jr}uEV5Wsr<>`5H{Xy~b(?l5m)w^Eat`>Cbz zB46mIr#0_9_Zun|r#)v^y~_Qc6vu@)7whIsud$d5cTay_5__Ls3$?Q6X1K}xOWpA% zs)48Z!7W9PxI~Lje(9-X0KKL5eC_VLsS4QwW5-4Fi_u^{Uti>tJ;S%&y4+c9kO8Nl zJxm8l<4;Ht@=2NXTS`?uD>5N&@P2xOWlic}J6i0&w#GGg6e0gjP4w0GUl z%>=YY3SEo}i+jjJ_|;O1GcxdLAF<9jPQljq#2`L4Ha6(M_UFm+smllP(>}-JEG2=AiM5lN7>UiD7X>2oE8=wYbq`RoyCucYoiY@^vHhFL-xWXj({*bs zjxH-hU!OmxKFC)N)3pdEG+PSW%~BprDIzq>S-gl_Ybg%!N2Vzzv5(N3`QG$_Pp^pi zka%R4m;DkhjLcKBTAOF`XJSi2RShpGu2CER z%wGes2C%@2_b z{;lUkXhbG+xFr4(=6E`|IQS-pMRECF{O_S>rlwEsv3BcGBhz$pch#$r9Qp+!GG@&- zzL&tJlvc+(##6icrvSenARNINlF4a@;7r~9Mt@r)tizFfXTCHsc=Wy_B1*xnbF`VR zF|V19gc~uMm*H5tAUz&g_va&{H1cE2w)no)z)x1r=#Ce2R)`Gi+fW2e_9kLWDlF&|=fb z&Y(r^eA#v)>}1kT+3$}s6I1^%j)OYEa(3p1;C<1?z#lJe!yNDE^A2)M2xbMwzgpoA zo)=c|2evqZ&OZUCz9eDBGJ8PV#UVzV^j!zj}F5$~~Yo3Uz>ri*bf? z|MX9g5B3o(jAC%AxbVG8nA-nxHjJokd6g5&y{dam#Q<^tSZJz~0&R`FMx+xS3kzvpe5% z9vdF%B3T_zt#Z~IIV2B);C#RJCQrRz)WqC8~6&}0k-*;Uq+3TU8jCC*Qv%JBp%shYNJ0t8 zPZ=w&+Tr%1g{ZMUHu86`i@C(sK|gNFJGM#VrjuVdN>tEyjfo8P_p; zk>amdsID=ML-N4PcgQaL$vlJ(bnpsk2AVgpFLCdRgUVFrH2~$7hAyPywN zMPn5t6w`+qFuA{7Fzt9Et9*!OQ{rew$gr#n%5VSI9&dv3laObX>PkQHA}N_kV(v)b!GrXu`bg z<=1@7aDr@_fJ#uvDZO?Or2{&7`jc^PVFo;_Eud^=esAr5AtlwVmL?gR>f1mhFW)LJ zal2w=2%9?~jYKovDC6M_-lsxnmFVA$LyCQN=q51`bM2|d0`0g@d=fu7+*HUxC%2NL znv%w6V$1AuP)kx^Vx{Tdve}MD+xj~0^Oj{S)t-c4^p05APm#z&Y#_Rh(<+M%8H^=~ zr83L;T88#xxen)n00U<1nrNN$Qig8wj^0vJk(UNbT)GwbaHkum?^M%v1i{FetLEHd|=G(@e&ho$J<>sZx^AMXO{oS-A5OtcmhOZtQbbsv+|1z%;M>t9}s3Q&33=Kz0*yvng zGL7*%&^C6rJ@ujn4KEursms3p;U3|#L#yAVnV*YG_6%m(_rzvGW0U8W)-$sFL6K*L zW04V>uI<}h$$3?p+1AOupqNIt(J5njRChfWEGPEHlP5c24`@nkPF?eq?G!vm9U!{* zKwkPw40su#xN!w6E$0<@?Z)xRRe71Ey-JWRcty-T010q(|2R?zq|t$)|wqffLF60Q?$^TJS02U=GUlB(btoEW8&@4%T;F zm&G{fIBIUB{gpO4tWB9KGN%Au1^4$WFp9!e}R^Cg-1!4#iXETKovOHyt1Kt*L^V19kEmxouCykEuq zp8opH#Am(ajp<>i*9ZJ<{0jEDqitKMQu%Mu>Paod#j%Yd0qCKhXyQmYFPfwtXl$I> zZu_kiGyJ8P7wrc>Cv(vFDuSH8B9NM?()|7Jm|`M+k11(MMO6RHq{Ud;|E^ zis4HIkkO2CcXYDPJLm0oAwQ}UvEbJ-Xx?neZ!Tf=PLc3$L(1CNS0a2!Yo(KX5g`6{ zf{^yvyy3JadgRc$iA|M?WfA&xm@)5@CE?k6CLxJ&Kt*F=`3;{7(^~HrEg{b1$pO?j z#(?NMFo9#sDk62H?TaQYU!8&BYv7g^qlIgv6Bl^0lb^Jq9|8-?id>}rQ1s&W`&DRjqgkN5UQ9!Uq{)4I zrafR(0R8a3#^<631t_!5uAC5ky+H$|jYx+lZHTZEl~GHQQDgY*$}vutlekmxsD*Zp zLGO?X2yqzcnSFf&Ulk8~g-~+5EYHO?*mL(q(rX>NPPzUI-|8?~?&Wn|E-qDPk)rD- zX$w{X$TP1D&6VEh`rS+o*rWJ%ce?!Z&P9dH@ZRCTf3#uYwpy>7E4&vBO<8|Ml%lv- zo)7K6zs$roeGzAEGo=_1nvFS!*TXjUmi8wi`r--QDZZi0iMalAy;?0_8>}^XJwCsF zw{fa>Y-NAwrzxA|V1&(gbhe7_{G-8s;cqY2Igb>! z_sd+dN&Je8O$qvE_$qJMXyLsI>|=Fx%#yKfxh`L@3*~~~Z0*mS&xgKnnZ-bpU_wNo zM31lG=SMY`k>{M*(gGSfHpX)jQ(vVoN`{@K`Cie51rxG_x9fuOexpRt_Z>3mTPUNm zG;`9w=E^w{NJrADH~LA=E?_;JrDq6@AH273oM>xjp|Uv{mrl6L1VgbjnG&56Z_U+v#;=>6qEt zWIeW;+&_!8FJ5Mw<-L&{3!s9_br|HOr zxpq`5w%jri|7Op7fV-sbk=GteMCGkoZ>4k0WSqIO!htRxx>HiWvD9<4mZTDyg#dXN zaE%Urk{&~^Kn=Ok}TfG)lG&?!iBG0&fujvm~@{dpc>wongnqO?9-Li7*D zZ~yb2<-H1ZUx+I+H@$(j-;|PpEnIEI=_0lc`3rNjN?*IZ{+PCKx*a~$k1PHEUXBjC z|689pg6k8Pm&4z0mXwuNlV!lD#=Uy1gxEb0XsJSe(0nQ_B?H1OrIh64$V$aSHrg2@ zOAm$KP_!#uOAkhAvp#si((@!ent|&g>>IDu;B!jqGWI

    7Oa+wFTL0UrZmR%aXh! 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

    HdJJW-Co+(*tocEs2gdmi7qz1mfNmPeb+B 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 z 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
    ZZEGJo~F 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≠sJ^zBr+I1e>!V4!zmd+)*XAn>pN0gOj? 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 z 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

    ~Q!sH5e(Z?)5H^xm3 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 ^x`Vy5|@37l^ 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�(+rigu@W38&{%3y1 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

  • |MfvJb?{p8{-3~F)K@+*I_5;S=JuhbEY@b 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 HVJkG9}mI|vL0H!VW9(?aX< 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%0Aok~G8$F?s{KHv%Bg_x9QVX7%M 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 H;mH33^(5kB literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion08.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion08.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e1fe7560db643aa1db0ec750017cc9de82b87cb9 GIT binary patch literal 7070 zcmai23p|ut*MA1NhnO@)wgGK&U?Q1`+o2H{PyhMe)igXt-aRzul1}w*2j(!AwKB$bHHqM zFus|;2(|?ldV=ci9n77A?JxbkEvH!v;)u` zy#tT$w5A?I`gyzC^VlPCNNp{&wwAV*4pP$x6+1fU32 zk%asyoKFw-7(yk1nv3;5q+f^>x@`LbEAn7*yGKAlO@VJf4@y~htf448;&zHH4 zr#cb_e zTLOG4z&#C=>zz^Xf0~z5D&@a7lj9v~kO{EmqagW5K}u!>rAJhCA)bXhAmCFIf;yY3 zdoWn{QSffi4ukK_+NrZk(&g*L~+vDx#@iw!96(f4Cd%0dtgji#a zV6RhO##B)Q9E{@VnJTpncB}k;!-D0O)pH;F66}Z@&-85&aRsBVo>}i7i*V=u9X=+& zZ%3aX+Osbp!xIGE;R*!`$y-9MNhIf@J5Y=*R}&84Sy46wITdEe1)kt+X;|{n{Et9V3z*$syF= z=>~1$PK@gl+Vj!y=c88-Z@BpH!1_CKAP_XsyqKiB$DuH0s6r!H%AcitXq6~*p#_A}}cfy_Oeh18I z*gPMl{UdU?A)<&)m^4ok_*dlQswTYBPuPY_QgTY#emeD7Tm~yWXO?wK^xq=q_~pX1 z%Z0I*r(+XT<5IojGKw2+`EknU8vkqiJ8}+El|TcLV@6f_N944k<@bSTs#3T5ysD#7 zKQL&B*@k~Q0D@W*#VvSw#Fn5jOu!5iG)Q(v|2bj+by(MGP!||B1%l)tXd7^HwA5v4 zqG^DUqcD0(X-ythFz+EEx=>0-9J^LAByq2bQGF7&P5koGIFv>zLIAnh@kl|O*%WY7^7 zm$Mnt9&u%06C?zFVtIXu>tQBv2yz!Eh@xWXgdWsXB7uW`BTv9%3$utlSmDjl9JF9| zloX07PwYWGl8^4*`z9-@Z?7r@AzNYKXR4~}X$DEDKraH7dXsf6;K~*s$<3FrJSmYl5)Q}KrJsSu2L^9Rxhb4{a&m-S65Wr zSjMTU%q%aNtNS3(|HkQq`2hBBVoh?9h6_mVfWHmMh%rype*ZIrm z7r(uy*7cy0Q}to4QL~1oU`wJsqq|Nz06qKF`{x~dj_M~ck7a<97MEofm&{cK>}V^UGqoj!yQWZ{?{an85b%6gg7fIrTg7i1 z)w=EhLFlLLC*<#H$-7SI0YY|FnOL|-$FfMT??ijhcZ10#T7HGN$fIij8!#?5WH=^y z3Oo2B^kI;o!Ze&HrN%~vNDtLa>ZgejiM zfLq`-?7IPw^9S~tvl!Au9IF~A!Z5sWVz86sQ-1P}k_;fymM{?gptN27hjy^Jed?RH8M5I!|dj>pmm+ySd3LOj^v z?AK6`>H`QFge$dEm&m02T|OP1bIhLj){osLonbTQi@Cj@Cy^}0c?v1mM~S5^XS>Dii)a0zsRC zoQcN@<=PJ(9<Z3M_sdIIdk^qHXTN*gA;O6Ly zs2N5ya8KSoJXTd1pag{i@BtA`C*#wD2?RA|W-2#z43Y5~fVYq`bA;dnq!p;D`<&C|Upoii-9Vq;^y67t(7 z`IYv2Ml&A17EtDqxt*}^>QzO6H|AZC1zJH8<38LAGFME1bDN<$YFtTeyRNR5mL6tW z1%*;UqqVis%BmRjv+XLXs@iDu!|mI5g1L@+1@J!w))h)#soj_ywuCj}bGd-jwyO$? z6^V;lE=kmXWL&2Z`7_ML(n}InWj1tXCGvvw8UE?TPqAmmb}J_58_Ofws7W2R>$2@l zs5O+C>yn2jzxOf7CXb)4ykgf0Iiw3&ht~ZH?&|hDb7uMEqPK5%On9$!3?ay&PKErd zV|&;>MJvsjN}-{k%o?{3MkNwPAAdQp!va&1;1ybw!%evPf{z=otDe8>_e?Es+~#eV zYw3661KV70k4-%@j~|0QW)WXFnJwwb_FWoTtybREQwcwQ;E`DKa*b#u?>a@?{X3d&Ix1CLuKLDU$1V>&XR!R| z)()pGakTR#=3kU+Y02Gj4Ed~kL@99!6a7c^-L2bW#3;ka@x)s48Jxl3uM3j%A33;u z$UM+_lT3%*?$cVI`?NQHp4fE>zR4p{^jwSa!}PDEHYzLj{fhi|Htycwe?LzUf-jb9 z9^0j#?3Ps;t{zGA`(6|w6UtY2zWVWZ$*=EHoUh-RwPN2tnzB9k*{c~Neidf51T_D# z90k34_3*=z#_`tmUIptMx>i#3WhNKMis?i|&kmP?bG6&jmA`Iyvp_seZieQ+aAH#h zJlPvc!nbW?5Q#b2ni(h2j-Fd?8)m2KdDFBS?qcp&|G1IZc$*XdtVCc%{^W(+AcpgK zW4Xga5j6j~U)Y1M>5ILCv-%ktIdK09-l#!?es2!#NnQaEiHamBaZDC>^11b3Sq{Hwr(oRm#zQ+){Jt( zAMe!CV)QjPrxb<-ucgX*i)keUb&udXtB9gwEoG5+ywc<|h1)rw7w>-#{$p*b$8;pLI+>*wVYrf6r zj6ak4+TbT~dh^7OiqwXiY_F>=MKfcnAGgmRyKOj1ev(u&u;XWS$EE3O7MF8a;r^(gY)(YFG{ z7a=Av(Jx^(Fm$6k+H2J3vN(Hx?;V&B48G{*5lNc#n(MZP7#=6DcLmrfy+7m-G;mtu zOu6xT%>=hM?W85mhkHLiRk_;|q|f)~7Y@R3-la*TUcBMu_xo2$hKD|l5R&fn86`Yu zdHvzMzwN}c{2i8;_{Kz!Bv@L6e1NTd6ctTJE1lmZ*Sz66QbXvn*WHU%f3!@~?#_qK zY!AEdz0Tnx2fpP}U-ohl;aR=}4RZO~6#dL#(C|HM&vk(}PeO zVftdo;Z+Cfc<;+F;}>a-FQ-T_yLWg7q6#PI|6Vfi{<)xSxq0x>xgxXPH$i4Z+bEmF za(eSN$nTx*I7$3vMUCp`0KS(Tq{@L)`Ax^-x5ZHD_-EebN>>N!UxrMy=(;T8Rqqi8 z_niAA9w=1wO6!QP@w3v>QtGc@^$aTRY9Vrub^Pw^?=`u8@$L0u3o|(FfFCf=AED5P z>K4PDQ;f~yFbOXI%qIc_UA(c$$k}=5zzXX@gLl1fk zTC_QvO6srEFs<~Iuf?}c<&r6i(6z*kEWfIyGco&MADW+Kcb>a71!C*~h-RgO1n%^z!w zm^pF{pI9o?{zXV>&6UzMPc1`2Vm_YbLvB9PDO-ed%rf@wJ(zQ+U99ZGH_VU98_J*b zh6O(?;o}@v4nr=V%J=JNd{4<+#4HQgq*lib(5u;5{kW-2n_{z1H4m1zKhqy}+DdvY zc`J$$cDa6yAFQI`%bT_XW`!s2uNcDU$-gG}PZ(#cb0~k*cA7PzZxkwe`t@3kN0Mss z<#;{K69QG3*IVbzBm+IxxD~pG7}O2#n<(Wgw#YCUGL2?HU#uf%FXife3A-mNsp7vp z?$PnHl6celz)g---!cEE@6TeJFP&PtlF+cjP=0oLOt@(lN zwRP9NrEUyU%h!mPm#M%fL$r3E6XPUZ3>&x2 zdBm;mA&=<5-4i)2Yq;di`$!u{!O{@o6VaWVr5Q1&5^Es)6T6%J{%zLl?HOms#{{Kko$ z)h#<&4r94$2a4k>Xrv5I*0s4q`~o7)kH`7c(@sF%dE%8}d;9UFNrbNh&%YMAj=VHJ zO1UX75LmqJBTupJNe*0@FkzscQ2GKUjOcDA}AkW-S#xaa=Y>FOj6*4&536!BcfrU z3Vf<5TgM00^pj7$)Ruq8NjHFkYD{yQ6(iK{_3CzR3ef72oOrdusnf|;TAm1Y^ppE| zZld+B%Hq3ikupY5BL+S?%R;(%?MOP&^X(fg!I+ve-C(ZU`ov!SrV{l!v^3{@-_XPN zKBFi8EpFQudQ7TGJca&vu{7}E=Lqa^b9$!4PEnkw0w z^~*sXe+KRU+4sgMS<)#tA`&I*nS`roJfbt2QjzgBW+8ZB!7T!Ns9FB(dn4`9rhCFa z{S6H@^Ws+|eUfa>Mp9$sN^&n;`{e4LqRtTBo=ccs$XgpA06DJM*A&ETU|O_7xzEZs z-8-+)tPGu_lujp=C-_~4q{CO0)x>;pMsX^zo&nDqC@;(^rK`U z;8*KaY`DI2jJ#=jBIbL` z-gfsJ&pjBDBI{92G9k!$)}#JhaZeu+70DUnsTP!s_9+M z%W0kUf-tR)tfevqCs=FR!iT!Vr|fYYCENxhNf9lEsdvpHQOn3Uhz%H}xl!&h>Qjf04 zrk;O8{8s`&A2Cod^gmqCAsGMw literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion09.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/motions/motion09.ogg new file mode 100644 index 0000000000000000000000000000000000000000..1160596b2774e698654bce24db391f58fc461336 GIT binary patch literal 8840 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract02.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract02.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e229e52fd2948a20c9e2231c4d43dc9a9bf096d1 GIT binary patch literal 7182 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract03.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/retract/retract03.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e2c18c31350a2ae286e50929b49861ae05c49f50 GIT binary patch literal 8078 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in01.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in01.ogg new file mode 100644 index 0000000000000000000000000000000000000000..8e5cdadef7a730bd7b367f76b34b193d09cdc0ab GIT binary patch literal 9726 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in02.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_in02.ogg new file mode 100644 index 0000000000000000000000000000000000000000..99947bbdedbbebb528b651f53447046a5fc6c772 GIT binary patch literal 11064 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out01.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out01.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d22f5a38bbcf32503ded9fe57be4cbcd57c536e3 GIT binary patch literal 6022 zcmai12Ut_dx4#ht0*F}%7&N${385%~AjFC^A%H+2w8$y}qy$hxv7#se=^_NgP?nN_ zlz>zfD=l;okWeg$3Mg1sS!GdK-z2!ZzJ2fe|L-?jkPhw0RSFu@eeyz={!qcGv%I?P_M^Y2HnQ$TU)udGFE|MtJ0E|C>R z+JNpBaKwMJJ>3rz6hL(mvd36s^!0H1dir_>7)|@&kbom}-!R{haIAngZ1u`ktf`p` z43YpdI9p76^!+q41R)^kpc+;tPm6ywmHrE`=(O5^+QBk z+=z>J)on4)Q(^st+1&3PT&M>IoW)d3(Fb8FAz&YmEy<<>Rx4^@plF~bwD%&?x(3-( zgYFwtv;UyuC}%imVBzF!M+Q%Xr)#9|Xk^4_WR!b$EaiHX`}J7LSghw_tQRWod;245 za#7HPs$*bKfhr;EIkre;ebF;Qkq9HMauwtwFbM{pu7XW3wk|moSQU`d=wHzo+*7O5 zQ!6c80(>eeI1N+~z$*Da?T~wx?|*M5{+(Ko39w~HD7qt5&4Q%XLDvx%TDTJeJ~bif zwA1l>!|)wpJ3!kTZr;VT?fcz!?O#q1xE+E_aOn0>bQiD&Nq3lT=o+?jG|X-km_lHY z|NNX4;sq$8oOdWIFj~SO%}Z#OXuuX#C}AeOA_**rFMNSv-DfTI5oB1?1!c&b;pPPA z9E}@g**QWF2K6gG2foC1Uv4kBe4iE8Z!OK@tc;G!=7@94hQOTSY_1GX(F?2#7-ET+ zjk|%p?)OL(1-%OYSI}#>xXgA=VV6^0u`?e1*nEN+; zOn~3K#!$SxbC{@9F)C8IL^)$)_yw7a0$eASy)h-#l2{hwKvLA;gr5^?j)5QsLWttO zIw6!-P|QwG)aur(>(%ek5p?8(4ow4g`XRA&f~TH5u2D8?(<8zT)7&A}18taS`2h zQO%}S&2C7i?*ra`0Y4~b;fS|$lJ^{xj~G_)b=vAVZ0I{o_8p1z9dGodT#IsV{2nkX zVKX@z@k8VYLPUj-`rImA^q3Ei30;}wD{;RSg$$v$Te`0BN zVrhKhY<#L_LRLTmtNcb$PJ$N6ucl8fYMLEa+-KM9w`N+6+Wft&ZKoijF%6 zfkDG9*8S)J2)dUhZ6nMhjwIa?lHmwRm+ZXrKSvCpj^OQv@xZW|5Tpn}s=&z%L?S)S zG-Rin1a3xcbs<5lunom1MHomE)~JN1?a|nIJ)O`hz3`NzDQ|8YlcH{vidoZAvfp~z zSOTOyQOFU3qH&$LHWa9K2G6_tWHICP)aPN|Pk9u}o4wb|#I~jhjZ3h=kHyr+$Kj5~CX@mKTG- za?now*bX$Kch7Wg%>6x@5QMo01Fxwj-p2w$Xh2Vos10P`ZIJ3Vaux$f-%&Zc(M)_F z-q6vR+#^RGaV9@hup>9B+sWD4{q9VbvmncphqLU+4Z#C=-*IR1V+Ha^qwhn7)2>+& zlLmI~;FAxi5u;gA2l&CB+22{&A%(2Al1Cd29mC0vWM6k@vU{Yj`y$zuA7ulmPIlw+ zWO5^U{9`aVD{3m7?CI(|zDRagI87Oio%$3tg%WTc9f<%`FTStWwTMa009HU?(Uo$R zW_fv8d1G*S6_--hhnQy#O-o)*$ zy_!>1G2i%3s#Ryc=}381RYmP<0kw%c&kLPz(mPdx?J234YbtAM3Yl*T?QIN37njf7 z(ducr+Fko@zG+K+gt8+!;xW_nxhv2k_`uP8%3^G7@bw_>!{{4eTV2Qo zFvnZa-tA?a%JE}?la_OH$}8q;L$q5f=S>~SQJ$H;lSZEI>p~`tQYoV;Mdfo%T0M7w zAlxI~Vf0NswCBUEfRI=+Cl!`mN!ddI15jnk`_*%8Wd7-<^gA#P3Br6aQ zSfr$enTfC}-vU7j5rplYcv8Q00D|62(1M%x z5;V%_wx(Dtxp$9*dP6?}i*4r5Wj~pnzXENN-?URG5sZmZEq69!)I6fvAOzMG&pkIiSFmM1y)R z=swd84Gy%DM-EC;rEzhXaA$q3pBT18jk2@AqTC1o1x|afMsf;cSY}f80ATT{%V*oUQ3?mT$Ry+?gBDdX*R;A40Q!5Qe0f-xBSTrIa z(E4sb(4*Dj&{dHQ7F#t?a4o!#P>E;^Fk><<4oOYK&E}d(VB51njZ=aWlpf$GE2uw` z0RepS0D&-z7Ie8`ss9uv|BT@Oj}m2|&ics({H=MY4H7F=xuvY|(>v&Uk)FNs`9sbA zPxbzvMid$sK<>v5GPx@$4t*2f;Ih{kyIx91pd&&C6nd@M;KX7|j5zE|HUqe)(2PjX zR0k-*VgY; zkRC$W;H(J@iRuJ`7@}CH;Of2mxc#D{FJ$#4r6oHUVM75T5Q5PueKFomT2`+7Y!P)E zF!t<@k0OGA%krF67v=HTKt=;oB@YGAkEg7_AR9J*mnG0*v5Ep++m(7r40{BAEQ&{v`C|YfGe0=<&)Z*6XB5LM740hWKQFS3% z&`C(FTvbGcW8noET(ATb2ohY71!AUpUcTG73A<@CZmXUlULS90WNf&_aGSmX9*;Mi z=o`KVgE_++;1VM8Z_BG?_n&(+el*a|x<)Kd`sf8*wd2>8q$eF;L)QjDx$j@jV1Bp6 zJ<0x*p$Tbw)dri2THS7E*u&nwXLHMp91AQbBVz5DMvUcpnUWdUb1{Ue*I}s1(* z0&_6^llZL<8MY;z{sBihQNHrw%WVe_B%7c9?VAinDgW(ZgZH-M$L1ymSs(l#)n%mD zJ>rk;e!K0}CHgvb3P&{e<0*rm4US@XcRxTw)RY{r;K1(Heh+!3sn;fs+_YWrQeX0H zzq5JZ7}Cg+guXo`=JP(BFBYjBI_xWQ#kpD}d-&b{YrDv`yT_F5Q`ZFF43gvQI+ba! z$`5E<6H&LbZTpS!f%IO9++{bFL|>_9fA4Q?a5tO5fSF+8$6a4G1#*8L%3N1|h;m_7 zY7Xkmwa+}~f~4eGW1pp}fc(gEJd8;_O~ZJ8`Fe6=r;q*%Sfyd&jfLUO{pSn%T@i2q3fk>8M}5y)8Er0dj0F~)Pyr3{vKL(NG|&S%TrY-9FR zR7hL)Y5oPC+ubGY<$4@m{oB5TPsj2{Eo<&Qwkbt#zq`|KVPY|3kLG-kf54lIS9*^= zn9+NYc*iwh=Luz1*KlVgDW-Du+I-dI0Yg3Z=}Rdi+B;81sqRz!BDcHlc+$M1I7P-z zI|=o$Z*uNT>Y}3Blj^FnxlIszP4&f(=;b%kL9E8euc}TP!havP%ng3hX-=czPg*Q} zbkc18m=hZ_QE?1fJQ->3z(^bim)^nr~+3niuC#6EWHfTY%Z+?+_v)J85Pf){e-5|48mJ(UF zxxuludNH2!LD$mtF7PEtpAAb>v$tNJZPPN=2YPz17p8T@(EcVXZ zQ}Rkqnq^$ex~?6nT&H-?-F4dsn?)?FJ%h~(MVwb3sEPbyTYK?k9OWo%{q&(t$%%dG z;RPR!DjdIL*)1#%NF7qGaBew$nREX>#9z?7+ij zsa|hvtKP=V8gpygGFi78+TJ~Uq%48WvnhSHp@U(cA98!uddU1$RRq-XYhmI->DvnF zdAG!+@S_-x#+JqGS%2=j8OPwkq;2obJC<}KC>eOUZ(oxBEPkN#KJj9p)mR5lxBa~l z?0WIqVM=utoHu>@n3DLJDngOU+tC}p;SX=QtMg>&Jc-+cH@dFl{>i=lq5&e%#MT2inCq zWY=8kDDWvWRDE2%u1+MVC;REt=2gwRe>YCrHe2X+$N>6^bh*{Ve{n7V+NwSG;+6N6 z($m&V?Y(^)*Re=6!$~WPNbNsr+HI>=OIKyf?^ddl>`0Mo359l#7*stTAkI-zvg$GW z*U{qxQ*PJTXRbdgyT59)0_6HK<6St>;Cz%PS?u8CGj;Lf6ZKiQGP1mlWc##Gt9K93 zRABj*zb|ChJ66(vVJq}q+qHj>xKkN^uI4>cUV6u!+A~YX^4EIUo&StGHFxu@_Uwr< zed~Z-a^1`R)z{xl81Lu|evW!zd`)D5INUXF#YM0G`a*uo8L=}w1t^U+Oxfnh7RSvP zmBtxqtoAkt%Cm8XL!qt))wbW7)$r2F&04QMQUY^aJsb_P;N_<8COSt~dkix^4Ih_P zxD~zNhGW1KI5meqka8aD77x9e-ax&D$^JCAR^9s@!*%~%`E0gq)`_<_nGWTCVQYdN zsH;v}Fxs+l29RpWJTm7J(ZnOzLc^8nBepr{b{c)^Yez}X@k_hSO zuiUwY-SS8#X`lTAmC}NlgEW?+IrC=j@G=(d8LAxG!O?$c_PW(7?^E(n<>o&w6{Qrv zH5oT+NgK#_k}<8gMT@$5BH&Eg82kSALFYdasanqL^KV(T-xlQuKFyvm=+KO~)WTYR zSg&ABSO@pIy#ueoRJbfn8A>uvd@YsUj~^eqDiZe+)KQZMqiSGyk3Uk2Iqoc^k!k@3;2y79n) zeV#S9!^2MH5fV;SZq3?1qezKgmDt?W8PU8vbl2xw?bZ5ry?f6F=h~^(@LgV9Z;P*) ziI9R}fK4$RQkZ%I;oQtv&+nDijGQTYeZZjN0I^xtWPIU#oT&EVbBjMmug)&a`kh!~ z(LcREe}`4G=Oy(Q>M5~k+5zrHM<_Tj@R@jIKIur(Jne|iYc2H2jcuLrEe?>3%>Mvz C$sA?? literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out02.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out02.ogg new file mode 100644 index 0000000000000000000000000000000000000000..293e1f27e432fbf5bb7d83b010e70f1918f92891 GIT binary patch literal 8304 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 UYlUiPd6s?@TPgIZfl3nie|1fF8~^|S literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out03.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out03.ogg new file mode 100644 index 0000000000000000000000000000000000000000..f2eebd14936e9929b61dc55f7aae33b357ebecdc GIT binary patch literal 7776 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 C8gJ(S literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out04.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out04.ogg new file mode 100644 index 0000000000000000000000000000000000000000..684556ac5ab08ee97d18a7eff24f180b7d8bedfa GIT binary patch literal 9637 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 TO1oSoP;!&gdNZA*FLD15SHnlK literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out05.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/shots/shot_out05.ogg new file mode 100644 index 0000000000000000000000000000000000000000..0deb6ddf5e9a4ae289ba93ece31d0cdfd9a43fe3 GIT binary patch literal 14597 zcmaia1z1&2xA#7DcZY&fhd!hS0g(_mfOL0vhtenr5+YL49ny_7DBay4AYBSbN_`vs zzxRE=`+d*7`{&Bw&8+oXYp>b+C|X*o0!Y9=7q0u?hSl86F617>!_mdW+V!pr zB3b;;kQb0YjXH?p-OT^G?q=RmOy%qF2p-@5Uso^kUq);oUCY|Zic`_WlFrWBMB@*8 zI$1g%ZWs?Y4>vCzi=w@QwUdjvtGR<4^o}O5PAAt)mTt~M$F>P1Y3{tb!BU1jo?K9~ZmLTFri z=<`!tuvkB3yvE|`p$nAf+0Kc1F0}nAJ5p$erFujho1=ORArG6>FfDCH)Aw;5f^Vk$ zQ(^wu2OYQ;aWqOZ0&$Ex_HUti2`(V3zuTe%0>Eto(jQ1^U+QWU?!&f`ujc%Q-egk%=LJ^H&l$;BDp_UD8O>uR%Co(t1V#N%HuYP_<;{`htYy zKNhqbqFe=S3GGYl%t#zbavhSzNy`1(@+3+|FDV!U`zSY4@VY3C!Ex4ON$3T$THsvm zk)(}J^(0^`G&Y&Jre5tYzkgs+V30EO1-=5#2%Sr|t;f^{TNh?2KL#=H`u;n7#6Y_l z&ro!wzNYhyL^C0w&!dmI=k^{iCIi+3&AIn3@)@EaP)UW7IoIv=pPkbIKoG$n6#uLG z1LeOc&WR3V?PIAL;OS?-tBTu)gm%l@2_$e}Ad1=fK@`8M*+_FOh0ECH)=cTJ<;JQY zpt*lN3N$L6S=3WB%0EsLoZ!+2%?G{WKMi-lFic20`X6QZkVct^5iFxT9ICt`>T2qm z9`@QF-RD2Q(VuZ&n)Y3qerHG?{NI7~U!DViP80OUCu2<_VBIN!&&4tS8u(wH<4n^1 zfu{EZgIpzp!WjGD5uf5OzENT+Wj+N}QvFd<_i-|FRRR5Re)Dm4^GOf$*;;em&%WBV z|2WKFw^^EY{}0c(bCCz|$W@tWlz)3p2214kN0AR@qZzcLnLOhyLz4;;)3yt;u>PCp zScT;$gyp{t+k6|z5*lwEnp9YyW!G1_+wi}Z|MDDZ7Y49^o+IVL@E@Mj2BVPxy{VF2 z;n&|iij0B^b(13hPXhozTNIAmpE#nd!ZE4BKdHi@t|sz7dkjdO=y^^{qpE*jpB^J`g!g;Y{L@ExOI zBpvS8JU!X!5HMlI_iq9D-q{#`eIOb7RJ2p&aoPyimqGBGc8B z>@nHioLCCEu+$tP)6f!d5`Yf=uro)ZNFib*0APZnf(3mOt1<-Lh*If;t<$I=;Q1-4 zLvRe*pgtH{Y9IkLmqv96+D#KQ@N_*TaO5cq0MNBTz@In^pP3Xuzzi%qF$~A>$&oP1 z5lan|m`@WcOvmvJ^6@LHsrM7BPpYZUlPRdzGAa-&C>*P)6HBR+s*lGjsMpvJ^O?`8 zsV|bLPu7~xllf`JyD#x7XoI&?Ak}?3-q&!-UO(Xg;`Q{^;-4W6MOY|-xW7?eNFS(Gj(k;Ki%m!E2q9I6nC7bC*47+ z@szpoXZIx*>m+NNtWSkWEQN&yg|+sDr6syqTZLJLeU+AlrNw=fWkscSyCAjnQ(+~0 zVIg}_W$|Sp`)=*0!iJK*%JSsWqTSk`*zN4Q4NirnrA3u{chrWG-7e?d25z4`Xn$VO zPD4RMgTrov^FXaVO?Kf<3v2(^^1jNSyA52`?)1v)?u)VdtD4|G>7Ko*w13=M za@hwm7(0g#JJQsH)2bY(!M?6vY)_0IMdn&2fh#R6NiHnft#n{(FWwbbR`=D9GhY(a z*CuyZ5{%THewS6a)4D&lZcIa}{U}*HN&k>z8u}TOV`q&B9 zE;_YH*`AabB7_w*c1#65Ggk>=n;AQ$Vq1|nVX{w`Lx!;Ch#k|hDd?Kei6As3;MvSE z8Eb) zkwc^_3+*CNwdd$cde}t=vg*AP{zt}oLM+HCF)$i|D2a8 z8&nAEk~`rAon*R^pm0@N{=6xhJA$sg$W)RmqGW19cwQrtG^8Wd+otxNjbWNF* z+zDr`h>He=Z_MdLz+=ZuKvvNz5#S76yZXF%8<3Es5?-@0=W54)I-yg1CqoW=Lb$%K zKhXfAP5?3>7JzAv1Oa;zMk8n#d8Ze);8;RAT0T=W)ju_#X;J||E z+|+nj34`Pj%muGNjh48BP{{2Z(6K40o)N<+c{9Z$#|T2fqNF212h3pizy!HPf}B!Kx|W*}JQ#*W<) zR1n*AU>`Ks?T!$uf&~DqPGCkg>^h(6Hn8AOK<2r6YkT>D002S-ifiP*6O(_pF#kWBC;;;;LJja~-9_)b5`oJ>l=|f8^}{&ffp0 zmac6H2)X~P0I^mqbl?`9T0{B~lnR^uZjS_bV4~Na0InE~HYganl@kP-CsP6eXJG_U z0)>L`0X;fa2a)KiqQc6U8-Eu%CUg)SAa8WW+$j|sP>K*nZuP{SsF|lS(A+7Va1PMr z5a6vz1b?s!IHYsNq!^YnrULpWg=sP9KsvVI(rr1QHB%-LHK6{%2r*?6&OfDt4JD1$f5DwDhz^DRZ6|~zDj<;9;pKN+#kwz; zLQ=VSe-FSA;2r=(HMs66l z03SCm3!P z2KzUd^-yDoRJ@{X^oDmax%-U0^A&j}3dd~a=Zv{y@|7n5P?p+3x@kC>u9 zY~D{N$8>vSMb_oi5rHB~Udbwr<6H0j+l}M(D-_(DGV=%S zI>K5gI`ba@_7f`wTj;2LMz5yfAWQ!f=0@IntKQO9p(6JydU>^2_%IGF*;BIpS;w#)xkkfaG;Y+YD-QqBVMX zrw#I)z(>Slyl6=ex0=bm<#_^csy<5y8Lpl~PPFioB)*4NL~bXKY}#3cGpKK&$Z96Y ziyD?uG%tu9u#?N7dufP0Pb zxv|~j5%ZVA98bUV6=3kuuE3c)Pf4+SEf?t#er4vP_yEo+Mg{#^rkI;QqAj>$yPWGi z!BOun(GZ zVi@-Aw_AoEulr9Kif+&1P^QaI1)2`AyqD{{w^a!)Gq6qnv%RWu7YC`2*f; z20};nyoScYITF}|&%Hl7oG!1UH0_7kjEv`#Qj13kV`r$$Y+(ml6v{QyrN}sA1_q?7 z2;GxEXbu^e^U7l+H*n@$H&lv1A6UQQIdJ09W>0BJBIf$`JI-P!KoOVRZJF~%*x|Od z$ozwHV9vJkYgi!gcgC@Uy$Le;UU3Yt-%4=)#1GTpzwuA03UDDm(Qk=_4UB1 z4<4zm;?|3Lvzkrp=yyLJ^WJlUMBvKhIDLRL5Pb zp38Oed(`Qa_s=G`eUdblA0(y(o&je2Va$#6oM>(S?hrdMbnX4eDv=4zPQEP-d=`em zQ;(1B@y`vfajrFa!v)njS|W=8wf)kHw)GsOp(|-*&RM4<`v`K%1AXm^5ZluehByKZ zrlMY|tIr<7fZjgB>QZf#W1>GDd91!(?|oQIuU+XhYjI3*<;QXTjC|((g`C6liIdwW zo3}UVe#0m}>!lR&?&KIt=+lXMzz>(z&EVwcu=??}8MBXLg^ir=ca&S4)YhT57fC9o zQxP>Kw~1oY4kl&Mg6cMOCd-)@qJ*nK(6_mx&mBH|o2uKH#J+6rh&le**69W#tM>LSv2B$72Z5CIb}p zTBb}AE9YNjbyQ>w7H@u)lUwi`#E;z%BXL5Ott{*Fhsc;8f1b7usc0GfA~E=o;d}`V z>xT$Rsj`t0!W@-h+XL&Bu_Dmo?EsnKE+$)s`M!uPWzo4Qxg1)La26@ub)BMYPPS3Xq zjHNNcenOL|Fo@cTK_Kw7N`n1_Ub(<(b?+LdH!rDjit%f*qt~zQPlumwLv`Nr1X)ow z&ojbT>@^@k7o~*e;$KaAuW2))%^}sk=85`!oNVvOlzwv7O&R9mp}fCQ5Fcl{AGIN@ z^TH%EY&st;<*-yd>f+JZg+G@VnGN@BvYdqw+fKNhs$kW1_V+-K!-#>dCq_OWessl# zGjj{39LEAF>wU4sfj^K9gxqX7AuOBL6E+-DCDu1;#viN8dUaoF3w_@_(tK`&uF*Ox zy6d86kzWQ$IrPCYONJ46Uk1340qRAQzvXi@%wO#!HRz30jqiWXmC~WHl(zlmp}K8J zxr6r%i2)6jSf_VQ|Ea70XP~dM%M;1fq2$bc_89tTT{I~vh`|Rw1m8-Jwr{kQA`&FU zzonE$LAe(*@!Fo4qag=7>w68AF}eI1QOi})5z^-C ztC?Q`D97t8)=2Ycku105)H2;dPo^?p(+|hLZ*?KRQ{322$f(U;BOmo6m-S4!moMM; z1N>wInLt(kPKCm|M?jr?%hUFqv;kVwyHK4NQEl(r>|3-Er?EV7?oQSn*5;L$SGn6d zlke*_j8eQ3meScLWv^9^taixOCf(O1sybSP=98YOG1T2Lnk6FS7$?{CX{$giE|JTT z&A5jisJ;MlNOa367VHBaPsp_K4fbkawv5fp-#}h&a#!bPK3o%z#a29finDMMYtTuw z8~C`C3nB*h=q4R>eTuBGw!3?=cg9+wWL72R3*QXY@!pG!ylI~OW#vf@G#x6@1u`cR z=4Q1bt2q-H{q)M6YDjAcJG-2q?cScCO-FZaCYxtX#HjCOZm^@sEAxt)f~;*5>~+e} z9{Aq^3)&H>G1gFCGI=nR+~54WZcS7Mu_C0htNg68kQ0OEJDVzY64E& z8b~b7$EI{vjOTZZAJY059;H~%4 zs?vKCIAPv~I?}YR#=vi!4iyF&2@h!qL%AL^Jk@YNV zBbuV7LZV8G<%6v4{okeniIIzSt7Le$T6p=)tzPrJW{L|`_XaAbs-aov7;l(#Bo|+` zmNVD_;!flykwLs0mbqbWf}be`xI)ra`x_3$p@)foBoY!Y~37 z_#Q)Sr-*-3;>Sm318zuNExlSUBs#WVA+7aPmrb)wr&q;gi%Q(HJXcXIY4EwF0{Pek zUCPZBzi0HXngl3VDW0O2_n)VVrAYe^VbasJ8|}(@JmX9@=1y)NvU(=fRH%);ylCE2 zEIUlXI2bS0bn}62#pL3*2foX8MXy`*@MettLtz$@Em;8?Jfyg$p+hsy^$|&5jFbDc zCu;4bbb&1za|MOCjot`P)#7*chDT#pwvqyjp6f@ymi_S>1qTdcLIh5nn9CM!A9TIF zO6ebt(WeeD0#Ac*1H8;{A93Q1!rXNMo`}bfWh*w!Hv>nz-icsJ_+`Ho>R38f5V8bJ zmWo1!KXEow5glW^aLW@B9183yEMs~(cH`q`(=KDtdY#?SRa>5pY3a8G=JA(9^{qC4PrmNehC{AN+?vU)y~2c zg6!Lyim!}y{?d4d&HaM&n}~fvSvcjsX3$Fr@$)L&{_G^vrgXRXwqY5O$JuRgtdj7K z3l|-usSYmHYGOtfsc?%dRsJwGRfb3%9P|V`lYcnVXx%fF;Z`0f(-~O*yl2q)_j?9pu*rBun54nP#Lvsa#0=x*;uUxVT7dM{(KS<-_VP@sx;bCUs;pS$6al@cof&zRnHZ~R> z9&RRf@RbD@jOX}Vm`J8ZGR4LlmvhCl&(YRpapn0aw?IV4ltWp4Zv(352qy`@HGWpVNy12(5oQNk6&0 zv?+Bw?pO>&e_F1!V19LRdwmNZS4~&@J}tqJ#y7ioZB~wSwdbLnHQ9)-=Zh8l!Ggl9n8&Stwc&%i7z2^$xA8k$h+OD1;^Q?0ZjuCgH1{UbGNT^f0q?_Y zdqLOVR@rzL@)M*I{2mlO8?S+@V=SBs)a_u|MJTzG>^zIzZcwhG>A3I|^=9HH6!K59 zM5_z022mHb5XBX0QzTTc(wUTo21Y5lPApDMW$(k0G$YP9lYgl%e!!fK86m_kopK;; z3|WHEoOs}`x>C$Wq&MS-R)(D|z2N*=nMdz`lO#By(o?Xjs5rXqGK$?tldA7UAc`6l z$_-pyVW}{(pmfZ8|9q^bjAy4~&Mg1Algq$uVyq|}PnOh&LB7sMC0H`T<|k2nHq!Bd zXw~(Qd82E-L4K9C88%V!?D2ya#8=R`*vs^qgttlZ`OXJ}eY1(@ZMg!+ zYX<52r=LfKLsD{Qr#kXpUX0i#PbUisF#pP#Jbh&TV3k=vcS>t_`^T3+R7jii&~dC< z%I&Q;uy$_np{7+npOg@>-25}@+4WYp<_4G9)XvFy@l?edfyT0hGPCVyOEq9~xaIhX z=lXv}s9E5W-6=Orfocp$SyYf!@Pp#!OG~u14m4DjIQ|>TUU4xAJUVX7%h{ zEyEl@_yoagb;J7V!HZT1>o#_F>-WUsh7zsf-6LUyWAa#V9%*C41g)|`;(67WT+w_z z*4{Z*sPjiRHkJ6)Gv(;`cGzHyyPn&{xIFgu@2mEl&Q;Ux-J^)^3B%~{K?BbK;^Vzi z*40%CZhGjZtwnSwZiAiR%R>7t{l>YsA0rJCXK<1+PF`@QY7eUFn(NjrPs zc&ijcJ&GswQQ2St*ISpC&*|PTy}b5qubo`**w!xJ!MHwVqLn>C;Wxk8%U!$q7-9-~ z!4=Lh4$6v_RYGA6Nfa3;`+V1g{&1S#;I|;~G^;j^#dEbHms{wnU&tQ5O>LS6>bMVX z{V9UT(IrQ^9iy(=(}pPcm37saQi6W-jGn<)^P};D*-ywQ`ySNx0X>*wXD=Yz-`QMe z&6yCc2i5((u}bV77e*gDHW}|Per8@G)t#bk{`Q!Z2DqZ}e>_UrPO?{K~3c+5; z|D=%>DxGFezc@Hw-!(Oj7jY0Wa;SyV(`<^Iia7-dx+<{?^Qw? zagx(xI^XHmirN?Y1GHs{H!c?Ufva=Th9hC>yf=+o$x%Zurh2`~7_0iVpbd>pKVnbP zX;(unQ*LAuj)jWOZ%0Od@6zsFd5Bx2CweUt`_TmGVxf~$&$v??Qp!2*4iNioU)N@w; zWpRtv)Nrk%ja{iMDz2io$JYpf{hlZWKG{&Orz2N^V1=cO#$+GM&N z^{aZQQS|p(%7dws@o z(M!}edewW$D)g$do-vuS8lOw7eIFC{_YWag9~OO1af}1@_F7V_4b7e(e~`zNnNCU< z&~vUG677xgn}@%rYg}D&y&^C*EQqy8Y)C#z&spp!8h^L8baA%MgBPp^2Wmuu_$&F+F|EL;>EZT!i;7UZ;W7qutb#v= zA{Tv3?S2!^=c{JSYu!@iG5jp&IEHSa(J!kS1|y-^!11|UE?LG#>n2iC6on2(&HY7{LG0wRu@opiG`)vwP>+&@D0Q&XM2!{e+gMG<>%BwQVt zOzP7hd*?0azr=ofl2q89rp2xkQ#W>aelz%WBY_q&J|9lrO~85MYE$elv(uraNy>ar z>N!>i52BFmU%wI=YkuZ zD96WF5sOa}eqa(9FX5mTyzV%gXy1vz#=BK&*qL8?KaQw<7FjdLx(BZO_dxECw|e}< zz%#KSFKax>!j?phC4&ur>Nw9ALY38D-|(OV$0*?seCz06e0nD;8LRxH(r%VFLZRQ! zuLS`@Nc{GFAz z9BJlVsv$GVF_wXRb#;vJ;%ir5jbJR_# zbH73qXO)}9_phqci$5?c=4$9Z+ygTb%1p}c$)rpphTB9vh1nV=VYbKK>0Wrf={}$8 z%%*g;KR&2N+B+}ZuFe0_A{6n&J-}rw=3`1-*Oa2+P~)nVP@Ud%afSj4J!#BbuFeNy z<|*X2{yPXv#|@_PXH~6w2=lYKq^6u*aL0zU~+LoZinpa9v4%%ynBLw?_Y}uwJ+b z5srR* zLCvt=M_J{)HzAia8St&GpZT*Z95*lYb{#5z^^t|S+v>a{9H}cEjAvI*=KyBNS0A}H zju&Z5Q6=d6Az^OpR|B00^xsnCQ>i5>9wPDMN%s8rDA0(HN&Kn4dv3ZA;MyroVrvWPeLIuT;`N$yv)-lBYwahoqx~4qQY9L)%NnG zc=o|uYkqxi_liF8Ew;}rqBv(}b-oa1fp>i0nC%I(?^do*E9Gz|=-s8b-#r;;T*C*> zdx>K2*$!f*)SKjyF?1g3&_vFQC>DA2G*6kI@x5tSlYUL{yM7T493S@~{n}%x4N4r^ z2@J)B{ge&9$7Eb68AR5%eU5tnyr4*?W2)B3QRa8x1CL8D&`}Ti5-tdGWSlefqbvYD zS(v-MuLbmwyRn~&T;0Z47=>{uL)!@7MQ{n{c+11dvY!W26)%~G=%o=EUJ{{{qzWbR zG&=R_K|EIYgTr@+0=92gZGDig4iMbjDul3J0q-FCqmhJpMO7ifdB@)}!B4OhNXn5{ z%okW;kOt1{|@Qe~1*fY4R+;O0{ywM*mt_*2BS> z^$h|Zd}#bN-YBsk-7rITCsM2=1F0&Eg@0Cw-q;n2^6t%G@npog)vSd;e7vZ3VPNR! zi-wD7DH~P=VqHH}o36hUWuo;aZS z#fD(eT5fc1ZJ%<2Mp2Gq*m`V-uQ7^_rwM%lOd&HCaNkva@Fb5S+Q7^1>I;@^h&t;m z#*!4JRuZ;Z?;{DaLt8udpg=IXgeaysBXO><8b~o#TXo|zzcld9+cJ_DZ1MSkWbTdx zI;BiplnvTEI|&V8t^LyeJ~J$4C)FJab-yY``7Y{O?Os2Dc6A>1DioGN+%*v*4!=K8 z)%v7&;LW^SvDBlp>fwtynTA#qmm>6hZ{zOcyh40qAKGB8<@d0VKIEA+Nw)v1g3lVMboh;5zjwfy&`>)&(ULuv^Q9T z0ymr-R0poMN(QBK^hKjCVSbM zPg@x|ax{Sj6918b&5%iE+siN%lPI0cN2G34fog;H&JAz;ZX>)BE{NoYws_nLGAqk_ z{GaAfHa>ZV3rh0HPx^G!1iL9hR~q{6RMqF9{lh2P&END}$PQduy5Rm$!6yaqORf&= zmQ0#c5&if)jMYEw)ti+iG^L?SIa3!kGdF*$S9LM(nM+~??XWx2lBfJ@Qm5r=R5w4G z5*?o#hzv^4uzor47LlEfZ<0^LqoO)`p5pVNWhSvjUg)`;(kQy?F|OoJ&33UF9Wn}- z0~u5`yf9uO)uHtN{0i+jZ1k0f$_C1b@m9l`dQy@HWAkt$+HOaKkYybbbG5aA5TUh?rL-eL>u1tWXl2QQZG$S|OuX(YTi zK!A^6j|iO+T#UN$`~bI`kSnV0I@Khr@S%J7No%2&arV+?>eK3}8=YNLAA@?%QPz(8 zET@sV+a~r_&^QNEBLs1A7iYejR>`#|XwS^nF=bdC&SG7iH_1K|nN7tJJpVl@R{#B2 za{lCGPmT4pYWMi?Z{wK4DT}N5z$xjE8O}~4Ez|Vt#GB7K^$Fwc54 zC^G)4xtr~QQ-j=ZM|P8MoaXTFljH^3g2y*2$<4{Pa62$nJP5P;J}b6CFXHJtvh?~H zQ>y!-B2vt{i?xcd_4}h*=@TrCQUcdbOKW_dl<5m<2cvF$HhKJR10Hc5 z7bH`4mcwgd?5B@VwNjlDyVFTkPM=TxioTG&QJ}u~0^_G-O#Hd`1}{nqoWnF4-)uEeS4db%%gR!7CIH{0P`t0xWDcG?r41eBT%rWA z^(HCEj~^wd&^Z>I#j^!&^6jo=rTV<26ZLI941WQ0%6R<(pEMSS^BRPV3W4CjaS_{4^2i=lOx8 z*i^k|5aZq?!)Szy3hce*xREKc-sc`bIV~q&Ws9$M=UMz0t)NLHh@bn!u8#)SfnC&E zH7&6XT}ha72ZB#EV`Hgh>^ec{{pI~KmEjP5+=tjf3BkH()AOX%atpRjT{ov(@-qR-pPrBQUL zhhCdUpIO@~6GZPbmYG}U92K>1dNMTZqndND3R2D9_Flhl>2=+ZP|&-~CCRPg7cr}U z7)boC?CkiLYt2Y%R_JdD2zpEw<7v_^suedCKX&`_2_0ctt9JI6fe##tgP)U_FLh2cEZm>vpJsoEs#YYlo8wlE9&78Ur#C&4?YAzr;JOD-OK$!BDE}kNEX1 z_l9ZuET@~La8`W83_1Hjq47Y{!+Q1~;pj%McJ9++#UQ1b>%CvkLW)2_pJr_T$nL*X3u`3R@eV_x`!@Yz ze&|N@U}pf=dJ6v&ulVW_B%RcLN})|1t7M^KcQc1lYT^fUWFXIk2gk*1>8D#Q&DRsF zxViAPP0CY){+EeWtx*1%0bgu8Jaz9Mp zKT2|t%8wT?(G+$=j?7U$XNQQMc+%%*t$?tJZ9ON^ap@(pwp8Cg8EQeXv zJkq%!8=S(zF*$55iC)x}G5Ow@^gdznDjFYs(16a6TwpkkQlZ02$m~Z1ET{qXX*v5U zBt-B{yQeJY+>OcN^Jj4r?Ap!|*3X0CLRs7DVgWW4XAj#&{T9A;&ZIG-=+V6-4bCZ~ z47`s&dCqs87VJ&D94zt*NHh1bd;d7oXr}E|sy?K;V0?};p8{CsbAMtl|NOcV?l&hh zb0b_i)XHF3Qt<0li$iQFcE!n${~C81SiSm*QQbAqRD`%tpu_6 zMo7;t$_JNa1vC;*oQ*M(i%=@^n(`c)Io+sSFO8cmu2hXE_b@$un(k3{aUJa2$cLY= zP80~ZG21^R*s={13&|K#_h1?!p{AK%3FSXlg-~m~Hf6U=gc5^)>NcOgTt_<*EUeIZ z_etbisHf_A<>^UIW0k#&Q;#|IWwO}kc-gJ2ck%1oMu{39HROsjtDkL+Ti!Ev8~2N^ zD?GsDRazK;`*StFy2*MY=;$3F%a6 z1mrv5|Gn?~-S2zuJ_V=%xK%%>H-+RKbL^l-$5QB{|tm4;^pFI zY3F`51yL;iXNfoDdQcD1zS{X;*VWD|3bT<|1gX^J|8@0X{$<1s((l_ne#EEkX3OYk zXK8TFo>7faNI+0XKuAECkyG2r+3v9$${pqG0lVT2!T!4yrf}B)0^or&f*On+0mG5{ z06+==4_RR(F*a(j9CQY!PXb!?YSbYRl7J5BfLetKcK)kl6tTVq09Zf(Cw@fMs+#>E z+!o3a;g$}!(~`(z#0}M1fa8BUWVdq8t(ziKf80ic?IN)&}!1w~&4?Z61D_8yN8 zx#)Vr(Cf=rKtETFIdX(xX3$5-uIbw&t~ z&H|5Hf@Lz_&ioF~#0-30i~(3*nS>EPni&?IrIu@BUuu_J`>3easjrf!uaf9`3FuR- ztJ47Kc1gMaPqQ(KL;def?ol@vAP3sA%ayjvl~oDJ+U3TBdu`!v0Q9LGlBdH>M8#dC z%Uv35DjteGXpMmbjT`@R!j;%}!* zKt-5RZQ|?$@Ps2RuFVnvvV~^g-6i@f30x4IvCNn>oU}gxCrMgKFQ7;s`xv5^%+XMg zkbLby$3f~P(3h~@#E$gD;UxD#HKL@Pzat;AWZaU1QE-lWErq0$+5%i>H=2Z7Fnu4~ zYc!m+n%_VHjzZAM95oGkfBF3bivqKh!H@7oa7V~YvO@#DDL6W{PT3v6znc5+@R0-k zW-$frOnu4d7lC6*!IaAsL+|m9Bqm+38+9700<<#M)AM8 zYbgIkadvblS1)H(zfd2~Rae|TD7jVMPI{L}5JWMLD2U?ln$D_MmIp?~e>(0b>(EWQk^g8zzi4&H*uXX-#H%YTqpzoL z=;dS-?>So+WIE+JKj}9=8E#JX`o9C~zaj?!gC_7gCefB*f?X-Ev=s3FI{06a<4VyL zMcWg_s$R*eIm$DzC!)PCGD5DTBciE$%XH+H=NKhQSIl%w6g8%g`r?I}u0@&D`5D#z z6EJ_nW`5H1KO*NUM3~_bORCXW|Bjq=&WP_45iDxatVYr7K5@1oNd<{%8wHsJ|1EMJ zh2|xM<^_kY1xIj(#My-;6*go#_Lgon{;%!7B1grI6>MPSD7mrzN942#(%uE5sgg%? z|F4cRBcMS&l&JpG0RYezNu++AM|6<9Uy!0-ki7bOGXHbLfYdJ{nqwlMVPgS+8UR>8 zCkK*-xBg}J?V;rg7~TEZa7Cy%%~cSHx4QcW6LG8T1v?p zRqM${L)Al5v&pPNO2AD3F8CqL7>>LJk)r?rOClrzEC`Jpgsny*dj(f$kqCHRitZpB zk1nuR5GVB&DJ+LpcM#S^8`v+ulJaU;o)Z8V+aTaafm6g<2_WSF79O(>#fYd=u&I+P z4N;&b$u%cqMFvDfb@cT6$o0SI>CaMX>esSql51)n=;@Oy>EF^Hi__GvaT*dqP3!4@ zqtyRWi<+f;ZW!k|FRW<<-cEp2&&fDH^9d)@gnw9RQe3kV)1Ryr)$!2R(MK8S=^J^W zjE?mUC;Zews;=hrEq(o3{pmv|{W!lx4}DWZ)bz2w5#@7}$)Lp}zeVU3=gBXgAk|_5 zWl`rj&uN!rXP=p0n8aCFSWsB&R9ISKlDS@(S=d`?TUc7$TUl0A>bM0`OY;jWc?t`8 ziYklG3VF6_^9vhGdMnG5ON+K@e-O6wY&AYEEG;dn+`ghVmTYypZZ!%#%Z2sj7Hu{b zG&VYKHM;iKI?-koZnkjseJby*{IS)@U+u}Hqwo0*ZMtL#>fvPWon~?zROuwuT5{G4 zG8jF954q4bfZM8^X~4P8eQsF`iHIEAB=Dq#CCP3Sx)bmW;;Mugw> z4#WoJ3LE6Tav4nA7y%Lx0EcOfLYI`QgVBQ&xyhKVCy2x3u7&8JMc+f%Goi=1IvMpM z)Vfn*$PkZU=usqYM$SEiLk4;R=}?h7Zn?vlO^LAMMUNWW7j%vrhuyRy6%`lc#L~$Kvg)}Kel24q0S&TBd=-sAl*C3O`%_}BSiOossKIKZ5gOpB5QvH( zq_LymL<-VT6g(BkDjMO)+i7f~QR7K!On7~&k{SaC`;tyjI9m=>9Z^w3>EJB5dK+O^ zawWW=gVH1d6t3$anmb{CMKE!anMiU+luV2x!$E?TQ}dUkr$7FsxurdC=Y(a+m2j?# z*l1Ar>Wpz196f3YvWiBAfjdkb8*=09K|&JpZO!V8yQApQxN-5740Z4c;r_1S&;ZOj z0hoYX06r@HDurByt$Z+YP|c3itsl;Xuh0+YC~(tIfWh?p#)Vq4C#On5F6nGieMCXGJ<&rhdKxPY8aYLVMWTE0|ot(LtzEl z7V1u-z(s8}D9;HCyoca|1(71u*mA%Emw*331&F z)Ie|)yZ|j);t4__r=#E4zNC6e4zDC=LP(trgn~`U=LBQ0g53rWw0)gN*nmh}Kn|J( z%7xpKTK2=&X+^-yNE=AP)tT~&L;>#>cnt7a)ei?dQU#Cz)_WPRz%BO~5|s?+!`qNL-#Ln%|vgk`blBnaY~)g%W5NN^Zv0D!O9xIj53jglA# z6rW4P`dWzqM$n95g0CqoLj~7T?&85Z62KZK7kX8CfDhTL`XdY^fVZ6>0lr3i)up&c z{JSvu_Xz+0qeKB%XWcXapVpmF8oa+%xiZ=Ht(D_HMS8;D+yAK9|6RTRPa_kD5)g9# z*#UB`1h~K@E{%bT1nf2;&y|j(Bw(S}o&cU0hc56nY&|;=bWg@z1e}u%Lb;^DVHqp)4krEiE z9vJCHYRPvK846daK&eu9K);{`b0~C?4(lnFVLIBWRtUlNVzdE*GwS82TXBt7Klnz; zhzAIeG6}zsgHS-bK|z67Fd)6WZCLOD7S=ME5CIWESD^c-9VS2;*i94_e4mJnyzoV) zr6h3w!NYFc0HW~l>bHdJLT%WlUp*NE8L^mxJ0KL10k6oNpL{3QuK$QHsgy(V_XQXN z&;vj;F(Wf`5RQ{5$UXZWE90`V2akF!BaKK=k{362hoSYn-Y;5cT;!={6 zUq<^|TgSOM*qXx9grT9;M+bI)Jx7K$&%k&Xg1oa_&hheGrWu2(J^>Q zLP~TV*@SV9MVK%}Vxs+o$5Qku6e4bi8y-TB*OwCRU40@e*tni3>Fay=c>(JH>o8@C zsxH4jMwcjnkX?8GwdCP=VaLWsSN&OQ4$&S#;FPMmDxe4P6E5FP%D5fD@xtM8^CJbl zubsl~ecIy9$fo0x1(-+z#YU(F1Ph{0bpz`r^I)x=STdz6eix4vjUBgk>$fAuk`Sp) zy*|cP-Nf)3YwYv$QjOQPDoY4~_Z=?17OdG9XUqO~y_A1;JA9V-B3*vxj1N)7e8&S5 z>%uGJwTjGq=&#FZN16w1Ik^6%Rc~?h4Ue%D9H8wE+;-T7gxq_CMEKsNUQb~ZM|-~w z(o;4%zX=N4lF$=oESZcptx3o=YL^=!jK(gfe|h@X zgc)7hZx3-2Rbt$bCFR!L?{(SF^D{$sGm#d=E}g;@A+h%W$3rirv(yoqbNjhEnZkOf zV-8(i?{?QR5|wQ#|4=7cw^^5O$~Lzb%Vj6ZRm;@aTt|UXC0csho=Hk(b%!_ZamT+t z`1HcJSPsHc34rVsaWj+c5K`fsm?Pt=p4`<#hl|+d+cmSguYMfD4EIG1=?3~mo)z$* zXFMIaVIe_@p2O92^K3dI`A<&AzH~pCnc+utShq$3&cDBevv>m7>o*cna;>8Et;T)E z{kfZ)l{>2VNmYSX-9foMzXSlIQ)zzDEfN!_!x;G|4a>dApXq3fhQml6V?J8Z0=qgP zZTFA-M?-9jhV;~r4SUCqP`G|4!?={qWI<{@fUlkb948DYE21C%jY(&d+#)PZ1`r*V zCWdQw`jZPX&)o_jN;2X<#3--Uy`>w3Y~6$Pnm22}2fG*16uE!{bly`e>C<*>f26U4Z%_XghMr_DYji@GhSK_8Z| z-+0h0f_om-WhHcXgwK1A;bzzPI0Z1HM8pAG%;hK?Qr}1*P;I{9x^p7-88E@r;M$Hy z>@$y0$TdGC-sxMai<|5V6t%Q%ed0T?YW<Q6vqJlcipW2#jHAFE?a(Ei?^#oo#Xr~tCQUrJ(SCE3~87`e6e zCnZ{XykS$aa(xjPR9K)P&D<2lRwshGKSQkDd#Hn3#qD~;q zL6Ds-!c>kCJxxZ5H%Yaq`OPK1$>Cc;hJ%8Li`efdW0m;d?|67oY=QEDeKMLdx=a(M z5YT+8Pg|?tK%ggmbnZA|xb~_==prc!zO^{ezVeiRZcY+z`cPY$@e6>0-Fb`G=e#@kKrQ zWtvz*%_$9B=~R!H_Ax#0j)KMB{rjT=#I5pMkpWw*-j$9YZrDT*5UCuVDy-!ZXashZ z){>SLlHW4v(EEM^KKznU$^Us0w-jw2Z;RPIAz~QAt4)inb8y<=HS1^&={v-r9tm_IU_sw;ZCdgD|q9_Tx0!@ z^@WQH<8?ou$fn?C3(Hk7`Q%sGCfrc12(o(|qki!PW%Ke+xq)iaC`(ExHV*I*oqJjH zhdgo{P;391_lr4hW`E_Z_))W{GCBQeOPfhtIXl;S(kwtMM;=nejgXZ{N(s-ms=#A$ z-+N2owU=YsfeG)YS;m}$C@ngDF0m|a58eEH&*;E;t4LXwGR!>2^+d+!EIQ!Hqb*?#)g zC(}t5?M##2hE8C%KCB@)FE+a{+s2QEh5xuYxe?;SyI1RlIaQZchWtoywqHy5F8!f~ zRLT-RmMa8Xz9)SOUyL4LzO~TG=c4oM^(O?(>7BzbF#*&|t{Z<)daF%)ogXd1bpP<< z33||*oOrl)(WW{&F%fbSb3=THYRv7l=-YM~1IO|1GU3_2aQs;R_D(KCU)2ib^gZ^U zndi)52e!6Dwf3oKE0pQR(33fe$>(1$=X_VqgA^rO6-Q+z_2>GAqdsqKbep!G8TeG* zZi_ULb+<};dr?#DZ=$AiUF&sTI>6E1HiMb z$-Jzkj#(j}mE*o};)?x5!qe9@Lte21ygPf&yGE%#A@_`Qgv&GQxpWk`1ajJ^ZOm6y zWCKLF@f@~Pm1vQI&5u&8G{A3JJDFn{}sJM-=DifV(_{4^4Fd0v`794DI} z;-dmmameNsYI!lNq@%kI6r=I;*bUvn+mGXo7s?{aVx7wNt?vpU9%l z{uGN_(gJe=-gPT?OQQb$ge+tBHq1VhBTLEP5SH%idkp~spJVU8)h@(zHx9Y zUvJuZ5EDDKvpcH+5M4-5c?|Flcw^jJjUgBCy)RAjlFtz@%AkJtBLkO9T7iH1v66pX z+CDjeh3)q-+*Wfhp>XFbwyJarrE>TZvNEpj_=}Gt=YH?bi0D@(pExl<5&y-nu5X`K zv`y=MsM2AWb2MeSdR*RK7zVNmseCPE>p1+ySagc`QJY1~a7CK0*=Iw<^#ts)sT$ui zekp6bR%ut@#{X=1o2@=F|4AxTiB7 zWk*8!MPOqt%A>&H@-&yx5jY19=^gUSn56Gj$k%Gq2M_>L7s|HHQ zI4gxyFFncW%(aHmYuoS$Of(oc4{=yv1-HH&x2-KSmkks@jrYO^NV`uvV5<~|S| zz>W{;}PymX>#<`Fz8%e$m=m%a zz`n~hZYSv|_zN*p&ra zX0q$&f4c7rT5Y2qq!Cb~X4D%SjeEKDEjk<898k-CNhBH;xl(K~Nh|SuYC4i`M&_OCGQo&i_V!jXJzO6zDgjS|^VoHB zan%vv>(CrZI{qM{8P4yJE(Uk~!zrC^XaSoRk4nUln_Yb|ZI@fQnj??9v^gmIzO3)v z9r_|K_Jh+-Lqn0wwdlq`u|%eegxtuFU#I3*`HaP-2>pqK+OFJsE16I-*dcXsjzcGwN|(Y*v+Vk#rBHAYrzCiegl#-67bA9@g?*Gl=yb48Ef%f^ft@5}~2x9NJ*^{R2N*TUsCuRN&8Njc3D| z2wg1(Rc*-nOgT?{ND>S{M9BVelq7He3eWwKr)eQG*pO<+)tR6=;Pss8^D9FsgB+R7 zF6z~SvbZPnJvLGYs9_@265R5FaGF77Q-kSACCHf~O;)=l|?2ec=yY!dJz1kjr0{ z{%<6inwzUpemGhf;}to^Bher5rn9&5f^uDmxiZ>JRz41=5$uF~Pa(N7^}Bs7Dglwl z3~Zj0Ko*0Rx`Punke}_e8sE9iVbr(X=ksPrb~`B0PNt7Wv!?jdCbQlKjAo%jtwZH` zmc-W=8YDH(d=}+~ipQC1oHoP);>5=$dEzBS*fY8taoJ)u}*OdBO}!o^bajZ|AMuKXBmoL23d9yYY= z&*$UNu2rGcvV?cPIiV!+cXW*ZR?v>v8SioL{D#(;p$GL-rW;Hmt-W$(EOc~cxYyrd zU;%#r{tg2Z9AXumm9os>jvEcnU}b?e-)6*l z4c{MVd(Qrx>L)H!{qDsQsplLU{gB+su14n8>HLTrr`;5>+j9v^hm}FDA0s4+a!&V( z8hzc8;rH&fJNHF%28=BJ$nz%0iY9C3*5rM~8Ef(o;WLX%|7axSi1`GNQh&Mf(lWdG z0sN-&uu*S_9+P$k%lbe%su7z}rbcA^Q=_1BgR;Y@OFQ5#R9CSaPN} zu23wskP>G|_}$E9aEDL{5}c~EyIbq(W?Jqqz2(yyeR|?^=$-h!^mM$i9BxL>nqku` z{~4ALlx6dhZJutXGlXl$lW>~(!a%I*V%g_Dkjv4%lUlyQWYROzn%;R^%V0v)NQmNz zG|`vynnshg;8GglsrJaptXEo-bB7jvH`{m&{j(_~_~82W&`yf}2B)9S)tU-4N%Ldc zfMMHiRq+imf)h__s*qm;as)A4`T|&-zyTKFOQYO7&jqV1l5!r?YPbw&s@1xgNdGv= zkqyR5t;4!$R$Na?0nPX`+0s1J6lun-Z`N&I!22Ue+cB6wik(^fJ1S)$K?s{iP(qS!!}s!c*iNxOl;tNWuZJz5iPC&CnBLpg(-8b#js^#Z9L5XF;8$_+tkSHkh?Y;)^WLp>a0geEZH_BP>#U zQrtPbM~t_c<4cE>zOO9byGX0e58NE99{AOOdP@6F%TLH{1dxg-bHlZF1h&`BqEHpR zXYX!4y=%1H@uoVSq|@_=r`mI2i1yIT>`=$}`-%N(!qS+#27#^pP8_|UYuU73fU@uX#SW4`5slW*xu&3LQ>z+0?B_%IBvWzYYO zDBQ!@|7X3Ae6HrLi`pcZzr`FyA0^v?fAQGDBhj~X#&(lsBESJ&vzp^4s5iy{{5L@_ z{I>iLy6GXE*LfbamvZ%ONC&DE!LMUTsk~C+D+fY6O*^ z(-%V>6fw{7-d2k4)>MsF?d$rz347_cJujoB$-Db|LxL{PnP^%jI?-uS0}9RO(dwbdzs zBoxo&QOgt`CEfZHRh8FM?m&WG&Pbe&BPSsE%=?UUEJlF#TcW|swZWP$j{(2rs=G>> z#-=D8Y}B9<)~HICHfF}7G}=As*Q=2vv6|fl2B%aPJ_4k z($_UahG3GP-4fuVyk)&@W6DyRU8~>s_5F!R-MQ^P#w*sywK={Y-uHer&9GRNKFmzd zht{he%O|C7xYrpd_iItuS}c@uCR9VR9w$W|$33${FOMF<6L_bYp+wn3yC)wkK6BC+ z+MMRbQc#e#ik9a!qpWfESz?4zDZa;g zKf8=#oiZdL=S1?+W1ZvF?G?%pDCF(Z#w`~%uD4qJ93RXkvomz=5VgvRp3{EJWWH63 zMU*$sUvX37?AUmG@u4ELR;h{XY?w8#PSJrq(tgA>N%`U}a8Bg<4aFm2E|_`fCj$(> zsi&k{ebQ^;LKk!RT8sG4izEMtA3?9x1EbBEYv$K&C-p%nhL14}kuBebzi!1S#~+)m zZO3t~d2nj3!Cw(yS6vvksrd^NMAH@pAcaR*97m%vtXyk}RxQXIWGV%jP3@7zZ+6>Vhl`VqX znU&vPTE@1FuGX!8_5cZT+4oy1a`V;FaD3_# zP9UN6XUUt@bHx63H)hq4nY!?YB>o!I)1w|^BC8bRx>CVk8y~C&A$)X6e^h4YYT7;g z%M$4$%3;y&ej)eReG-Va-X(unHl(M&XX>iaL81RJLxwObrgo9mNnG~rO(aiDd=4RA zqCxn4K0|spdiKpL{EXt(0a_mQS`;~xhu2C!_k-B?Z=HwDX?1h$8|sY3i_X_6d7OL* zFq2MtT_bsL4lIi>fNiMEke2I_A7RY`bU+A-f4prYEW5)L^rS^sqiN{{!No3I&(0A0 zfTEe-jA$^lU9^j$jpRn@R0NeK$M zAuIjJ$Tss*TISse*735=6tS_;)|kctt@n9lsK>c_`fQGmDX=Thx{=$w*@IuJg7ErL zHzK_2GkD*SZ~)$hj2HPXx4%qVc3I}49(8_e^OeGcB=&HIHdK<|VVSL(!@cuB&`z?l za%lb8Yt7#5Zni!u1f3{eG>X+cd47$%jZgVz)A4E#Gs5vi*x_!67;3SuQJZvX7 zxgeH}=VIX^?Yom~MS&})M&!w_yiq%J_%MbL?`n%7zn{}iLHXf+-b14z->RiMxqSA9 zlgKAiS7t(YD2uA0EL9l2`FWXfd@WI$w28^~T0J9ojqrN$3c{7M%;GJ9TJGxYOKeE=`ckP#Gdhr+25xK=t%y9@f*qWmnQoVPKh}uSLPPr{KVI&_)#sc`Exuqs-R< zvX)MeVUjT(|KW`_=Sc(BHuS^K)zdWs!L?5u_?~Z?Pb!Q zoh6$R*r<9iMP0;VQ!OzF0YA5jnp5HI&iA4SxzPZU!KB;$+GFyKw2g7$GEz6~dd8Y! z_J!=cgC6r|Z^TMs9?&-Pk2rU&(TxxzQD+JdiDl`i15xqG#euDyuHznu%nQyfwO>c2 z#H-IU8N$#22yb!!umtZZzGSeZ+In|(R?wgjE!G*orNoz(uRXdvD}{Lgt=TLuX!V!o z6W0cm+f4oAa};AuQjVW4PZzm_p*K%cc1*Y-2ef?*&z<-VaOcn{0EOY0{nlQ{Pi!# UZBr1Hi|MR`@fX3;CSj2O0~^E}@c;k- literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet01.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet01.ogg new file mode 100644 index 0000000000000000000000000000000000000000..db85d9bf4deac462e0830917fea284b49909c362 GIT binary patch literal 7353 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 d_YBDv2$aNq0BWAX3uO-$wuM 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 R$^~$p(!KBu7^6(3`ac?7-nak& literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet07.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet07.ogg new file mode 100644 index 0000000000000000000000000000000000000000..241281ce0b58617b27b2bd351691c91f259e329c GIT binary patch literal 8810 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 E2T!`I8UO$Q literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet08.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet08.ogg new file mode 100644 index 0000000000000000000000000000000000000000..c57dceb6d0d581b4f1879cff379d0bc13e6ce2a2 GIT binary patch literal 7790 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet09.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet09.ogg new file mode 100644 index 0000000000000000000000000000000000000000..84cdc02cd654d9a2de66b202cbea9c54c37b87df GIT binary patch literal 8610 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%0D}}|Hk)Q zziarJ_58jI5B5Dmw?-B#b5lP#{mN`hs-TeFj|~lp(~8`IcM_6_E$L|{9vtVda%u?r zh;8dEp4^F@8wm3!LQ09gX z=vlzwaSS1W4=AeWu6k-`U}R)am$J&~W^_?z`RNHw*J63`0K;*XdH;U7_1D2dRx)}h z-z;H)*+}yAwGd#=U5JsDiDU@zcDZX^TTbD?9=ha1um8IY3O%FaF9#g$9%e^9Y~el!oSN^ H0FM6y<|NB# literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet12.ogg b/src/main/resources/assets/needsofnature/sounds/actionsounds/wet/wet12.ogg new file mode 100644 index 0000000000000000000000000000000000000000..66906e409d12bce6c76c8d04e8c7857a718d50e9 GIT binary patch literal 7201 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