Repository: toxicity188/BetterModel Branch: v3 Commit: 37d7fae20bb1 Files: 608 Total size: 2.7 MB Directory structure: gitextract_aj8f7_rc/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── package.yml │ ├── pr-test.yml │ └── publish.yml ├── .gitignore ├── .idea/ │ └── codeStyles/ │ └── codeStyleConfig.xml ├── AGENTS.md ├── BANNER.md ├── LICENSE.md ├── LICENSE_HEADER ├── README.md ├── SECURITY.md ├── api/ │ ├── build.gradle.kts │ ├── bukkit-api/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── api/ │ │ └── bukkit/ │ │ ├── BetterModelBukkit.java │ │ ├── BukkitModelEventBus.java │ │ ├── entity/ │ │ │ ├── BaseBukkitEntity.java │ │ │ └── BaseBukkitPlayer.java │ │ ├── event/ │ │ │ ├── BetterModelBukkitEvent.java │ │ │ └── BukkitEventApplication.java │ │ ├── platform/ │ │ │ ├── BukkitAdapter.java │ │ │ ├── BukkitEntity.java │ │ │ ├── BukkitItemStack.java │ │ │ ├── BukkitLivingEntity.java │ │ │ ├── BukkitLocation.java │ │ │ ├── BukkitOfflinePlayer.java │ │ │ ├── BukkitPlayer.java │ │ │ └── BukkitWorld.java │ │ └── scheduler/ │ │ └── BukkitModelScheduler.java │ ├── mod-api/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── api/ │ │ └── mod/ │ │ ├── BetterModelMod.java │ │ ├── entity/ │ │ │ ├── BaseModEntity.java │ │ │ └── BaseModPlayer.java │ │ ├── platform/ │ │ │ ├── ModAdapter.java │ │ │ ├── ModEntity.java │ │ │ ├── ModItemStack.java │ │ │ ├── ModLivingEntity.java │ │ │ ├── ModLocation.java │ │ │ ├── ModOfflinePlayer.java │ │ │ ├── ModPlayer.java │ │ │ ├── ModRegionHolder.java │ │ │ └── ModWorld.java │ │ └── scheduler/ │ │ └── ModModelScheduler.java │ └── src/ │ └── main/ │ └── java/ │ └── kr/ │ └── toxicity/ │ └── model/ │ └── api/ │ ├── BetterModel.java │ ├── BetterModelConfig.java │ ├── BetterModelEvaluator.java │ ├── BetterModelEventBus.java │ ├── BetterModelLogger.java │ ├── BetterModelPlatform.java │ ├── animation/ │ │ ├── AnimationIterator.java │ │ ├── AnimationKeyframe.java │ │ ├── AnimationModifier.java │ │ ├── AnimationOverrideState.java │ │ ├── AnimationProgress.java │ │ ├── AnimationStateHandler.java │ │ ├── RunningAnimation.java │ │ ├── Timed.java │ │ ├── TimedStorage.java │ │ └── VectorPoint.java │ ├── armor/ │ │ ├── ArmorItem.java │ │ └── PlayerArmor.java │ ├── bone/ │ │ ├── BoneEventDispatcher.java │ │ ├── BoneEventHandler.java │ │ ├── BoneIKSolver.java │ │ ├── BoneItemMapper.java │ │ ├── BoneMovement.java │ │ ├── BoneName.java │ │ ├── BonePosition.java │ │ ├── BoneRenderContext.java │ │ ├── BoneTag.java │ │ ├── BoneTagRegistry.java │ │ ├── BoneTags.java │ │ └── RenderedBone.java │ ├── config/ │ │ ├── DebugConfig.java │ │ ├── IndicatorConfig.java │ │ ├── ModuleConfig.java │ │ └── PackConfig.java │ ├── data/ │ │ ├── Float2.java │ │ ├── Float3.java │ │ ├── Float4.java │ │ ├── ModelAsset.java │ │ ├── blueprint/ │ │ │ ├── AnimationGenerator.java │ │ │ ├── BlueprintAnimation.java │ │ │ ├── BlueprintAnimator.java │ │ │ ├── BlueprintElement.java │ │ │ ├── BlueprintImage.java │ │ │ ├── BlueprintJson.java │ │ │ ├── BlueprintLoadContext.java │ │ │ ├── BlueprintTexture.java │ │ │ ├── ModelBlueprint.java │ │ │ └── ModelBoundingBox.java │ │ ├── raw/ │ │ │ ├── KeyframeChannel.java │ │ │ ├── ModelAnimation.java │ │ │ ├── ModelAnimator.java │ │ │ ├── ModelData.java │ │ │ ├── ModelDatapoint.java │ │ │ ├── ModelElement.java │ │ │ ├── ModelFace.java │ │ │ ├── ModelGroup.java │ │ │ ├── ModelKeyframe.java │ │ │ ├── ModelLoadContext.java │ │ │ ├── ModelLoadResult.java │ │ │ ├── ModelMeta.java │ │ │ ├── ModelOutliner.java │ │ │ ├── ModelPlaceholder.java │ │ │ ├── ModelResolution.java │ │ │ ├── ModelTexture.java │ │ │ └── ModelUV.java │ │ └── renderer/ │ │ ├── ModelRenderer.java │ │ ├── RenderPipeline.java │ │ ├── RenderSource.java │ │ └── RendererGroup.java │ ├── entity/ │ │ ├── BaseEntity.java │ │ └── BasePlayer.java │ ├── event/ │ │ ├── AnimationSignalEvent.java │ │ ├── CancellableEvent.java │ │ ├── CloseTrackerEvent.java │ │ ├── CreateDummyTrackerEvent.java │ │ ├── CreateEntityTrackerEvent.java │ │ ├── CreatePlayerSkinEvent.java │ │ ├── DismountModelEvent.java │ │ ├── ModelAssetsEvent.java │ │ ├── ModelDamageSource.java │ │ ├── ModelDespawnAtPlayerEvent.java │ │ ├── ModelEvent.java │ │ ├── ModelEventApplication.java │ │ ├── ModelEventListener.java │ │ ├── ModelImportedEvent.java │ │ ├── ModelSpawnAtPlayerEvent.java │ │ ├── MountModelEvent.java │ │ ├── PlayerHideTrackerEvent.java │ │ ├── PlayerPerAnimationEndEvent.java │ │ ├── PlayerPerAnimationStartEvent.java │ │ ├── PlayerShowTrackerEvent.java │ │ ├── PluginEndReloadEvent.java │ │ ├── PluginStartReloadEvent.java │ │ ├── RemovePlayerSkinEvent.java │ │ └── hitbox/ │ │ ├── HitBoxCreateEvent.java │ │ ├── HitBoxDamagedEvent.java │ │ ├── HitBoxDismountEvent.java │ │ ├── HitBoxEvent.java │ │ ├── HitBoxInteractAtEvent.java │ │ ├── HitBoxMountEvent.java │ │ └── HitBoxRemoveEvent.java │ ├── manager/ │ │ ├── ModelManager.java │ │ ├── PlayerManager.java │ │ ├── ProfileManager.java │ │ ├── ReloadInfo.java │ │ ├── ScriptManager.java │ │ └── SkinManager.java │ ├── mount/ │ │ ├── MountController.java │ │ └── MountControllers.java │ ├── nms/ │ │ ├── AnimationBundler.java │ │ ├── DisplayTransformer.java │ │ ├── HitBox.java │ │ ├── HitBoxListener.java │ │ ├── Identifiable.java │ │ ├── ModAnimationBundler.java │ │ ├── ModelDisplay.java │ │ ├── ModelInteractionHand.java │ │ ├── ModelNametag.java │ │ ├── NMS.java │ │ ├── NMSVersion.java │ │ ├── PacketBundler.java │ │ ├── PlayerChannelHandler.java │ │ └── Profiled.java │ ├── pack/ │ │ ├── PackAssets.java │ │ ├── PackBuilder.java │ │ ├── PackBuiltInAssets.java │ │ ├── PackByte.java │ │ ├── PackMeta.java │ │ ├── PackNamespace.java │ │ ├── PackObfuscator.java │ │ ├── PackOverlay.java │ │ ├── PackPath.java │ │ ├── PackResource.java │ │ ├── PackResult.java │ │ └── PackZipper.java │ ├── platform/ │ │ ├── PlatformAdapter.java │ │ ├── PlatformBillboard.java │ │ ├── PlatformEntity.java │ │ ├── PlatformItemStack.java │ │ ├── PlatformItemTransform.java │ │ ├── PlatformLivingEntity.java │ │ ├── PlatformLocation.java │ │ ├── PlatformNamespace.java │ │ ├── PlatformOfflinePlayer.java │ │ ├── PlatformPlayer.java │ │ ├── PlatformRegionHolder.java │ │ └── PlatformWorld.java │ ├── player/ │ │ ├── PlayerLimb.java │ │ └── PlayerSkinParts.java │ ├── profile/ │ │ ├── ModelProfile.java │ │ ├── ModelProfileInfo.java │ │ ├── ModelProfileSkin.java │ │ └── ModelProfileSupplier.java │ ├── scheduler/ │ │ ├── ModelScheduler.java │ │ └── ModelTask.java │ ├── script/ │ │ ├── AnimationScript.java │ │ ├── BlueprintScript.java │ │ ├── ScriptBuilder.java │ │ └── TimeScript.java │ ├── skin/ │ │ └── SkinData.java │ ├── tracker/ │ │ ├── DummyTracker.java │ │ ├── EntityBodyRotator.java │ │ ├── EntityHideOption.java │ │ ├── EntityTracker.java │ │ ├── EntityTrackerRegistry.java │ │ ├── ModelRotation.java │ │ ├── ModelRotator.java │ │ ├── ModelScaler.java │ │ ├── PlayerTracker.java │ │ ├── Tracker.java │ │ ├── TrackerAnimation.java │ │ ├── TrackerBuiltInAnimation.java │ │ ├── TrackerData.java │ │ ├── TrackerExtraAnimation.java │ │ ├── TrackerModifier.java │ │ └── TrackerUpdateAction.java │ ├── util/ │ │ ├── CollectionUtil.java │ │ ├── EntityUtil.java │ │ ├── EventUtil.java │ │ ├── FunctionUtil.java │ │ ├── HttpUtil.java │ │ ├── InterpolationUtil.java │ │ ├── LogUtil.java │ │ ├── MathUtil.java │ │ ├── PackUtil.java │ │ ├── ReflectionUtil.java │ │ ├── TransformedItemStack.java │ │ ├── collection/ │ │ │ ├── PriorityMap.java │ │ │ └── SingletonSequencedSet.java │ │ ├── function/ │ │ │ ├── BonePredicate.java │ │ │ ├── BooleanConstantSupplier.java │ │ │ ├── Float2FloatConstantFunction.java │ │ │ ├── Float2FloatFunction.java │ │ │ ├── FloatConstantFunction.java │ │ │ ├── FloatConstantSupplier.java │ │ │ ├── FloatFunction.java │ │ │ └── FloatSupplier.java │ │ ├── interpolator/ │ │ │ └── VectorInterpolator.java │ │ ├── json/ │ │ │ ├── JsonArrayBuilder.java │ │ │ └── JsonObjectBuilder.java │ │ ├── lazy/ │ │ │ └── LazyFloatProvider.java │ │ └── lock/ │ │ ├── DuplexLock.java │ │ └── SingleLock.java │ └── version/ │ └── MinecraftVersion.java ├── build.gradle.kts ├── buildSrc/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── Extensions.kt │ ├── bukkit-conventions.gradle.kts │ ├── modrinth-conventions.gradle.kts │ ├── paperweight-conventions.gradle.kts │ ├── plugin-conventions.gradle.kts │ ├── publish-conventions.gradle.kts │ └── standard-conventions.gradle.kts ├── changelog/ │ ├── 3.0.1.md │ ├── 3.0.2.md │ ├── v1/ │ │ ├── 1.10.0.md │ │ ├── 1.10.1.md │ │ ├── 1.10.2.md │ │ ├── 1.10.3.md │ │ ├── 1.11.0.md │ │ ├── 1.11.1.md │ │ ├── 1.11.2.md │ │ ├── 1.11.3.md │ │ ├── 1.11.4.md │ │ ├── 1.12.0.md │ │ ├── 1.12.1.md │ │ ├── 1.13.0.md │ │ ├── 1.13.1.md │ │ ├── 1.13.2.md │ │ ├── 1.13.3.md │ │ ├── 1.13.4.md │ │ ├── 1.14.0.md │ │ ├── 1.14.1.md │ │ ├── 1.14.2.md │ │ ├── 1.15.0.md │ │ ├── 1.15.1.md │ │ ├── 1.15.2.md │ │ ├── 1.3.2.md │ │ ├── 1.3.3.md │ │ ├── 1.4.1.md │ │ ├── 1.4.2.md │ │ ├── 1.4.3.md │ │ ├── 1.4.md │ │ ├── 1.5.1.md │ │ ├── 1.5.2.md │ │ ├── 1.5.3.md │ │ ├── 1.5.4.md │ │ ├── 1.5.5.md │ │ ├── 1.5.md │ │ ├── 1.6.0.md │ │ ├── 1.6.1.md │ │ ├── 1.7.0.md │ │ ├── 1.8.0.md │ │ ├── 1.8.1.md │ │ ├── 1.9.0.md │ │ ├── 1.9.1.md │ │ ├── 1.9.2.md │ │ └── 1.9.3.md │ ├── v2/ │ │ ├── 2.0.0-pre1.md │ │ ├── 2.0.0-pre2.md │ │ ├── 2.0.0.md │ │ ├── 2.0.1.md │ │ ├── 2.1.0.md │ │ └── 2.2.0.md │ └── v3/ │ └── 3.0.0.md ├── core/ │ ├── build.gradle.kts │ ├── bukkit-core/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── bukkit/ │ │ │ ├── AbstractBetterModelPlugin.java │ │ │ ├── BetterModelLibrary.java │ │ │ └── BetterModelLibraryManager.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ ├── BetterModelConfigImpl.kt │ │ ├── BetterModelPlugin.kt │ │ ├── BetterModelProperties.kt │ │ ├── BukkitModelEventBusImpl.kt │ │ ├── audience/ │ │ │ ├── AudiencePlayer.kt │ │ │ ├── AudienceSender.kt │ │ │ └── BukkitAudience.kt │ │ ├── command/ │ │ │ └── Commands.kt │ │ ├── compatibility/ │ │ │ ├── Compatibility.kt │ │ │ ├── citizens/ │ │ │ │ ├── CitizensCompatibility.kt │ │ │ │ ├── command/ │ │ │ │ │ ├── AnimateCommand.kt │ │ │ │ │ ├── LimbCommand.kt │ │ │ │ │ └── ModelCommand.kt │ │ │ │ └── trait/ │ │ │ │ └── ModelTrait.kt │ │ │ ├── mythicmobs/ │ │ │ │ ├── MythicMobsCompatibility.kt │ │ │ │ ├── MythicMobsValue.kt │ │ │ │ ├── condition/ │ │ │ │ │ └── ModelHasPassengerCondition.kt │ │ │ │ ├── mechanic/ │ │ │ │ │ ├── AbstractSkillMechanic.kt │ │ │ │ │ ├── BillboardMechanic.kt │ │ │ │ │ ├── BindHitBoxMechanic.kt │ │ │ │ │ ├── BodyRotationMechanic.kt │ │ │ │ │ ├── BrightnessMechanic.kt │ │ │ │ │ ├── ChangePartMechanic.kt │ │ │ │ │ ├── DefaultStateMechanic.kt │ │ │ │ │ ├── DismountAllModelMechanic.kt │ │ │ │ │ ├── DismountModelMechanic.kt │ │ │ │ │ ├── EnchantMechanic.kt │ │ │ │ │ ├── GlowMechanic.kt │ │ │ │ │ ├── LockModelMechanic.kt │ │ │ │ │ ├── ModelMechanic.kt │ │ │ │ │ ├── MountModelMechanic.kt │ │ │ │ │ ├── PairModelMechanic.kt │ │ │ │ │ ├── PartVisibilityMechanic.kt │ │ │ │ │ ├── PlayLimbAnimMechanic.kt │ │ │ │ │ ├── RemapModelMechanic.kt │ │ │ │ │ ├── StateMechanic.kt │ │ │ │ │ └── TintMechanic.kt │ │ │ │ └── targeter/ │ │ │ │ └── ModelPartTargeter.kt │ │ │ ├── nexo/ │ │ │ │ └── NexoCompatibility.kt │ │ │ └── skinsrestorer/ │ │ │ └── SkinsRestorerCompatibility.kt │ │ ├── configuration/ │ │ │ └── PluginConfiguration.kt │ │ ├── manager/ │ │ │ ├── CompatibilityManager.kt │ │ │ ├── EntityManager.kt │ │ │ └── PlayerManagerImpl.kt │ │ ├── scheduler/ │ │ │ ├── BukkitScheduler.kt │ │ │ └── PaperScheduler.kt │ │ └── util/ │ │ ├── BukkitWrappers.kt │ │ ├── Entities.kt │ │ ├── Events.kt │ │ ├── Plugins.kt │ │ ├── Senders.kt │ │ └── Yamls.kt │ └── src/ │ └── main/ │ ├── java/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── BetterModelPlatformImpl.java │ ├── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ ├── BetterModelEvaluatorImpl.kt │ │ ├── BetterModelEventBusImpl.kt │ │ ├── command/ │ │ │ ├── CommandBuildContext.kt │ │ │ ├── CommandBuilder.kt │ │ │ ├── CommandExtensions.kt │ │ │ └── CommandLike.kt │ │ ├── manager/ │ │ │ ├── ArmorManager.kt │ │ │ ├── GlobalManager.kt │ │ │ ├── ModelManagerImpl.kt │ │ │ ├── ProfileManagerImpl.kt │ │ │ ├── ReloadPipeline.kt │ │ │ ├── ScriptManagerImpl.kt │ │ │ ├── SkinManagerImpl.kt │ │ │ └── debug/ │ │ │ ├── BossBarIndicator.kt │ │ │ └── ReloadIndicator.kt │ │ ├── profile/ │ │ │ ├── DefaultHttpModelProfileSupplier.kt │ │ │ └── HttpModelProfileSupplier.kt │ │ ├── script/ │ │ │ ├── BrightnessScript.kt │ │ │ ├── ChangePartScript.kt │ │ │ ├── EnchantScript.kt │ │ │ ├── PartVisibilityScript.kt │ │ │ ├── RemapScript.kt │ │ │ └── TintScript.kt │ │ └── util/ │ │ ├── Buffers.kt │ │ ├── Collections.kt │ │ ├── Events.kt │ │ ├── Files.kt │ │ ├── Functions.kt │ │ ├── Gsons.kt │ │ ├── Indicators.kt │ │ ├── Packs.kt │ │ ├── Platforms.kt │ │ ├── Scripts.kt │ │ └── Senders.kt │ └── resources/ │ ├── blue_wizard.bbmodel │ ├── config.yml │ ├── demon_knight.bbmodel │ └── steve.bbmodel ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── nms/ │ ├── v1_21_R3/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── bukkit/ │ │ │ └── nms/ │ │ │ └── v1_21_R3/ │ │ │ └── AbstractHitBox.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ └── nms/ │ │ └── v1_21_R3/ │ │ ├── BaseEntityImpl.kt │ │ ├── BasePlayerImpl.kt │ │ ├── BukkitWrappers.kt │ │ ├── EntityData.kt │ │ ├── Functions.kt │ │ ├── HitBoxImpl.kt │ │ ├── HitBoxInteraction.kt │ │ ├── ModAnimationBundlerImpl.kt │ │ ├── ModelDamageSourceImpl.kt │ │ ├── ModelDisplayImpl.kt │ │ ├── ModelGameProfile.kt │ │ ├── ModelNametagImpl.kt │ │ ├── NMSImpl.kt │ │ ├── PacketBundlers.kt │ │ ├── PlayerArmorImpl.kt │ │ ├── ProfiledImpl.kt │ │ └── TypeAliases.kt │ ├── v1_21_R4/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── bukkit/ │ │ │ └── nms/ │ │ │ └── v1_21_R4/ │ │ │ └── AbstractHitBox.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ └── nms/ │ │ └── v1_21_R4/ │ │ ├── BaseEntityImpl.kt │ │ ├── BasePlayerImpl.kt │ │ ├── BukkitWrappers.kt │ │ ├── EntityData.kt │ │ ├── Functions.kt │ │ ├── HitBoxImpl.kt │ │ ├── HitBoxInteraction.kt │ │ ├── ModAnimationBundlerImpl.kt │ │ ├── ModelDamageSourceImpl.kt │ │ ├── ModelDisplayImpl.kt │ │ ├── ModelGameProfile.kt │ │ ├── ModelNametagImpl.kt │ │ ├── NMSImpl.kt │ │ ├── PacketBundlers.kt │ │ ├── PlayerArmorImpl.kt │ │ ├── ProfiledImpl.kt │ │ └── TypeAliases.kt │ ├── v1_21_R5/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── bukkit/ │ │ │ └── nms/ │ │ │ └── v1_21_R5/ │ │ │ └── AbstractHitBox.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ └── nms/ │ │ └── v1_21_R5/ │ │ ├── BaseEntityImpl.kt │ │ ├── BasePlayerImpl.kt │ │ ├── BukkitWrappers.kt │ │ ├── EntityData.kt │ │ ├── Functions.kt │ │ ├── HitBoxImpl.kt │ │ ├── HitBoxInteraction.kt │ │ ├── ModAnimationBundlerImpl.kt │ │ ├── ModelDamageSourceImpl.kt │ │ ├── ModelDisplayImpl.kt │ │ ├── ModelGameProfile.kt │ │ ├── ModelNametagImpl.kt │ │ ├── NMSImpl.kt │ │ ├── PacketBundlers.kt │ │ ├── PlayerArmorImpl.kt │ │ ├── ProfiledImpl.kt │ │ └── TypeAliases.kt │ ├── v1_21_R6/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── bukkit/ │ │ │ └── nms/ │ │ │ └── v1_21_R6/ │ │ │ └── AbstractHitBox.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ └── nms/ │ │ └── v1_21_R6/ │ │ ├── BaseEntityImpl.kt │ │ ├── BasePlayerImpl.kt │ │ ├── BukkitWrappers.kt │ │ ├── EntityData.kt │ │ ├── Functions.kt │ │ ├── HitBoxImpl.kt │ │ ├── HitBoxInteraction.kt │ │ ├── ModAnimationBundlerImpl.kt │ │ ├── ModelDamageSourceImpl.kt │ │ ├── ModelDisplayImpl.kt │ │ ├── ModelGameProfile.kt │ │ ├── ModelNametagImpl.kt │ │ ├── NMSImpl.kt │ │ ├── PacketBundlers.kt │ │ ├── PlayerArmorImpl.kt │ │ ├── ProfiledImpl.kt │ │ └── TypeAliases.kt │ ├── v1_21_R7/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── bukkit/ │ │ │ └── nms/ │ │ │ └── v1_21_R7/ │ │ │ └── AbstractHitBox.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ └── nms/ │ │ └── v1_21_R7/ │ │ ├── BaseEntityImpl.kt │ │ ├── BasePlayerImpl.kt │ │ ├── BukkitWrappers.kt │ │ ├── EntityData.kt │ │ ├── Functions.kt │ │ ├── HitBoxImpl.kt │ │ ├── HitBoxInteraction.kt │ │ ├── ModAnimationBundlerImpl.kt │ │ ├── ModelDamageSourceImpl.kt │ │ ├── ModelDisplayImpl.kt │ │ ├── ModelGameProfile.kt │ │ ├── ModelNametagImpl.kt │ │ ├── NMSImpl.kt │ │ ├── PacketBundlers.kt │ │ ├── PlayerArmorImpl.kt │ │ ├── ProfiledImpl.kt │ │ └── TypeAliases.kt │ └── v26_R1/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── bukkit/ │ │ └── nms/ │ │ └── v26_R1/ │ │ └── AbstractHitBox.java │ └── kotlin/ │ └── kr/ │ └── toxicity/ │ └── model/ │ └── bukkit/ │ └── nms/ │ └── v26_R1/ │ ├── BaseEntityImpl.kt │ ├── BasePlayerImpl.kt │ ├── BukkitWrappers.kt │ ├── EntityData.kt │ ├── Functions.kt │ ├── HitBoxImpl.kt │ ├── HitBoxInteraction.kt │ ├── ModAnimationBundlerImpl.kt │ ├── ModelDamageSourceImpl.kt │ ├── ModelDisplayImpl.kt │ ├── ModelGameProfile.kt │ ├── ModelNametagImpl.kt │ ├── NMSImpl.kt │ ├── PacketBundlers.kt │ ├── PlayerArmorImpl.kt │ ├── ProfiledImpl.kt │ └── TypeAliases.kt ├── platform/ │ ├── fabric/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── kr/ │ │ │ │ └── toxicity/ │ │ │ │ └── model/ │ │ │ │ ├── impl/ │ │ │ │ │ └── fabric/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ ├── AbstractArmorStand.java │ │ │ │ │ │ └── EntityHook.java │ │ │ │ │ └── network/ │ │ │ │ │ └── BetterModelBundlePacket.java │ │ │ │ └── mixin/ │ │ │ │ ├── AvatarAccessor.java │ │ │ │ ├── ClientboundBundlePacketMixin.java │ │ │ │ ├── ConnectionAccessor.java │ │ │ │ ├── DisplayAccessor.java │ │ │ │ ├── EntityAccessor.java │ │ │ │ ├── EntityMixin.java │ │ │ │ ├── ItemDisplayAccessor.java │ │ │ │ ├── LivingEntityMixin.java │ │ │ │ ├── MobAccessor.java │ │ │ │ ├── ServerCommonPacketListenerImplAccessor.java │ │ │ │ ├── ServerLevelEntityCallbacksMixin.java │ │ │ │ └── SynchedEntityDataAccessor.java │ │ │ ├── kotlin/ │ │ │ │ └── kr/ │ │ │ │ └── toxicity/ │ │ │ │ └── model/ │ │ │ │ └── impl/ │ │ │ │ └── fabric/ │ │ │ │ ├── BetterModelFabricImpl.kt │ │ │ │ ├── BetterModelLoggerImpl.kt │ │ │ │ ├── BetterModelNMSImpl.kt │ │ │ │ ├── Constants.kt │ │ │ │ ├── Entities.kt │ │ │ │ ├── FabricWrappers.kt │ │ │ │ ├── Functions.kt │ │ │ │ ├── armor/ │ │ │ │ │ └── PlayerArmorImpl.kt │ │ │ │ ├── attachment/ │ │ │ │ │ └── BetterModelAttachments.kt │ │ │ │ ├── audience/ │ │ │ │ │ ├── AudienceCommandSource.kt │ │ │ │ │ ├── AudiencePlayer.kt │ │ │ │ │ └── AudienceSourceStack.kt │ │ │ │ ├── chat/ │ │ │ │ │ └── Components.kt │ │ │ │ ├── command/ │ │ │ │ │ └── Commands.kt │ │ │ │ ├── config/ │ │ │ │ │ └── BetterModelConfigImpl.kt │ │ │ │ ├── entity/ │ │ │ │ │ ├── BaseFabricEntityImpl.kt │ │ │ │ │ ├── BaseFabricPlayerImpl.kt │ │ │ │ │ ├── DisplayTransformerImpl.kt │ │ │ │ │ ├── HitBoxEntityImpl.kt │ │ │ │ │ ├── InteractionEntityImpl.kt │ │ │ │ │ ├── ModelDisplayEntityImpl.kt │ │ │ │ │ ├── ModelNametagImpl.kt │ │ │ │ │ ├── PlayerChannelHandlerImpl.kt │ │ │ │ │ ├── ProfiledImpl.kt │ │ │ │ │ └── TransformationData.kt │ │ │ │ ├── events/ │ │ │ │ │ ├── ServerEntityDismountCallback.kt │ │ │ │ │ ├── ServerLivingEntityJumpCallback.kt │ │ │ │ │ ├── ServerMobEffectLoadCallback.kt │ │ │ │ │ └── ServerMobEffectUnloadCallback.kt │ │ │ │ ├── manager/ │ │ │ │ │ ├── EntityManager.kt │ │ │ │ │ ├── PlayerManagerImpl.kt │ │ │ │ │ └── Syncers.kt │ │ │ │ ├── network/ │ │ │ │ │ ├── ModAnimationBundlerImpl.kt │ │ │ │ │ ├── PacketBundlers.kt │ │ │ │ │ └── Packets.kt │ │ │ │ ├── profile/ │ │ │ │ │ └── ModelProfileImpl.kt │ │ │ │ ├── scheduler/ │ │ │ │ │ └── FabricModelSchedulerImpl.kt │ │ │ │ └── world/ │ │ │ │ ├── Chunks.kt │ │ │ │ └── damagesource/ │ │ │ │ └── ModelDamageSourceImpl.kt │ │ │ └── resources/ │ │ │ ├── bettermodel.accesswidener │ │ │ └── bettermodel.mixins.json │ │ └── testmod/ │ │ ├── kotlin/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── test/ │ │ │ └── RollTest.kt │ │ └── resources/ │ │ ├── knight.bbmodel │ │ ├── knight_line.json │ │ └── knight_sword.json │ ├── paper/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── kr/ │ │ │ └── toxicity/ │ │ │ └── model/ │ │ │ └── paper/ │ │ │ └── BetterModelLoader.java │ │ └── kotlin/ │ │ └── kr/ │ │ └── toxicity/ │ │ └── model/ │ │ └── paper/ │ │ └── BetterModelPaper.kt │ └── spigot/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── kr/ │ └── toxicity/ │ └── model/ │ └── spigot/ │ └── BetterModelSpigot.kt ├── purpur/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── kr/ │ └── toxicity/ │ └── model/ │ └── bukkit/ │ └── purpur/ │ └── PurpurHook.kt ├── renovate.json ├── settings.gradle.kts └── test-plugin/ ├── build.gradle.kts └── src/ └── main/ ├── java/ │ └── kr/ │ └── toxicity/ │ └── model/ │ └── test/ │ ├── BetterModelTest.java │ ├── FightTester.java │ ├── ModelTester.java │ └── RollTester.java └── resources/ ├── knight.bbmodel ├── knight_line.json └── knight_sword.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: https://editorconfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] charset = utf-8 end_of_line = lf indent_style = space indent_size = 4 insert_final_newline = true max_line_length = off trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/CONTRIBUTING.md ================================================ ## Contributing Thanks for considering contributing! * **Step 1.** Forks BetterModel to your repository. * **Step 2.** Commits your change. * **Step 3.** Opens pull request at 'dev' branch. * **Step 4.** Waits for merging or reviewing ## Rule * I can't handle a huge change about API module. * You should note that type of your PR. (e.g., `Bug fix` / `API update`) * You should inform your features that what you want to merge by this way. ``` Document Image Video ``` ## 📄 Commit Message Format Use present tense: `fix:`, `feat:`, `docs:`, etc. ## 📜 License Contributions are under the MIT License. ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [toxicity188] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: toxicity188 thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a bug to help us improve BetterModel title: "[Bug] " labels: bug assignees: toxicity188 --- ### ✔️ Pre-check - [ ] Tested with the **latest snapshot** of BetterModel from [Modrinth](https://modrinth.com/plugin/bettermodel) - [ ] Confirmed the issue occurs **without** other optional/experimental plugins or clients (see Disclaimer below) --- ### 🐞 Problem Description Detailed information about your problem. --- ### 📜 Server Log Your error log if exists. --- ### 🖼️ Screenshot / Video Your in-game screenshot. --- ### 🧪 Test Model / Code Upload the model, resource pack, or test code that can reproduce the issue if possible. --- ### 🌍 Environment - OS: (Windows, Linux, etc.) - Server software & version: (Paper 1.21.1, etc.) --- ``` Disclaimer The following environments are not supported, and issues occurring under these conditions will not be handled: - Informal / modified launchers (e.g., Feather client) - Closed-source mods/plugins (Optifine, ItemsAdder, Nexo, etc.) - Hybrid server platforms (e.g., Arclight) ``` ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea or enhancement for BetterModel title: "[Feature] " labels: help wanted assignees: toxicity188 --- ### 🐞 Feature Description Describe the feature you want to request. Explain **what problem this feature solves**, **why it's needed**, and **how you expect it to work**. --- ### 🧪 Example / Mock-up (Optional) Provide model files, resource packs, code snippets, or conceptual mock-ups that help illustrate the idea. --- ### 🌍 Environment (Optional but Helpful) - Server software & version: - BetterModel version: - Java version: --- ``` Disclaimer The following environments are not supported, and feature requests relying on these conditions will not be considered: - Informal / modified launchers (e.g., Feather Client) - Closed-source mods/plugins (Optifine, ItemsAdder, Nexo, etc.) - Hybrid server platforms (e.g., Arclight) - Legacy server versions (1.20.1 or lower) - Bedrock Edition - Extremely outdated CPU / hardware - Features fundamentally **impossible on server-side** due to engine or protocol limitations ``` ================================================ FILE: .github/workflows/package.yml ================================================ name: Package plugin on: push: branches: [ "master", "v2", "v3" ] permissions: contents: read packages: write deployments: write jobs: build: runs-on: ubuntu-latest env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} PACKAGES_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_NUMBER: ${{ github.run_number }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} steps: - uses: actions/checkout@v6 - name: Set up JDK 25 uses: actions/setup-java@v5 with: java-version: '25' distribution: 'temurin' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Create Deployment id: deployment run: | response=$(curl -s -X POST \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos/${{ github.repository }}/deployments \ -d '{ "ref": "master", "auto_merge": false, "required_contexts": [], "payload": "{ \"timestamp\": \"'$(date +%s)'\" }", "environment": "github-packages", "transient_environment": false, "description": "Publishing to GitHub Packages" }') echo "$response" deployment_id=$(echo "$response" | jq -r '.id') echo "id=$deployment_id" >> $GITHUB_OUTPUT shell: bash - name: Publish package run: ./gradlew publishAllPublicationToGitHubPackagesRepository --stacktrace - name: Set Deployment Status (success) if: success() run: | curl -X POST \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses \ -d '{"state":"success","environment_url":"https://github.com/${{ github.repository }}/packages","description":"Deployment succeeded"}' shell: bash - name: Set Deployment Status (failure) if: failure() run: | curl -X POST \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses \ -d '{"state":"failure","environment_url":"https://github.com/${{ github.repository }}/packages","description":"Deployment failed"}' shell: bash ================================================ FILE: .github/workflows/pr-test.yml ================================================ name: PR Test on: pull_request: branches: [ "dev", "v2-dev", "v3-dev" ] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up JDK 25 uses: actions/setup-java@v5 with: java-version: '25' distribution: 'temurin' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle run: ./gradlew build --stacktrace - name: Run Tests run: ./gradlew test --stacktrace ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish plugin on: push: branches: [ "master", "v2", "v3" ] permissions: contents: read jobs: build: if: "!contains(github.event.head_commit.message, '[publish skip]')" runs-on: ubuntu-latest env: HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} MODRINTH_API_TOKEN: ${{ secrets.MODRINTH_API_TOKEN }} BUILD_NUMBER: ${{ github.run_number }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} steps: - uses: actions/checkout@v6 - name: Set up JDK 25 uses: actions/setup-java@v5 with: java-version: '25' distribution: 'temurin' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build all file run: ./gradlew build --stacktrace - name: Publish to Modrinth run: ./gradlew modrinth --stacktrace - name: Publish to Hangar run: ./gradlew publishPluginPublicationToHangar --stacktrace ================================================ FILE: .gitignore ================================================ .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ ### IntelliJ IDEA ### .idea/* !.idea/codeStyles !.idea/inspectionProfiles !.idea/icon.png ### Kotlin ### .kotlin ### Eclipse ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ ### VS Code ### .vscode/ ### Mac OS ### .DS_Store ### Minecraft ### run/ plugins/ ================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: AGENTS.md ================================================ # BetterModel AGENT GUIDE (for Codex, Gemini, and other LLM agents) This document defines repository-wide operating rules for automated contributors. Scope: entire repository unless a deeper AGENTS.md overrides this file. ## 0) Mission and Priorities - Primary mission: act as a maintenance assistant for safe updates and long-term stability. - Prioritize documentation quality, API clarity, and compatibility over refactoring. - Do not introduce architectural churn or speculative abstractions. - Prefer minimal, reversible changes that preserve current behavior. --- ## 1) Module Responsibilities ### MUST - Respect module boundaries: - `api`: architecture contracts, data definitions, pure logic, interfaces, domain exceptions. - `core`: implementation of `api`, orchestration, external I/O integration. - `platform/*`: platform packaging and platform-specific business integration using `core`. - `nms/*`: Minecraft-version-specific low-level adapters only. - Keep dependency direction one-way where possible: `api -> core -> platform/nms` usage semantics. - If a feature needs cross-module changes, start from `api` contract, then implement in `core`, then bind in `platform`. ### FORBIDDEN - Do not place platform/runtime-specific details in `api`. - Do not move business logic into `nms` unless version coupling is unavoidable. - Do not bypass module contracts with ad-hoc cross-module shortcuts. --- ## 2) Language and File Placement Rules ### MUST - Use Modern Java (language level 25) and Kotlin. - `api` module is Java-only for production code (Kotlin DSL build scripts are allowed). - Non-`api` modules should be Kotlin-first unless existing local code is explicitly Java-bound (e.g., mixin/accessor interop). - Follow existing package root `kr.toxicity.model...` and module-specific suffixes. ### FORBIDDEN - Do not add Kotlin source files to `api/src/main`. - Do not introduce new language stacks or code generators without explicit request. --- ## 3) Allowed vs Forbidden Change Rules ### MUST - Preserve existing behavior unless the task explicitly requests behavior change. - Match local style of touched files (imports, naming, nullability annotations, formatting). - Keep diffs small and scoped to the requested issue. - Update/extend Javadoc when touching public Java APIs. ### RECOMMENDED - Prefer additive changes over invasive rewrites. - Prefer extension/composition over inheritance. ### FORBIDDEN - No drive-by refactors. - No broad renaming/reformatting-only commits mixed with functional changes. - No “cleanup” changes unrelated to the requested task. --- ## 4) Code Patterns and Conventions ### Java rules #### MUST - Add English Javadoc for public API types/methods. - Use `var` for local variables where legal and clear. - When using primitive-key/value collections, prefer fastutil specialized types over boxed `java.util` alternatives. - Declare classes `final` when inheritance is not intended. - Use `of(...)` for factory method naming. - Prefer enum-based singleton pattern when a singleton is required. #### RECOMMENDED - Prefer `record` for immutable data carriers. - Interface-based API should expose its own factory method where practical. #### DISCOURAGED - Inheritance-heavy designs. #### FORBIDDEN - Do not depend on Kotlin classes from Java in `api`. ### Kotlin rules #### MUST - For Boolean arguments in calls, use named arguments (`parameter = value`) where available. - Keep Kotlin style concise and explicit; avoid hidden side effects. #### RECOMMENDED - Avoid `abstract class` when interface + default methods or delegation works. #### FORBIDDEN - No Kotlin production code in `api` module. --- ## 5) Javadoc Policy ### MUST - English only. - Use `@since` matching current `gradle.properties` project version policy. - Include `@return` for non-void methods. - For records, document all components using `@param` at type level. - Include `@throws` for throw-capable public methods. - Public API docs must include a usage example (`@example` or code block). - Keep terminology consistent with project domain (Molang, item_display, packet-based rendering, etc.). --- ## 6) Exception Handling Policy ### MUST - Domain exceptions must be declared in `api` and reused by `core/platform`. - Use unchecked exceptions for domain errors (`RuntimeException` subclasses). - Prefer explicit domain exception types over anonymous `RuntimeException` for new logic. ### FORBIDDEN - No new checked exceptions in public API surface unless explicitly requested. - No swallowing exceptions without logging/context. --- ## 7) Architectural Boundary Enforcement ### MUST - Enforce separation of concerns: - parsing/model contracts in `api` - orchestration and state management in `core` - runtime platform hooks in `platform` - protocol/version internals in `nms` - Keep NMS version modules behaviorally equivalent unless version-specific differences are required. ### FORBIDDEN - Do not leak platform classes into generic API contracts. - Do not duplicate core logic in multiple platform modules. --- ## 8) Change Management Principles ### MUST - Before coding, identify smallest viable patch. - Keep commits logically atomic. - Use conventional commits (`docs:`, `fix:`, `refactor:`, `chore:`). - Document why a change is necessary, not only what changed. ### RECOMMENDED - Prefer one concern per PR. - If migration is unavoidable, provide transitional compatibility notes. --- ## 9) Minimal Change Principle (Anti-Overengineering) ### MUST - Solve the concrete problem only. - Reuse existing utilities/conventions before introducing new abstractions. - Avoid framework-like internal layers unless repeatedly justified by current code. ### FORBIDDEN - No speculative extensibility. - No premature optimization without evidence. --- ## 10) Backward Compatibility Policy ### MUST - Preserve existing public API behavior by default. - Treat changes to signatures, semantics, serialization shape, config keys, and command contracts as breaking. - For unavoidable breaking changes: document impact, migration path, and versioning intent. ### RECOMMENDED - Prefer deprecation + transition period over hard removal. --- ## 11) Dependency Introduction Policy ### MUST - Prefer existing dependencies and in-repo utilities. - Any new dependency requires clear justification: purpose, scope, size, and maintenance cost. - Add dependency versions through central version catalog/patterns already in use. ### FORBIDDEN - Do not add dependencies for trivial utilities already present in JDK/Kotlin stdlib/current stack. - Do not introduce overlapping libraries providing the same capability. --- ## 12) Diff and PR Format Guidelines ### MUST - Keep PRs reviewable: focused scope, clear title, concise rationale. - Include: - summary of changes - impacted modules - compatibility notes - tests/checks run - Separate mechanical formatting from logic changes whenever possible. ### RECOMMENDED - Use checklist format for validation and risk points. --- ## 13) Test Policy ### MUST - Run the most relevant checks for touched modules. - Validate both compile-time and behavior-level impact where feasible. - If tests are absent, run targeted build/lint/verification tasks and report limitations. ### RECOMMENDED - Prefer adding focused tests only when directly related to changed behavior. - Keep test fixtures lightweight; do not add broad test frameworks without request. --- ## 14) Existing Code Precedence Rule ### MUST - Existing local code patterns take precedence over generic best practices when conflicts arise. - Follow deeper-scope AGENTS.md if present. - If repository reality conflicts with this guide, preserve behavior first and propose policy alignment separately. ### FORBIDDEN - Do not force style unification across untouched files. --- ## 15) Final Operational Checklist Before finishing, verify: - Scope is minimal and task-aligned. - Module boundaries are respected. - Public API docs are updated (if touched). - Exception policy and compatibility policy are respected. - Changes are validated with relevant checks. - PR description contains rationale, risks, and verification results. ================================================ FILE: BANNER.md ================================================
![](https://github.com/user-attachments/assets/89e191ba-ed4f-44ab-bb98-634cfe568dca) # BetterModel *- Modern Bedrock model engine for Minecraft Java Edition -* [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/plugin/bettermodel) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/hangar_vector.svg)](https://hangar.papermc.io/toxicity188/BetterModel) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/github_vector.svg)](https://github.com/toxicity188/BetterModel)
* * * ![](https://github.com/user-attachments/assets/5a6c1a8c-6fe2-4a67-a10e-e63e40825d35) ![](https://github.com/user-attachments/assets/ff515577-6a72-48ba-9943-81f00dddb375) * * * (In BlockBench / In Minecraft) # ✨ What is BetterModel? **BetterModel** is a server-based engine that provides runtime BlockBench model rendering & animating for Minecraft Java Edition. It implements **fully server-side 3D models** by using an item display entity packet. - Importing Generic BlockBench model `.bbmodel` - Auto-generating resource pack - Playing animation - Syncing with base entity - Custom hit box - 12-limb player animation ## 🚀 Comparison with ModelEngine The main reason I created it is: - To reduce network cost—MEG’s network optimization is outdated and insufficient for modern servers. - To enable faster updates—We can’t afford to wait for MEG’s slow update cycle anymore. - To provide a more flexible API—MEG is closed-source with a very limited API, which makes extending or integrating difficult. - To restore vanilla behavior-MEG breaks several vanilla entity features and physics, which this project aims to fix. Also, you can refer [my document](https://github.com/toxicity188/BetterModel/wiki/Compare-with-ModelEngine) to compare both ModelEngine and BetterModel. ## 🌎 Generic BlockBench model with animation ![](https://github.com/user-attachments/assets/b4e69aef-a446-4ac3-b84e-eb42fe4f069d) * * * [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/youtube-singular_vector.svg)](https://youtu.be/f3U7Lmo3aA8?si=SnglL0YKn20CrR7Y) BetterModel supports Generic BlockBench models with full animation. #### Custom hitbox * * * ![](https://github.com/user-attachments/assets/94aee9ed-9c2f-4975-92c4-3ea84ae31d24) * * * BetterModel provides **custom hitbox** both client and server. (tracking animation rotation) #### MythicMobs support * * * ![](https://github.com/user-attachments/assets/eb2d64ef-7b6e-4306-8c31-d92d0266dbac) * * * Like MEG, BetterModel supports **MythicMobs**, you can use some MEG's mechanics in BetterModel too. ## 💡 Player model with animation ![](https://github.com/user-attachments/assets/0c13bec2-898f-4d9a-a709-10e0571337f3) ![](https://github.com/user-attachments/assets/034dd64c-6889-4a01-961d-e69679b1c71b) * * * BetterModel supports **player model with using user's custom skin without textures**. ## 📚 Official wiki [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/ghpages_vector.svg)](https://github.com/toxicity188/BetterModel/wiki) ## 🏗️ Supported environment [![](https://img.shields.io/badge/minecraft-1.21.4%7E26.1.x-8FCA5C?style=for-the-badge)](https://www.minecraft.net/en-us/download/server) [![](https://img.shields.io/badge/java-25%7E-ED8B00?style=for-the-badge)](https://adoptium.net/) ### Bukkit [![](https://img.shields.io/badge/folia-supported-blue?style=for-the-badge)](https://papermc.io/downloads/folia) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/paper_vector.svg)](https://papermc.io/downloads/paper) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/purpur_vector.svg)](https://purpurmc.org/download/purpur) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/spigot_vector.svg)](https://www.spigotmc.org/) ### Mod [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/supported/fabric_vector.svg)](https://fabricmc.net/) ## 🌈 My community [![](https://discord.com/api/guilds/1012718460297551943/widget.png?style=banner2)](https://discord.com/invite/rePyFESDbk) ## 📊 Project Stats (plugin) [![](https://bstats.org/signatures/bukkit/BetterModel.svg)](https://bstats.org/plugin/bukkit/BetterModel/24237) ## 💖 Support my project [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/buymeacoffee-singular_vector.svg)](https://buymeacoffee.com/toxicity188) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/ghsponsors-singular_vector.svg)](https://github.com/sponsors/toxicity188) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/paypal-singular_vector.svg)](https://www.paypal.com/paypalme/toxicity188?country.x=KR&locale.x=en_US) ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2024–2026 toxicity188 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LICENSE_HEADER ================================================ This source file is part of BetterModel. Copyright (c) ${CREATION_YEAR} toxicity188 Licensed under the MIT License. See LICENSE.md file for full license text. #year_selection file ================================================ FILE: README.md ================================================
![](https://github.com/user-attachments/assets/89e191ba-ed4f-44ab-bb98-634cfe568dca) # BetterModel *- Modern Bedrock model engine for Minecraft Java Edition -* [![](https://img.shields.io/maven-central/v/io.github.toxicity188/bettermodel-api?style=flat-square&logo=sonatype)](https://central.sonatype.com/artifact/io.github.toxicity188/bettermodel-api) [![](https://img.shields.io/github/actions/workflow/status/toxicity188/BetterModel/publish.yml?style=flat-square)](https://modrinth.com/plugin/bettermodel/versions) [![](https://img.shields.io/github/issues/toxicity188/BetterModel?style=flat-square&logo=github)](https://github.com/toxicity188/BetterModel/issues) [![](https://img.shields.io/bstats/servers/24237?style=flat-square)](https://bstats.org/plugin/bukkit/BetterModel/24237)
* * * ![](https://github.com/user-attachments/assets/5a6c1a8c-6fe2-4a67-a10e-e63e40825d35) ![](https://github.com/user-attachments/assets/ff515577-6a72-48ba-9943-81f00dddb375) * * * (In BlockBench / In Minecraft) # ✨ Introduction **BetterModel** is a server-based engine that provides runtime BlockBench model rendering & animating for Minecraft Java Edition. It implements **fully server-side 3D models** by using an item display entity packet. - Importing Generic BlockBench model `.bbmodel` - Auto-generating resource pack - Playing animation - Syncing with base entity - Custom hit box - 12-limb player animation
In-Game Screenshots ![](https://github.com/user-attachments/assets/b4e69aef-a446-4ac3-b84e-eb42fe4f069d) ![](https://github.com/user-attachments/assets/94aee9ed-9c2f-4975-92c4-3ea84ae31d24) ![](https://github.com/user-attachments/assets/eb2d64ef-7b6e-4306-8c31-d92d0266dbac) ![](https://github.com/user-attachments/assets/034dd64c-6889-4a01-961d-e69679b1c71b)
## 🚀 Key Features & Focus BetterModel aims to be a reliable engine that provides stable, high-quality animations for Paper-based high-traffic servers. - **Stability First**: We take a conservative approach to feature expansion. By avoiding the implementation of features that are difficult to maintain or have limited use cases, we focus on providing a stable API and ensuring overall operational safety. - **Performance Optimized**: Our goal is to minimize runtime computation, memory footprint, and network overhead. Through asynchronous design and optimized packet handling, we ensure the engine runs efficiently even under heavy server loads. - **Tailored for Large-scale Servers**: We provide essential features specifically designed for high-population servers and MMORPG content creation. - **Per-player Animation**: Individual animation control tailored to each player's perspective. - **Player Model Animation**: Support for sophisticated 12-limb animations based on player models. ## 📚 Wiki [![](https://img.shields.io/badge/GitHub%20Wiki-181717?logo=github&logoColor=white)](https://github.com/toxicity188/BetterModel/wiki) [![](https://deepwiki.com/badge.svg)](https://deepwiki.com/toxicity188/BetterModel) ## 🛠️ Build info [![](https://img.shields.io/badge/minecraft-1.21.4%7E26.1.x-8FCA5C)](https://www.minecraft.net/en-us/download/server) [![](https://img.shields.io/badge/java-25%7E-ED8B00)](https://adoptium.net/) #### Build [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/built-with/gradle_vector.svg)](https://gradle.org/) `./gradlew build`: Builds all jars `./gradlew shadowJar`: Builds plugin jar `./gradlew javadocJar`: Builds Javadoc jar `./gradlew runServer`: Runs Paper test server with test plugin #### Library - [Kotlin stdlib](https://github.com/JetBrains/kotlin): modern functional programming - [semver4j](https://github.com/semver4j/semver4j): semver parser - [cloud](https://github.com/Incendo/cloud-minecraft): command - [adventure](https://github.com/KyoriPowered/adventure): component - [stable player display](https://github.com/bradleyq/stable_player_display): player animation - [caffeine](https://github.com/ben-manes/caffeine): concurrent map cache - [DynamicUV](https://github.com/toxicity188/DynamicUV): player model - [ArmorModel](https://github.com/toxicity188/ArmorModel): armor in player model - [java-mesh](https://github.com/toxicity188/java-mesh): mesh rendering - [molang-compiler](https://github.com/Ocelot5836/molang-compiler): compiling and evaluating molang expression - [libby](https://github.com/AlessioDP/libby): runtime library downloader #### Tested Bukkit Server Platform - [Paper](https://papermc.io/downloads/paper) - [Purpur](https://purpurmc.org/download/purpur) - [Spigot](https://www.spigotmc.org/) - [Folia](https://papermc.io/downloads/folia) - [Leaf](https://www.leafmc.one/download) - [Canvas](https://canvasmc.io/downloads/canvas) #### Tested Mod Server Platform - [Fabric Loader](https://fabricmc.net/) ## 💻 API [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/available/maven-central_vector.svg)](https://central.sonatype.com/artifact/io.github.toxicity188/bettermodel) > [!NOTE]\ > For more detailed API specifications, please refer to our [GitHub Wiki](https://github.com/toxicity188/BetterModel/wiki/API-example).
Gradle (Kotlin) #### Release ```kotlin repositories { mavenCentral() maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION") // bukkit(spigot, paper, etc) api //api("io.github.toxicity188:bettermodel-fabric:VERSION") // mod(fabric) } ``` #### Snapshot ```kotlin repositories { maven("https://maven.pkg.github.com/toxicity188/BetterModel") { credentials { username = YOUR_GITHUB_USERNAME password = YOUR_GITHUB_TOKEN } } maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION-SNAPSHOT") // bukkit(spigot, paper, etc) api //api("io.github.toxicity188:bettermodel-fabric:VERSION-SNAPSHOT") // mod(fabric) } ```
Gradle (Groovy) #### Release ```groovy repositories { mavenCentral() maven 'https://maven.blamejared.com/' // For transitive dependency in bettermodel-fabric maven 'https://maven.nucleoid.xyz/' // For transitive dependency in bettermodel-fabric } dependencies { compileOnly 'io.github.toxicity188:bettermodel-bukkit-api:VERSION' // bukkit(spigot, paper, etc) api //api 'io.github.toxicity188:bettermodel-fabric:VERSION' // mod(fabric) } ``` #### Snapshot ```groovy repositories { maven { url "https://maven.pkg.github.com/toxicity188/BetterModel" credentials { username = YOUR_GITHUB_USERNAME password = YOUR_GITHUB_TOKEN } } maven 'https://maven.blamejared.com/' // For transitive dependency in bettermodel-fabric maven 'https://maven.nucleoid.xyz/' // For transitive dependency in bettermodel-fabric } dependencies { compileOnly 'io.github.toxicity188:bettermodel-bukkit-api:VERSION-SNAPSHOT' // bukkit(spigot, paper, etc) api //api 'io.github.toxicity188:bettermodel-fabric:VERSION-SNAPSHOT' // mod(fabric) } ```
Maven #### Release ```xml central https://repo.maven.apache.org/maven2 io.github.toxicity188 bettermodel-bukkit-api VERSION provided ``` #### Snapshot ```xml github https://maven.pkg.github.com/toxicity188/BetterModel io.github.toxicity188 bettermodel-api VERSION-SNAPSHOT provided io.github.toxicity188 bettermodel-bukkit-api VERSION-SNAPSHOT provided ```
Example code #### Gets some model or limb ```java BetterModel.model("demon_knight"); //A model file in BetterModel/models (for general model with saving) BetterModel.limb("steve"); //A model file in BetterModel/players (for player model with no saveing) BetterModel.modelOrNull("demon_knight"); //general model or null BetterModel.limbOrNull("steve"); //player model or null ``` #### Creates model (entity) ```java EntityTracker tracker = BetterModel.model("demon_knight") .map(r -> r.getOrCreate(BukkitAdapter.adapt(entity))) //Gets or creates entity tracker by this renderer to some entity. .orElse(null); ``` ```java EntityTracker tracker = BetterModel.model("demon_knight") .map(r -> r.create(BukkitAdapter.adapt(entity), TrackerModifier.DEFAULT, t -> t.update(TrackerUpdateAction.tint(0x0026FF)))) //Creates entity tracker with pre-spawn task. .orElse(null); ``` #### Creates model (dummy) ```java DummyTracker tracker = BetterModel.model("demon_knight") .map(r -> r.create(BukkitAdapter.adapt(location))) //Creates some dummy tracker to this location. .orElse(null); ``` ```java DummyTracker tracker = BetterModel.limb("steve") .map(r -> r.create(BukkitAdapter.adapt(location), ModelProfile.of(BukkitAdapter.adapt(player)))) //Creates some dummy tracker to this location and player's skin profile. .orElse(null); ``` #### Update some tracker's display data ```java BetterModel.model("demon_knight") .map(r -> r.create(BukkitAdapter.adapt(entity), TrackerModifier.DEFAULT, t -> { t.update(TrackerUpdateAction.tint(rgb)); //Tint t.update(TrackerUpdateAction.enchant(true), bone -> true); //Enchant with predicate })) .ifPresent(tracker -> tracker.update(TrackerUpdateAction.composite( //Composite TrackerUpdateAction.brightness(15, 15) //Brightness TrackerUpdateAction.billboard(Display.Billboard.CENTER) //Billboard ))); } ```
## 💬 Community [![](https://discord.com/api/guilds/1012718460297551943/widget.png?style=banner2)](https://discord.com/invite/rePyFESDbk) ## 💖 Support [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/buymeacoffee-singular_vector.svg)](https://buymeacoffee.com/toxicity188) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/ghsponsors-singular_vector.svg)](https://github.com/sponsors/toxicity188) [![](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/paypal-singular_vector.svg)](https://www.paypal.com/paypalme/toxicity188?country.x=KR&locale.x=en_US) ================================================ FILE: SECURITY.md ================================================ ## Security Policy BetterModel is a server-side 3D model engine that operates in an isolated environment without direct connections to external clients. As such, the risk of traditional security vulnerabilities is minimal. #### Key Points - 🔒 **No Client Connection** BetterModel does not expose any network interface or accept input from external clients. - 📦 **No Data Leakage** Animation and bone data are handled on the server and are never sent to the client directly. Only processed vector packets are transmitted, which do not include raw model data. - 🧱 **Model Privacy** Your model files are converted into a Minecraft-compatible resource pack. During this conversion process, most of the original model information is stripped or transformed, minimizing the risk of leakage. ================================================ FILE: api/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.publish) } dependencies { compileOnly(libs.bundles.minecraft) } ================================================ FILE: api/bukkit-api/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.publish) alias(libs.plugins.convention.bukkit) } dependencies { api(project(":bettermodel-api")) } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/BetterModelBukkit.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.BetterModelPlatform; import kr.toxicity.model.api.bukkit.platform.BukkitAdapter; import kr.toxicity.model.api.bukkit.scheduler.BukkitModelScheduler; import org.jetbrains.annotations.NotNull; import static kr.toxicity.model.api.util.ReflectionUtil.classExists; /** * Represents the Bukkit-specific platform interface for BetterModel. *

* This interface extends {@link BetterModelPlatform} to provide Bukkit-specific implementations * for scheduling and entity adaptation. *

* * @since 2.0.0 */ public interface BetterModelBukkit extends BetterModelPlatform { /** * Checks if the server is running on the Folia platform. * @since 2.0.0 */ boolean IS_FOLIA = classExists("io.papermc.paper.threadedregions.RegionizedServer"); /** * Checks if the server is running on the Purpur platform. * @since 2.0.0 */ boolean IS_PURPUR = classExists("org.purpurmc.purpur.PurpurConfig"); /** * Checks if the server is running on the Paper platform (or a fork like Purpur/Folia). * @since 2.0.0 */ boolean IS_PAPER = IS_PURPUR || IS_FOLIA || classExists("io.papermc.paper.configuration.PaperConfigurations"); /** * Returns the current {@link BetterModelBukkit} instance. * * @return the current platform instance * @since 2.0.0 */ static @NotNull BetterModelBukkit platform() { return (BetterModelBukkit) BetterModel.platform(); } /** * Returns the Bukkit-specific scheduler. * * @return the scheduler * @since 2.0.0 */ @Override @NotNull BukkitModelScheduler scheduler(); /** * Returns the Bukkit-specific adapter. * * @return the adapter * @since 2.0.0 */ @Override @NotNull BukkitAdapter adapter(); /** * Returns the Bukkit-specific event bus. * * @return the event bus * @since 2.0.0 */ @Override @NotNull BukkitModelEventBus eventBus(); } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/BukkitModelEventBus.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit; import kr.toxicity.model.api.BetterModelEventBus; import kr.toxicity.model.api.bukkit.event.BukkitEventApplication; import kr.toxicity.model.api.event.ModelEvent; import kr.toxicity.model.api.event.ModelEventListener; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; /** * A Bukkit-specific extension of the {@link BetterModelEventBus}. *

* This interface provides convenience methods for subscribing to events using a Bukkit {@link Plugin} instance. *

* * @since 2.0.0 */ public interface BukkitModelEventBus extends BetterModelEventBus { /** * Subscribes a consumer to a specific event type, associated with a Bukkit plugin. * * @param plugin the plugin that subscribes to the event * @param eventClass the class of the event to subscribe to * @param consumer the consumer to handle the event * @param the type of the event * @return a listener handle that can be used to unregister the subscription * @since 2.0.0 */ @NotNull default ModelEventListener subscribe(@NotNull Plugin plugin, @NotNull Class eventClass, @NotNull Consumer consumer) { return subscribe(BukkitEventApplication.of(plugin), eventClass, consumer); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/entity/BaseBukkitEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.entity; import kr.toxicity.model.api.bukkit.platform.BukkitAdapter; import kr.toxicity.model.api.bukkit.platform.BukkitEntity; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.util.TransformedItemStack; import org.bukkit.NamespacedKey; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.persistence.PersistentDataHolder; import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; /** * Represents a Bukkit-specific entity adapter. *

* This interface extends {@link BaseEntity} and {@link PersistentDataHolder} to provide * access to the underlying Bukkit entity and its persistent data container. *

* * @since 2.0.0 */ public interface BaseBukkitEntity extends BaseEntity, PersistentDataHolder { /** * The namespaced key used for storing tracker data in the entity's persistent data container. * @since 2.0.0 */ @NotNull NamespacedKey TRACKING_ID = Objects.requireNonNull(NamespacedKey.fromString("bettermodel_tracker")); /** * Returns the underlying Bukkit entity. * * @return the Bukkit entity * @since 2.0.0 */ default @NotNull Entity entity() { return ((BukkitEntity) platform()).source(); } /** * Returns the item in the entity's main hand. * * @return the main hand item * @since 2.0.0 */ @Override default @NotNull TransformedItemStack mainHand() { if (entity() instanceof LivingEntity livingEntity) { var equipment = livingEntity.getEquipment(); if (equipment != null) return TransformedItemStack.of(BukkitAdapter.adapt(equipment.getItemInMainHand())); } return TransformedItemStack.empty(); } /** * Returns the item in the entity's offhand. * * @return the offhand item * @since 2.0.0 */ @Override default @NotNull TransformedItemStack offHand() { if (entity() instanceof LivingEntity livingEntity) { var equipment = livingEntity.getEquipment(); if (equipment != null) return TransformedItemStack.of(BukkitAdapter.adapt(equipment.getItemInOffHand())); } return TransformedItemStack.empty(); } /** * Retrieves the model data stored in the entity's persistent data container. * * @return the model data string, or null if not present * @since 2.0.0 */ default @Nullable String modelData() { return getPersistentDataContainer().get(TRACKING_ID, PersistentDataType.STRING); } /** * Stores the model data in the entity's persistent data container. * * @param modelData the model data string, or null to remove it * @since 2.0.0 */ default void modelData(@Nullable String modelData) { var container = getPersistentDataContainer(); if (modelData == null) container.remove(TRACKING_ID); else container.set(TRACKING_ID, PersistentDataType.STRING, modelData); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/entity/BaseBukkitPlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.entity; import kr.toxicity.model.api.bukkit.platform.BukkitPlayer; import kr.toxicity.model.api.entity.BasePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; /** * Represents a Bukkit-specific player adapter. *

* This interface extends {@link BaseBukkitEntity} and {@link BasePlayer} to provide * access to the underlying Bukkit player. *

* * @since 2.0.0 */ public interface BaseBukkitPlayer extends BaseBukkitEntity, BasePlayer { /** * Returns the underlying Bukkit player. * * @return the Bukkit player * @since 2.0.0 */ @Override default @NotNull Player entity() { return ((BukkitPlayer) platform()).source(); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/event/BetterModelBukkitEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.event; import kr.toxicity.model.api.event.ModelEvent; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; import java.util.function.Supplier; /** * A wrapper class that adapts {@link ModelEvent} to Bukkit's {@link Event} system. *

* This allows Bukkit plugins to listen for BetterModel events using the standard Bukkit event API. * The underlying {@link ModelEvent} is lazily initialized when accessed. *

* * @since 2.0.0 */ public final class BetterModelBukkitEvent extends Event { private static final HandlerList HANDLER_LIST = new HandlerList(); private final Class eventClass; private final @NotNull Supplier supplier; private volatile ModelEvent source; /** * Creates a new BetterModelBukkitEvent. * * @param eventClass the class of the model event * @param supplier a supplier that creates the model event * @since 2.0.0 */ @ApiStatus.Internal public BetterModelBukkitEvent(@NotNull Class eventClass, @NotNull Supplier supplier) { super(!Bukkit.isPrimaryThread()); this.eventClass = eventClass; this.supplier = supplier; } /** * Checks if the wrapped event is an instance of the specified class. * * @param eventClass the class to check against * @param the type of the event * @return true if the wrapped event is assignable to the class * @since 2.0.0 */ public boolean is(@NotNull Class eventClass) { return eventClass.isAssignableFrom(this.eventClass); } /** * Casts the wrapped event to the specified class if possible. *

* This method initializes the underlying event if it hasn't been created yet. *

* * @param eventClass the class to cast to * @param the type of the event * @return the cast event, or null if the cast is not possible * @since 2.0.0 */ public @Nullable T as(@NotNull Class eventClass) { if (!is(eventClass)) return null; var event = source; if (event == null) { synchronized (this) { event = source; if (event == null) event = source = supplier.get(); } } return eventClass.cast(event); } /** * Executes a consumer if the wrapped event is of the specified type. * * @param eventClass the class to check against * @param consumer the consumer to execute * @param the type of the event * @since 2.0.0 */ public void as(@NotNull Class eventClass, @NotNull Consumer consumer) { var get = as(eventClass); if (get != null) consumer.accept(get); } /** * Returns the underlying model event, if initialized. * * @return the model event, or null if not yet initialized * @since 2.0.0 */ @ApiStatus.Internal public @Nullable ModelEvent source() { return source; } @Override public @NotNull HandlerList getHandlers() { return HANDLER_LIST; } /** * Returns the handler list for this event. * * @return the handler list * @since 2.0.0 */ public static HandlerList getHandlerList() { return HANDLER_LIST; } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/event/BukkitEventApplication.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.event; import kr.toxicity.model.api.event.ModelEventApplication; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import java.lang.ref.WeakReference; /** * An implementation of {@link ModelEventApplication} for Bukkit plugins. *

* This record holds a weak reference to a Bukkit plugin to prevent memory leaks * and checks if the plugin is enabled. *

* * @param name the name of the plugin * @param pluginRef a weak reference to the plugin instance * @since 2.0.0 */ public record BukkitEventApplication(@NotNull String name, @NotNull WeakReference pluginRef) implements ModelEventApplication { /** * Creates a new BukkitEventApplication for the given plugin. * * @param plugin the Bukkit plugin * @return the event application wrapper * @since 2.0.0 */ public static @NotNull BukkitEventApplication of(@NotNull Plugin plugin) { return new BukkitEventApplication(plugin.getName(), new WeakReference<>(plugin)); } @Override public boolean isEnabled() { var get = pluginRef().get(); return get != null && get.isEnabled(); } @Override public boolean equals(Object o) { if (!(o instanceof BukkitEventApplication that)) return false; return name.equals(that.name); } @Override public int hashCode() { return name.hashCode(); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitAdapter.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.platform.*; import org.bukkit.*; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Provides an adapter for converting Bukkit objects to BetterModel platform objects. *

* This class implements {@link PlatformAdapter} and offers static utility methods for adapting * entities, players, items, locations, and worlds. *

* * @since 2.0.0 */ public final class BukkitAdapter implements PlatformAdapter { /** * Adapts a Bukkit entity to a {@link PlatformEntity}. * * @param entity the Bukkit entity * @return the platform entity * @since 2.0.0 */ public static @NotNull PlatformEntity adapt(@NotNull Entity entity) { return new BukkitEntity(entity); } /** * Adapts a Bukkit living entity to a {@link PlatformLivingEntity}. * * @param livingEntity the Bukkit living entity * @return the platform living entity * @since 2.0.0 */ public static @NotNull PlatformLivingEntity adapt(@NotNull LivingEntity livingEntity) { return new BukkitLivingEntity(livingEntity); } /** * Adapts a Bukkit offline player to a {@link PlatformOfflinePlayer}. * * @param player the Bukkit offline player * @return the platform offline player * @since 2.0.0 */ public static @NotNull PlatformOfflinePlayer adapt(@NotNull OfflinePlayer player) { return new BukkitOfflinePlayer(player); } /** * Adapts a Bukkit player to a {@link PlatformPlayer}. * * @param player the Bukkit player * @return the platform player * @since 2.0.0 */ public static @NotNull PlatformPlayer adapt(@NotNull Player player) { return new BukkitPlayer(player); } /** * Adapts a Bukkit item stack to a {@link PlatformItemStack}. * * @param itemStack the Bukkit item stack * @return the platform item stack * @since 2.0.0 */ public static @NotNull PlatformItemStack adapt(@NotNull ItemStack itemStack) { return new BukkitItemStack(itemStack); } /** * Adapts a Bukkit location to a {@link PlatformLocation}. * * @param location the Bukkit location * @return the platform location * @since 2.0.0 */ public static @NotNull PlatformLocation adapt(@NotNull Location location) { return new BukkitLocation(location); } /** * Adapts a Bukkit world to a {@link PlatformWorld}. * * @param world the Bukkit world * @return the platform world * @since 2.0.0 */ public static @NotNull PlatformWorld adapt(@NotNull World world) { return new BukkitWorld(world); } @Override public @Nullable PlatformPlayer player(@NotNull UUID uuid) { var bukkit = Bukkit.getPlayer(uuid); return bukkit != null ? adapt(bukkit) : null; } @Override public @NotNull PlatformOfflinePlayer offlinePlayer(@NotNull UUID uuid) { return adapt(Bukkit.getOfflinePlayer(uuid)); } @Override public int serverViewDistance() { return Bukkit.getViewDistance(); } @Override public boolean isTickThread() { return Bukkit.isPrimaryThread(); } @Override public boolean isRegionSafe() { return !BetterModelBukkit.IS_FOLIA || isTickThread(); } @Override public @NotNull PlatformItemStack air() { return adapt(new ItemStack(Material.AIR)); } @Override public @NotNull PlatformLocation zero() { return adapt(new Location(null, 0, 0, 0)); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformLocation; import lombok.EqualsAndHashCode; import lombok.ToString; import org.bukkit.entity.Entity; import org.jetbrains.annotations.NotNull; import java.util.UUID; /** * Represents a Bukkit entity wrapped as a {@link PlatformEntity}. * * @since 2.0.0 */ @ToString @EqualsAndHashCode public class BukkitEntity implements PlatformEntity { private final Entity source; /** * Creates a new BukkitEntity wrapper. * * @param source the source Bukkit entity * @since 2.0.0 */ public BukkitEntity(@NotNull Entity source) { this.source = source; } /** * Returns the underlying Bukkit entity. * * @return the source entity * @since 2.0.0 */ public Entity source() { return source; } @Override public @NotNull UUID uuid() { return source.getUniqueId(); } @Override public @NotNull PlatformLocation location() { return BukkitAdapter.adapt(source.getLocation()); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitItemStack.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.platform.PlatformItemStack; import kr.toxicity.model.api.platform.PlatformNamespace; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a Bukkit item stack wrapped as a {@link PlatformItemStack}. * * @param source the source Bukkit item stack * @since 2.0.0 */ public record BukkitItemStack(@NotNull ItemStack source) implements PlatformItemStack { @Override public boolean isAir() { return source.getType().isAir() || source.getAmount() <= 0; } @Override public @NotNull PlatformItemStack enchant(boolean enchant) { var meta = source.getItemMeta(); if (meta == null) return this; meta.setEnchantmentGlintOverride(enchant); source.setItemMeta(meta); return this; } @SuppressWarnings("deprecation") @Override public @NotNull PlatformItemStack modelData(int customModelData, @Nullable PlatformNamespace namespace) { var meta = source.getItemMeta(); if (meta == null) return this; meta.setCustomModelData(customModelData); meta.setItemModel(namespace == null ? null : new NamespacedKey(namespace.namespace(), namespace.path())); source.setItemMeta(meta); return this; } @Override public @NotNull PlatformItemStack clone() { return BukkitAdapter.adapt(source.clone()); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitLivingEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.platform.PlatformLivingEntity; import kr.toxicity.model.api.platform.PlatformLocation; import org.bukkit.entity.LivingEntity; import org.jetbrains.annotations.NotNull; /** * Represents a Bukkit living entity wrapped as a {@link PlatformLivingEntity}. * * @since 2.0.0 */ public class BukkitLivingEntity extends BukkitEntity implements PlatformLivingEntity { /** * Creates a new BukkitLivingEntity wrapper. * * @param source the source Bukkit living entity * @since 2.0.0 */ public BukkitLivingEntity(@NotNull LivingEntity source) { super(source); } /** * Returns the underlying Bukkit living entity. * * @return the source living entity * @since 2.0.0 */ @Override public LivingEntity source() { return (LivingEntity) super.source(); } @Override public @NotNull PlatformLocation eyeLocation() { return BukkitAdapter.adapt(source().getEyeLocation()); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitLocation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformWorld; import kr.toxicity.model.api.scheduler.ModelTask; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a Bukkit location wrapped as a {@link PlatformLocation}. * * @param source the source Bukkit location * @since 2.0.0 */ public record BukkitLocation(@NotNull Location source) implements PlatformLocation { @Override public @NotNull PlatformWorld world() { return BukkitAdapter.adapt(source.getWorld()); } @Override public double x() { return source.getX(); } @Override public double y() { return source.getY(); } @Override public double z() { return source.getZ(); } @Override public float pitch() { return source.getPitch(); } @Override public float yaw() { return source.getYaw(); } @Override public @NotNull PlatformLocation add(double x, double y, double z) { return BukkitAdapter.adapt(source.clone().add(x, y, z)); } @Override public @Nullable ModelTask task(@NotNull Runnable runnable) { return BetterModelBukkit.platform().scheduler().task(source, runnable); } @Override public @Nullable ModelTask taskLater(long delay, @NotNull Runnable runnable) { return BetterModelBukkit.platform().scheduler().taskLater(source, delay, runnable); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitOfflinePlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.platform.PlatformOfflinePlayer; import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Represents a Bukkit offline player wrapped as a {@link PlatformOfflinePlayer}. * * @param source the source Bukkit offline player * @since 2.0.0 */ public record BukkitOfflinePlayer(@NotNull OfflinePlayer source) implements PlatformOfflinePlayer { @Override public @NotNull UUID uuid() { return source.getUniqueId(); } @Override public @Nullable String name() { return source.getName(); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitPlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.platform.PlatformPlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; /** * Represents a Bukkit player wrapped as a {@link PlatformPlayer}. * * @since 2.0.0 */ public final class BukkitPlayer extends BukkitLivingEntity implements PlatformPlayer { /** * Creates a new BukkitPlayer wrapper. * * @param source the source Bukkit player * @since 2.0.0 */ public BukkitPlayer(@NotNull Player source) { super(source); } /** * Returns the underlying Bukkit player. * * @return the source player * @since 2.0.0 */ public @NotNull Player source() { return (Player) super.source(); } @Override public @NotNull String name() { return source().getName(); } } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/platform/BukkitWorld.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.platform; import kr.toxicity.model.api.platform.PlatformWorld; import org.bukkit.World; import org.jetbrains.annotations.NotNull; /** * Represents a Bukkit world wrapped as a {@link PlatformWorld}. * * @param source the source Bukkit world * @since 2.0.0 */ public record BukkitWorld(@NotNull World source) implements PlatformWorld { } ================================================ FILE: api/bukkit-api/src/main/java/kr/toxicity/model/api/bukkit/scheduler/BukkitModelScheduler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bukkit.scheduler; import kr.toxicity.model.api.scheduler.ModelScheduler; import kr.toxicity.model.api.scheduler.ModelTask; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a Bukkit-specific scheduler for model tasks. *

* This interface extends {@link ModelScheduler} to provide methods for scheduling tasks * that are synchronized with specific locations (e.g., for Folia compatibility). *

* * @since 2.0.0 */ public interface BukkitModelScheduler extends ModelScheduler { /** * Schedules a task to run on the next tick, synchronized with the given location. * * @param location the location to synchronize with * @param runnable the task to run * @return the scheduled task, or null if scheduling failed * @since 2.0.0 */ @Nullable ModelTask task(@NotNull Location location, @NotNull Runnable runnable); /** * Schedules a task to run after a delay, synchronized with the given location. * * @param location the location to synchronize with * @param delay the delay in ticks * @param runnable the task to run * @return the scheduled task, or null if scheduling failed * @since 2.0.0 */ @Nullable ModelTask taskLater(@NotNull Location location, long delay, @NotNull Runnable runnable); } ================================================ FILE: api/mod-api/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.publish) id("net.neoforged.moddev") } dependencies { api(project(":bettermodel-api")) } neoForge { enable { neoFormVersion = libs.versions.neoform.get() } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/BetterModelMod.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.BetterModelPlatform; import kr.toxicity.model.api.mod.scheduler.ModModelScheduler; import net.minecraft.server.MinecraftServer; import org.jetbrains.annotations.NotNull; /** * Represents the Mod-specific platform interface for BetterModel. *

* This interface extends {@link BetterModelPlatform} to provide access to the underlying * Minecraft server instance and region holder for thread-safe operations. *

* * @since 2.0.0 */ public interface BetterModelMod extends BetterModelPlatform { /** * Returns the current {@link BetterModelMod} instance. * * @return the current platform instance * @since 2.0.0 */ static @NotNull BetterModelMod platform() { return (BetterModelMod) BetterModel.platform(); } /** * Returns the underlying Minecraft server instance. * * @return the Minecraft server * @since 2.0.0 */ @NotNull MinecraftServer server(); /** * Returns the Mod-specific scheduler. * * @return the scheduler * @since 2.0.0 */ @Override @NotNull ModModelScheduler scheduler(); } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/entity/BaseModEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.entity; import kr.toxicity.model.api.entity.BaseEntity; import net.minecraft.world.entity.Entity; import org.jetbrains.annotations.NotNull; /** * Represents a Mod-specific entity adapter. *

* This interface extends {@link BaseEntity} to provide access to the underlying NMS entity. *

* * @since 2.0.0 */ public interface BaseModEntity extends BaseEntity { /** * Returns the underlying NMS entity. * * @return the NMS entity * @since 2.0.0 */ default @NotNull Entity entity() { return (Entity) handle(); } /** * Sets the underlying NMS entity. * * @param entity the NMS entity * @since 2.0.0 */ void entity(@NotNull Entity entity); } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/entity/BaseModPlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.entity; import kr.toxicity.model.api.entity.BasePlayer; import net.minecraft.server.level.ServerPlayer; import org.jetbrains.annotations.NotNull; /** * Represents a Mod-specific player adapter. *

* This interface extends {@link BaseModEntity} and {@link BasePlayer} to provide * access to the underlying NMS server player. *

* * @since 2.0.0 */ public interface BaseModPlayer extends BaseModEntity, BasePlayer { /** * Returns the underlying NMS server player. * * @return the server player * @since 2.0.0 */ @Override default @NotNull ServerPlayer entity() { return (ServerPlayer) handle(); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModAdapter.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import com.mojang.authlib.GameProfile; import kr.toxicity.model.api.mod.BetterModelMod; import kr.toxicity.model.api.platform.*; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.ServerPlayerConnection; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Provides an adapter for converting Mod/NMS objects to BetterModel platform objects. *

* This class implements {@link PlatformAdapter} and offers static utility methods for adapting * entities, players, items, and worlds. *

* * @since 2.0.0 */ public final class ModAdapter implements PlatformAdapter { /** * Adapts an NMS entity to a {@link PlatformEntity}. * * @param entity the NMS entity * @return the platform entity * @since 2.0.0 */ public static @NotNull PlatformEntity adapt(@NotNull Entity entity) { return ModEntity.of(entity); } /** * Adapts an NMS living entity to a {@link PlatformLivingEntity}. * * @param livingEntity the NMS living entity * @return the platform living entity * @since 2.0.0 */ public static @NotNull PlatformLivingEntity adapt(@NotNull LivingEntity livingEntity) { return ModLivingEntity.of(livingEntity); } /** * Adapts an NMS player connection to a {@link PlatformPlayer}. * * @param connection the NMS player connection * @return the platform player * @since 2.0.0 */ public static @NotNull PlatformPlayer adapt(@NotNull ServerPlayerConnection connection) { return ModPlayer.of(connection); } /** * Adapts an NMS server player to a {@link PlatformPlayer}. * * @param player the NMS server player * @return the platform player * @since 2.0.0 */ public static @NotNull PlatformPlayer adapt(@NotNull ServerPlayer player) { return adapt(player.connection); } /** * Adapts a UUID to a {@link PlatformOfflinePlayer}. * * @param uuid the player UUID * @return the platform offline player * @since 2.0.0 */ public static @NotNull PlatformOfflinePlayer adapt(@NotNull UUID uuid) { return ModOfflinePlayer.of(uuid, null); } /** * Adapts a GameProfile to a {@link PlatformOfflinePlayer}. * * @param profile the game profile * @return the platform offline player * @since 2.0.0 */ public static @NotNull PlatformOfflinePlayer adapt(@NotNull GameProfile profile) { return ModOfflinePlayer.of(profile.id(), profile.name()); } /** * Adapts an NMS item stack to a {@link PlatformItemStack}. * * @param itemStack the NMS item stack * @return the platform item stack * @since 2.0.0 */ public static @NotNull PlatformItemStack adapt(@NotNull ItemStack itemStack) { return ModItemStack.of(itemStack); } /** * Adapts an NMS level to a {@link PlatformWorld}. * * @param world the NMS level * @return the platform world * @since 2.0.0 */ public static @NotNull PlatformWorld adapt(@NotNull Level world) { return ModWorld.of(world); } @Override public int serverViewDistance() { return server().getPlayerList().getViewDistance(); } @Override public boolean isTickThread() { return server().isSameThread(); } @Override public boolean isRegionSafe() { return true; } @Override public @Nullable PlatformPlayer player(@NotNull UUID uuid) { var player = server().getPlayerList().getPlayer(uuid); return player == null ? null : adapt(player); } @Override public @NotNull PlatformOfflinePlayer offlinePlayer(@NotNull UUID uuid) { var profile = server().services().profileResolver().fetchById(uuid).orElse(null); return profile == null ? adapt(uuid) : adapt(profile); } @Override public @NotNull PlatformItemStack air() { return adapt(ItemStack.EMPTY); } @Override public @NotNull PlatformLocation zero() { return ModLocation.of(null, 0, 0, 0); } private @NotNull MinecraftServer server() { return BetterModelMod.platform().server(); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformLocation; import net.minecraft.world.entity.Entity; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.UUID; /** * Represents a Mod entity wrapped as a {@link PlatformEntity}. * * @param source the source NMS entity * @since 2.0.0 */ public record ModEntity(@NotNull Entity source) implements PlatformEntity { @ApiStatus.Internal public ModEntity { } /** * Creates a ModEntity from the source. * * @param source the source entity * @return the instance * @since 2.0.0 */ public static @NotNull ModEntity of(@NotNull Entity source) { return new ModEntity(source); } @Override public @NotNull UUID uuid() { return source.getUUID(); } @Override public @NotNull PlatformLocation location() { return ModLocation.of(source); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModItemStack.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformItemStack; import kr.toxicity.model.api.platform.PlatformNamespace; import net.minecraft.core.component.DataComponents; import net.minecraft.resources.Identifier; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.CustomModelData; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; /** * Represents a Mod item stack wrapped as a {@link PlatformItemStack}. * * @param source the source NMS item stack * @since 2.0.0 */ public record ModItemStack(@NotNull ItemStack source) implements PlatformItemStack { @ApiStatus.Internal public ModItemStack { } /** * Creates a ModItemStack from the source. * * @param source the source item stack * @return the instance * @since 2.0.0 */ public static @NotNull ModItemStack of(@NotNull ItemStack source) { return new ModItemStack(source); } @Override public boolean isAir() { return source.isEmpty(); } @Override public @NotNull PlatformItemStack enchant(boolean enchant) { source.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, enchant); return this; } @Override public @NotNull PlatformItemStack modelData(int customModelData, @Nullable PlatformNamespace namespace) { source.set( DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(List.of((float) customModelData), List.of(), List.of(), List.of()) ); source.set( DataComponents.ITEM_MODEL, namespace == null ? null : Identifier.fromNamespaceAndPath(namespace.namespace(), namespace.path()) ); return this; } @Override public @NotNull PlatformItemStack clone() { return ModAdapter.adapt(source.copy()); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModLivingEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformLivingEntity; import kr.toxicity.model.api.platform.PlatformLocation; import net.minecraft.world.entity.LivingEntity; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.UUID; /** * Represents a Mod living entity wrapped as a {@link PlatformLivingEntity}. * * @param source the source NMS living entity * @since 2.0.0 */ public record ModLivingEntity(@NotNull LivingEntity source) implements PlatformLivingEntity { @ApiStatus.Internal public ModLivingEntity { } /** * Creates a ModLivingEntity from the source. * * @param source the source living entity * @return the instance * @since 2.0.0 */ public static @NotNull ModLivingEntity of(@NotNull LivingEntity source) { return new ModLivingEntity(source); } @Override public @NotNull UUID uuid() { return source.getUUID(); } @Override public @NotNull PlatformLocation location() { return ModLocation.of(source); } @Override public @NotNull PlatformLocation eyeLocation() { return ModLocation.ofEye(source); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModLocation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.mod.BetterModelMod; import kr.toxicity.model.api.mod.scheduler.ModModelScheduler; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformWorld; import kr.toxicity.model.api.scheduler.ModelTask; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a Mod location wrapped as a {@link PlatformLocation}. * * @param level the NMS level * @param x the x coordinate * @param y the y coordinate * @param z the z coordinate * @param pitch the pitch * @param yaw the yaw * @since 2.0.0 */ public record ModLocation(@Nullable Level level, double x, double y, double z, float pitch, float yaw) implements PlatformLocation { @ApiStatus.Internal public ModLocation { } /** * Creates a ModLocation from the coordinates. * * @param level the NMS level * @param x the x coordinate * @param y the y coordinate * @param z the z coordinate * @param pitch the pitch * @param yaw the yaw * @return the instance * @since 2.0.0 */ public static @NotNull ModLocation of(@Nullable Level level, double x, double y, double z, float pitch, float yaw) { return new ModLocation( level, x, y, z, pitch, yaw ); } /** * Creates a ModLocation from the coordinates with zero pitch and yaw. * * @param level the NMS level * @param x the x coordinate * @param y the y coordinate * @param z the z coordinate * @return the instance * @since 2.0.0 */ public static @NotNull ModLocation of(@Nullable Level level, double x, double y, double z) { return new ModLocation( level, x, y, z, 0.0f, 0.0f ); } /** * Creates a ModLocation from the position vector. * * @param level the NMS level * @param position the position vector * @param pitch the pitch * @param yaw the yaw * @return the instance * @since 2.0.0 */ public static @NotNull ModLocation of(@Nullable Level level, Vec3 position, float pitch, float yaw) { return new ModLocation( level, position.x, position.y, position.z, pitch, yaw ); } /** * Creates a ModLocation from the position vector with zero pitch and yaw. * * @param level the NMS level * @param position the position vector * @return the instance * @since 2.0.0 */ public static @NotNull ModLocation of(@Nullable Level level, Vec3 position) { return new ModLocation( level, position.x, position.y, position.z, 0.0f, 0.0f ); } /** * Creates a ModLocation from an entity's position. * * @param entity the entity * @return the location * @since 2.0.0 */ public static @NotNull ModLocation of(@NotNull Entity entity) { return new ModLocation( entity.level(), entity.getX(), entity.getY(), entity.getZ(), entity.getXRot(), entity.getYRot() ); } /** * Creates a ModLocation from an entity's eye position. * * @param entity the entity * @return the eye location * @since 2.0.0 */ public static @NotNull ModLocation ofEye(@NotNull Entity entity) { return new ModLocation( entity.level(), entity.getX(), entity.getEyeY(), entity.getZ(), entity.getXRot(), entity.getYRot() ); } @Override public @NotNull PlatformWorld world() { if (level == null) { throw new IllegalStateException("level is not set"); } return ModAdapter.adapt(level); } @Override public @NotNull PlatformLocation add(double x, double y, double z) { return new ModLocation( this.level, this.x + x, this.y + y, this.z + z, this.pitch, this.yaw ); } @Override public @Nullable ModelTask task(@NotNull Runnable runnable) { return scheduler().task(runnable); } @Override public @Nullable ModelTask taskLater(long delay, @NotNull Runnable runnable) { return scheduler().taskLater(delay, runnable); } private @NotNull ModModelScheduler scheduler() { return BetterModelMod.platform().scheduler(); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModOfflinePlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformOfflinePlayer; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Represents a Mod offline player wrapped as a {@link PlatformOfflinePlayer}. * * @param uuid the player UUID * @param name the player name, or null if unknown * @since 2.0.0 */ public record ModOfflinePlayer(@NotNull UUID uuid, @Nullable String name) implements PlatformOfflinePlayer { @ApiStatus.Internal public ModOfflinePlayer { } /** * Creates a ModOfflinePlayer from the UUID and name. * * @param uuid the player uuid * @param name the player name * @return the instance * @since 2.0.0 */ public static @NotNull ModOfflinePlayer of(@NotNull UUID uuid, @Nullable String name) { return new ModOfflinePlayer(uuid, name); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModPlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import net.minecraft.server.network.ServerPlayerConnection; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.UUID; /** * Represents a Mod player wrapped as a {@link PlatformPlayer}. * * @param source the source NMS player connection * @since 2.0.0 */ public record ModPlayer(@NotNull ServerPlayerConnection source) implements PlatformPlayer { @ApiStatus.Internal public ModPlayer { } /** * Creates a ModPlayer from the source. * * @param source the source player connection * @return the instance * @since 2.0.0 */ public static @NotNull ModPlayer of(@NotNull ServerPlayerConnection source) { return new ModPlayer(source); } @Override public @NotNull UUID uuid() { return source.getPlayer().getUUID(); } @Override public @NotNull PlatformLocation location() { return ModLocation.of(source.getPlayer()); } @Override public @NotNull PlatformLocation eyeLocation() { return ModLocation.ofEye(source.getPlayer()); } @Override public @NotNull String name() { return source.getPlayer().getPlainTextName(); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModRegionHolder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformRegionHolder; /** * Represents a Mod-specific region holder for managing thread-safe operations. *

* This interface extends {@link PlatformRegionHolder} to provide Mod-specific functionality * for scheduling tasks within specific regions or contexts. *

* * @since 2.0.0 */ public interface ModRegionHolder extends PlatformRegionHolder { } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/platform/ModWorld.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.platform; import kr.toxicity.model.api.platform.PlatformWorld; import net.minecraft.world.level.Level; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Represents a Fabric world wrapped as a {@link PlatformWorld}. * * @param level the source NMS level * @since 2.0.0 */ public record ModWorld(@NotNull Level level) implements PlatformWorld { @ApiStatus.Internal public ModWorld { } /** * Creates a FabricWorld from the level. * * @param level the source level * @return the instance * @since 2.0.0 */ public static @NotNull ModWorld of(@NotNull Level level) { return new ModWorld(level); } } ================================================ FILE: api/mod-api/src/main/java/kr/toxicity/model/api/mod/scheduler/ModModelScheduler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mod.scheduler; import kr.toxicity.model.api.scheduler.ModelScheduler; import kr.toxicity.model.api.scheduler.ModelTask; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a Mod-specific scheduler for model tasks. *

* This interface extends {@link ModelScheduler} to provide methods for scheduling tasks * within the Mod environment. *

* * @since 2.0.0 */ public interface ModModelScheduler extends ModelScheduler { /** * Schedules a task to run on the next tick. * * @param runnable the task to run * @return the scheduled task, or null if scheduling failed * @since 2.0.0 */ @Nullable ModelTask task(@NotNull Runnable runnable); /** * Schedules a task to run after a delay. * * @param delay the delay in ticks * @param runnable the task to run * @return the scheduled task, or null if scheduling failed * @since 2.0.0 */ @Nullable ModelTask taskLater(long delay, @NotNull Runnable runnable); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/BetterModel.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.nms.NMS; import kr.toxicity.model.api.nms.PlayerChannelHandler; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.tracker.EntityTrackerRegistry; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.*; /** * The main entry point for the BetterModel API. *

* This class provides static access to the platform instance, configuration, model managers, * NMS handlers, and entity registries. It serves as a service provider for interacting with the BetterModel engine. *

* * @since 1.15.2 */ public final class BetterModel { /** * Private initializer to prevent instantiation. */ private BetterModel() { throw new RuntimeException(); } /** * The singleton platform instance. */ private static BetterModelPlatform instance; /** * Returns the platform configuration manager. * * @return the configuration manager * @since 1.15.2 */ public static @NotNull BetterModelConfig config() { return platform().config(); } /** * Retrieves a model renderer by its name, wrapped in an Optional. * * @param name the name of the model * @return an optional containing the renderer if found * @since 1.15.2 */ public static @NotNull Optional model(@NotNull String name) { return Optional.ofNullable(modelOrNull(name)); } /** * Retrieves a model renderer by its name, or null if not found. * * @param name the name of the model * @return the renderer, or null * @since 1.15.2 */ public static @Nullable ModelRenderer modelOrNull(@NotNull String name) { return platform().modelManager().model(name); } /** * Retrieves a player limb renderer by its name, wrapped in an Optional. * * @param name the name of the limb model * @return an optional containing the renderer if found * @since 1.15.2 */ public static @NotNull Optional limb(@NotNull String name) { return Optional.ofNullable(limbOrNull(name)); } /** * Retrieves a player limb renderer by its name, or null if not found. * * @param name the name of the limb model * @return the renderer, or null * @since 1.15.2 */ public static @Nullable ModelRenderer limbOrNull(@NotNull String name) { return platform().modelManager().limb(name); } /** * Retrieves a player channel handler by the player's UUID. * * @param uuid the player's UUID * @return an optional containing the channel handler if found * @since 1.15.2 */ public static @NotNull Optional player(@NotNull UUID uuid) { return Optional.ofNullable(platform().playerManager().player(uuid)); } /** * Retrieves an entity tracker registry by the entity's UUID. * * @param uuid the entity's UUID * @return an optional containing the registry if found * @since 1.15.2 */ public static @NotNull Optional registry(@NotNull UUID uuid) { return Optional.ofNullable(registryOrNull(uuid)); } /** * Retrieves an entity tracker registry for a Bukkit entity. * * @param entity the Bukkit entity * @return an optional containing the registry if found * @since 1.15.2 */ public static @NotNull Optional registry(@NotNull PlatformEntity entity) { return Optional.ofNullable(registryOrNull(entity)); } /** * Retrieves an entity tracker registry for a base entity. * * @param entity the base entity * @return an optional containing the registry if found * @since 1.15.2 */ public static @NotNull Optional registry(@NotNull BaseEntity entity) { return Optional.ofNullable(registryOrNull(entity)); } /** * Retrieves an entity tracker registry by the entity's UUID, or null if not found. * * @param uuid the entity's UUID * @return the registry, or null * @since 1.15.2 */ public static @Nullable EntityTrackerRegistry registryOrNull(@NotNull UUID uuid) { return EntityTrackerRegistry.registry(uuid); } /** * Retrieves an entity tracker registry for a Bukkit entity, or null if not found. * * @param entity the Bukkit entity * @return the registry, or null * @since 1.15.2 */ public static @Nullable EntityTrackerRegistry registryOrNull(@NotNull PlatformEntity entity) { return registryOrNull(nms().adapt(entity)); } /** * Retrieves an entity tracker registry for a base entity, or null if not found. * * @param entity the base entity * @return the registry, or null * @since 1.15.2 */ public static @Nullable EntityTrackerRegistry registryOrNull(@NotNull BaseEntity entity) { return EntityTrackerRegistry.registry(entity); } /** * Returns a collection of all loaded model renderers. * * @return an unmodifiable collection of models * @since 1.15.2 */ public static @NotNull @Unmodifiable Collection models() { return platform().modelManager().models(); } /** * Returns a collection of all loaded player limb renderers. * * @return an unmodifiable collection of limb models * @since 1.15.2 */ public static @NotNull @Unmodifiable Collection limbs() { return platform().modelManager().limbs(); } /** * Returns a set of all loaded model names. * * @return an unmodifiable set of model keys * @since 1.15.2 */ public static @NotNull @Unmodifiable Set modelKeys() { return platform().modelManager().modelKeys(); } /** * Returns a set of all loaded player limb model names. * * @return an unmodifiable set of limb keys * @since 1.15.2 */ public static @NotNull @Unmodifiable Set limbKeys() { return platform().modelManager().limbKeys(); } /** * Returns the singleton instance of the BetterModel platform. * * @return the platform instance * @throws NullPointerException if the platform has not been initialized * @since 2.0.0 */ public static @NotNull BetterModelPlatform platform() { return Objects.requireNonNull(instance, "BetterModel hasn't been initialized yet!"); } /** * Returns the NMS handler instance. * * @return the NMS handler * @since 1.15.2 */ public static @NotNull NMS nms() { return platform().nms(); } /** * Returns the event bus. * * @return the event bus * @since 2.0.0 */ public static @NotNull BetterModelEventBus eventBus() { return platform().eventBus(); } /** * Registers the platform instance. *

* This method is intended for internal use only during platform initialization. *

* * @param instance the platform instance * @throws RuntimeException if an instance is already registered * @since 1.15.2 */ @ApiStatus.Internal public static void register(@NotNull BetterModelPlatform instance) { Objects.requireNonNull(instance, "instance cannot be null."); if (BetterModel.instance == instance) throw new RuntimeException("Duplicated instance."); BetterModel.instance = instance; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/BetterModelConfig.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api; import kr.toxicity.model.api.config.DebugConfig; import kr.toxicity.model.api.config.IndicatorConfig; import kr.toxicity.model.api.config.ModuleConfig; import kr.toxicity.model.api.config.PackConfig; import kr.toxicity.model.api.mount.MountController; import kr.toxicity.model.api.platform.PlatformItemStack; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * Represents the main configuration interface for BetterModel. *

* This interface provides access to various configuration settings, including debug options, * pack generation settings, module toggles, and runtime behaviors. *

* * @since 1.15.2 */ public interface BetterModelConfig { /** * Returns the debug configuration. * * @return the debug config * @since 1.15.2 */ @NotNull DebugConfig debug(); /** * Returns the indicator configuration. * * @return the indicator config * @since 1.15.2 */ @NotNull IndicatorConfig indicator(); /** * Returns the module configuration. * * @return the module config * @since 1.15.2 */ @NotNull ModuleConfig module(); /** * Returns the resource pack configuration. * * @return the pack config * @since 1.15.2 */ @NotNull PackConfig pack(); /** * Checks if metrics collection is enabled. * * @return true if enabled, false otherwise * @since 1.15.2 */ boolean metrics(); /** * Checks if sight tracing (visibility checking) is enabled. * * @return true if enabled, false otherwise * @since 1.15.2 */ boolean sightTrace(); /** * Checks if BetterModel should attempt to merge its resource pack with external plugins/mods. * * @return true to merge, false otherwise * @since 1.15.2 */ boolean mergeWithExternalResources(); /** * Returns a supplier for the platform item stack used as the base for model items. * * @return a supplier providing the target item stack * @since 2.0.0 */ @NotNull Supplier item(); /** * Returns the item model string identifier used for the resource pack target item. * * @return the item model string * @since 2.0.0 */ @NotNull String itemModel(); /** * Returns the namespace used for the target item. * * @return the item namespace * @since 1.15.2 */ @NotNull String itemNamespace(); /** * Returns the maximum range for sight tracing. * * @return the max range * @since 1.15.2 */ double maxSight(); /** * Returns the minimum range for sight tracing. * * @return the min range * @since 1.15.2 */ double minSight(); /** * Returns the namespace used for the generated resource pack. * * @return the namespace * @since 1.15.2 */ @NotNull String namespace(); /** * Returns the type of resource pack generation (Folder, Zip, or None). * * @return the pack type * @since 1.15.2 */ @NotNull PackType packType(); /** * Returns the location of the build folder for resource packs. * * @return the build folder path * @since 1.15.2 */ @NotNull String buildFolderLocation(); /** * Checks if model trackers should follow the source entity's invisibility status. * * @return true to follow invisibility, false otherwise * @since 1.15.2 */ boolean followMobInvisibility(); /** * Checks if Purpur's AFK API should be used. * * @return true to use Purpur AFK, false otherwise * @since 1.15.2 */ boolean usePurpurAfk(); /** * Checks if version update notifications should be sent to OPs on join. * * @return true to send notifications, false otherwise * @since 1.15.2 */ boolean versionCheck(); /** * Returns the default mount controller used for entities. * * @return the default mount controller * @see kr.toxicity.model.api.mount.MountControllers * @since 1.15.2 */ @NotNull MountController defaultMountController(); /** * Returns the interpolation frame time (lerp) in milliseconds. * * @return the lerp frame time * @since 1.15.2 */ int lerpFrameTime(); /** * Checks if inventory swap packets should be cancelled for players with active models. * * @return true to cancel, false otherwise * @since 1.15.2 */ boolean cancelPlayerModelInventory(); /** * Returns the delay in ticks before hiding a player's model after they become invisible. * * @return the hide delay * @since 1.15.2 */ long playerHideDelay(); /** * Returns the threshold size for packet bundling. * * @return the packet bundling size * @since 1.15.2 */ int packetBundlingSize(); /** * Checks if strict loading mode is enabled. *

* Strict loading causes the platform to fail fast on model loading errors. *

* * @return true if strict loading is enabled, false otherwise * @since 1.15.2 */ boolean enableStrictLoading(); /** * Enumerates the types of resource pack generation. * * @since 1.15.2 */ enum PackType { /** * Generate the resource pack as a folder structure. * @since 1.15.2 */ FOLDER, /** * Generate the resource pack as a ZIP archive. * @since 1.15.2 */ ZIP, /** * Do not generate a resource pack. * @since 1.15.2 */ NONE } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/BetterModelEvaluator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api; import kr.toxicity.model.api.util.function.Float2FloatFunction; import org.jetbrains.annotations.NotNull; /** * Evaluator */ public interface BetterModelEvaluator { /** * Compiles molang expression * @param expression expression * @return compiled function */ @NotNull Float2FloatFunction compile(@NotNull String expression); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/BetterModelEventBus.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api; import kr.toxicity.model.api.event.ModelEvent; import kr.toxicity.model.api.event.ModelEventApplication; import kr.toxicity.model.api.event.ModelEventListener; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; import java.util.function.Supplier; /** * A central event bus for handling model-related events. *

* This interface allows subscribing to and publishing {@link ModelEvent}s. * It serves as a decoupling mechanism between different parts of the engine. *

* * @since 2.0.0 */ public interface BetterModelEventBus { /** * Subscribes a consumer to a specific event type. * * @param application the application that subscribes to the event * @param eventClass the class of the event to subscribe to * @param consumer the consumer to handle the event * @param the type of the event * @return a listener handle that can be used to unregister the subscription * @since 2.0.0 */ @NotNull ModelEventListener subscribe(@NotNull ModelEventApplication application, @NotNull Class eventClass, @NotNull Consumer consumer); /** * Publishes an event to all registered subscribers. *

* The event is created lazily using the provided supplier if there are subscribers. *

* * @param eventClass the class of the event * @param eventSupplier a supplier that creates the event * @param the type of the event * @return the result of the event call * @since 2.0.0 */ @NotNull Result call(@NotNull Class eventClass, @NotNull Supplier eventSupplier); /** * Publishes an event to all registered subscribers. * * @param event the event to publish * @return the result of the event call * @since 2.0.0 */ default @NotNull Result call(@NotNull ModelEvent event) { return call(event.getClass(), () -> event); } /** * Represents the outcome of an event publication. * * @since 2.0.0 */ @RequiredArgsConstructor enum Result { /** * The event was successfully processed by at least one subscriber. * @since 2.0.0 */ SUCCESS(true), /** * The event processing failed or was canceled. * @since 2.0.0 */ FAIL(false), /** * No handlers were registered for this event type. * @since 2.0.0 */ NO_EVENT_HANDLER(true) ; private final boolean triggered; /** * Checks if the event was considered "triggered" (i.e., not canceled or failed). *

* Note that {@link #NO_EVENT_HANDLER} is considered triggered as the operation wasn't blocked. *

* * @return true if triggered, false otherwise * @since 2.0.0 */ public boolean triggered() { return triggered; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/BetterModelLogger.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; /** * BetterModel's logger */ public interface BetterModelLogger { /** * Infos messages * @param message message */ void info(@NotNull Component... message); /** * Warns message * @param message message */ void warn(@NotNull Component... message); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/BetterModelPlatform.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api; import kr.toxicity.model.api.event.ModelEventApplication; import kr.toxicity.model.api.manager.*; import kr.toxicity.model.api.nms.NMS; import kr.toxicity.model.api.pack.PackResult; import kr.toxicity.model.api.pack.PackZipper; import kr.toxicity.model.api.platform.PlatformAdapter; import kr.toxicity.model.api.scheduler.ModelScheduler; import kr.toxicity.model.api.version.MinecraftVersion; import lombok.RequiredArgsConstructor; import net.kyori.adventure.audience.Audience; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.semver4j.Semver; import java.io.File; import java.io.InputStream; import java.util.function.Consumer; /** * Represents the main platform interface for BetterModel. * * @see BetterModel * @since 1.15.2 */ public interface BetterModelPlatform extends ModelEventApplication { /** * Returns the data folder for the BetterModel plugin. * This is where configuration files, data files, and other plugin-specific resources are stored. * * @return the data folder as a {@link File} object. * @since 2.0.0 */ @NotNull File dataFolder(); /** * Returns the type of JAR file this platform is running on (e.g., SPIGOT, PAPER, FABRIC). * * @return the {@link JarType} enum representing the platform's JAR type. * @since 2.0.0 */ @NotNull JarType jarType(); /** * Reloads the platform with default settings (console sender). * * @return the result of the reload operation * @since 2.0.0 */ default @NotNull ReloadResult reload() { return reload(ReloadInfo.DEFAULT); } /** * Reloads the platform, specifying the command sender who initiated it. * * @param sender the command sender * @return the result of the reload operation * @since 1.15.2 */ default @NotNull ReloadResult reload(@NotNull Audience sender) { return reload(ReloadInfo.builder().sender(sender).build()); } /** * Reloads the platform with specific reload information. * * @param info the reload configuration * @return the result of the reload operation * @since 1.15.2 */ @NotNull ReloadResult reload(@NotNull ReloadInfo info); /** * Checks if the running version of BetterModel is a snapshot build. * * @return true if snapshot, false otherwise * @since 1.15.2 */ boolean isSnapshot(); /** * Returns the platform's configuration manager. * * @return the configuration * @since 1.15.2 */ @NotNull BetterModelConfig config(); /** * Returns the Minecraft version of the running server. * * @return the Minecraft version * @since 1.15.2 */ @NotNull MinecraftVersion version(); /** * Returns the semantic version of the platform. * * @return the semantic version * @since 1.15.2 */ @NotNull Semver semver(); /** * Returns the NMS (Net.Minecraft.Server) handler for version-specific operations. * * @return the NMS handler * @since 1.15.2 */ @NotNull NMS nms(); /** * Returns the model manager. * * @return the model manager * @since 1.15.2 */ @NotNull ModelManager modelManager(); /** * Returns the player manager. * * @return the player manager * @since 1.15.2 */ @NotNull PlayerManager playerManager(); /** * Returns the script manager. * * @return the script manager * @since 1.15.2 */ @NotNull ScriptManager scriptManager(); /** * Returns the skin manager. * * @return the skin manager * @since 1.15.2 */ @NotNull SkinManager skinManager(); /** * Returns the profile manager. * * @return the profile manager * @since 1.15.2 */ @NotNull ProfileManager profileManager(); /** * Returns the platform's scheduler. * * @return the scheduler * @since 1.15.2 */ @NotNull ModelScheduler scheduler(); /** * Return the platform's adapter * @return the adapter */ @NotNull PlatformAdapter adapter(); /** * Registers a handler to be executed when a reload starts. * * @param consumer the handler, receiving the {@link PackZipper} * @since 1.15.2 */ void addReloadStartHandler(@NotNull Consumer consumer); /** * Registers a handler to be executed when a reload ends. * * @param consumer the handler, receiving the {@link ReloadResult} * @since 1.15.2 */ void addReloadEndHandler(@NotNull Consumer consumer); /** * Returns the platform's logger. * * @return the logger * @since 1.15.2 */ @NotNull BetterModelLogger logger(); /** * Returns the expression evaluator. * * @return the evaluator * @since 1.15.2 */ @NotNull BetterModelEvaluator evaluator(); /** * Returns the event bus. * * @return the event bus * @since 2.0.0 */ @NotNull BetterModelEventBus eventBus(); /** * Retrieves a resource from the platform's JAR file. * * @param path the path to the resource * @return an input stream for the resource, or null if not found * @since 1.15.2 */ @Nullable InputStream getResource(@NotNull String path); /** * Represents the outcome of a platform reload operation. * * @since 1.15.2 */ sealed interface ReloadResult { /** * Indicates a successful reload. * * @param firstLoad true if this is the first load (startup), false otherwise * @param assetsTime the time taken to reload assets in milliseconds * @param packResult the result of the resource pack generation * @since 1.15.2 */ record Success(boolean firstLoad, long assetsTime, @NotNull PackResult packResult) implements ReloadResult { /** * Returns the time taken to generate the resource pack. * * @return the packing time in milliseconds * @since 1.15.2 */ public long packingTime() { return packResult().time(); } /** * Returns the total time taken for the reload operation. * * @return the total time in milliseconds * @since 1.15.2 */ public long totalTime() { return assetsTime + packingTime(); } /** * Returns the size of the generated resource pack. * * @return the size in bytes * @since 1.15.2 */ public long length() { var dir = packResult.directory(); return dir != null && dir.isFile() ? dir.length() : packResult.stream().mapToLong(b -> b.bytes().length).sum(); } } /** * Indicates that a reload is currently in progress. * @since 1.15.2 */ enum OnReload implements ReloadResult { /** * Singleton instance. * @since 1.15.2 */ INSTANCE } /** * Indicates a failed reload. * * @param throwable the exception that caused the failure * @since 1.15.2 */ record Failure(@NotNull Throwable throwable) implements ReloadResult { } } /** * Represents the type of JAR file the platform is running on. * This enum helps identify the specific server implementation (e.g., Spigot, Paper, Fabric). * * @since 2.0.0 */ @RequiredArgsConstructor enum JarType { /** * Indicates a Spigot-based server. * @since 2.0.0 */ SPIGOT("spigot"), /** * Indicates a Paper-based server. * @since 2.0.0 */ PAPER("paper"), /** * Indicates a Fabric-based server. * @since 2.0.0 */ FABRIC("fabric"); private final String raw; /** * Returns the raw string representation of the JAR type. * * @return the raw string (e.g., "spigot", "paper", "fabric") * @since 2.0.0 */ public String raw() { return raw; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/AnimationIterator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import com.google.gson.annotations.SerializedName; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import java.util.Iterator; /** * An iterator for traversing animation keyframes. *

* This interface supports different looping modes (play once, loop, hold on last) * and allows resetting the iteration state. *

* * @param the type of keyframe (must implement {@link Timed}) * @since 1.15.2 */ public sealed interface AnimationIterator extends Iterator { /** * Resets the iterator to its initial state. * @since 1.15.2 */ void clear(); /** * Returns the type of this animation iterator. * * @return the animation type * @since 1.15.2 */ @NotNull Type type(); /** * Defines the behavior of the animation iterator. * @since 1.15.2 */ @RequiredArgsConstructor enum Type { /** * Plays the animation once and then stops. * @since 1.15.2 */ @SerializedName("once") PLAY_ONCE { @Override public @NotNull AnimationIterator create(@NotNull TimedStorage keyframes) { return new PlayOnce<>(keyframes); } }, /** * Loops the animation continuously. * @since 1.15.2 */ @SerializedName("loop") LOOP { @Override public @NotNull AnimationIterator create(@NotNull TimedStorage keyframes) { return new Loop<>(keyframes); } }, /** * Plays the animation once and holds the last frame. * @since 1.15.2 */ @SerializedName("hold") HOLD_ON_LAST { @Override public @NotNull AnimationIterator create(@NotNull TimedStorage keyframes) { return new HoldOnLast<>(keyframes); } } ; /** * Creates a new iterator for the given keyframes based on this type. * * @param keyframes the keyframes to iterate over * @param the type of keyframe * @return a new animation iterator * @since 1.15.2 */ public abstract @NotNull AnimationIterator create(@NotNull TimedStorage keyframes); } /** * Implementation for {@link Type#PLAY_ONCE}. * * @param the type of keyframe * @since 1.15.2 */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) final class PlayOnce implements AnimationIterator { private final TimedStorage keyframe; private int index = 0; @Override public void clear() { index = Integer.MAX_VALUE; } @Override public boolean hasNext() { return index < keyframe.size(); } @Override @NotNull public T next() { return keyframe.get(index++); } @NotNull @Override public Type type() { return Type.PLAY_ONCE; } } /** * Implementation for {@link Type#HOLD_ON_LAST}. * * @param the type of keyframe * @since 1.15.2 */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) final class HoldOnLast implements AnimationIterator { private final TimedStorage keyframe; private int index = 0; @Override public void clear() { index = 0; } @Override public boolean hasNext() { return true; } @Override @NotNull public T next() { if (index >= keyframe.size()) return keyframe.getLast(); return keyframe.get(index++); } @NotNull @Override public Type type() { return Type.HOLD_ON_LAST; } } /** * Implementation for {@link Type#LOOP}. * * @param the type of keyframe * @since 1.15.2 */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) final class Loop implements AnimationIterator { private final TimedStorage keyframe; private int index = 0; @Override public void clear() { index = 0; } @Override public boolean hasNext() { return true; } @Override @NotNull public T next() { if (index >= keyframe.size()) index = 0; return keyframe.get(index++); } @NotNull @Override public Type type() { return Type.LOOP; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/AnimationKeyframe.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import kr.toxicity.model.api.bone.BoneMovement; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import java.util.Arrays; import static kr.toxicity.model.api.util.MathUtil.isNotZero; /** * Represents a collection of animation keyframes, optimized for efficient storage and access. *

* This record stores an array of {@link AnimationProgress} objects, which define the state of a bone * at specific time intervals. It implements {@link TimedStorage} for indexed access. *

* * @param progresses the array of animation progresses * @since 2.0.0 */ public record AnimationKeyframe( @NotNull AnimationProgress[] progresses ) implements TimedStorage { /** * Creates a new builder for constructing an AnimationKeyframe. * * @param size the number of keyframes * @param rotateGlobal whether rotation should be applied globally * @return a new builder instance * @since 2.0.0 */ public static @NotNull Builder builder(int size, boolean rotateGlobal) { return new Builder(size, rotateGlobal); } private record AnimationArray( boolean rotateGlobal, boolean[] skipInterpolation, float[] times, float[] position, float[] scale, float[] rotation ) { AnimationArray(int size, boolean rotateGlobal) { this( rotateGlobal, new boolean[size], new float[size], new float[size * 3], new float[size * 3], new float[size * 3] ); } } /** * Builder for {@link AnimationKeyframe}. *

* This builder allows for efficient population of keyframe data using primitive arrays. *

* * @since 2.0.0 */ public static final class Builder { private final AnimationArray set; private final AnimationProgress[] progresses; private int index = 0; private Builder(int size, boolean rotateGlobal) { set = new AnimationArray(size, rotateGlobal); progresses = new AnimationProgress[size]; } /** * Writes a keyframe data point. * * @param time the time of the keyframe * @param position the position vector * @param scale the scale vector * @param rotation the rotation vector * @param skipInterpolation whether to skip interpolation for this keyframe * @since 2.0.0 */ public void write( float time, @NotNull Vector3f position, @NotNull Vector3f scale, @NotNull Vector3f rotation, boolean skipInterpolation ) { var i = index++; var x = i * 3; var y = x + 1; var z = x + 2; set.times[i] = time; set.position[x] = position.x; set.position[y] = position.y; set.position[z] = position.z; set.scale[x] = scale.x + 1; set.scale[y] = scale.y + 1; set.scale[z] = scale.z + 1; set.rotation[x] = rotation.x; set.rotation[y] = rotation.y; set.rotation[z] = rotation.z; set.skipInterpolation[i] = skipInterpolation; this.progresses[i] = isNotZero(position) || isNotZero(scale) || isNotZero(rotation) ? new ArrayProgress(set, i) : AnimationProgress.empty(time); } /** * Builds the {@link AnimationKeyframe}. * * @return the created keyframe collection * @since 2.0.0 */ public @NotNull AnimationKeyframe build() { return new AnimationKeyframe(progresses); } } private record ArrayProgress(@NotNull AnimationArray array, int index) implements AnimationProgress { @Override public @NotNull BoneMovement animate(@NotNull BoneMovement movement, @NotNull BoneMovement dest) { var destPos = movement.position().get(dest.position()); var destScl = movement.scale().get(dest.scale()); var destRot = movement.rotation().get(dest.rotation()); var destRawRot = movement.rawRotation().get(dest.rawRotation()); var position = array.position; var scale = array.scale; var rotation = array.rotation; var x = index * 3; var y = x + 1; var z = x + 2; destPos.add(position[x], position[y], position[z]); destScl.mul(scale[x], scale[y], scale[z]); MathUtil.toQuaternion(destRawRot.add(rotation[x], rotation[y], rotation[z]), destRot); return dest; } @Override public boolean skipInterpolation() { return array.skipInterpolation[index]; } @Override public boolean globalRotation() { return array.rotateGlobal; } @Override public float time() { return array.times[index]; } } @Override public @NotNull AnimationProgress get(int i) { return progresses[i]; } @Override public @NotNull AnimationProgress getLast() { return get(progresses.length - 1); } @Override public int size() { return progresses.length; } /** * Converts this keyframe collection to a storage of empty progresses. * * @return a new timed storage with empty progresses * @since 2.0.0 */ public @NotNull TimedStorage toEmpty() { return TimedStorage.listOf(Arrays.stream(progresses) .map(AnimationProgress::toEmpty) .toList()); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/AnimationModifier.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.util.FunctionUtil; import kr.toxicity.model.api.util.MathUtil; import kr.toxicity.model.api.util.function.FloatSupplier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.BooleanSupplier; /** * A modifier of animation. * @param predicate predicate * @param start start lerp * @param end end lerp * @param priority priority * @param type animation type * @param speed speed modifier * @param override override * @param player player */ public record AnimationModifier( @Nullable BooleanSupplier predicate, int start, int end, int priority, @Nullable AnimationIterator.Type type, @Nullable FloatSupplier speed, @Nullable Boolean override, @Nullable PlatformPlayer player ) { /** * Default modifier */ @NotNull public static final AnimationModifier DEFAULT = builder().build(); /** * Default with play once modifier */ public static final AnimationModifier DEFAULT_WITH_PLAY_ONCE = builder().type(AnimationIterator.Type.PLAY_ONCE).build(); /** * Creates builder * @return builder */ public static @NotNull Builder builder() { return new Builder(); } /** * Makes this modifier as builder * @return builder */ public @NotNull Builder toBuilder() { return builder() .predicate(predicate) .start(start) .end(end) .type(type) .speed(speed) .override(override) .player(player); } /** * Builder */ public static final class Builder { private BooleanSupplier predicate = null; private int start = 1; private int end = 0; private int priority = 0; private AnimationIterator.Type type = null; private FloatSupplier speed = null; private Boolean override = null; private PlatformPlayer player = null; /** * Private initializer */ private Builder() { } /** * Sets the predicate of this modifier * @param predicate predicate * @return self */ public @NotNull Builder predicate(@Nullable BooleanSupplier predicate) { this.predicate = predicate == null ? null : FunctionUtil.throttleTickBoolean(predicate); return this; } /** * Sets the lerp-in time of this modifier * @param start lerp-in time * @return self */ public @NotNull Builder start(int start) { this.start = start; return this; } /** * Sets the lerp-out time of this modifier * @param end lerp-out time * @return self */ public @NotNull Builder end(int end) { this.end = end; return this; } /** * Sets the priority of this modifier * @param priority priority * @return self */ public @NotNull Builder priority(int priority) { this.priority = priority; return this; } /** * Sets the animation type of this modifier * @param type animation type * @return self */ public @NotNull Builder type(@Nullable AnimationIterator.Type type) { this.type = type; return this; } /** * Sets the speed modifier of this modifier * @param speed speed * @return self */ public @NotNull Builder speed(float speed) { this.speed = toSupplier(speed); return this; } /** * Sets the speed modifier of this modifier * @param speed speed modifier * @return self */ public @NotNull Builder speed(@Nullable FloatSupplier speed) { this.speed = speed == null ? null : FunctionUtil.throttleTickFloat(speed); return this; } /** * Sets the override flag of this modifier * @param override override flag * @return self */ public @NotNull Builder override(@Nullable Boolean override) { this.override = override; return this; } /** * Sets the target player of this modifier * @param player target player * @return self */ public @NotNull Builder player(@Nullable PlatformPlayer player) { this.player = player; return this; } /** * Merges non-default value with other modifier * @param modifier modifier * @return self */ public @NotNull Builder mergeNotDefault(@NotNull AnimationModifier modifier) { if (modifier.predicate != null) predicate(modifier.predicate); if (modifier.start >= 0) start(modifier.start); if (modifier.end >= 0) end(modifier.end); if (modifier.type != null) type(modifier.type); if (modifier.speed != null) speed(modifier.speed); if (modifier.override != null) override(modifier.override); if (modifier.player != null) player(modifier.player); return this; } /** * Builds animation modifier * @return build */ public @NotNull AnimationModifier build() { return new AnimationModifier( predicate, start, end, priority, type, speed, override, player ); } } /** * Creates modifier * * @param start start time * @param end end time */ public AnimationModifier(int start, int end) { this(start, end, null, null); } /** * Creates modifier * * @param start start time * @param end end time * @param speedValue speed value */ public AnimationModifier(int start, int end, float speedValue) { this(start, end, null, FloatSupplier.of(speedValue)); } /** * Creates modifier * * @param start start time * @param end end time * @param supplier speed supplier */ public AnimationModifier(int start, int end, @Nullable FloatSupplier supplier) { this(start, end, null, supplier); } /** * Creates modifier * * @param start start time * @param end end time * @param type type */ public AnimationModifier(int start, int end, @Nullable AnimationIterator.Type type) { this(start, end, type, null); } /** * Creates modifier * * @param start start time * @param end end time * @param type type * @param speed speed */ public AnimationModifier(int start, int end, @Nullable AnimationIterator.Type type, @Nullable FloatSupplier speed) { this(null, start, end, type, speed); } /** * Creates modifier * * @param predicate animation predicate * @param start start time * @param end end time * @param type type * @param speed speed */ public AnimationModifier(@Nullable BooleanSupplier predicate, int start, int end, @Nullable AnimationIterator.Type type, @Nullable FloatSupplier speed) { this(predicate, start, end, 0, type, speed, null, null); } /** * Gets modifier's type or default value * @param defaultType default value * @return modifier's type or default value */ public @NotNull AnimationIterator.Type type(@NotNull AnimationIterator.Type defaultType) { return type != null ? type : defaultType; } /** * Gets speed value * @return speed value */ public float speedValue() { return speed != null ? speed.getAsFloat() : 1F; } /** * Gets predicate value * @return predicate value */ public boolean predicateValue() { return predicate == null || predicate.getAsBoolean(); } /** * Gets override * @param original original value * @return override */ public boolean override(boolean original) { return override != null ? override : original; } private static @Nullable FloatSupplier toSupplier(float speed) { return MathUtil.isSimilar(speed, 1F) ? null : FloatSupplier.of(speed); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/AnimationOverrideState.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; public enum AnimationOverrideState { NOT_MATCHED, MATCHED ; public boolean shouldSkip() { return this == NOT_MATCHED; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/AnimationProgress.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import kr.toxicity.model.api.bone.BoneMovement; import org.jetbrains.annotations.NotNull; import java.util.List; /** * Represents the state of an animation at a specific keyframe. *

* This interface defines how to apply the keyframe's transformation to a bone's movement. *

* * @since 2.0.0 */ public interface AnimationProgress extends Timed { /** * An empty animation progress that applies no transformation. * @since 2.0.0 */ AnimationProgress EMPTY = empty(0); /** * Checks if interpolation should be skipped after this keyframe. * * @return true to skip interpolation, false otherwise * @since 2.0.0 */ boolean skipInterpolation(); /** * Checks if the rotation in this keyframe should be applied globally. * * @return true for global rotation, false for local * @since 2.0.0 */ boolean globalRotation(); /** * Creates an empty animation progress at a specific time. * * @param time the time of the keyframe * @return an empty progress * @since 2.0.0 */ static @NotNull AnimationProgress empty(float time) { return new EmptyProgress(time); } /** * Converts this progress to empty progress at the same time. * * @return an empty progress * @since 2.0.0 */ default @NotNull AnimationProgress toEmpty() { var time = time(); return time <= 0 ? EMPTY : empty(time); } /** * Creates an empty timed storage with a start and end keyframe. * * @param time the duration of the empty animation * @return the timed storage * @since 2.0.0 */ static @NotNull TimedStorage emptyStorage(float time) { return TimedStorage.listOf(List.of( EMPTY, empty(time) )); } /** * Applies this keyframe's animation to a bone's movement. * * @param movement the current bone movement * @param dest the destination object to store the result * @return the resulting bone movement * @since 2.0.0 */ @NotNull BoneMovement animate(@NotNull BoneMovement movement, @NotNull BoneMovement dest); /** * An implementation of {@link AnimationProgress} that represents an empty keyframe. * * @param time the time of the keyframe * @since 2.0.0 */ record EmptyProgress(float time) implements AnimationProgress { @Override public @NotNull BoneMovement animate(@NotNull BoneMovement movement, @NotNull BoneMovement dest) { return dest.set(movement); } @Override public @NotNull AnimationProgress toEmpty() { return this; } @Override public boolean skipInterpolation() { return false; } @Override public boolean globalRotation() { return false; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/AnimationStateHandler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import kr.toxicity.model.api.tracker.Tracker; import kr.toxicity.model.api.util.MathUtil; import kr.toxicity.model.api.util.collection.PriorityMap; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Iterator; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; /** * Animation state handler * @param timed value */ @RequiredArgsConstructor @ApiStatus.Internal public final class AnimationStateHandler { private final T initialValue; private final BiConsumer setConsumer; private final PriorityMap animators = new PriorityMap<>(); @Getter private int delay; private volatile TreeIterator currentIterator = null; private volatile T beforeKeyframe = null, afterKeyframe = null; /** * Checks this keyframe has been finished * @return finished */ public boolean keyframeFinished() { return delay <= 0; } /** * Gets before keyframe * @return before keyframe */ public T beforeKeyframe() { return beforeKeyframe; } /** * Gets after keyframe * @return after keyframe */ public T afterKeyframe() { return afterKeyframe; } /** * Gets before keyframe * @param defaultValue default value * @return before keyframe */ @NotNull public T beforeKeyframe(@NotNull T defaultValue) { var value = beforeKeyframe; return value != null ? value : defaultValue; } /** * Gets after keyframe * @param defaultValue default value * @return after keyframe */ @NotNull public T afterKeyframe(@NotNull T defaultValue) { var value = afterKeyframe; return value != null ? value : defaultValue; } /** * Gets running animation * @return animation */ public @Nullable RunningAnimation runningAnimation() { var iterator = currentIterator; return iterator != null ? iterator.animation : null; } /** * Ticks this state handler * @return keyframe has been shifted or not */ public boolean tick() { return tick(() -> {}); } /** * Ticks this state handler * @param ifEmpty callback if animator is empty * @return keyframe has been shifted or not */ public boolean tick(@NotNull Runnable ifEmpty) { delay--; if (animators.isEmpty()) { ifEmpty.run(); return false; } return shouldUpdateAnimation() && updateAnimation(); } /** * Gets the progress of current keyframe * @return progress */ public float progress() { var frame = frame(); return frame == 0 ? 0 : Math.clamp((float) delay / frame, 0F, 1F); } private boolean shouldUpdateAnimation() { return (afterKeyframe != null && keyframeFinished()) || delay % Tracker.MINECRAFT_TICK_MULTIPLIER == 0; } private boolean updateAnimation() { synchronized (animators) { var iterator = animators.valueIterator(); while (iterator.hasNext()) { var next = iterator.next(); if (!next.getAsBoolean()) continue; if (currentIterator == null) { if (updateKeyframe(iterator, next)) { currentIterator = next; return setAfterKeyframe(next.next()); } } else if (currentIterator != next) { if (updateKeyframe(iterator, next)) { currentIterator.clear(); currentIterator = next; return setAfterKeyframe(next.next()); } } else if (keyframeFinished()) { if (updateKeyframe(iterator, next)) { return setAfterKeyframe(next.next()); } } else { return false; } } } return setAfterKeyframe(null); } private boolean updateKeyframe(@NotNull Iterator iterator, @NotNull TreeIterator next) { if (!next.hasNext()) { next.removeTask.run(); iterator.remove(); return false; } else { return true; } } private boolean setAfterKeyframe(@Nullable T next) { if (afterKeyframe == next) return false; setConsumer.accept( beforeKeyframe = afterKeyframe, afterKeyframe = next ); delay = Math.round(frame()); return true; } /** * Adds animation * @param name name * @param iterator iterator * @param modifier modifier * @param removeTask remove task */ public void addAnimation(@NotNull String name, @NotNull AnimationIterator iterator, @NotNull AnimationModifier modifier, @NotNull Runnable removeTask) { synchronized (animators) { animators.put(name, new TreeIterator(name, iterator, modifier, removeTask), modifier.priority()); } } /** * Replaces animation * @param name name * @param iterator iterator * @param modifier modifier */ public void replaceAnimation(@NotNull String name, @NotNull AnimationIterator iterator, @NotNull AnimationModifier modifier) { synchronized (animators) { animators.replace(name, v -> new TreeIterator(name, iterator, v.modifier.toBuilder() .mergeNotDefault(modifier) .build(), v.removeTask)); } } /** * Remove animation * @param name name * @return success */ public boolean stopAnimation(@NotNull String name) { synchronized (animators) { if (animators.remove(name) != null) { return true; } } return false; } /** * Gets ticking frame of current keyframe * @return ticking frame */ public float frame() { return afterKeyframe != null ? 20 * Tracker.MINECRAFT_TICK_MULTIPLIER * (currentIterator.time + MathUtil.FRAME_EPSILON) : 0F; } private class TreeIterator implements BooleanSupplier { private final RunningAnimation animation; private final AnimationIterator iterator; private final AnimationModifier modifier; private final Runnable removeTask; private final T previous; private boolean started = false; private boolean ended = false; private float time = 0; public TreeIterator(String name, AnimationIterator iterator, AnimationModifier modifier, Runnable removeTask) { animation = new RunningAnimation(name, iterator.type()); this.iterator = iterator; this.modifier = modifier; this.removeTask = removeTask; previous = afterKeyframe != null ? afterKeyframe : initialValue; } @Override public boolean getAsBoolean() { return modifier.predicateValue(); } public boolean hasNext() { return iterator.hasNext() || (modifier.end() > 0 && !ended); } public @NotNull T next() { if (!started) { started = true; time = (float) modifier.start() / 20; return iterator.next(); } if (!iterator.hasNext()) { ended = true; time = (float) modifier.end() / 20; return previous; } var nxt = iterator.next(); time = nxt.time() / modifier.speedValue(); return nxt; } public void clear() { iterator.clear(); started = ended = !iterator.hasNext(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/RunningAnimation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import org.jetbrains.annotations.NotNull; /** * Running animation * @param name name * @param type type */ public record RunningAnimation(@NotNull String name, @NotNull AnimationIterator.Type type) { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/Timed.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.NotNull; /** * Object with keyframe time */ public interface Timed extends Comparable { default int compareTo(@NotNull Timed o) { return MathUtil.FRAME_COMPARATOR.compare(time(), o.time()); } /** * Gets time * @return time */ float time(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/TimedStorage.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.NonNull; import java.util.List; /** * A read-only storage for timed elements (keyframes), allowing indexed access. *

* This interface abstracts the underlying data structure (e.g., List, Array) used to store animation frames. *

* * @param the type of timed element * @since 2.0.0 */ public interface TimedStorage { /** * Creates a TimedStorage backed by a List. * * @param list the list of elements * @param the type of element * @return a new TimedStorage * @since 2.0.0 */ @NotNull static TimedStorage listOf(@NotNull List list) { return new ListDelegate<>(list); } /** * Retrieves the element at the specified index. * * @param index the index of the element * @return the element * @throws IndexOutOfBoundsException if the index is out of range * @since 2.0.0 */ @NotNull T get(int index); /** * Returns the number of elements in the storage. * * @return the size * @since 2.0.0 */ int size(); /** * Retrieves the last element in the storage. * * @return the last element * @throws java.util.NoSuchElementException if the storage is empty * @since 2.0.0 */ @NotNull T getLast(); /** * A {@link TimedStorage} implementation that delegates to a {@link List}. * * @param list the backing list * @param the type of element * @since 2.0.0 */ record ListDelegate(@NotNull List list) implements TimedStorage { @Override public @NonNull T get(int index) { return list.get(index); } @Override public int size() { return list.size(); } @Override public @NonNull T getLast() { return list.getLast(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/animation/VectorPoint.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.animation; import kr.toxicity.model.api.util.function.FloatFunction; import kr.toxicity.model.api.util.interpolator.VectorInterpolator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; /** * Represents a keyframe point in an animation timeline. *

* This record holds the value of a vector (position, rotation, or scale) at a specific time, * along with interpolation information to create smooth transitions between keyframes. *

* * @param function a function to get the vector value, which may be dynamic (e.g., based on Molang expressions) * @param time the time of this keyframe in seconds * @param bezier the bezier curve configuration for interpolation, if applicable * @param interpolator the interpolation method to use (e.g., linear, bezier, catmull-rom) * @since 1.15.2 */ public record VectorPoint(@NotNull FloatFunction function, float time, @NotNull BezierConfig bezier, @NotNull VectorInterpolator interpolator) implements Timed { private static final Vector3f ZERO = new Vector3f(); /** * An empty, default vector point at time 0 with linear interpolation. * @since 1.15.2 */ public static final VectorPoint EMPTY = new VectorPoint( FloatFunction.of(ZERO), 0F, new BezierConfig(null, null, null, null), VectorInterpolator.LINEAR ); /** * Gets the vector value at this keyframe's specific time. * * @return the vector value * @since 1.15.2 */ public @NotNull Vector3f vector() { return vector(time); } /** * Gets the vector value at a specific time, evaluating the function if necessary. * * @param time the time to evaluate at * @return the calculated vector * @since 1.15.2 */ public @NotNull Vector3f vector(float time) { return function.apply(time); } /** * Checks if the interpolation method for this point is continuous. * * @return true if continuous (e.g., linear), false if stepped * @since 1.15.2 */ public boolean isContinuous() { return interpolator.isContinuous(); } /** * Configuration for bezier curve interpolation. * * @param leftTime the time offset for the incoming (left) handle * @param leftValue the value offset for the incoming (left) handle * @param rightTime the time offset for the outgoing (right) handle * @param rightValue the value offset for the outgoing (right) handle * @since 1.15.2 */ public record BezierConfig(@Nullable Vector3f leftTime, @Nullable Vector3f leftValue, @Nullable Vector3f rightTime, @Nullable Vector3f rightValue) { /** * Gets the time offset for the incoming (left) handle. * If null, returns a zero vector. * @return the left time offset vector * @since 1.15.2 */ @Override public @NotNull Vector3f leftTime() { return leftTime != null ? leftTime : ZERO; } /** * Gets the value offset for the incoming (left) handle. * If null, returns a zero vector. * @return the left value offset vector * @since 1.15.2 */ @Override public @NotNull Vector3f leftValue() { return leftValue != null ? leftValue : ZERO; } /** * Gets the time offset for the outgoing (right) handle. * If null, returns a zero vector. * @return the right time offset vector * @since 1.15.2 */ @Override public @NotNull Vector3f rightTime() { return rightTime != null ? rightTime : ZERO; } /** * Gets the value offset for the outgoing (right) handle. * If null, returns a zero vector. * @return the right value offset vector * @since 1.15.2 */ @Override public @NotNull Vector3f rightValue() { return rightValue != null ? rightValue : ZERO; } } @Override public boolean equals(Object o) { if (!(o instanceof VectorPoint that)) return false; return Float.compare(time, that.time) == 0; } @Override public int hashCode() { return Float.hashCode(time); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/armor/ArmorItem.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.armor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Armor item * @param tint tint value * @param type armor type * @param trim trim * @param palette palette */ public record ArmorItem(int tint, @NotNull String type, @Nullable String trim, @Nullable String palette) { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/armor/PlayerArmor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.armor; import org.jetbrains.annotations.Nullable; /** * Player armor */ public interface PlayerArmor { /** * Empty armor */ PlayerArmor EMPTY = new PlayerArmor() { @Override public @Nullable ArmorItem helmet() { return null; } @Override public @Nullable ArmorItem chestplate() { return null; } @Override public @Nullable ArmorItem leggings() { return null; } @Override public @Nullable ArmorItem boots() { return null; } }; /** * Gets helmet * @return helmet */ @Nullable ArmorItem helmet(); /** * Gets chestplate * @return chestplate */ @Nullable ArmorItem chestplate(); /** * Gets leggings * @return leggings */ @Nullable ArmorItem leggings(); /** * Gets boots * @return boots */ @Nullable ArmorItem boots(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneEventDispatcher.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import kr.toxicity.model.api.nms.HitBoxListener; import lombok.AllArgsConstructor; import org.jetbrains.annotations.NotNull; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.BiFunction; /** * Dispatches events related to bone lifecycle and interaction. *

* This class manages handlers for hitbox creation, state creation, and state removal. * It allows for extending behavior by chaining dispatchers. *

* * @since 1.15.2 */ public final class BoneEventDispatcher { private final EventFunction builder = new EventFunction(); private EventFunction applier = builder; /** * Extends this dispatcher with another dispatcher's handlers. *

* The handlers from the provided dispatcher will be executed before the handlers in this dispatcher. *

* * @param dispatcher the dispatcher to extend * @throws UnsupportedOperationException if attempting to extend self * @since 1.15.2 */ public synchronized void extend(@NotNull BoneEventDispatcher dispatcher) { if (dispatcher == this) throw new UnsupportedOperationException("cannot extend self"); applier = EventFunction.concat(dispatcher.applier, builder); } /** * Registers a handler for hitbox creation. * * @param function the function to modify the hitbox listener builder * @since 1.15.2 */ public synchronized void handleCreateHitBox(@NotNull BiFunction function) { var before = builder.createHitBox; builder.createHitBox = (b, l) -> function.apply(b, before.apply(b, l)); } /** * Registers a handler for state creation (e.g., when a bone is initialized for a player). * * @param function the consumer to handle state creation * @since 1.15.2 */ public synchronized void handleStateCreate(@NotNull BiConsumer function) { builder.stateCreate = builder.stateCreate.andThen(function); } /** * Registers a handler for state removal (e.g., when a bone is removed for a player). * * @param function the consumer to handle state removal * @since 1.15.2 */ public synchronized void handleStateRemove(@NotNull BiConsumer function) { builder.stateRemove = builder.stateRemove.andThen(function); } @NotNull HitBoxListener.Builder onCreateHitBox(@NotNull RenderedBone bone, @NotNull HitBoxListener.Builder builder) { return applier.createHitBox.apply(bone, builder); } void onStateCreated(@NotNull RenderedBone bone, @NotNull UUID uuid) { applier.stateCreate.accept(bone, uuid); } void onStateRemoved(@NotNull RenderedBone bone, @NotNull UUID uuid) { applier.stateRemove.accept(bone, uuid); } @AllArgsConstructor private static class EventFunction { private BiFunction createHitBox; private BiConsumer stateCreate; private BiConsumer stateRemove; EventFunction() { this( (_, l) -> l, (_, _) -> {}, (_, _) -> {} ); } static @NotNull EventFunction concat(@NotNull EventFunction first, @NotNull EventFunction second) { return new EventFunction( (b, l) -> second.createHitBox.apply(b, first.createHitBox.apply(b, l)), (b, u) -> { first.stateCreate.accept(b, u); second.stateCreate.accept(b, u); }, (b, u) -> { first.stateRemove.accept(b, u); second.stateRemove.accept(b, u); } ); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneEventHandler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import org.jetbrains.annotations.NotNull; public interface BoneEventHandler { @NotNull BoneEventDispatcher eventDispatcher(); default void extend(@NotNull BoneEventHandler eventHandler) { eventDispatcher().extend(eventHandler.eventDispatcher()); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneIKSolver.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import kr.toxicity.model.api.util.InterpolationUtil; import kr.toxicity.model.api.util.MathUtil; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.Map; import java.util.UUID; import static kr.toxicity.model.api.util.CollectionUtil.newSequencedAddressingMap; /** * Bone IK solver */ @ApiStatus.Internal @RequiredArgsConstructor public final class BoneIKSolver { private static final int MAX_IK_ITERATION = 20; private static final Vector3f FROM_VECTOR = new Vector3f(0, -1, 0).normalize(); private final Map boneMap; private final Object2ObjectLinkedOpenHashMap locators = newSequencedAddressingMap(); /** * Adds some external locator to this solver * @param ikSource nullable source * @param ikTarget target bone * @param locator locator bone */ public void addLocator(@Nullable UUID ikSource, @NotNull UUID ikTarget, @NotNull RenderedBone locator) { var target = boneMap.get(ikTarget); if (target == null) return; var source = ikSource == null ? target.root : boneMap.getOrDefault(ikSource, target.root); var chainArray = source.flatten() .filter(bone -> !bone.flattenBones().contains(locator) && bone.flattenBones().contains(target)) .toArray(RenderedBone[]::new); if (chainArray.length < 2) return; locators.put(locator, new IKChain(chainArray)); } /** * Solves ik */ public void solve() { solve(null); } /** * Solves ik * @param uuid player uuid */ public void solve(@Nullable UUID uuid) { if (locators.isEmpty()) return; locators.object2ObjectEntrySet().fastForEach(entry -> { var locator = entry.getKey(); var value = entry.getValue(); fabrik( value.movements(uuid), value.invertedFirstRotation(uuid), value.cache.lengths, locator.state(uuid).after().position().get(value.cache.destination) .add(locator.root.group.getPosition()) .sub(value.first().root.group.getPosition()) ); }); } private record IKChain(@NotNull RenderedBone[] bones, @NotNull IKCache cache) { private IKChain(@NotNull RenderedBone[] bones) { this(bones, new IKCache(bones.length)); } private @NotNull RenderedBone first() { return bones[0]; } private @NotNull Quaternionf invertedFirstRotation(@Nullable UUID uuid) { return first().state(uuid).after().rotation().invert(cache.rotation); } private @NotNull BoneMovement[] movements(@Nullable UUID uuid) { var movements = cache.movements; for (int i = 0; i < bones.length; i++) { movements[i] = bones[i].state(uuid).after(); } return movements; } } private record IKCache(@NotNull BoneMovement[] movements, float[] lengths, @NotNull Vector3f destination, @NotNull Quaternionf rotation) { private IKCache(int length) { this(new BoneMovement[length], new float[length - 1], new Vector3f(), new Quaternionf()); } } private static void fabrik(@NotNull BoneMovement[] bones, @NotNull Quaternionf firstRot, float[] lengths, @NotNull Vector3f target) { var first = bones[0].position(); var last = bones[bones.length - 1].position(); var vecCache = new Vector3f(); var rootPos = first.get(vecCache); for (int i = 0; i < bones.length - 1; i++) { var before = bones[i]; var after = bones[i + 1]; lengths[i] = before.position().distance(after.position()); } for (int iter = 0; iter < MAX_IK_ITERATION; iter++) { // Forward last.set(target); for (int i = bones.length - 2; i >= 0; i--) { var current = bones[i].position(); var next = bones[i + 1].position(); var dist = current.distanceSquared(next); if (dist < MathUtil.VECTOR_COMPARISON_EPSILON_SQ) continue; InterpolationUtil.lerp(next, current, lengths[i] / (float) Math.sqrt(dist), current); } // Backward first.set(rootPos); for (int i = 0; i < bones.length - 1; i++) { var current = bones[i].position(); var next = bones[i + 1].position(); var dist = current.distanceSquared(next); if (dist < MathUtil.VECTOR_COMPARISON_EPSILON_SQ) continue; InterpolationUtil.lerp(current, next, lengths[i] / (float) Math.sqrt(dist), next); } // Check if (last.distanceSquared(target) < MathUtil.VECTOR_COMPARISON_EPSILON_SQ) break; } var rotCache = new Quaternionf(); for (int i = 0; i < bones.length - 1; i++) { var current = bones[i]; var next = bones[i + 1]; var dir = next.position().sub(current.position(), vecCache); current.rotation().set(rotCache.identity().rotateTo(FROM_VECTOR, dir.normalize()).mul(firstRot).mul(current.rotation())); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneItemMapper.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import kr.toxicity.model.api.data.renderer.RenderSource; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.platform.PlatformItemTransform; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.util.TransformedItemStack; import org.jetbrains.annotations.NotNull; import java.util.function.BiFunction; import java.util.function.Function; /** * Item-mapper of bone */ public interface BoneItemMapper extends BiFunction { @Override @NotNull TransformedItemStack apply(@NotNull BoneRenderContext context, @NotNull TransformedItemStack transformedItemStack); /** * Empty */ BoneItemMapper EMPTY = new BoneItemMapper() { @NotNull @Override public PlatformItemTransform transform() { return PlatformItemTransform.FIXED; } @Override @NotNull public TransformedItemStack apply(@NotNull BoneRenderContext context, @NotNull TransformedItemStack transformedItemStack) { return transformedItemStack; } }; /** * Mapped if a render source is player * @param transform transformation * @param mapper mapper * @return bone item mapper */ static @NotNull BoneItemMapper player(@NotNull PlatformItemTransform transform, @NotNull Function mapper) { return new BoneItemMapper() { private static final TransformedItemStack AIR = TransformedItemStack.empty(); @NotNull @Override public PlatformItemTransform transform() { return transform; } @Override public @NotNull TransformedItemStack apply(@NotNull BoneRenderContext context, @NotNull TransformedItemStack transformedItemStack) { if (context.source() instanceof RenderSource.BasePlayer(PlatformPlayer player)) { var get = mapper.apply(player); return get == null ? AIR : get; } return transformedItemStack; } }; } /** * Mapped if a render source is entity * @param transform transformation * @param mapper mapper * @return bone item mapper */ static @NotNull BoneItemMapper entity(@NotNull PlatformItemTransform transform, @NotNull Function mapper) { return new BoneItemMapper() { private static final TransformedItemStack AIR = TransformedItemStack.empty(); @NotNull @Override public PlatformItemTransform transform() { return transform; } @Override public @NotNull TransformedItemStack apply(@NotNull BoneRenderContext context, @NotNull TransformedItemStack transformedItemStack) { if (context.source() instanceof RenderSource.Entity entity) { var get = mapper.apply(entity.entity()); return get == null ? AIR : get; } return transformedItemStack; } }; } /** * Gets this mapper's display is fixed * @return fixed */ default boolean fixed() { return transform() == PlatformItemTransform.FIXED; } /** * Gets item display transformation * @return transformation */ @NotNull PlatformItemTransform transform(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneMovement.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import kr.toxicity.model.api.util.InterpolationUtil; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionf; import org.joml.Vector3f; /** * Represents the transformation state of a single bone, including its position, scale, and rotation. *

* This record is used to calculate the final transformation of a bone after applying animations. *

* * @param position the local position of the bone * @param scale the local scale of the bone * @param rotation the final local rotation of the bone as a quaternion * @param rawRotation the local rotation of the bone in Euler angles (degrees) before being converted to a quaternion * @since 1.15.2 */ public record BoneMovement( @NotNull Vector3f position, @NotNull Vector3f scale, @NotNull Quaternionf rotation, @NotNull Vector3f rawRotation ) { /** * Creates a new BoneMovement with default (identity) transformations. * @since 1.15.2 */ public BoneMovement() { this( new Vector3f(), new Vector3f(1), new Quaternionf(), new Vector3f() ); } /** * Copies the values from another BoneMovement into this one. * * @param movement the source movement * @return this movement instance * @since 1.15.2 */ public @NotNull BoneMovement set(@NotNull BoneMovement movement) { position.set(movement.position); scale.set(movement.scale); rotation.set(movement.rotation); rawRotation.set(movement.rawRotation); return this; } /** * Linearly interpolates between this movement and another movement. * * @param to the target movement * @param alpha the interpolation factor (0.0 to 1.0) * @param dest the destination movement to store the result * @return the destination movement * @since 2.1.0 */ public @NotNull BoneMovement lerp(@NotNull BoneMovement to, float alpha, @NotNull BoneMovement dest) { InterpolationUtil.lerp(position, to.position, alpha, dest.position); InterpolationUtil.lerp(scale, to.scale, alpha, dest.scale); MathUtil.toQuaternion(InterpolationUtil.lerp(rawRotation, to.rawRotation, alpha, dest.rawRotation), dest.rotation); return dest; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneName.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import com.google.gson.JsonDeserializer; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.Objects; import java.util.Set; /** * A tagged name of some bone * @param tags tags * @param name name * @param rawName original name */ public record BoneName(@NotNull @Unmodifiable Set tags, @NotNull String name, @NotNull String rawName) { /** * A JSON deserializer for parsing BoneName from a string. * @since 2.0.1 */ public static final JsonDeserializer PARSER = (json, _, _) -> BoneName.of(json.getAsString()); /** * Internal constructor for BoneName. */ @ApiStatus.Internal public BoneName { } /** * Creates a new BoneName by parsing the raw name string. * @param rawName the raw string to parse * @since 2.0.1 * @return a parsed BoneName instance */ public static @NotNull BoneName of(@NotNull String rawName) { return BoneTag.REGISTRY.parse(rawName); } /** * Checks this name has some tags * @param tags tags * @return any match */ public boolean tagged(@NotNull BoneTag... tags) { for (BoneTag boneTag : tags) { if (this.tags.contains(boneTag)) return true; } return false; } /** * Gets an item mapper of this bone name. * @return item mapper */ public @NotNull BoneItemMapper toItemMapper() { return tags.isEmpty() ? BoneItemMapper.EMPTY : tags.stream().map(BoneTag::itemMapper).filter(Objects::nonNull).findFirst().orElse(BoneItemMapper.EMPTY); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BoneName boneName)) return false; return rawName.equals(boneName.rawName); } @Override public int hashCode() { return rawName.hashCode(); } @Override public @NotNull String toString() { return rawName; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BonePosition.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; import java.util.UUID; /** * Represents the position and state of a bone in a model. * * @param globalOffset the global offset vector * @param localOffset the local offset vector * @param state the unique identifier of the current state, or null if none * @since 2.1.0 */ public record BonePosition( @NotNull Vector3f globalOffset, @NotNull Vector3f localOffset, @Nullable UUID state ) { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneRenderContext.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.data.renderer.RenderSource; import kr.toxicity.model.api.skin.SkinData; import org.jetbrains.annotations.NotNull; /** * Render item context * @param source source * @param skin skin */ public record BoneRenderContext(@NotNull RenderSource source, @NotNull SkinData skin) { /** * Creates default context * @param source source */ public BoneRenderContext(@NotNull RenderSource source) { this(source, BetterModel.platform().skinManager().fallback()); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneTag.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.List; /** * A tag of bone */ public interface BoneTag { /** * The default registry for bone tags. * @since 2.0.1 */ BoneTagRegistry REGISTRY = new BoneTagRegistry(); /** * Gets tag name * @return tag name */ @NotNull String name(); /** * Gets an item mapper * @return item mapper */ @Nullable BoneItemMapper itemMapper(); /** * Gets a tag list like 'h', 'hi', 'b' * @since 2.0.1 * @return tags */ @NotNull @Unmodifiable List tags(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneTagRegistry.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import it.unimi.dsi.fastutil.objects.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Locale; import java.util.Optional; import static kr.toxicity.model.api.util.CollectionUtil.newAddressingMap; /** * Bone tag registry */ public final class BoneTagRegistry { private static final String TAG_SPLITTER = "_"; private final Object2ObjectMap byName = newAddressingMap(); BoneTagRegistry() { for (BoneTags value : BoneTags.values()) { addTag(value); } } /** * Adds some tag to this registry * @param tag tag */ public void addTag(@NotNull BoneTag tag) { BoneTag checkDuplicate; for (String s : tag.tags()) { if ((checkDuplicate = byName.put(s, tag)) != null) throw new RuntimeException("Duplicated tags: " + tag.name() + " between " + checkDuplicate.name()); } } /** * Gets a bone tag by its name wrapped in an Optional. * @param tag tag name * @return bone tag * @since 1.15.2 */ public @NotNull Optional byTagName(@NotNull String tag) { return Optional.ofNullable(byTagNameOrNull(tag)); } /** * Gets a bone tag by its name. * @param tag tag name * @return bone tag or null * @since 2.1.0 */ public @Nullable BoneTag byTagNameOrNull(@NotNull String tag) { return byName.get(tag); } /** * Parses bone name by raw group name * @param rawName raw name * @return bone name */ public @NotNull BoneName parse(@NotNull String rawName) { rawName = rawName.toLowerCase(Locale.ROOT); var tagArray = rawName.split(TAG_SPLITTER); if (tagArray.length < 2) return new BoneName(ObjectSets.emptySet(), rawName, rawName); var tagList = List.of(tagArray); var maxSize = tagList.size() - 1; ObjectSet set = maxSize <= 4 ? new ObjectArraySet<>(maxSize) : new ObjectOpenHashSet<>(maxSize); for (String s : tagList) { var tag = byTagNameOrNull(s); if (tag != null && set.size() < maxSize) set.add(tag); else return new BoneName( set.isEmpty() ? ObjectSets.emptySet() : ObjectSets.unmodifiable(set), set.isEmpty() ? rawName : String.join(TAG_SPLITTER, tagList.subList(set.size(), tagList.size())), rawName ); } return new BoneName( ObjectSets.unmodifiable(set), String.join(TAG_SPLITTER, tagList.subList(set.size(), tagList.size())), rawName ); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/BoneTags.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.nms.Profiled; import kr.toxicity.model.api.platform.PlatformItemTransform; import kr.toxicity.model.api.player.PlayerLimb; import kr.toxicity.model.api.util.TransformedItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.List; /** * Builtin tags */ public enum BoneTags implements BoneTag { /** * Follows entity's head rotation */ HEAD("h"), /** * Follows entity's head rotation */ HEAD_WITH_CHILDREN("hi"), /** * Creates a hitbox following this bone */ HITBOX("b", "ob"), /** * It can be used as a seat */ SEAT("p"), /** * It can be used as a seat but not controllable */ SUB_SEAT("sp"), /** * Nametag */ TAG("tag"), /** * Mob's nametag */ MOB_TAG("mtag"), /** * Player's nametag */ PLAYER_TAG("ptag"), /** * Glow */ GLOW("glow"), /** * Entity's item in left hand */ LEFT_ITEM(BoneItemMapper.entity( PlatformItemTransform.THIRDPERSON_LEFTHAND, BaseEntity::offHand ), "pli", "li"), /** * Entity's item in right hand */ RIGHT_ITEM(BoneItemMapper.entity( PlatformItemTransform.THIRDPERSON_RIGHTHAND, BaseEntity::mainHand ), "pri", "ri"), /** * Player head */ PLAYER_HEAD(PlayerLimb.HEAD.getItemMapper(), "ph"), /** * Player right arm */ PLAYER_RIGHT_ARM(PlayerLimb.RIGHT_ARM.getItemMapper(), "pra"), /** * Player right forearm */ PLAYER_RIGHT_FOREARM(PlayerLimb.RIGHT_FOREARM.getItemMapper(), "prfa"), /** * Player left arm */ PLAYER_LEFT_ARM(PlayerLimb.LEFT_ARM.getItemMapper(), "pla"), /** * Player left forearm */ PLAYER_LEFT_FOREARM(PlayerLimb.LEFT_FOREARM.getItemMapper(), "plfa"), /** * Player left hip */ PLAYER_HIP(PlayerLimb.HIP.getItemMapper(), "phip"), /** * Player left waist */ PLAYER_WAIST(PlayerLimb.WAIST.getItemMapper(), "pw"), /** * Player left chest */ PLAYER_CHEST(PlayerLimb.CHEST.getItemMapper(), "pc"), /** * Player right leg */ PLAYER_RIGHT_LEG(PlayerLimb.RIGHT_LEG.getItemMapper(), "prl"), /** * Player right foreleg */ PLAYER_RIGHT_FORELEG(PlayerLimb.RIGHT_FORELEG.getItemMapper(), "prfl"), /** * Player left leg */ PLAYER_LEFT_LEG(PlayerLimb.LEFT_LEG.getItemMapper(), "pll"), /** * Player left foreleg */ PLAYER_LEFT_FORELEG(PlayerLimb.LEFT_FORELEG.getItemMapper(), "plfl"), /** * Cape */ CAPE(new BoneItemMapper() { @Override public @NotNull TransformedItemStack apply(@NotNull BoneRenderContext context, @NotNull TransformedItemStack transformedItemStack) { TransformedItemStack cape = null; if (context.source() instanceof Profiled profiled && profiled.skinParts().isCapeEnabled()) { cape = context.skin().cape(profiled.armors()); } return cape != null ? cape : TransformedItemStack.empty(); } @Override public @NotNull PlatformItemTransform transform() { return PlatformItemTransform.FIXED; } }, "cape") ; BoneTags(@NotNull String... tags) { this(null, tags); } BoneTags(@Nullable BoneItemMapper itemMapper, @NotNull String... tags) { this.itemMapper = itemMapper; this.tags = List.of(tags); } @Nullable private final BoneItemMapper itemMapper; @NotNull private final List tags; @Nullable @Override public BoneItemMapper itemMapper() { return itemMapper; } @NotNull @Unmodifiable @Override public List tags() { return tags; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/bone/RenderedBone.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.bone; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectSortedSets; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.animation.*; import kr.toxicity.model.api.data.blueprint.BlueprintAnimation; import kr.toxicity.model.api.data.blueprint.BlueprintElement; import kr.toxicity.model.api.data.blueprint.ModelBoundingBox; import kr.toxicity.model.api.data.renderer.RenderSource; import kr.toxicity.model.api.data.renderer.RendererGroup; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.nms.*; import kr.toxicity.model.api.platform.PlatformItemStack; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.ModelRotation; import kr.toxicity.model.api.tracker.Tracker; import kr.toxicity.model.api.util.*; import kr.toxicity.model.api.util.collection.SingletonSequencedSet; import kr.toxicity.model.api.util.function.BonePredicate; import kr.toxicity.model.api.util.function.FloatConstantSupplier; import kr.toxicity.model.api.util.function.FloatSupplier; import kr.toxicity.model.api.util.lock.DuplexLock; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.*; import java.util.stream.Collectors; import java.util.stream.Stream; /** * A rendered item-display. */ public final class RenderedBone implements BoneEventHandler { private static final int INITIAL_TINT_VALUE = 0xFFFFFF; private static final Vector3f EMPTY_VECTOR = new Vector3f(); private static final BonePosition EMPTY_POSITION = new BonePosition(EMPTY_VECTOR, EMPTY_VECTOR, null); @Getter @NotNull final RendererGroup group; private final BoneMovement defaultFrame; private volatile BoneRenderContext renderContext; private final BoneEventDispatcher eventDispatcher = new BoneEventDispatcher(); @NotNull @Getter final RenderedBone root; @Nullable @Getter final RenderedBone parent; final RenderedBone[] children; private volatile SequencedSet flattenBones; private final Int2ObjectMap tintCacheMap = new Int2ObjectOpenHashMap<>(); @Getter private final boolean dummyBone; private final Object itemLock = new Object(); //Resource @Getter @Nullable private final ModelDisplay display; @Getter @Nullable private HitBox hitBox; @Getter @Nullable private ModelNametag nametag; //Item @Getter @Setter private BoneItemMapper itemMapper; private volatile int previousTint = INITIAL_TINT_VALUE, tint = INITIAL_TINT_VALUE; private volatile TransformedItemStack itemStack; //Animation private final BoneStateHandler globalState; private final Map perPlayerState = new ConcurrentHashMap<>(); private volatile ModelRotation rotation = ModelRotation.EMPTY; private Supplier defaultPosition = FunctionUtil.asSupplier(EMPTY_VECTOR); private FloatSupplier scale = FloatConstantSupplier.ONE; private Function positionModifier = p -> p; private Vector3f lastModifiedPosition = new Vector3f(); private Function localRotModifier = r -> r, globalRotModifier = r -> r; private Quaternionf lastModifiedLocalRot = new Quaternionf(), lastModifiedGlobalRot = new Quaternionf(); /** * Creates entity. * @param group group * @param parent parent entity * @param context render context * @param movement spawn movement * @param childrenMapper mapper */ @ApiStatus.Internal public RenderedBone( @NotNull RendererGroup group, @Nullable RenderedBone parent, @NotNull BoneRenderContext context, @NotNull BoneMovement movement, @NotNull Function childrenMapper ) { this.group = group; this.parent = parent; this.renderContext = context; itemMapper = group.getItemMapper(); root = parent != null ? parent.root : this; this.itemStack = itemMapper.apply(renderContext, group.getItemStack()); this.dummyBone = group.getItemStack().isAir() && itemMapper == BoneItemMapper.EMPTY; defaultFrame = movement; children = childrenMapper.apply(this); if (!dummyBone) { display = BetterModel.nms().create(context.source().location(), context.source() instanceof RenderSource.Entity ? -4096 : 0, d -> { d.display(itemMapper.transform()); d.invisible(!group.getParent().visibility()); d.viewRange(EntityUtil.entityModelViewRadius()); applyItem(d); }); } else display = null; globalState = new BoneStateHandler(null, _ -> {}); } public void locator(@NotNull BoneIKSolver solver) { if (getGroup().getParent() instanceof BlueprintElement.NullObject nullObject) { var ikTarget = nullObject.ikTarget(); if (ikTarget == null) return; solver.addLocator(nullObject.ikSource(), ikTarget, this); } } private @NotNull BoneStateHandler state(@Nullable PlatformPlayer player) { return state(player != null ? player.uuid() : null); } @NotNull BoneStateHandler state(@Nullable UUID uuid) { return uuid == null ? globalState : perPlayerState.getOrDefault(uuid, globalState); } private @NotNull BoneStateHandler getOrCreateState(@Nullable PlatformPlayer player) { return getOrCreateState(player != null ? player.uuid() : null); } private @NotNull BoneStateHandler getOrCreateState(@Nullable UUID uuid) { return uuid == null ? globalState : perPlayerState.computeIfAbsent(uuid, u -> { eventDispatcher.onStateCreated(this, u); return new BoneStateHandler(u, targetUUID -> eventDispatcher.onStateRemoved(this, targetUUID)); }); } public @Nullable RunningAnimation runningAnimation() { return globalState.state.runningAnimation(); } @Override public @NotNull BoneEventDispatcher eventDispatcher() { return eventDispatcher; } public boolean updateItem(@NotNull Predicate predicate) { return itemStack(predicate, itemMapper.apply(renderContext, itemStack)); } public boolean updateItem(@NotNull BoneRenderContext context) { synchronized (this) { renderContext = context; } return updateItem(_ -> true); } /** * Creates hit box. * @param entity target entity * @param predicate predicate * @param listener hit box listener * @return success */ public boolean createHitBox(@NotNull BaseEntity entity, @NotNull Predicate predicate, @Nullable HitBoxListener listener) { if (predicate.test(this)) { var previous = hitBox; synchronized (this) { if (previous != hitBox) return false; var h = group.getHitBox(); if (h == null) h = ModelBoundingBox.MIN; var l = eventDispatcher.onCreateHitBox(this, (listener != null ? listener : HitBoxListener.EMPTY).toBuilder()).build(); if (hitBox != null) hitBox.removeHitBox(); hitBox = BetterModel.nms().createHitBox(entity, this, h, group.getMountController(), l); return hitBox != null; } } return false; } /** * Creates nametag * @param predicate predicate * @param consumer nametag consumer * @return success */ public boolean createNametag(@NotNull Predicate predicate, @NotNull Consumer consumer) { if (nametag == null && predicate.test(this)) { synchronized (this) { if (nametag != null) return false; nametag = BetterModel.nms().createNametag(this, consumer); } return true; } return false; } /** * Make item has enchantment or not * @param predicate predicate * @param enchant should enchant * @return success or not */ public boolean enchant(@NotNull Predicate predicate, boolean enchant) { return itemStack(predicate, itemStack.modify(i -> i.enchant(enchant))); } /** * Sets the scale of this bone * @param scale scale */ public void scale(@NotNull FloatSupplier scale) { this.scale = scale; } /** * Applies some function at display * @param predicate predicate * @param consumer consumer * @return success or not */ public boolean applyAtDisplay(@NotNull Predicate predicate, @NotNull Consumer consumer) { if (display != null && predicate.test(this)) { consumer.accept(display); return true; } return false; } /** * Changes displayed item * @param predicate predicate * @param itemStack target item * @return success */ public boolean itemStack(@NotNull Predicate predicate, @NotNull TransformedItemStack itemStack) { if (this.itemStack != itemStack && predicate.test(this)) { synchronized (itemLock) { if (this.itemStack == itemStack) return false; this.itemStack = itemStack; if (display != null) display.invisible(itemStack.isAir()); tintCacheMap.clear(); return applyItem(); } } return false; } /** * Adds local rot modifier. * @param predicate predicate * @param function animation consumer * @return whether to success */ public synchronized boolean addLocalRotModifier(@NotNull Predicate predicate, @NotNull Function function) { if (predicate.test(this)) { localRotModifier = localRotModifier.andThen(function); return true; } return false; } /** * Adds global rot modifier. * @param predicate predicate * @param function animation consumer * @return whether to success */ public synchronized boolean addGlobalRotModifier(@NotNull Predicate predicate, @NotNull Function function) { if (predicate.test(this)) { globalRotModifier = globalRotModifier.andThen(function); return true; } return false; } /** * Adds position modifier. * @param predicate predicate * @param function animation consumer * @return whether to success */ public synchronized boolean addPositionModifier(@NotNull Predicate predicate, @NotNull Function function) { if (predicate.test(this)) { positionModifier = positionModifier.andThen(function); return true; } return false; } public boolean rotate(@NotNull ModelRotation rotation, @NotNull PacketBundler bundler) { this.rotation = rotation; if (display != null) { display.rotate(rotation, bundler); return true; } return false; } public boolean tick() { return globalState.tick(); } public boolean tick(@NotNull UUID uuid) { var get = perPlayerState.get(uuid); return get != null && get.tick(); } public void dirtyUpdate(@NotNull PacketBundler bundler) { var d = display; if (d != null) d.sendDirtyEntityData(bundler); } public void forceUpdate(boolean showItem, @NotNull PacketBundler bundler) { var d = display; if (d != null) d.sendEntityData(showItem, bundler); } public void forceUpdate(@NotNull PacketBundler bundler) { var d = display; if (d != null) d.sendEntityData(!d.invisible(), bundler); } public void sendTransformation(@Nullable UUID uuid, @NotNull AnimationBundler bundler) { state(uuid).sendTransformation(bundler); } public void forceTransformation(@NotNull PacketBundler bundler) { var d = globalState.transformer; if (d != null) d.sendTransformation(bundler); } public int interpolationDuration() { return globalState.interpolationDuration(); } public @NotNull Vector3f worldPosition() { return worldPosition(EMPTY_POSITION); } public @NotNull Vector3f worldPosition(@NotNull BonePosition position) { return worldPosition(position, new BoneMovement()); } public @NotNull Vector3f worldPosition(@NotNull BoneMovement cache) { return worldPosition(EMPTY_POSITION, cache); } public @NotNull Vector3f worldPosition(@NotNull BonePosition position, @NotNull BoneMovement cache) { return state(position.state()).worldPosition(position, cache); } public @NotNull Vector3f worldRotation() { return worldRotation(null); } public @NotNull Vector3f worldRotation(@Nullable UUID uuid) { return state(uuid).worldRotation(); } public void defaultPosition(@NotNull Supplier movement) { defaultPosition = movement; } private @NotNull Vector3f modifiedPosition(boolean preventModifierUpdate) { return preventModifierUpdate ? lastModifiedPosition : (lastModifiedPosition = positionModifier.apply(lastModifiedPosition.set(EMPTY_VECTOR))); } private @NotNull Quaternionf modifiedLocalRot(boolean preventModifierUpdate) { return preventModifierUpdate ? lastModifiedLocalRot : (lastModifiedLocalRot = localRotModifier.apply(lastModifiedLocalRot.identity())); } private @NotNull Quaternionf modifiedGlobalRot(boolean preventModifierUpdate) { return preventModifierUpdate ? lastModifiedGlobalRot : (lastModifiedGlobalRot = globalRotModifier.apply(lastModifiedGlobalRot.identity())); } public boolean tint(@NotNull Predicate predicate) { return tint(predicate, previousTint); } public boolean tint(@NotNull Predicate predicate, int tint) { if (this.tint != tint && predicate.test(this)) { synchronized (itemLock) { if (this.tint == tint) return false; this.previousTint = this.tint; this.tint = tint; return applyItem(); } } return false; } private boolean applyItem() { if (display != null) { applyItem(display); return true; } return false; } private void applyItem(@NotNull ModelDisplay targetDisplay) { targetDisplay.item(itemStack.isAir() ? itemStack.itemStack() : tintCacheMap.computeIfAbsent(tint, i -> BetterModel.nms().tint(itemStack.itemStack(), i))); } public void teleport(@NotNull PlatformLocation location, @NotNull PacketBundler bundler) { if (display != null) display.teleport(location, bundler); } public void spawn(boolean hide, @NotNull PacketBundler bundler) { if (display != null) display.spawn(!hide && !display.invisible(), bundler); var transformer = globalState.transformer; if (transformer != null) transformer.sendTransformation(bundler); } public boolean addAnimation(@NotNull AnimationOverrideState overrideState, @NotNull BlueprintAnimation animator, @NotNull AnimationModifier modifier, @NotNull Runnable removeTask) { var get = animator.animator().get(name()); if (get == null && modifier.override(animator.override()) && overrideState.shouldSkip()) return false; var type = modifier.type(animator.loop()); var iterator = get != null ? get.iterator(type) : animator.emptyIterator(type); getOrCreateState(modifier.player()).state.addAnimation(animator.name(), iterator, modifier, removeTask); return true; } public boolean replaceAnimation(@NotNull AnimationOverrideState overrideState, @NotNull String target, @NotNull BlueprintAnimation animator, @NotNull AnimationModifier modifier) { var get = animator.animator().get(name()); if (get == null && modifier.override(animator.override()) && overrideState.shouldSkip()) return false; var type = modifier.type(animator.loop()); var iterator = get != null ? get.iterator(type) : animator.emptyIterator(type); state(modifier.player()).state.replaceAnimation(target, iterator, modifier); return true; } /** * Stops bone's animation * @param filter filter * @param name animation's name * @param player player */ public boolean stopAnimation(@NotNull Predicate filter, @NotNull String name, @Nullable PlatformPlayer player) { return filter.test(this) && state(player).state.stopAnimation(name); } /** * Removes model's display * @param bundler packet bundler */ public void remove(@NotNull PacketBundler bundler) { if (display != null) display.remove(bundler); if (nametag != null) nametag.remove(bundler); } public @NotNull Stream flatten() { return flattenBones().stream(); } @Unmodifiable @NotNull public SequencedSet flattenBones() { SequencedSet set; if ((set = flattenBones) != null) return set; synchronized (this) { if ((set = flattenBones) != null) return set; return flattenBones = children.length == 0 ? SingletonSequencedSet.of(this) : Stream.concat( Stream.of(this), Arrays.stream(children).flatMap(RenderedBone::flatten) ).collect(Collectors.collectingAndThen( Collectors.toCollection(ObjectLinkedOpenHashSet::new), ObjectSortedSets::unmodifiable )); } } public boolean matchTree(@NotNull BonePredicate predicate, @NotNull BiPredicate mapper) { var parentResult = mapper.test(this, predicate); var childPredicate = predicate.children(parentResult); for (RenderedBone value : children) { if (value.matchTree(childPredicate, mapper)) parentResult = true; } return parentResult; } public boolean matchAnimation(@NotNull AnimationOverrideState overrideState, @NotNull BiPredicate mapper) { var parentResult = mapper.test(this, overrideState); if (parentResult) overrideState = AnimationOverrideState.MATCHED; for (RenderedBone value : children) { if (value.matchAnimation(overrideState, mapper)) parentResult = true; } return parentResult; } @NotNull public Vector3f hitBoxPosition() { return hitBoxPosition(new BoneMovement()); } @NotNull public Vector3f hitBoxPosition(@NotNull BoneMovement cache) { var box = getGroup().getHitBox(); if (box != null) return worldPosition(new BonePosition(EMPTY_VECTOR, group.getHitBoxPoint(), null), cache); return worldPosition(cache); } public float hitBoxScale() { return scale.getAsFloat(); } @NotNull public ModelRotation rotation() { return rotation; } final class BoneStateHandler { private final @Nullable UUID uuid; private final Consumer consumer; //States private final AnimationStateHandler state; private final BoneMovement before = new BoneMovement(), after = new BoneMovement(), current = new BoneMovement(); private final DisplayTransformer transformer = display != null ? display.createTransformer() : null; //Flags private boolean firstTick = true; private boolean skipInterpolation = false; private final AtomicBoolean updateAfter = new AtomicBoolean(); private final AtomicBoolean updateCurrent = new AtomicBoolean(); //Caches private final Vector3f positionCache = new Vector3f(), scaleCache = new Vector3f(); private final Quaternionf localRotCache = new Quaternionf(), globalRotCache = new Quaternionf(); //Lock private final DuplexLock lock = new DuplexLock(); private BoneStateHandler(@Nullable UUID uuid, @NotNull Consumer consumer) { this.uuid = uuid; this.consumer = consumer; state = new AnimationStateHandler<>( AnimationProgress.EMPTY, (_, a) -> skipInterpolation = (a != null && a.skipInterpolation()) || (parent != null && parent.state(uuid).skipInterpolation) ); } @NotNull BoneMovement after() { if (!updateAfter.compareAndSet(true, false)) return after; var keyframe = state.afterKeyframe(AnimationProgress.EMPTY); var preventModifierUpdate = interpolationDuration() < 1; var def = keyframe.animate(defaultFrame, after); if (parent != null) { var p = parent.state(uuid).after(); MathUtil.fma( def.position().rotate(p.rotation()), p.scale(), p.position() ).sub(parent.lastModifiedPosition) .add(modifiedPosition(preventModifierUpdate)); def.scale().mul(p.scale()); def.rotation().set(parent.lastModifiedGlobalRot.invert(globalRotCache) .mul(modifiedGlobalRot(preventModifierUpdate)) .mul((keyframe.globalRotation() ? localRotCache.identity() : p.rotation().div(parent.lastModifiedLocalRot, localRotCache)).mul(def.rotation())) .mul(modifiedLocalRot(preventModifierUpdate)) ); } else { def.position().add(modifiedPosition(preventModifierUpdate)); def.rotation().set(modifiedGlobalRot(preventModifierUpdate).get(globalRotCache) .mul(def.rotation()) .mul(modifiedLocalRot(preventModifierUpdate))); } return def; } private boolean tick() { var result = state.tick(() -> { if (uuid != null) { perPlayerState.remove(uuid); consumer.accept(uuid); } }) || firstTick; if (result && updateAfter.compareAndSet(false, true)) { lock.accessToWriteLock(() -> before.set(current)); updateCurrent.set(true); } firstTick = false; return result; } private float progress() { return 1F - state.progress(); } private int interpolationDuration() { if (skipInterpolation) return 0; var frame = state.frame() / (float) Tracker.MINECRAFT_TICK_MULTIPLIER; return Math.round(frame + MathUtil.FLOAT_COMPARISON_EPSILON); } private void sendTransformation(@NotNull AnimationBundler bundler) { if (!updateCurrent.compareAndSet(true, false)) return; var after = after(); var movement = lock.accessToWriteLock(() -> current.set(after)); if (transformer == null) return; var mul = scale.getAsFloat(); transformer.transform( interpolationDuration(), MathUtil.fma( itemStack.offset().rotate(movement.rotation(), positionCache) .add(movement.position()) .add(root.group.getPosition()), mul, itemStack.position() ).add(defaultPosition.get()), movement.scale() .mul(itemStack.scale(), scaleCache) .mul(mul) .max(EMPTY_VECTOR), movement.rotation(), bundler ); } private @NotNull Vector3f worldPosition(@NotNull BonePosition position, @NotNull BoneMovement cache) { var progress = progress(); var interpolated = lock.accessToReadLock(() -> before.lerp(current, progress, cache)); return MathUtil.fma( interpolated.position() .add(itemStack.offset()) .add(position.localOffset()) .rotate(interpolated.rotation()), interpolated.scale(), position.globalOffset() ) .add(root.getGroup().getPosition()) .mul(scale.getAsFloat()) .rotateX(-rotation.radianX()) .rotateY(-rotation.radianY()); } private @NotNull Vector3f worldRotation() { var progress = progress(); return lock.accessToReadLock(() -> InterpolationUtil.lerp(before.rawRotation(), current.rawRotation(), progress)); } } public @NotNull BoneName name() { return getGroup().name(); } public @NotNull UUID uuid() { return getGroup().uuid(); } @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof RenderedBone bone)) return false; return uuid().equals(bone.uuid()); } @Override public int hashCode() { return uuid().hashCode(); } @Override public String toString() { return name().toString(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/config/DebugConfig.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.config; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; /** * Debug config * @param options options */ public record DebugConfig(@NotNull @Unmodifiable Set options) { /** * Debug option */ @RequiredArgsConstructor public enum DebugOption { /** * Debug stack trace of exception */ EXCEPTION("exception"), /** * Debug hit-box entity */ HITBOX("hitbox"), /** * Debug packing resource pack */ PACK("pack"), /** * Debug tracker thread */ TRACKER("tracker") ; private final String config; } /** * Checks this config has this option * @param option option * @return has or not */ public boolean has(@NotNull DebugOption option) { return options.contains(option); } /** * Default config */ public static final DebugConfig DEFAULT = new DebugConfig(Collections.emptySet()); /** * Creates config from YAML * @param predicate predicate * @return config */ public static @NotNull DebugConfig from(@NotNull Predicate predicate) { return new DebugConfig(Collections.unmodifiableSet(Arrays.stream(DebugOption.values()) .filter(o -> predicate.test(o.config)) .collect(Collectors.toCollection(() -> EnumSet.noneOf(DebugOption.class))))); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/config/IndicatorConfig.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.config; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; /** * Indicator config * @param options options */ public record IndicatorConfig(@NotNull @Unmodifiable Set options) { /** * Indicator option */ @RequiredArgsConstructor public enum IndicatorOption { /** * Progress bar */ PROGRESS_BAR("progress_bar"), ; private final String config; } /** * Default config */ public static final IndicatorConfig DEFAULT = new IndicatorConfig(Collections.emptySet()); /** * Creates config from YAML * @param predicate predicate * @return config */ public static @NotNull IndicatorConfig from(@NotNull Predicate predicate) { return new IndicatorConfig(Collections.unmodifiableSet(Arrays.stream(IndicatorOption.values()) .filter(o -> predicate.test(o.config)) .collect(Collectors.toCollection(() -> EnumSet.noneOf(IndicatorOption.class))))); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/config/ModuleConfig.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.config; import org.jetbrains.annotations.NotNull; import java.util.function.Predicate; /** * Module config * @param model creates model * @param playerAnimation create player animation */ public record ModuleConfig( boolean model, boolean playerAnimation ) { /** * Default config */ public static final ModuleConfig DEFAULT = new ModuleConfig( true, true ); /** * Creates config from YAML * @param predicate predicate * @return config */ public static @NotNull ModuleConfig from(@NotNull Predicate predicate) { return new ModuleConfig( predicate.test("model"), predicate.test("player-animation") ); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/config/PackConfig.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.config; import org.jetbrains.annotations.NotNull; import java.util.function.Predicate; /** * Pack config * @param generateModernModel generate modern model * @param generateLegacyModel generate legacy model * @param useObfuscation use obfuscation */ public record PackConfig( boolean generateModernModel, boolean generateLegacyModel, boolean useObfuscation ) { /** * Default config */ public static final PackConfig DEFAULT = new PackConfig(true, true, false); /** * Creates config from YAML * @param predicate predicate * @return config */ public static @NotNull PackConfig from(@NotNull Predicate predicate) { return new PackConfig( predicate.test("generate-modern-model"), predicate.test("generate-legacy-model"), predicate.test("use-obfuscation") ); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/Float2.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data; import com.google.gson.JsonDeserializer; import org.jetbrains.annotations.NotNull; import org.joml.Vector2f; /** * A simple record representing two float values. * * @param x the x value * @param y the y value * @since 3.0.0 */ public record Float2( float x, float y ) { /** * A GSON deserializer for {@link Float2}. * @since 3.0.0 */ public static final JsonDeserializer PARSER = (json, _, _) -> { var array = json.getAsJsonArray(); return new Float2( array.get(0).getAsFloat(), array.get(1).getAsFloat() ); }; /** * Converts this record to a {@link Vector2f}. * * @return a new vector instance * @since 3.0.0 */ public @NotNull Vector2f toVector() { return new Vector2f(x, y); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/Float3.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializer; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionf; import org.joml.Vector3f; /** * A three float value (origin, rotation) * @param x x * @param y y * @param z z */ @ApiStatus.Internal public record Float3( float x, float y, float z ) { /** * Creates floats * @param value scala */ public Float3(float value) { this(value, value, value); } /** * Center */ public static final Float3 CENTER = new Float3(8, 8, 8); /** * Zero */ public static final Float3 ZERO = new Float3(0, 0, 0); public static final Float3 MESH_TRIANGLE_FROM = new Float3(-8, 0, 0); public static final Float3 MESH_TRIANGLE_TO = new Float3(0, 8, 0); /** * Parser */ public static final JsonDeserializer PARSER = (json, _, _) -> { var array = json.getAsJsonArray(); return new Float3( array.get(0).getAsFloat(), array.get(1).getAsFloat(), array.get(2).getAsFloat() ); }; /** * Adds other floats. * @param other other floats * @return new floats */ public @NotNull Float3 plus(@NotNull Float3 other) { return new Float3( x + other.x, y + other.y, z + other.z ); } /** * Converts zxy euler to xyz euler (Minecraft) * @return new float */ public @NotNull Float3 convertToMinecraftDegree() { var vec = MathUtil.toXYZEuler(toVector()); return new Float3(vec.x, vec.y, vec.z); } /** * Rotates this float * @param quaternionf rotation * @return new float */ public @NotNull Float3 rotate(@NotNull Quaternionf quaternionf) { var vec = toVector().rotate(quaternionf); return new Float3(vec.x, vec.y, vec.z); } /** * Subtracts other floats. * @param other other floats * @return new floats */ public @NotNull Float3 minus(@NotNull Float3 other) { return new Float3( x - other.x, y - other.y, z - other.z ); } /** * Converts item model scale to block scale * @return block */ public @NotNull Float3 toBlockScale() { return div(MathUtil.MODEL_TO_BLOCK_MULTIPLIER); } /** * Multiplies floats. * @param value multiplier * @return new floats */ public @NotNull Float3 times(float value) { return new Float3( x * value, y * value, z * value ); } /** * Divides floats. * @param value multiplier * @return new floats */ public @NotNull Float3 div(float value) { return new Float3( x / value, y / value, z / value ); } /** * Inverts XZ * @return new floats */ public @NotNull Float3 invertXZ() { return new Float3( -x, y, -z ); } /** * Converts floats to JSON array. * @return json array */ public @NotNull JsonArray toJson() { var array = new JsonArray(3); array.add(x); array.add(y); array.add(z); return array; } public @NotNull Quaternionf toQuaternionZYX() { return new Quaternionf() .rotateZYX( z * MathUtil.DEGREES_TO_RADIANS, y * MathUtil.DEGREES_TO_RADIANS, x * MathUtil.DEGREES_TO_RADIANS ); } public @NotNull Quaternionf toQuaternionXYZ() { return new Quaternionf() .rotateXYZ( x * MathUtil.DEGREES_TO_RADIANS, y * MathUtil.DEGREES_TO_RADIANS, z * MathUtil.DEGREES_TO_RADIANS ); } /** * Converts floats to vector. * @return vector */ public @NotNull Vector3f toVector() { return new Vector3f(x, y, z); } @Override public int hashCode() { var hash = 31; var value = 1; value = value * hash + MathUtil.similarHashCode(x); value = value * hash + MathUtil.similarHashCode(y); value = value * hash + MathUtil.similarHashCode(z); return value; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof Float3(float x1, float y1, float z1))) return false; return MathUtil.isSimilar(x, x1) && MathUtil.isSimilar(y, y1) && MathUtil.isSimilar(z, z1); } @Override public @NotNull String toString() { return toJson().toString(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/Float4.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializer; import kr.toxicity.model.api.data.raw.ModelResolution; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * A four float values (uv) * @param dx from-x * @param dz from-z * @param tx to-x * @param tz to-z */ @ApiStatus.Internal public record Float4( float dx, float dz, float tx, float tz ) { /** * Parser */ public static final JsonDeserializer PARSER = (json, _, _) -> { var array = json.getAsJsonArray(); return new Float4( array.get(0).getAsFloat(), array.get(1).getAsFloat(), array.get(2).getAsFloat(), array.get(3).getAsFloat() ); }; public static final Float4 MAX_UV = new Float4(0, 0, 16, 16); /** * Divides floats by resolution. * @param resolution model resolution * @return new floats */ public @NotNull Float4 div(@NotNull ModelResolution resolution) { return div((float) resolution.width() / MathUtil.MODEL_TO_BLOCK_MULTIPLIER, (float) resolution.height() / MathUtil.MODEL_TO_BLOCK_MULTIPLIER); } /** * Divides floats by width, height * @param width width * @param height height * @return new floats */ public @NotNull Float4 div(float width, float height) { return new Float4( dx / width, dz / height, tx / width, tz / height ); } /** * Checks validity of this uv * @return is valid */ public boolean isValid() { return dx >= 0 && dx <= 16 && dz >= 0 && dz <= 16 && tx >= 0 && tx <= 16 && tz >= 0 && tz <= 16; } /** * Converts floats to JSON array. * @return json array */ public @NotNull JsonArray toJson() { var array = new JsonArray(4); array.add(dx); array.add(dz); array.add(tx); array.add(tz); return array; } @Override public int hashCode() { var hash = 31; var value = 1; value = value * hash + MathUtil.similarHashCode(dx); value = value * hash + MathUtil.similarHashCode(dz); value = value * hash + MathUtil.similarHashCode(tx); value = value * hash + MathUtil.similarHashCode(tz); return value; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof Float4(float dx1, float dz1, float tx1, float tz1))) return false; return MathUtil.isSimilar(dx, dx1) && MathUtil.isSimilar(dz, dz1) && MathUtil.isSimilar(tx, tx1) && MathUtil.isSimilar(tz, tz1); } @Override public @NotNull String toString() { return toJson().toString(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/ModelAsset.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data; import com.google.gson.JsonParseException; import kr.toxicity.model.api.data.raw.ModelData; import kr.toxicity.model.api.data.raw.ModelLoadResult; import kr.toxicity.model.api.util.PackUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; /** * Represents a raw model asset that can be loaded into the engine. *

* This record encapsulates the source of the model data (e.g., a file or stream), its name, and metadata. * It provides methods to load and parse the model data into a usable format. *

* * @param rawName the original raw name or path of the asset * @param name the sanitized, pack-compliant name of the asset * @param sizeAssume the estimated size of the asset in bytes (0 if unknown) * @param supplier a supplier for the input stream containing the model data * @since 2.0.0 */ public record ModelAsset( @NotNull String rawName, @NotNull String name, long sizeAssume, @NotNull StreamSupplier supplier ) implements Comparable { /** * Internal constructor for ModelAsset. */ @ApiStatus.Internal public ModelAsset { } /** * Creates a new ModelAsset from a name and byte array. * * @param name the name of the asset * @param bytes the byte array containing the model data * @return the created asset * @since 2.0.0 */ public static @NotNull ModelAsset of(@NotNull String name, byte[] bytes) { return of(name, bytes.length, () -> new ByteArrayInputStream(bytes)); } /** * Creates a new ModelAsset from a name and stream supplier. * * @param name the name of the asset * @param supplier the stream supplier * @return the created asset * @since 2.0.0 */ public static @NotNull ModelAsset of(@NotNull String name, @NotNull StreamSupplier supplier) { return of(name, 0, supplier); // Unknown size } /** * Creates a new ModelAsset from a name, stream supplier, and estimated size. * * @param name the name of the asset * @param sizeAssume the estimated size in bytes * @param supplier the stream supplier * @return the created asset * @since 2.0.0 */ public static @NotNull ModelAsset of(@NotNull String name, long sizeAssume, @NotNull StreamSupplier supplier) { PackUtil.assertPackName(name); return new ModelAsset(name, name, sizeAssume, supplier); } /** * Creates a new ModelAsset from a file. * * @param file the source file * @return the created asset * @since 2.0.0 */ public static @NotNull ModelAsset of(@NotNull File file) { return new ModelAsset(file.getPath(), nameWithoutExtension(file.getName()), file.length(), () -> new FileInputStream(file)); } /** * Creates a new ModelAsset from a path. * * @param path the source path * @return the created asset * @throws RuntimeException if an I/O error occurs * @since 2.0.0 */ public static @NotNull ModelAsset of(@NotNull Path path) { try { return new ModelAsset(path.toString(), nameWithoutExtension(path.getFileName().toString()), Files.size(path), () -> Files.newInputStream(path)); } catch (IOException e) { throw new RuntimeException(e); } } private static @NotNull String nameWithoutExtension(@NotNull String name) { var index = name.lastIndexOf('.'); return PackUtil.toPackName(index > 0 ? name.substring(0, index) : name); } /** * Loads and parses the model data from this asset. * * @return the result of the load operation * @throws RuntimeException if an I/O or parsing error occurs * @since 2.0.0 */ public @NotNull ModelLoadResult toResult() { try ( var stream = supplier.get(); var reader = new InputStreamReader(stream, StandardCharsets.UTF_8) ) { var result = ModelData.GSON.fromJson(reader, ModelData.class); result.assertSupported(); return result.loadBlueprint(name); } catch (IOException e) { throw new RuntimeException("Unable to load this asset: " + this, e); } catch (JsonParseException e) { throw new RuntimeException("Unable to parse this json asset: " + this, e); } } @Override public int compareTo(@NotNull ModelAsset o) { return name.compareTo(o.name); } @Override public boolean equals(Object o) { if (!(o instanceof ModelAsset that)) return false; return name.equals(that.name); } @Override public int hashCode() { return name.hashCode(); } @Override public @NotNull String toString() { return rawName; } /** * A functional interface for supplying an input stream. * * @since 2.0.0 */ public interface StreamSupplier { /** * Gets the input stream. * * @return the input stream * @throws IOException if an I/O error occurs * @since 2.0.0 */ @NotNull InputStream get() throws IOException; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/AnimationGenerator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import it.unimi.dsi.fastutil.floats.*; import kr.toxicity.model.api.animation.VectorPoint; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.util.InterpolationUtil; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Stream; import static kr.toxicity.model.api.util.CollectionUtil.*; /** * Generates animation data by interpolating keyframes and calculating bone movements. *

* This class processes raw animation points and generates smooth transitions for position, rotation, and scale. * It handles the creation of intermediate frames to ensure fluid motion, especially for rotations. *

* * @since 1.15.2 */ @ApiStatus.Internal public final class AnimationGenerator { private static final Vector3f EMPTY = new Vector3f(); private final Map pointMap; private final List trees; /** * Creates a map of blueprint animators from the provided animation data. *

* This method calculates all necessary interpolation frames and builds the final animation structures for each bone. *

* * @param length the total length of the animation in seconds * @param children the list of root blueprint elements (bones) * @param pointMap a map containing raw animation data for each bone * @return a map of generated blueprint animators keyed by bone name * @since 1.15.2 */ public static @NotNull Map createMovements( float length, @NotNull List children, @NotNull Map pointMap ) { var floatSet = mapFloat(pointMap.values() .stream() .flatMap(BlueprintAnimator.AnimatorData::allPoints), VectorPoint::time, () -> new FloatAVLTreeSet(MathUtil.FRAME_COMPARATOR)); floatSet.add(0F); floatSet.add(length); InterpolationUtil.insertLerpFrame(floatSet); var generator = new AnimationGenerator(pointMap, children); generator.interpolateRotation(floatSet); generator.interpolateStep(floatSet); return mapValue(pointMap, v -> new BlueprintAnimator( v.name(), InterpolationUtil.buildAnimation( v.position(), v.rotation(), v.scale(), v.rotationGlobal(), floatSet ) )); } private AnimationGenerator( @NotNull Map pointMap, @NotNull List children ) { this.pointMap = pointMap; trees = filterIsInstance(children, BlueprintElement.Group.class) .map(g -> new AnimationTree(g, pointMap.get(g.name()))) .flatMap(AnimationTree::flatten) .toList(); } private float firstTime = 0F; private float secondTime = 0F; /** * Inserts additional keyframes to smooth out large rotations. *

* This ensures that rotations larger than 90 degrees between frames are broken down into smaller steps. *

* * @param floats the set of keyframe times to update * @since 1.15.2 */ public void interpolateRotation(@NotNull FloatSortedSet floats) { var iterator = new FloatArrayList(floats).iterator(); var time = 0.05F; while (iterator.hasNext()) { firstTime = secondTime; secondTime = iterator.nextFloat(); if (secondTime - firstTime <= 0) continue; var minus = trees.stream() .mapToDouble(t -> t.tree(firstTime, secondTime, BlueprintAnimator.AnimatorData::rotation)) .max() .orElse(0); var length = (float) Math.ceil(minus / 90); if (length < 2) continue; var addTime = Math.max( InterpolationUtil.lerp(0, secondTime - firstTime, 1F / length), time ); for (float f = 1; f < length; f++) { if (secondTime - addTime < time + MathUtil.FRAME_EPSILON) continue; floats.add(firstTime + f * addTime); } } } /** * Inserts keyframes for step interpolation (non-continuous transitions). * * @param floats the set of keyframe times to update * @since 1.15.2 */ public void interpolateStep(@NotNull FloatSortedSet floats) { trees.stream() .map(tree -> tree.data) .filter(Objects::nonNull) .forEach(data -> { interpolateStep(floats, data.position()); interpolateStep(floats, data.rotation()); interpolateStep(floats, data.scale()); }); } private void interpolateStep(@NotNull FloatSortedSet floats, @NotNull List points) { if (points.size() < 2) return; for (int i = 1; i < points.size(); i++) { var before = points.get(i - 1); if (before.isContinuous()) continue; var time = points.get(i).time() - 0.05F; if (time < 0 || time - before.time() < 0) continue; floats.add(time); } } private class AnimationTree { private final AnimationTree parent; private final List children; private final BlueprintAnimator.AnimatorData data; private int searchCache = 0; private final Float2ObjectMap valueCache = new Float2ObjectOpenHashMap<>(); AnimationTree(@NotNull BlueprintElement.Group group, @Nullable BlueprintAnimator.AnimatorData data) { this(null, group, data); } AnimationTree( @Nullable AnimationTree parent, @NotNull BlueprintElement.Group group, @Nullable BlueprintAnimator.AnimatorData data ) { this.parent = parent; this.data = data; children = filterIsInstance(group.children(), BlueprintElement.Group.class) .map(g -> new AnimationTree(this, g, pointMap.get(g.name()))) .toList(); } @NotNull Stream flatten() { return children.isEmpty() ? Stream.of(this) : Stream.concat( Stream.of(this), children.stream().flatMap(AnimationTree::flatten) ); } private float tree(float first, float second, @NotNull Function> mapper) { var value = data != null ? mapper.apply(data) : Collections.emptyList(); return findTree(first, second, value).length(); } private @NotNull Vector3f findTree(float first, float second, @NotNull List target) { var get = find(first, second, target); return parent != null ? parent.findTree(first, second, target).add(get) : get; } private @NotNull Vector3f find(float first, float second, @NotNull List target) { return find(second, target).sub(find(first, target), new Vector3f()); } private @NotNull Vector3f find(float time, @NotNull List target) { return valueCache.computeIfAbsent(time, _ -> { if (target.size() <= 1) return EMPTY; var i = searchCache; for (; i < target.size(); i++) { if (target.get(i).time() >= time) break; } searchCache = i; if (i == 0) return EMPTY; if (i == target.size()) return EMPTY; var first = target.get(i - 1); var second = target.get(i); var t1 = first.time(); var t2 = second.time(); var a = InterpolationUtil.alpha(t1, t2, time); return second.time() == time ? second.vector() : InterpolationUtil.lerp( first.vector(InterpolationUtil.lerp(t1, t2, a)), second.vector(), a ); }); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintAnimation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import kr.toxicity.model.api.animation.AnimationIterator; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.animation.AnimationProgress; import kr.toxicity.model.api.animation.TimedStorage; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.script.BlueprintScript; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.Map; /** * Represents a complete, processed animation for a model. *

* This record contains all the necessary data to play an animation, including keyframes for each bone, * loop settings, and associated scripts. *

* * @param name the name of the animation * @param loop the default loop mode * @param length the length of the animation in seconds * @param override whether this animation overrides others * @param animator a map of animators for each bone * @param script the script associated with this animation, if any * @param emptyAnimator a list of empty movements, used as a fallback or for initialization * @since 1.15.2 */ public record BlueprintAnimation( @NotNull String name, @NotNull AnimationIterator.Type loop, float length, boolean override, @NotNull @Unmodifiable Map animator, @Nullable BlueprintScript script, @NotNull TimedStorage emptyAnimator ) { /** * Retrieves the script for this animation, considering the provided modifier. *

* If the modifier overrides the animation or specifies a player, the script may be suppressed. *

* * @param modifier the animation modifier * @return the script, or null if suppressed * @since 1.15.2 */ public @Nullable BlueprintScript script(@NotNull AnimationModifier modifier) { return modifier.override(override) || modifier.player() != null ? null : script; } /** * Creates an iterator for the empty animation sequence. * * @param type the loop type * @return an animation iterator * @since 1.15.2 */ public @NotNull AnimationIterator emptyIterator(@NotNull AnimationIterator.Type type) { return type.create(emptyAnimator); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintAnimator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import kr.toxicity.model.api.animation.AnimationIterator; import kr.toxicity.model.api.animation.AnimationKeyframe; import kr.toxicity.model.api.animation.AnimationProgress; import kr.toxicity.model.api.animation.VectorPoint; import kr.toxicity.model.api.bone.BoneName; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.stream.Stream; /** * Represents the processed animation data for a single bone within a model blueprint. *

* This record holds the sequence of keyframes that define the bone's movement over time. *

* * @param name the name of the bone this animator applies to * @param keyframe a list of animation movements representing the keyframes * @since 1.15.2 */ public record BlueprintAnimator( @NotNull BoneName name, @NotNull AnimationKeyframe keyframe ) { /** * Holds the raw, separated animation data points for a bone before final processing. * * @param name the name of the bone * @param position a list of position keyframes * @param scale a list of scale keyframes * @param rotation a list of rotation keyframes * @param rotationGlobal whether the rotation is applied globally * @since 1.15.2 */ public record AnimatorData( @NotNull BoneName name, @NotNull List position, @NotNull List scale, @NotNull List rotation, boolean rotationGlobal ) { /** * Returns a stream containing all keyframe points (position, scale, and rotation). * * @return a stream of all vector points * @since 1.15.2 */ public @NotNull Stream allPoints() { return Stream.concat( Stream.concat( position.stream(), scale.stream() ), rotation.stream() ); } } /** * Creates an iterator for the keyframes based on a specified loop type. * * @param type the loop type (e.g., play_once, loop) * @return an animation iterator * @since 1.15.2 */ public @NotNull AnimationIterator iterator(@NotNull AnimationIterator.Type type) { return type.create(keyframe); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintElement.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import com.google.gson.JsonObject; import io.github.toxicity188.javamesh.MeshBuilder; import io.github.toxicity188.javamesh.MeshPoint; import io.github.toxicity188.javamesh.MeshShape; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.bone.BoneTags; import kr.toxicity.model.api.data.Float2; import kr.toxicity.model.api.data.Float3; import kr.toxicity.model.api.data.raw.ModelFace; import kr.toxicity.model.api.pack.PackObfuscator; import kr.toxicity.model.api.util.MathUtil; import kr.toxicity.model.api.util.PackUtil; import kr.toxicity.model.api.util.json.JsonObjectBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.joml.Quaternionf; import java.util.*; import java.util.stream.Stream; import static kr.toxicity.model.api.util.CollectionUtil.*; /** * Represents a processed element within a model blueprint. *

* This is a sealed interface with implementations for different types of elements like bones, groups, cubes, and locators. *

* * @since 1.15.2 */ public sealed interface BlueprintElement { String MESH_TRIANGLE_SINGLE = "mesh_triangle_single"; String MESH_TRIANGLE_DUPLEX = "mesh_triangle_duplex"; String MESH_PIXEL = "mesh_pixel"; /** * Represents an element that acts as a bone in the model's armature. * * @since 1.15.2 */ sealed interface Bone extends BlueprintElement { /** * Returns the UUID of the bone. * * @return the UUID * @since 1.15.2 */ @NotNull UUID uuid(); /** * Returns the name of the bone. * * @return the bone name * @since 1.15.2 */ @NotNull BoneName name(); /** * Returns the origin (pivot point) of the bone. * * @return the origin * @since 1.15.2 */ @NotNull Float3 origin(); } /** * Returns the rotation of the element. * * @return the rotation, defaulting to zero * @since 1.15.2 */ default @NotNull Float3 rotation() { return Float3.ZERO; } /** * Checks if the element is visible. * * @return true if visible, false otherwise * @since 1.15.2 */ default boolean visibility() { return false; } /** * Represents a group of elements, forming a bone in the hierarchy. * * @param uuid the UUID of the group * @param name the name of the group/bone * @param origin the pivot point of the group * @param rotation the rotation of the group * @param children the list of child elements * @param visibility whether the group is visible * @since 1.15.2 */ record Group( @NotNull UUID uuid, @NotNull BoneName name, @NotNull Float3 origin, @NotNull Float3 rotation, @NotNull List children, boolean visibility ) implements Bone { /** * Returns the origin with inverted X and Z axes. * * @return the inverted origin * @since 1.15.2 */ @Override @NotNull public Float3 origin() { return origin.invertXZ(); } private @NotNull String jsonName(@NotNull BlueprintLoadContext context) { return PackUtil.toPackName(context.name() + "_" + name.rawName()); } /** * Builds the JSON representation for legacy clients (1.21.3 or under). * * @param obfuscator the obfuscator for model and texture names * @param context the load context * @return the generated blueprint JSON, or null if not applicable * @since 1.15.2 */ public @Nullable BlueprintJson buildLegacyJson( @NotNull PackObfuscator.Pair obfuscator, @NotNull BlueprintLoadContext context ) { return buildJson(-2, 1, scale(), obfuscator, context, Float3.ZERO, filterIsInstance(children, Cube.class).filter(element -> MathUtil.checkValidDegree(element.identifierDegree()))); } /** * Builds the JSON representation for modern clients. * * @param obfuscator the obfuscator for model and texture names * @param context the load context * @return a list of generated blueprint JSONs, or null if not applicable * @since 1.15.2 */ @Nullable @Unmodifiable public List buildModernJson( @NotNull PackObfuscator.Pair obfuscator, @NotNull BlueprintLoadContext context ) { var scale = scale(); var list = mapIndexed( group( filterIsInstance(children, Cube.class), Cube::identifierDegree ), (i, entry) -> buildJson(0, i + 1, scale, obfuscator, context, entry.getKey(), entry.getValue().stream()) ).filter(Objects::nonNull) .toList(); return list.isEmpty() ? null : list; } /** * Builds the JSON representation for a mesh-based item model. * * @param context the load context * @return the generated mesh JSON, or null if no meshes are present * @since 3.0.0 */ public @Nullable JsonObject buildMeshItemModel( @NotNull BlueprintLoadContext context ) { var scale = 1F / scale(); var meshes = filterIsInstance(children, Mesh.class).toList(); if (meshes.isEmpty()) return null; var builder = MeshBuilder.of(context.triangleName()) .matrixModifier(mat -> mat.scale(scale)) .image(context.imageByIndex()); meshes.forEach(mesh -> builder.load(mesh.toShape(origin))); return builder.toJson(); } private @Nullable BlueprintJson buildJson( int tint, int number, float scale, @NotNull PackObfuscator.Pair obfuscator, @NotNull BlueprintLoadContext context, @NotNull Float3 identifier, @NotNull Stream cubes ) { var cubeElement = cubes .filter(Cube::hasTexture) .toList(); var selectedTextures = cubeElement.stream() .flatMapToInt(tex -> tex.faces().textureIndex()) .distinct() .sorted() .mapToObj(i -> Map.entry(Integer.toString(i), context.texture(i).packNamespace(obfuscator.textures()))) .toList(); if (selectedTextures.isEmpty()) return null; return new BlueprintJson(obfuscator.models().obfuscate(jsonName(context) + "_" + number), () -> JsonObjectBuilder.builder() .jsonObject("textures", textures -> textures .stringProperties(selectedTextures) .property("particle", selectedTextures.getFirst().getValue())) .jsonArray("elements", mapToJson(cubeElement, cube -> cube.buildJson(tint, scale, context, this, identifier))) .jsonObject("display", display -> display.jsonObject("fixed", fixed -> { if (!identifier.equals(Float3.ZERO)) { fixed.jsonArray("rotation", identifier.convertToMinecraftDegree().toJson()); } })) .build()); } /** * Calculates the required scale for the cubes in this group. * * @return the scale factor * @since 1.15.2 */ public float scale() { return (float) Math.max(filterIsInstance(children, Cube.class) .mapToDouble(e -> e.max(origin) / 16F) .max() .orElse(1F), 1F); } /** * Calculates the bounding box for this group to be used as a hitbox. * * @return the named bounding box, or null if no cubes are present * @since 1.15.2 */ public @Nullable ModelBoundingBox hitBox() { return filterIsInstance(children, Cube.class) .map(element -> { var from = element.from() .minus(origin) .toBlockScale(); var to = element.to() .minus(origin) .toBlockScale(); return ModelBoundingBox.of( from.x(), from.y(), from.z(), to.x(), to.y(), to.z() ).invert(); }) .max(Comparator.comparingDouble(ModelBoundingBox::length)) .orElse(null); } } /** * Represents a locator element, used as a named attachment point. * * @param uuid the UUID of the locator * @param name the name of the locator * @param origin the position of the locator * @since 1.15.2 */ record Locator( @NotNull UUID uuid, @NotNull BoneName name, @NotNull Float3 origin ) implements Bone { /** * Returns the origin with inverted X and Z axes. * * @return the inverted origin * @since 1.15.2 */ @Override @NotNull public Float3 origin() { return origin.invertXZ(); } } /** * Represents a camera element (currently a placeholder). * * @param uuid the UUID of the camera * @since 1.15.2 */ record Camera( @NotNull UUID uuid ) implements BlueprintElement { } /** * Represents a null object, often used for IK or as a simple bone. * * @param uuid the UUID of the null object * @param name the name of the null object * @param ikTarget the UUID of the IK target bone * @param ikSource the UUID of the IK source bone * @param origin the position of the null object * @since 1.15.2 */ record NullObject( @NotNull UUID uuid, @NotNull BoneName name, @Nullable UUID ikTarget, @Nullable UUID ikSource, @NotNull Float3 origin ) implements Bone { /** * Returns the origin with inverted X and Z axes. * * @return the inverted origin * @since 1.15.2 */ @Override @NotNull public Float3 origin() { return origin.invertXZ(); } } /** * Represents a cube element, the basic building block of a model. * * @param name the name of the cube * @param from the starting coordinate (min corner) * @param to the ending coordinate (max corner) * @param inflate the inflation value * @param rotation the rotation of the cube * @param origin the pivot point of the cube * @param faces the UV mapping for the faces * @param lightEmission the light emission level (1-15 or null if 0) * @param visibility whether the cube is visible * @since 1.15.2 */ record Cube( @NotNull String name, @NotNull Float3 from, @NotNull Float3 to, float inflate, @NotNull Float3 rotation, @NotNull Float3 origin, @Nullable ModelFace faces, @Nullable Integer lightEmission, boolean visibility ) implements BlueprintElement { private @NotNull Float3 identifierDegree() { return MathUtil.identifier(rotation()); } private static @NotNull Float3 centralize(@NotNull Float3 target, @NotNull Float3 groupOrigin, float scale) { return target.minus(groupOrigin).div(scale); } private static @NotNull Float3 deltaPosition(@NotNull Float3 target, @NotNull Quaternionf quaternionf) { return target.rotate(quaternionf).minus(target); } private @NotNull JsonObject buildJson( int tint, float scale, @NotNull BlueprintLoadContext parent, @NotNull BlueprintElement.Group group, @NotNull Float3 identifier ) { var qua = identifier.toQuaternionZYX().invert(); var centerOrigin = centralize(origin(), group.origin, scale); var groupDelta = deltaPosition(centerOrigin, qua); var inflate = new Float3(inflate() / scale); return JsonObjectBuilder.builder() .property("light_emission", group.name.tagged(BoneTags.GLOW) ? Integer.valueOf(15) : lightEmission) .jsonArray("from", centralize(from(), group.origin, scale) .plus(groupDelta) .plus(Float3.CENTER) .minus(inflate) .toJson()) .jsonArray("to", centralize(to(), group.origin, scale) .plus(groupDelta) .plus(Float3.CENTER) .plus(inflate) .toJson()) .jsonObject("faces", faces().toJson(parent, tint)) .jsonObject("rotation", Optional.of(rotation().minus(identifier)) .filter(r -> !Float3.ZERO.equals(r)) .map(rot -> { var rotation = getRotation(rot); rotation.add("origin", centerOrigin .plus(groupDelta) .plus(Float3.CENTER) .toJson()); return rotation; }) .orElse(null)) .build(); } /** * Calculates the maximum distance from the origin to any corner of the cube. * * @param origin the reference origin * @return the maximum length * @since 1.15.2 */ public float max(@NotNull Float3 origin) { var f = from().minus(origin); var t = to().minus(origin); var max = 0F; max = Math.max(max, Math.abs(f.x())); max = Math.max(max, Math.abs(f.y())); max = Math.max(max, Math.abs(f.z())); max = Math.max(max, Math.abs(t.x())); max = Math.max(max, Math.abs(t.y())); max = Math.max(max, Math.abs(t.z())); return max; } @Override public @NotNull ModelFace faces() { return Objects.requireNonNull(faces); } /** * Checks if this cube has any textures defined. * * @return true if it has textures, false otherwise * @since 1.15.2 */ public boolean hasTexture() { return faces != null && faces.hasTexture(); } private @NotNull JsonObject getRotation(@NotNull Float3 rot) { var rotation = new JsonObject(); if (Math.abs(rot.x()) > 0) { rotation.addProperty("angle", rot.x()); rotation.addProperty("axis", "x"); } else if (Math.abs(rot.y()) > 0) { rotation.addProperty("angle", rot.y()); rotation.addProperty("axis", "y"); } else if (Math.abs(rot.z()) > 0) { rotation.addProperty("angle", rot.z()); rotation.addProperty("axis", "z"); } return rotation; } } /** * Represents a mesh element, allowing for complex geometry beyond simple cubes. * * @param origin the pivot point of the mesh * @param rotation the rotation of the mesh * @param faces the list of faces forming the mesh * @param visibility whether the mesh is visible * @since 3.0.0 */ record Mesh( @NotNull Float3 origin, @NotNull Float3 rotation, @NotNull List faces, boolean visibility ) implements BlueprintElement { /** * Converts this mesh into a list of {@link MeshShape} for rendering. * * @param parentOrigin the origin of the parent bone * @return an unmodifiable list of mesh shapes * @since 3.0.0 */ @NotNull @Unmodifiable public List toShape(@NotNull Float3 parentOrigin) { var deltaOrigin = origin().minus(parentOrigin).toVector(); var pointRotation = rotation().toQuaternionXYZ(); return faces.stream() .map(face -> new MeshShape( face.points.stream() .map(p -> new MeshPoint( p.vertices.toVector() .rotate(pointRotation) .add(deltaOrigin) .mul(-1F, 1F, -1F) .div(MathUtil.MODEL_TO_BLOCK_MULTIPLIER), p.uv.toVector() .div(MathUtil.MODEL_TO_BLOCK_MULTIPLIER) )) .toList(), Integer.toString(face.texture) )) .toList(); } /** * Represents a single face of a mesh. * * @param points the vertices and UV coordinates of the face * @param texture the index of the texture used by this face * @since 3.0.0 */ public record Face(@NotNull @Unmodifiable List points, int texture) {} /** * Represents a single point (vertex) in a mesh face. * * @param vertices the 3D coordinates of the vertex * @param uv the 2D UV coordinates for texture mapping * @since 3.0.0 */ public record Point(@NotNull Float3 vertices, @NotNull Float2 uv) {} } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintImage.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents an image file to be generated as part of the resource pack. *

* This record holds the image's name, its binary content, and an optional .mcmeta file for animations. *

* * @param name the name of the image file (including extension) * @param image the binary content of the image * @param mcmeta the JSON object for the .mcmeta file, if any * @since 1.15.2 */ public record BlueprintImage(@NotNull String name, byte[] image, @Nullable JsonObject mcmeta) { /** * Returns the estimated size of the image in bytes. * * @return the image size * @since 1.15.2 */ public long estimatedSize() { return image.length; } /** * Returns the name of the image file with a .png extension. * * @return the png file name * @since 2.0.1 */ public @NotNull String pngName() { return name + ".png"; } /** * Returns the name of the metadata file associated with the png. * * @return the mcmeta file name * @since 2.0.1 */ public @NotNull String mcmetaName() { return pngName() + ".mcmeta"; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintJson.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import com.google.gson.JsonElement; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * Represents a JSON file to be generated as part of the resource pack. *

* This record holds the file name and a supplier for the JSON content. *

* * @param name the name of the JSON file (without extension) * @param element a supplier that provides the JSON content * @since 1.15.2 */ public record BlueprintJson( @NotNull String name, @NotNull Supplier element ) { /** * Returns the name of the JSON file with a .json extension. * * @return the JSON file name * @since 2.0.1 */ public @NotNull String jsonName() { return name + ".json"; } /** * Builds and returns the JSON content by invoking the supplier. * * @since 2.0.1 * @return the generated JSON element */ public @NotNull JsonElement buildJson() { return element.get(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintLoadContext.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import io.github.toxicity188.javamesh.MeshImage; import io.github.toxicity188.javamesh.MeshTriangleName; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.data.raw.ModelResolution; import kr.toxicity.model.api.pack.PackObfuscator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import javax.imageio.ImageIO; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; /** * A context class for loading blueprints. * * @since 3.0.0 */ @ApiStatus.Internal public final class BlueprintLoadContext { private final String name; private final ModelResolution resolution; private final TextureRef[] textureRefs; private final boolean canBeRendered; private volatile Map imageRefMap; private final MeshTriangleName triangleName = new MeshTriangleName( BetterModel.config().namespace() + ":" + BlueprintElement.MESH_TRIANGLE_SINGLE, BetterModel.config().namespace() + ":" + BlueprintElement.MESH_TRIANGLE_DUPLEX ); BlueprintLoadContext( @NotNull String name, @NotNull ModelResolution resolution, @NotNull List textures ) { this.name = name; this.resolution = resolution; this.textureRefs = new TextureRef[textures.size()]; var i = 0; var canBeRendered = false; for (BlueprintTexture texture : textures) { canBeRendered |= texture.canBeRendered(); this.textureRefs[i++] = new TextureRef(texture); } this.canBeRendered = canBeRendered; } /** * Gets the name of the blueprint. * * @return the name * @since 3.0.0 */ public @NotNull String name() { return name; } /** * Gets the mesh triangle name. * * @return the triangle name * @since 3.0.0 */ public @NotNull MeshTriangleName triangleName() { return triangleName; } /** * Gets the model resolution. * * @return the resolution * @since 3.0.0 */ public @NotNull ModelResolution resolution() { return resolution; } /** * Gets a texture by its index. * * @param index the index * @return the texture * @since 3.0.0 */ public @NotNull BlueprintTexture texture(int index) { return Objects.requireNonNull(textureRefs[index]).texture(); } @NotNull @Unmodifiable Map imageByIndex() { Map map; if ((map = imageRefMap) != null) return map; synchronized (this) { if ((map = imageRefMap) != null) return map; return imageRefMap = Collections.unmodifiableMap(new AbstractMap<>() { @Override public MeshImage get(Object key) { var get = textureRefs[Integer.parseInt(key.toString())]; return get != null ? get.image() : null; } @Override @Unmodifiable public @NotNull Set> entrySet() { return IntStream.range(0, textureRefs.length) .mapToObj(i -> Map.entry(Integer.toString(i), textureRefs[i].image())) .collect(Collectors.toUnmodifiableSet()); } }); } } /** * Returns whether this context can be rendered. * * @return true if renderable * @since 3.0.0 */ public boolean canBeRendered() { return canBeRendered; } /** * Builds a stream of blueprint images using the provided obfuscator. * * @param obfuscator the obfuscator * @return a stream of images * @since 3.0.0 */ @NotNull public Stream buildImage(@NotNull PackObfuscator obfuscator) { if (!canBeRendered()) return Stream.empty(); return Arrays.stream(textureRefs) .filter(TextureRef::canBeRendered) .map(ref -> new BlueprintImage( ref.texture.packName(obfuscator), ref.texture.image(), ref.texture.isAnimatedTexture() ? ref.texture.toMcmeta() : null) ); } private static final class TextureRef { private final BlueprintTexture texture; private final AtomicBoolean referenced = new AtomicBoolean(); private volatile MeshImage image; private TextureRef(BlueprintTexture texture) { this.texture = texture; } public boolean canBeRendered() { return referenced.get() && texture.canBeRendered(); } public @NotNull BlueprintTexture texture() { referenced.set(true); return texture; } public @NotNull MeshImage image() { MeshImage img; if ((img = image) != null) return img; synchronized (this) { if ((img = image) != null) return img; try (var input = new ByteArrayInputStream(texture.image())) { return image = MeshImage.from(ImageIO.read(input)); } catch (IOException e) { throw new RuntimeException(e); } } } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/BlueprintTexture.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import com.google.gson.JsonObject; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.data.raw.ModelResolution; import kr.toxicity.model.api.pack.PackObfuscator; import kr.toxicity.model.api.util.json.JsonObjectBuilder; import org.jetbrains.annotations.NotNull; /** * Represents a processed texture in a model blueprint. *

* This record holds the texture's name, binary image data, dimensions, and rendering properties. *

* * @param name the internal name of the texture * @param image the binary content of the texture image * @param width the original width of the texture in pixels * @param height the original height of the texture in pixels * @param uvWidth the UV width of the texture, if specified * @param uvHeight the UV height of the texture, if specified * @param canBeRendered whether this texture should be included in the resource pack * @param frameTime the frame time of the texture * @param frameInterpolate the interpolation flag of the texture * @since 1.15.2 */ public record BlueprintTexture( @NotNull String name, byte[] image, int width, int height, int uvWidth, int uvHeight, boolean canBeRendered, int frameTime, boolean frameInterpolate ) { /** * Checks if this texture is an animated texture (a texture atlas for animation). * * @return true if it is an animated texture, false otherwise * @since 1.15.2 */ public boolean isAnimatedTexture() { if (hasUVSize()) { var h = (float) height / uvHeight; var w = (float) width / uvWidth; return h > w; } else { return height > 0 && width > 0 && height / width > 1; } } /** * Generates the .mcmeta file content for this texture if it is animated. * * @return the JSON object for the .mcmeta file * @since 1.15.2 */ public @NotNull JsonObject toMcmeta() { return JsonObjectBuilder.builder() .jsonObject("animation", animation -> { animation.property("interpolate", frameInterpolate()); animation.property("frametime", frameTime()); }) .build(); } /** * Generates the pack-compliant file name for this texture. * * @param obfuscator the obfuscator to use for the name * @return the obfuscated file name * @since 1.15.2 */ public @NotNull String packName(@NotNull PackObfuscator obfuscator) { return obfuscator.obfuscate(name()); } /** * Generates the full resource pack namespace path for this texture. * * @param obfuscator the obfuscator to use for the name * @return the texture's namespace path * @since 1.15.2 */ public @NotNull String packNamespace(@NotNull PackObfuscator obfuscator) { return BetterModel.config().namespace() + ":item/" + packName(obfuscator); } /** * Checks if this texture has a specific UV size defined. * * @return true if UV width and height are specified, false otherwise * @since 1.15.2 */ public boolean hasUVSize() { return uvWidth > 0 && uvHeight > 0; } /** * Returns the effective resolution for this texture's UV mapping. * * @param resolution the parent model's resolution * @return the UV resolution, or the parent resolution if not specified * @since 1.15.2 */ public @NotNull ModelResolution resolution(@NotNull ModelResolution resolution) { if (!hasUVSize()) return resolution; return resolution.width() == width && resolution.height() == height ? resolution : new ModelResolution(uvWidth, uvHeight); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/ModelBlueprint.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import kr.toxicity.model.api.data.raw.ModelResolution; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Map; /** * Represents a fully processed model blueprint, ready for generation and rendering. *

* This record contains all the necessary data derived from a raw model file, including * textures, structural elements (bones/cubes), and animations. *

* * @param name the name of the model * @param resolution the texture resolution of the model * @param textures the list of textures used by the model * @param elements the hierarchical list of model elements (bones) * @param animations a map of animations available for this model * @since 1.15.2 */ @ApiStatus.Internal public record ModelBlueprint( @NotNull String name, @NotNull ModelResolution resolution, @NotNull List textures, @NotNull List elements, @NotNull Map animations ) { /** * Creates a new load context for this blueprint. * * @since 3.0.0 * @return a new blueprint load context */ public @NotNull BlueprintLoadContext context() { return new BlueprintLoadContext( name(), resolution(), textures() ); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/blueprint/ModelBoundingBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.blueprint; import org.jetbrains.annotations.NotNull; import org.joml.Quaterniond; import org.joml.Vector3d; import org.joml.Vector3f; /** * Represents an axis-aligned bounding box (AABB) for a model part. *

* This record defines the spatial extent of a model element or group, used for hitboxes and collision detection. *

* * @param minX the minimum X coordinate * @param minY the minimum Y coordinate * @param minZ the minimum Z coordinate * @param maxX the maximum X coordinate * @param maxY the maximum Y coordinate * @param maxZ the maximum Z coordinate * @since 1.15.2 */ public record ModelBoundingBox( double minX, double minY, double minZ, double maxX, double maxY, double maxZ ) { /** * A minimal bounding box size (0.1 x 0.1 x 0.1). * @since 1.15.2 */ public static final ModelBoundingBox MIN = of(0.1, 0.1, 0.1); /** * Creates a bounding box from two corner vectors. * * @param min the minimum corner vector * @param max the maximum corner vector * @return the bounding box * @since 1.15.2 */ public static @NotNull ModelBoundingBox of(@NotNull Vector3d min, @NotNull Vector3d max) { return of( min.x, min.y, min.z, max.x, max.y, max.z ); } /** * Creates a bounding box centered at the origin with the given dimensions. * * @param x the width (X-axis) * @param y the height (Y-axis) * @param z the depth (Z-axis) * @return the centered bounding box * @since 1.15.2 */ public static @NotNull ModelBoundingBox of(double x, double y, double z) { return of( -x / 2, -y / 2, -z / 2, x / 2, y / 2, z / 2 ); } /** * Creates a bounding box from explicit min/max coordinates. *

* This method automatically ensures that min values are less than or equal to max values. *

* * @param minX the first X coordinate * @param minY the first Y coordinate * @param minZ the first Z coordinate * @param maxX the second X coordinate * @param maxY the second Y coordinate * @param maxZ the second Z coordinate * @return the normalized bounding box * @since 1.15.2 */ public static @NotNull ModelBoundingBox of( double minX, double minY, double minZ, double maxX, double maxY, double maxZ ) { return new ModelBoundingBox( Math.min(minX, maxX), Math.min(minY, maxY), Math.min(minZ, maxZ), Math.max(minX, maxX), Math.max(minY, maxY), Math.max(minZ, maxZ) ); } /** * Returns the width of the bounding box (X-axis extent). * * @return the width * @since 1.15.2 */ public double x() { return maxX - minX; } /** * Returns the height of the bounding box (Y-axis extent). * * @return the height * @since 1.15.2 */ public double y() { return maxY - minY; } /** * Returns the Y coordinate of the center of the bounding box. * * @return the center Y * @since 1.15.2 */ public double centerY() { return (maxY + minY) / 2; } /** * Returns the depth of the bounding box (Z-axis extent). * * @return the depth * @since 1.15.2 */ public double z() { return maxZ - minZ; } /** * Returns the center point of the bounding box as a vector. * * @return the center vector * @since 1.15.2 */ public @NotNull Vector3f centerPoint() { return new Vector3f( (float) (minX + maxX), (float) (minY + maxY), (float) (minZ + maxZ) ).div(2F); } /** * Scales the bounding box by a uniform factor. * * @param scale the scale factor * @return the scaled bounding box * @since 1.15.2 */ public @NotNull ModelBoundingBox times(double scale) { return of( minX * scale, minY * scale, minZ * scale, maxX * scale, maxY * scale, maxZ * scale ); } /** * Returns a new bounding box with the same dimensions but centered at the origin (0,0,0). * * @return the centered bounding box * @since 1.15.2 */ public @NotNull ModelBoundingBox center() { var center = centerPoint(); return of( minX - center.x, minY - center.y, minZ - center.z, maxX - center.x, maxY - center.y, maxZ - center.z ); } /** * Inverts the X and Z coordinates of the bounding box. * * @return the inverted bounding box * @since 1.15.2 */ public @NotNull ModelBoundingBox invert() { return of( -minX, minY, -minZ, -maxX, maxY, -maxZ ); } /** * Rotates the bounding box around its center. * * @param quaterniond the rotation quaternion * @return the rotated bounding box * @since 1.15.2 */ public @NotNull ModelBoundingBox rotate(@NotNull Quaterniond quaterniond) { var centerVec = centerPoint(); return of( min().sub(centerVec).rotate(quaterniond).add(centerVec), max().sub(centerVec).rotate(quaterniond).add(centerVec) ); } /** * Returns the minimum corner as a vector. * * @return the min vector * @since 1.15.2 */ public @NotNull Vector3d min() { return new Vector3d(minX, minY, minZ); } /** * Returns the maximum corner as a vector. * * @return the max vector * @since 1.15.2 */ public @NotNull Vector3d max() { return new Vector3d(maxX, maxY, maxZ); } /** * Calculates the diagonal length in the XZ plane. * * @return the XZ diagonal length * @since 1.15.2 */ public double lengthZX() { return Math.sqrt(Math.pow(x(), 2) + Math.pow(z(), 2)); } /** * Calculates the full diagonal length of the bounding box. * * @return the diagonal length * @since 1.15.2 */ public double length() { return Math.sqrt(Math.pow(x(), 2) + Math.pow(y(), 2) + Math.pow(z(), 2)); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/KeyframeChannel.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.annotations.SerializedName; import org.jetbrains.annotations.ApiStatus; /** * Represents the type of property a keyframe affects. * * @since 1.15.2 */ @ApiStatus.Internal public enum KeyframeChannel { /** * Affects the position of a bone. * @since 1.15.2 */ @SerializedName("position") POSITION, /** * Affects the rotation of a bone. * @since 1.15.2 */ @SerializedName("rotation") ROTATION, /** * Affects the scale of a bone. * @since 1.15.2 */ @SerializedName("scale") SCALE, /** * Represents a timeline for sound or particle effects. * @since 1.15.2 */ @SerializedName("timeline") TIMELINE, /** * Represents a sound effect. * @since 1.15.2 */ @SerializedName("sound") SOUND, /** * Represents a particle effect. * @since 1.15.2 */ @SerializedName("particle") PARTICLE, /** * Represents an unknown or unsupported channel. * @since 1.15.2 */ NOT_FOUND } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelAnimation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.animation.AnimationIterator; import kr.toxicity.model.api.animation.AnimationProgress; import kr.toxicity.model.api.animation.VectorPoint; import kr.toxicity.model.api.data.blueprint.AnimationGenerator; import kr.toxicity.model.api.data.blueprint.BlueprintAnimation; import kr.toxicity.model.api.data.blueprint.BlueprintAnimator; import kr.toxicity.model.api.data.blueprint.BlueprintElement; import kr.toxicity.model.api.script.AnimationScript; import kr.toxicity.model.api.script.BlueprintScript; import kr.toxicity.model.api.script.TimeScript; import kr.toxicity.model.api.util.InterpolationUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import static kr.toxicity.model.api.util.CollectionUtil.associate; /** * Represents a raw animation definition from a model file. *

* This record holds the properties of an animation, such as its name, length, loop mode, * and the animators that define the keyframes for each bone group. *

* * @param name the name of the animation * @param loop the loop mode (e.g., play_once, loop, hold) * @param override whether this animation should override others * @param uuid the unique identifier of the animation * @param length the total length of the animation in seconds * @param animators a map of animators, keyed by the UUID of the bone group they affect * @since 1.15.2 */ @ApiStatus.Internal public record ModelAnimation( @NotNull String name, @Nullable AnimationIterator.Type loop, boolean override, @NotNull String uuid, float length, @Nullable Map animators ) { /** * Converts this raw animation data into a processed {@link BlueprintAnimation}. * * @param context the model loading context * @param children the list of root blueprint elements * @return the blueprint animation * @since 1.15.2 */ public @NotNull BlueprintAnimation toBlueprint( @NotNull ModelLoadContext context, @NotNull List children ) { var animators = AnimationGenerator.createMovements(length(), children, associate( animators().entrySet().stream() .filter(e -> context.availableUUIDs.contains(e.getKey())) .map(Map.Entry::getValue) .filter(ModelAnimator::isAvailable) .map(a -> buildAnimationData(context, a)), BlueprintAnimator.AnimatorData::name )); return new BlueprintAnimation( name(), loop(), length(), override(), animators, Optional.ofNullable(animators().get("effects")) .filter(ModelAnimator::isNotEmpty) .map(a -> toScript(a, context.placeholder)) .orElseGet(() -> BlueprintScript.fromEmpty(this)), animators.isEmpty() ? AnimationProgress.emptyStorage(length()) : animators.values() .iterator() .next() .keyframe() .toEmpty() ); } private @NotNull BlueprintScript toScript(@NotNull ModelAnimator animator, @NotNull ModelPlaceholder placeholder) { var set = new ObjectAVLTreeSet(); set.add(TimeScript.EMPTY); set.add(TimeScript.EMPTY.time(length())); animator.stream() .filter(f -> f.point().hasScript()) .map(d -> AnimationScript.of(Arrays.stream(placeholder.parseVariable(d.point().script()).split("\n")) .map(BetterModel.platform().scriptManager()::build) .filter(Objects::nonNull) .toList()) .time(d.time())) .forEach(set::add); var array = new TimeScript[set.size()]; var before = 0F; var i = 0; for (TimeScript timeScript : set) { var t = timeScript.time(); array[i++] = timeScript.time(InterpolationUtil.roundTime(t - before)); before = t; } return new BlueprintScript( name(), loop(), length(), List.of(array) ); } /** * Returns the loop mode of the animation. * * @return the loop mode, defaulting to {@link AnimationIterator.Type#PLAY_ONCE} if null * @since 1.15.2 */ @Override public @NotNull AnimationIterator.Type loop() { return loop != null ? loop : AnimationIterator.Type.PLAY_ONCE; } /** * Returns the map of animators for this animation. * * @return the animators, or an empty map if null * @since 1.15.2 */ @Override @NotNull public Map animators() { return animators != null ? animators : Collections.emptyMap(); } @NotNull private BlueprintAnimator.AnimatorData buildAnimationData(@NotNull ModelLoadContext context, @NotNull ModelAnimator animator) { var position = new ArrayList(); var rotation = new ArrayList(); var scale = new ArrayList(); var version = context.meta.formatVersion(); animator.stream().filter(keyframe -> keyframe.time() <= length()).forEach(keyframe -> { switch (keyframe.channel()) { case POSITION -> position.add(keyframe.point(context, version::convertAnimationPosition)); case ROTATION -> rotation.add(keyframe.point(context, version::convertAnimationRotation)); case SCALE -> scale.add(keyframe.point(context, version::convertAnimationScale)); } }); return new BlueprintAnimator.AnimatorData( animator.name(), position, scale, rotation, animator.rotationGlobal() ); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelAnimator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.bone.BoneName; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Stream; /** * Represents a raw animator from a model file, containing a set of keyframes for a specific bone group. * * @param name the name of the bone group this animator affects * @param keyframes the list of keyframes * @param _rotationGlobal whether the rotation is applied globally (true) or locally (false/null) * @since 1.15.2 */ @ApiStatus.Internal public record ModelAnimator( @Nullable BoneName name, @Nullable List keyframes, @Nullable @SerializedName("rotation_global") Boolean _rotationGlobal ) { /** * Checks if the rotation should be applied globally. * * @return true if rotation is global, false otherwise * @since 1.15.2 */ public boolean rotationGlobal() { return Boolean.TRUE.equals(_rotationGlobal); } /** * Checks if this animator is valid and has keyframes. * * @return true if available, false otherwise * @since 1.15.2 */ public boolean isAvailable() { return name != null && isNotEmpty(); } /** * Checks if this animator contains any keyframes. * * @return true if not empty, false otherwise * @since 1.15.2 */ public boolean isNotEmpty() { return !keyframes().isEmpty(); } /** * Returns the name of the bone group this animator affects. * * @return the name of the bone group */ @Override public @NotNull BoneName name() { return Objects.requireNonNull(name); } /** * Returns the list of keyframes for this animator. If no keyframes are present, an empty list is returned. * * @return the list of keyframes */ @Override public @NotNull List keyframes() { return keyframes != null ? keyframes : Collections.emptyList(); } /** * Returns a sorted stream of valid keyframes. * * @return the stream of keyframes * @since 1.15.2 */ public @NotNull Stream stream() { return keyframes().stream() .filter(ModelKeyframe::hasPoint) .sorted(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelData.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.data.Float2; import kr.toxicity.model.api.data.Float3; import kr.toxicity.model.api.data.Float4; import kr.toxicity.model.api.data.blueprint.BlueprintAnimation; import kr.toxicity.model.api.data.blueprint.ModelBlueprint; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; import static kr.toxicity.model.api.util.CollectionUtil.*; /** * Represents the raw data structure of a model file, typically parsed from a .bbmodel JSON file. *

* This record holds all the top-level components of a BlockBench model, including metadata, elements, textures, and animations. * It serves as the initial data container before being processed into a {@link ModelBlueprint}. *

* * @param meta the metadata of the model * @param resolution the texture resolution * @param elements the list of cube/mesh elements * @param outliner the hierarchical structure of the model * @param textures the list of textures used in the model * @param animations the list of animations * @param groups the list of groups (used in BlockBench 5.0.0+) * @param placeholder the animation variable placeholders * @since 1.15.2 */ @ApiStatus.Internal public record ModelData( @NotNull ModelMeta meta, @NotNull ModelResolution resolution, @NotNull List elements, @NotNull List outliner, @NotNull List textures, @Nullable List animations, @Nullable List groups, @Nullable @SerializedName("animation_variable_placeholders") ModelPlaceholder placeholder ) { /** * The GSON parser configured for deserializing model data. * @since 1.15.2 */ public static final Gson GSON = new GsonBuilder() .registerTypeAdapter(Float2.class, Float2.PARSER) .registerTypeAdapter(Float3.class, Float3.PARSER) .registerTypeAdapter(Float4.class, Float4.PARSER) .registerTypeAdapter(BoneName.class, BoneName.PARSER) .registerTypeAdapter(ModelMeta.class, ModelMeta.PARSER) .registerTypeAdapter(ModelOutliner.class, ModelOutliner.PARSER) .registerTypeAdapter(ModelPlaceholder.class, ModelPlaceholder.PARSER) .registerTypeAdapter(ModelElement.class, ModelElement.PARSER) .create(); /** * Converts this raw model data into a processed {@link ModelBlueprint}. * * @param name the name to assign to the blueprint * @return the result of the loading process, containing the blueprint and any errors * @since 1.15.2 */ public @NotNull ModelLoadResult loadBlueprint(@NotNull String name) { return loadBlueprint(name, BetterModel.config().enableStrictLoading()); } /** * Converts this raw model data into a processed {@link ModelBlueprint} with a specific loading mode. * * @param name the name to assign to the blueprint * @param strict whether to use strict loading mode (fail on unsupported features) * @return the result of the loading process, containing the blueprint and any errors * @since 1.15.2 */ public @NotNull ModelLoadResult loadBlueprint(@NotNull String name, boolean strict) { var context = new ModelLoadContext( name, placeholder(), meta(), associate(elements(), ModelElement::uuid), associate(groups(), ModelGroup::uuid), mapToSet(outliner().stream().flatMap(ModelOutliner::flatten), ModelOutliner::uuid), strict ); var group = mapToList(outliner(), outliner -> outliner.toBlueprint(context)); return new ModelLoadResult( new ModelBlueprint( context.name, resolution(), mapToList(textures(), texture -> texture.toBlueprint(context)), group, associate(animations().stream().map(raw -> raw.toBlueprint(context, group)), BlueprintAnimation::name) ), context.errors ); } /** * Asserts that the model does not contain any unsupported element types. * * @throws RuntimeException if an unsupported element is found * @since 1.15.2 */ public void assertSupported() { elements().stream() .filter(e -> !e.isSupported()) .findFirst() .ifPresent(e -> { throw new RuntimeException("This model file has unsupported element type: " + e.type()); }); } /** * Returns the animation variable placeholders, or an empty placeholder if none are defined. * * @return the animation variable placeholders * @since 1.15.2 */ @Override public @NotNull ModelPlaceholder placeholder() { return placeholder != null ? placeholder : ModelPlaceholder.EMPTY; } /** * Returns the list of animations, or an empty list if none are defined. * * @return the list of animations * @since 1.15.2 */ @Override @NotNull public List animations() { return animations != null ? animations : Collections.emptyList(); } /** * Returns the list of groups, or an empty list if none are defined. * * @return the list of groups * @since 1.15.2 */ @Override public @NotNull List groups() { return groups != null ? groups : Collections.emptyList(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelDatapoint.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonPrimitive; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.util.function.Float2FloatConstantFunction; import kr.toxicity.model.api.util.function.Float2FloatFunction; import kr.toxicity.model.api.util.function.FloatFunction; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; import java.util.Objects; /** * Represents a single data point within a keyframe, which can be a static value or a Molang script. *

* This record holds the raw JSON values for x, y, and z coordinates, or a script string. *

* * @param x the x-coordinate value or script * @param y the y-coordinate value or script * @param z the z-coordinate value or script * @param script the script string (used for sound/particle effects) * @since 1.15.2 */ @ApiStatus.Internal public record ModelDatapoint( @Nullable JsonPrimitive x, @Nullable JsonPrimitive y, @Nullable JsonPrimitive z, @Nullable String script ) { /** * Checks if this data point contains a script. * * @return true if a script is present, false otherwise * @since 1.15.2 */ public boolean hasScript() { return script != null; } /** * Returns the script string. * * @return the script * @throws NullPointerException if no script is present * @since 1.15.2 */ @Override public @NotNull String script() { return Objects.requireNonNull(script); } /** * Converts this data point into a function that returns a {@link Vector3f}. *

* If the data point contains static values, it returns a constant function. * If it contains Molang expressions, it compiles them into a function that evaluates the expressions. *

* * @param context the model loading context * @return a function to get the vector value * @since 1.15.2 */ public @NotNull FloatFunction toFunction(@NotNull ModelLoadContext context) { var xb = build(x, context); var yb = build(y, context); var zb = build(z, context); if (xb instanceof Float2FloatConstantFunction(float xc) && yb instanceof Float2FloatConstantFunction(float yc) && zb instanceof Float2FloatConstantFunction(float zc) ) { return FloatFunction.of(new Vector3f(xc, yc, zc)); } else { return f -> new Vector3f( xb.applyAsFloat(f), yb.applyAsFloat(f), zb.applyAsFloat(f) ); } } private static @NotNull Float2FloatFunction build(@Nullable JsonPrimitive primitive, @NotNull ModelLoadContext context) { if (primitive == null) return Float2FloatFunction.ZERO; if (primitive.isNumber()) return Float2FloatFunction.of(primitive.getAsFloat()); var string = primitive.getAsString().trim(); if (string.isEmpty()) return Float2FloatFunction.ZERO; try { return Float2FloatFunction.of(Float.parseFloat(string)); } catch (NumberFormatException ignored) { return context.trySupply( () -> BetterModel.platform().evaluator().compile(context.placeholder.parseVariable(string)), error -> new ModelLoadContext.Fallback<>( Float2FloatFunction.ZERO, "Cannot parse this datapoint: " + primitive + ", reason: " + error.getMessage() ) ); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelElement.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonDeserializer; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.data.Float2; import kr.toxicity.model.api.data.Float3; import kr.toxicity.model.api.data.blueprint.BlueprintElement; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * Represents a raw element within a model file. *

* This interface is a sealed type that permits specific implementations for different element types * found in BlockBench models, such as cubes, locators, null objects, and cameras. *

* * @since 1.15.2 */ @ApiStatus.Internal public sealed interface ModelElement { /** * The type identifier for a null object element. * @since 2.2.1 */ String NULL_OBJECT = "null_object"; /** * The type identifier for a locator element. * @since 2.2.1 */ String LOCATOR = "locator"; /** * The type identifier for a camera element. * @since 2.2.1 */ String CAMERA = "camera"; /** * The type identifier for a cube element. * @since 2.2.1 */ String CUBE = "cube"; /** * The type identifier for a mesh element. * @since 3.0.0 */ String MESH = "mesh"; /** * A JSON deserializer that automatically dispatches to the correct {@link ModelElement} implementation based on the "type" field. * @since 1.15.2 */ JsonDeserializer PARSER = (json, _, context) -> { var t = json.getAsJsonObject().getAsJsonPrimitive("type"); var select = t != null ? t.getAsString() : CUBE; return switch (select) { case NULL_OBJECT -> context.deserialize(json, NullObject.class); case LOCATOR -> context.deserialize(json, Locator.class); case CAMERA -> context.deserialize(json, Camera.class); case CUBE -> context.deserialize(json, Cube.class); case MESH -> context.deserialize(json, Mesh.class); default -> new Unsupported(select); }; }; /** * Returns the unique identifier (UUID) of this element. * * @return the UUID string * @since 1.15.2 */ @NotNull String uuid(); /** * Returns the type identifier of this element (e.g., "cube", "locator"). * * @return the type string * @since 1.15.2 */ @NotNull String type(); /** * Converts this raw element into a processed {@link BlueprintElement}. * * @return the blueprint element * @since 1.15.2 */ @NotNull BlueprintElement toBlueprint(); /** * Checks if this element type is supported by the engine. * * @return true if supported, false otherwise * @since 1.15.2 */ default boolean isSupported() { return true; } /** * Represents a locator element, used for positioning attachments or particles. * * @param name the name of the locator * @param uuid the UUID of the locator * @param position the position of the locator * @since 1.15.2 */ record Locator( @NotNull String name, @NotNull String uuid, @Nullable Float3 position ) implements ModelElement { @Override public @NotNull String type() { return LOCATOR; } /** * Returns the position of the locator. * * @return the position, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override public @NotNull Float3 position() { return position != null ? position : Float3.ZERO; } @Override public @NotNull BlueprintElement toBlueprint() { return new BlueprintElement.Locator( UUID.fromString(uuid), BoneName.of(name()), position() ); } } /** * Represents a camera element (currently used as a placeholder). * * @param uuid the UUID of the camera * @since 1.15.2 */ record Camera( @NotNull String uuid ) implements ModelElement { @Override public @NotNull String type() { return CAMERA; } @Override public @NotNull BlueprintElement toBlueprint() { return new BlueprintElement.Camera( UUID.fromString(uuid) ); } } /** * Represents a null object, often used for grouping or IK targets. * * @param name the name of the null object * @param uuid the UUID of the null object * @param ikTarget the UUID of the IK target, if any * @param ikSource the UUID of the IK source, if any * @param position the position of the null object * @since 1.15.2 */ record NullObject( @NotNull String name, @NotNull String uuid, @Nullable @SerializedName("ik_target") String ikTarget, @Nullable @SerializedName("ik_source") String ikSource, @Nullable Float3 position ) implements ModelElement { @Override public @NotNull String type() { return NULL_OBJECT; } /** * Returns the position of the null object. * * @return the position, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override public @NotNull Float3 position() { return position != null ? position : Float3.ZERO; } @Override public @NotNull BlueprintElement toBlueprint() { return new BlueprintElement.NullObject( UUID.fromString(uuid), BoneName.of(name()), Optional.ofNullable(ikTarget()) .filter(str -> !str.isEmpty()) .map(UUID::fromString) .orElse(null), Optional.ofNullable(ikSource()) .filter(str -> !str.isEmpty()) .map(UUID::fromString) .orElse(null), position() ); } } /** * Represents an unsupported element type. * * @param type the unsupported type string * @since 1.15.2 */ record Unsupported(@NotNull String type) implements ModelElement { @Override public @NotNull String uuid() { throw new UnsupportedOperationException(type()); } @Override public boolean isSupported() { return false; } @Override public @NotNull BlueprintElement toBlueprint() { throw new UnsupportedOperationException(type()); } } /** * Represents a standard cube element. * * @param name the name of the cube * @param uuid the UUID of the cube * @param from the starting coordinate (min corner) * @param to the ending coordinate (max corner) * @param inflate the inflation value (size increase) * @param rotation the rotation of the cube * @param origin the pivot point (origin) of the cube * @param faces the UV mapping for the faces * @param lightEmission the light emission level (0-15) * @param _visibility the visibility state (null means visible) * @since 1.15.2 */ record Cube( @NotNull String name, @NotNull String uuid, @Nullable Float3 from, @Nullable Float3 to, float inflate, @Nullable Float3 rotation, @NotNull Float3 origin, @Nullable ModelFace faces, @SerializedName("light_emission") int lightEmission, @SerializedName("visibility") @Nullable Boolean _visibility ) implements ModelElement { @Override public @NotNull String type() { return CUBE; } /** * Returns the starting coordinate (min corner). * * @return the from vector, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override public @NotNull Float3 from() { return from != null ? from : Float3.ZERO; } /** * Returns the ending coordinate (max corner). * * @return the to vector, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override public @NotNull Float3 to() { return to != null ? to : Float3.ZERO; } /** * Checks if the cube is visible. * * @return true if visible, false otherwise * @since 1.15.2 */ public boolean visibility() { return !Boolean.FALSE.equals(_visibility); } /** * Returns the rotation of the cube. * * @return the rotation, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override public @NotNull Float3 rotation() { return rotation != null ? rotation : Float3.ZERO; } /** * Returns the light emission level of the cube. * * @return the light emission level (0-15) * @since 2.1.0 */ @Override public int lightEmission() { return name().toLowerCase().contains("glow") ? 15 : lightEmission; } @Override public @NotNull BlueprintElement toBlueprint() { return new BlueprintElement.Cube( name(), from(), to(), inflate(), rotation(), origin(), faces(), Optional.of(lightEmission()).filter(i -> i > 0).orElse(null), visibility() ); } } /** * Represents a mesh element, allowing for complex geometry beyond simple cubes. * * @param uuid the UUID of the mesh * @param origin the pivot point (origin) of the mesh * @param rotation the rotation of the mesh * @param vertices a map of vertex identifiers to their 3D positions * @param faces a map of face identifiers to their face data * @param _visibility the visibility state (null means visible) * @since 3.0.0 */ record Mesh( @NotNull String uuid, @Nullable Float3 origin, @Nullable Float3 rotation, @NotNull Map vertices, @NotNull Map faces, @SerializedName("visibility") @Nullable Boolean _visibility ) implements ModelElement { /** * Returns the pivot point (origin) of the mesh. * * @return the origin vector, or {@link Float3#ZERO} if not specified * @since 3.0.0 */ @Override public @NotNull Float3 origin() { return origin != null ? origin : Float3.ZERO; } /** * Returns the rotation of the mesh. * * @return the rotation vector, or {@link Float3#ZERO} if not specified * @since 3.0.0 */ @Override public @NotNull Float3 rotation() { return rotation != null ? rotation : Float3.ZERO; } /** * Returns the type identifier of this element. * * @return {@link #MESH} * @since 3.0.0 */ @Override public @NotNull String type() { return MESH; } public boolean visibility() { return !Boolean.FALSE.equals(_visibility); } @Override public @NotNull BlueprintElement toBlueprint() { return new BlueprintElement.Mesh( origin(), rotation(), faces.values() .stream() .map(face -> new BlueprintElement.Mesh.Face( face.vertices.stream() .map(n -> new BlueprintElement.Mesh.Point( Objects.requireNonNull(vertices.get(n)), Objects.requireNonNull(face.uv.get(n)) )) .toList(), face.texture )) .toList(), visibility() ); } /** * Represents a single face of a mesh. * * @param uv a map of vertex identifiers to their UV coordinates * @param vertices a set of vertex identifiers that form this face * @param texture the index of the texture used by this face * @since 3.0.0 */ public record Face(@NotNull Map uv, @NotNull Set vertices, int texture) {} } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelFace.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonObject; import kr.toxicity.model.api.data.blueprint.BlueprintLoadContext; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.stream.IntStream; /** * Represents the UV mappings for all six faces of a cube element. * * @param north the UV mapping for the north face * @param east the UV mapping for the east face * @param south the UV mapping for the south face * @param west the UV mapping for the west face * @param up the UV mapping for the up face * @param down the UV mapping for the down face * @since 1.15.2 */ @ApiStatus.Internal public record ModelFace( @NotNull ModelUV north, @NotNull ModelUV east, @NotNull ModelUV south, @NotNull ModelUV west, @NotNull ModelUV up, @NotNull ModelUV down ) { /** * Converts the face UV data to a JSON object for the Minecraft model file. *

* Only faces with a defined texture will be included in the output. *

* * @param parent the parent model blueprint, used for texture resolution * @param tint the tint index to apply * @return the generated JSON object * @since 1.15.2 */ public @NotNull JsonObject toJson(@NotNull BlueprintLoadContext parent, int tint) { var object = new JsonObject(); JsonObject add; if ((add = north.toJson(parent, tint)) != null) object.add("north", add); if ((add = east.toJson(parent, tint)) != null) object.add("east", add); if ((add = south.toJson(parent, tint)) != null) object.add("south", add); if ((add = west.toJson(parent, tint)) != null) object.add("west", add); if ((add = up.toJson(parent, tint)) != null) object.add("up", add); if ((add = down.toJson(parent, tint)) != null) object.add("down", add); return object; } /** * Checks if any face has a texture defined. * * @return true if at least one face has a texture, false otherwise * @since 1.15.2 */ public boolean hasTexture() { return north.hasTexture() || east.hasTexture() || south.hasTexture() || west.hasTexture() || up.hasTexture() || down.hasTexture(); } public @NotNull IntStream textureIndex() { var builder = IntStream.builder(); if (north.hasTexture()) builder.add(north.textureIndex()); if (east.hasTexture()) builder.add(east.textureIndex()); if (south.hasTexture()) builder.add(south.textureIndex()); if (west.hasTexture()) builder.add(west.textureIndex()); if (up.hasTexture()) builder.add(up.textureIndex()); if (down.hasTexture()) builder.add(down.textureIndex()); return builder.build(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelGroup.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.data.Float3; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a group definition in the raw model data. *

* Groups are used to organize elements and other groups hierarchically. * This record corresponds to the group structure found in newer BlockBench versions (>= 5.0.0). *

* * @param name the name of the group * @param uuid the unique identifier of the group * @param origin the pivot point (origin) of the group * @param rotation the rotation of the group * @param lightEmission the light emission level (0-15) * @param _visibility the visibility state of the group (null means visible) * @since 1.15.2 */ public record ModelGroup( @NotNull String name, @NotNull String uuid, @Nullable Float3 origin, @Nullable Float3 rotation, @SerializedName("light_emission") int lightEmission, @Nullable @SerializedName("visibility") Boolean _visibility ) { /** * Returns the origin (pivot point) of the group. * * @return the origin, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override @NotNull public Float3 origin() { return origin != null ? origin : Float3.ZERO; } /** * Returns the rotation of the group. * * @return the rotation, or {@link Float3#ZERO} if not specified * @since 1.15.2 */ @Override @NotNull public Float3 rotation() { return rotation != null ? rotation : Float3.ZERO; } /** * Checks if the group is visible. * * @return true if visible, false otherwise * @since 1.15.2 */ public boolean visibility() { return !Boolean.FALSE.equals(_visibility); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelKeyframe.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.animation.Timed; import kr.toxicity.model.api.animation.VectorPoint; import kr.toxicity.model.api.data.Float3; import kr.toxicity.model.api.util.interpolator.VectorInterpolator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; import java.util.List; import java.util.Optional; import java.util.function.Function; /** * Represents a single keyframe in an animation timeline. *

* A keyframe defines the state of a bone (position, rotation, or scale) at a specific time, * along with interpolation data (linear, catmull-rom, bezier) to smooth transitions. *

* * @param channel the channel this keyframe affects (position, rotation, scale) * @param dataPoints the list of data points (values) for this keyframe * @param bezierLeftTime the time offset for the left bezier handle * @param bezierLeftValue the value offset for the left bezier handle * @param bezierRightTime the time offset for the right bezier handle * @param bezierRightValue the value offset for the right bezier handle * @param interpolation the interpolation type (e.g., linear, catmullrom, bezier) * @param time the time of the keyframe in seconds * @since 1.15.2 */ @ApiStatus.Internal public record ModelKeyframe( @Nullable KeyframeChannel channel, @SerializedName("data_points") @NotNull List dataPoints, @SerializedName("bezier_left_time") @Nullable Float3 bezierLeftTime, @SerializedName("bezier_left_value") @Nullable Float3 bezierLeftValue, @SerializedName("bezier_right_time") @Nullable Float3 bezierRightTime, @SerializedName("bezier_right_value") @Nullable Float3 bezierRightValue, @Nullable VectorInterpolator interpolation, float time ) implements Timed { /** * Checks if this keyframe contains any data points. * * @return true if data points exist, false otherwise * @since 1.15.2 */ public boolean hasPoint() { return !dataPoints.isEmpty(); } /** * Returns the first data point in the list. * * @return the first data point * @throws java.util.NoSuchElementException if the list is empty * @since 1.15.2 */ public @NotNull ModelDatapoint point() { return dataPoints.getFirst(); } /** * Converts this keyframe into a processed {@link VectorPoint}. * * @param context the model loading context * @param function a transformation function to apply to the vector values (e.g., coordinate conversion) * @return the vector point * @since 1.15.2 */ public @NotNull VectorPoint point(@NotNull ModelLoadContext context, @NotNull Function function) { return new VectorPoint( point().toFunction(context).map(function).memoize(), time(), new VectorPoint.BezierConfig( Optional.ofNullable(bezierLeftTime).map(Float3::toVector).orElse(null), Optional.ofNullable(bezierLeftValue).map(Float3::toVector).map(function).orElse(null), Optional.ofNullable(bezierRightTime).map(Float3::toVector).orElse(null), Optional.ofNullable(bezierRightValue).map(Float3::toVector).map(function).orElse(null) ), interpolation() ); } /** * Returns the interpolation type for this keyframe. * * @return the interpolation type, defaulting to {@link VectorInterpolator#LINEAR} if null * @since 1.15.2 */ @Override public @NotNull VectorInterpolator interpolation() { return interpolation != null ? interpolation : VectorInterpolator.LINEAR; } /** * Returns the channel this keyframe affects. * * @return the channel, defaulting to {@link KeyframeChannel#NOT_FOUND} if null * @since 1.15.2 */ @Override public @NotNull KeyframeChannel channel() { return channel != null ? channel : KeyframeChannel.NOT_FOUND; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelLoadContext.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.function.Function; import java.util.function.Supplier; /** * Holds the context and state during the model loading process. *

* This class provides access to all parts of the raw model data and accumulates errors * that occur during processing. It also controls the loading mode (strict or lenient). *

* * @since 1.15.2 */ @RequiredArgsConstructor @ApiStatus.Internal public final class ModelLoadContext { final @NotNull String name; final @NotNull ModelPlaceholder placeholder; final @NotNull ModelMeta meta; final @NotNull Map elements; final @NotNull Map groups; final @NotNull Set availableUUIDs; private final boolean strict; private final List _errors = new ArrayList<>(); final List errors = Collections.unmodifiableList(_errors); /** * Tries to execute a supplier, catching exceptions in lenient mode. * * @param supplier the supplier to execute * @param fallbackFunction a function to provide a fallback value and error message on exception * @param the return type * @return the result of the supplier or the fallback value * @since 1.15.2 */ @NotNull T trySupply(@NotNull Supplier supplier, @NotNull Function> fallbackFunction) { if (strict) return supplier.get(); try { return supplier.get(); } catch (Exception e) { var fallback = fallbackFunction.apply(e); _errors.add(fallback.message); return fallback.value; } } /** * Represents a fallback value and an associated error message. * * @param value the fallback value * @param message the error message * @param the type of the value * @since 1.15.2 */ record Fallback(@NotNull T value, @NotNull String message) {} } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelLoadResult.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import kr.toxicity.model.api.data.blueprint.ModelBlueprint; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.List; /** * Represents the result of loading and processing a raw model file. *

* This record contains the successfully created {@link ModelBlueprint} and a list of any errors * or warnings that occurred during the loading process. *

* * @param blueprint the processed model blueprint * @param errors a list of error or warning messages generated during loading * @since 1.15.2 */ public record ModelLoadResult(@NotNull ModelBlueprint blueprint, @NotNull @Unmodifiable List errors) { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelMeta.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonDeserializer; import kr.toxicity.model.api.util.MathUtil; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import org.semver4j.Semver; import java.util.Arrays; import java.util.Objects; /** * Represents metadata about the model file, specifically the format version. *

* This record is used to handle differences in coordinate systems and animation data between different BlockBench versions. *

* * @param formatVersion the detected format version of the model file * @since 1.15.2 */ public record ModelMeta( @NotNull FormatVersion formatVersion ) { /** * A JSON deserializer for parsing {@link ModelMeta} from the "meta" object in a .bbmodel file. * @since 1.15.2 */ public static final JsonDeserializer PARSER = (json, _, _) -> new ModelMeta( FormatVersion.find(Objects.requireNonNull(Semver.coerce(json.getAsJsonObject().getAsJsonPrimitive("format_version").getAsString())).getMajor()) ); /** * Enumerates supported BlockBench format versions and their specific coordinate conversions. * @since 1.15.2 */ @RequiredArgsConstructor public enum FormatVersion { /** * BlockBench version 5.0.0 and later. * @since 1.15.2 */ BLOCKBENCH_5(5) { @Override public @NotNull Vector3f convertAnimationRotation(@NotNull Vector3f vector) { vector.x = -vector.x; vector.z = -vector.z; return vector; } @Override public @NotNull Vector3f convertAnimationPosition(@NotNull Vector3f vector) { vector.x = -vector.x; vector.z = -vector.z; return vector.div(MathUtil.MODEL_TO_BLOCK_MULTIPLIER); } }, /** * Legacy BlockBench versions (pre-5.0.0). * @since 1.15.2 */ BLOCKBENCH_LEGACY(0) { @Override public @NotNull Vector3f convertAnimationRotation(@NotNull Vector3f vector) { vector.y = -vector.y; vector.z = -vector.z; return vector; } @Override public @NotNull Vector3f convertAnimationPosition(@NotNull Vector3f vector) { vector.z = -vector.z; return vector.div(MathUtil.MODEL_TO_BLOCK_MULTIPLIER); } }, ; private final int major; /** * Finds the appropriate format version based on the major version number. * * @param major the major version number * @return the matching format version * @throws java.util.NoSuchElementException if no matching version is found * @since 1.15.2 */ public static @NotNull FormatVersion find(int major) { return Arrays.stream(values()) .filter(v -> v.major <= major) .findFirst() .orElseThrow(); } /** * Converts animation rotation values to the engine's coordinate system. * * @param vector the raw rotation vector * @return the converted rotation vector * @since 1.15.2 */ public abstract @NotNull Vector3f convertAnimationRotation(@NotNull Vector3f vector); /** * Converts animation position values to the engine's coordinate system. * * @param vector the raw position vector * @return the converted position vector * @since 1.15.2 */ public abstract @NotNull Vector3f convertAnimationPosition(@NotNull Vector3f vector); /** * Converts animation scale values to the engine's format (relative to 1.0). * * @param vector the raw scale vector * @return the converted scale vector * @since 1.15.2 */ public @NotNull Vector3f convertAnimationScale(@NotNull Vector3f vector) { vector.sub(1F, 1F, 1F); return vector; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelOutliner.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonDeserializer; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.data.blueprint.BlueprintElement; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.stream.Stream; import static kr.toxicity.model.api.util.CollectionUtil.filterIsInstance; import static kr.toxicity.model.api.util.CollectionUtil.mapToList; /** * Represents the hierarchical structure (outliner) of a model. *

* The outliner defines the parent-child relationships between groups and elements (cubes, locators, etc.). * It can be either a direct reference to an element (UUID string) or a tree node (Group) containing children. *

* * @since 1.15.2 */ @ApiStatus.Internal public sealed interface ModelOutliner { /** * A JSON deserializer that parses the outliner structure. *

* It distinguishes between leaf nodes (UUID strings) and branch nodes (Groups with children). *

* @since 1.15.2 */ JsonDeserializer PARSER = (json, _, context) -> { if (json.isJsonPrimitive()) return new Reference(json.getAsString()); else if (json.isJsonObject()) { var children = json.getAsJsonObject().getAsJsonArray("children"); return new Tree( context.deserialize(json, ModelGroup.class), children.asList() .stream() .map(child -> (ModelOutliner) context.deserialize(child, ModelOutliner.class)) .toList() ); } else throw new RuntimeException(); }; /** * Converts this outliner node into a processed {@link BlueprintElement}. * * @param context the model loading context * @return the blueprint element * @since 1.15.2 */ @NotNull BlueprintElement toBlueprint(@NotNull ModelLoadContext context); /** * Flattens the outliner tree into a stream of all nodes. * * @return a stream of all outliner nodes * @since 1.15.2 */ @NotNull Stream flatten(); /** * Returns the UUID of this outliner node. * * @return the UUID string * @since 1.15.2 */ @NotNull String uuid(); /** * Represents a leaf node in the outliner, referencing a specific element by UUID. * * @param uuid the UUID of the referenced element * @since 1.15.2 */ record Reference(@NotNull String uuid) implements ModelOutliner { @Override public @NotNull BlueprintElement toBlueprint(@NotNull ModelLoadContext context) { return Objects.requireNonNull(context.elements.get(uuid())).toBlueprint(); } @Override public @NotNull Stream flatten() { return Stream.of(this); } } /** * Represents a branch node (Group) in the outliner, containing child nodes. * * @param group the group definition * @param children the list of child outliner nodes * @since 1.15.2 */ record Tree( @NotNull ModelGroup group, @NotNull @Unmodifiable List children ) implements ModelOutliner { @Override public @NotNull BlueprintElement toBlueprint(@NotNull ModelLoadContext context) { var child = mapToList(children, c -> c.toBlueprint(context)); var filtered = filterIsInstance(child, BlueprintElement.Cube.class).toList(); var selectedGroup = context.groups.getOrDefault(uuid(), group); return new BlueprintElement.Group( UUID.fromString(selectedGroup.uuid()), BoneName.of(selectedGroup.name()), selectedGroup.origin(), selectedGroup.rotation().invertXZ(), child, filtered.isEmpty() ? selectedGroup.visibility() : filtered.stream().anyMatch(BlueprintElement.Cube::visibility) ); } @Override public @NotNull Stream flatten() { return children.isEmpty() ? Stream.of(this) : Stream.concat( Stream.of(this), children.stream().flatMap(ModelOutliner::flatten) ); } @Override public @NotNull String uuid() { return group.uuid(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelPlaceholder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonDeserializer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.Arrays; import java.util.Collections; import java.util.Map; import static kr.toxicity.model.api.util.CollectionUtil.associate; /** * Represents animation variable placeholders defined in a model file. *

* These placeholders allow for defining reusable values or expressions that can be referenced within Molang scripts in animations. *

* * @param variables a map of placeholder variable names to their string values * @since 1.15.2 */ public record ModelPlaceholder( @NotNull @Unmodifiable Map variables ) { /** * An empty placeholder instance with no variables. * @since 1.15.2 */ public static final ModelPlaceholder EMPTY = new ModelPlaceholder(Collections.emptyMap()); /** * A JSON deserializer for parsing placeholders from a multi-line string. * @since 1.15.2 */ public static final JsonDeserializer PARSER = (json, _, _) -> new ModelPlaceholder(associate( Arrays.stream(json.getAsString().split("\n")) .map(line -> line.split("=", 2)) .filter(array -> array.length == 2), array -> array[0].trim(), array -> array[1].trim() )); /** * Replaces all placeholder variables in a given expression with their corresponding values. * * @param expression the expression containing placeholders * @return the expression with placeholders substituted * @since 1.15.2 */ public @NotNull String parseVariable(@NotNull String expression) { for (var entry : variables.entrySet()) { expression = expression.replace(entry.getKey(), entry.getValue()); } return expression; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelResolution.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import org.jetbrains.annotations.ApiStatus; /** * Represents the texture resolution of a model. * * @param width the width of the texture in pixels * @param height the height of the texture in pixels * @since 1.15.2 */ @ApiStatus.Internal public record ModelResolution( int width, int height ) { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelTexture.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.data.blueprint.BlueprintTexture; import kr.toxicity.model.api.util.PackUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.Base64; /** * Represents a raw texture definition from a model file. *

* This record contains the texture's metadata and its content encoded as a Base64 string. *

* * @param name the name of the texture file (e.g., "texture.png") * @param source the Base64-encoded content of the texture image * @param width the width of the texture in pixels * @param height the height of the texture in pixels * @param uvWidth the UV width of the texture * @param uvHeight the UV height of the texture * @param frameTime the frame time of the texture * @param frameInterpolate the interpolation flag of the texture * @since 1.15.2 */ @ApiStatus.Internal public record ModelTexture( @NotNull String name, @NotNull String source, int width, int height, @SerializedName("uv_width") int uvWidth, @SerializedName("uv_height") int uvHeight, @SerializedName("frame_time") int frameTime, @SerializedName("frame_interpolate") boolean frameInterpolate ) { /** * Converts this raw texture into a processed {@link BlueprintTexture}. *

* This method decodes the Base64 source, generates a pack-compliant name, and determines if the texture should be included in the pack. *

* * @param context the model loading context * @return the blueprint texture * @since 1.15.2 */ public @NotNull BlueprintTexture toBlueprint(@NotNull ModelLoadContext context) { var name = nameWithoutExtension(); return new BlueprintTexture( PackUtil.toPackName(name.startsWith("global_") ? name : context.name + "_" + name), Base64.getDecoder().decode(source().substring(source().indexOf(',') + 1)), width(), height(), uvWidth(), uvHeight(), !name.startsWith("-"), frameTime(), frameInterpolate() ); } /** * Returns the texture name without its file extension. * * @return the name without extension * @since 1.15.2 */ public @NotNull String nameWithoutExtension() { var name = name(); var nameIndex = name.lastIndexOf('.'); return nameIndex >= 0 ? name.substring(0, nameIndex) : name; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/raw/ModelUV.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.raw; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import kr.toxicity.model.api.data.Float4; import kr.toxicity.model.api.data.blueprint.BlueprintLoadContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; /** * Represents the UV mapping data for a model face. *

* This record holds the UV coordinates, rotation, and texture index for a specific face of a model element. *

* * @param uv the UV coordinates as a {@link Float4} (u1, v1, u2, v2) * @param rotation the rotation of the UV map in degrees (0, 90, 180, 270) * @param texture the JSON element representing the texture index, can be null * @since 1.15.2 */ public record ModelUV( @NotNull Float4 uv, int rotation, @Nullable JsonElement texture ) { /** * Checks if this UV mapping has a valid texture index. * * @return true if a texture is defined, false otherwise * @since 1.15.2 */ public boolean hasTexture() { return texture != null && texture.isJsonPrimitive() && texture.getAsJsonPrimitive().isNumber(); } /** * Returns the texture index associated with this UV mapping. * * @return the texture index * @throws NullPointerException if no texture is defined * @since 1.15.2 */ public int textureIndex() { return Objects.requireNonNull(texture).getAsInt(); } /** * Converts this UV data to a JSON object for the Minecraft model file. * * @param context the blueprint context, used for texture resolution * @param tint the tint index to apply * @return the generated JSON object * @since 1.15.2 */ public @Nullable JsonObject toJson(@NotNull BlueprintLoadContext context, int tint) { if (!hasTexture()) return null; var div = uv.div(context.texture(textureIndex()).resolution(context.resolution())); if (!div.isValid()) return null; var object = new JsonObject(); object.add("uv", div.toJson()); if (rotation != 0) object.addProperty("rotation", rotation); object.addProperty("tintindex", tint); object.addProperty("texture", "#" + texture); return object; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/renderer/ModelRenderer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.renderer; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.data.blueprint.BlueprintAnimation; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.profile.ModelProfile; import kr.toxicity.model.api.tracker.DummyTracker; import kr.toxicity.model.api.tracker.EntityTracker; import kr.toxicity.model.api.tracker.TrackerModifier; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.SequencedMap; import java.util.function.Consumer; import java.util.stream.Stream; /** * A blueprint renderer. * * @param name name * @param type type * @param rendererGroups group map * @param animations animations */ public record ModelRenderer( @NotNull String name, @NotNull Type type, @NotNull @Unmodifiable SequencedMap rendererGroups, @NotNull @Unmodifiable Map animations ) { /** * Gets a renderer group by tree * * @param name part name * @return group or null */ public @Nullable RendererGroup groupByTree(@NotNull BoneName name) { return groupByTree0(rendererGroups, name); } private static @Nullable RendererGroup groupByTree0(@NotNull Map map, @NotNull BoneName name) { if (map.isEmpty()) return null; var get = map.get(name); if (get != null) return get; else return map.values() .stream() .map(g -> groupByTree0(g.getChildren(), name)) .filter(Objects::nonNull) .findFirst() .orElse(null); } /** * Gets flatten groups. * @return flatten groups */ public @NotNull Stream flatten() { return rendererGroups.values().stream().flatMap(RendererGroup::flatten); } /** * Gets blueprint animation by name * * @param name name * @return optional animation */ public @NotNull Optional animation(@NotNull String name) { return Optional.ofNullable(animations().get(name)); } //----- Dummy ----- /** * Creates tracker by location * * @param location location * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location) { return create(location, TrackerModifier.DEFAULT); } /** * Creates tracker by location * * @param location location * @param modifier modifier * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, @NotNull TrackerModifier modifier) { return create(location, modifier, _ -> { }); } /** * Creates tracker by location * * @param location location * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { var source = RenderSource.of(location); return source.create( pipeline(source), modifier, preUpdateConsumer ); } /** * Creates tracker by location and completed profile * * @param location location * @param profile profile * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, @NotNull ModelProfile profile) { return create(location, profile.asUncompleted()); } /** * Creates tracker by location and completed profile * * @param location location * @param profile profile * @param modifier modifier * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, ModelProfile profile, @NotNull TrackerModifier modifier) { return create(location, profile.asUncompleted(), modifier); } /** * Creates tracker by location and completed profile * * @param location location * @param profile profile * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, @NotNull ModelProfile profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return create(location, profile.asUncompleted(), modifier, preUpdateConsumer); } /** * Creates tracker by location and uncompleted profile * * @param location location * @param profile profile * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, @NotNull ModelProfile.Uncompleted profile) { return create(location, profile, TrackerModifier.DEFAULT); } /** * Creates tracker by location and uncompleted profile * * @param location location * @param profile profile * @param modifier modifier * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier) { return create(location, profile, modifier, _ -> { }); } /** * Creates tracker by location and uncompleted profile * * @param location location * @param profile profile * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return empty tracker */ public @NotNull DummyTracker create(@NotNull PlatformLocation location, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { var source = RenderSource.of(location, profile); return source.create( pipeline(source), modifier, preUpdateConsumer ); } //----- Entity ----- /** * Creates tracker by entity * * @param entity entity * @return entity tracker */ public @NotNull EntityTracker create(@NotNull PlatformEntity entity) { return create(BaseEntity.of(entity)); } /** * Creates tracker by entity * * @param entity entity * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker create(@NotNull PlatformEntity entity, @NotNull TrackerModifier modifier) { return create(BaseEntity.of(entity), modifier); } /** * Creates tracker by entity * * @param entity entity * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker create(@NotNull PlatformEntity entity, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return create(BaseEntity.of(entity), modifier, preUpdateConsumer); } /** * Creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker create(@NotNull PlatformEntity entity, @NotNull ModelProfile.Uncompleted profile) { return create(BaseEntity.of(entity), profile); } /** * Creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker create(@NotNull PlatformEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier) { return create(BaseEntity.of(entity), profile, modifier); } /** * Creates tracker by entity and uncompleted profile * * @param entity entity * @param profile skin * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker create(@NotNull PlatformEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return create(BaseEntity.of(entity), profile, modifier, preUpdateConsumer); } /** * Gets or creates tracker by entity * * @param entity entity * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity) { return getOrCreate(BaseEntity.of(entity)); } /** * Gets or creates tracker by entity * * @param entity entity * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull TrackerModifier modifier) { return getOrCreate(BaseEntity.of(entity), modifier); } /** * Gets or creates tracker by entity * * @param entity entity * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return getOrCreate(BaseEntity.of(entity), modifier, preUpdateConsumer); } /** * Gets or creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull ModelProfile profile) { return getOrCreate(entity, profile.asUncompleted()); } /** * Gets or creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull ModelProfile profile, @NotNull TrackerModifier modifier) { return getOrCreate(entity, profile.asUncompleted(), modifier); } /** * Gets or creates tracker by entity and completed profile * * @param entity entity * @param profile skin * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull ModelProfile profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return getOrCreate(entity, profile.asUncompleted(), modifier, preUpdateConsumer); } /** * Gets or creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull ModelProfile.Uncompleted profile) { return getOrCreate(BaseEntity.of(entity), profile); } /** * Gets or creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier) { return getOrCreate(BaseEntity.of(entity), profile, modifier); } /** * Gets or creates tracker by entity and uncompleted profile * * @param entity entity * @param profile skin * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull PlatformEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return getOrCreate(BaseEntity.of(entity), profile, modifier, preUpdateConsumer); } /** * Creates tracker by entity * * @param entity entity * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity) { return create(entity, TrackerModifier.DEFAULT); } /** * Creates tracker by entity * * @param entity entity * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull TrackerModifier modifier) { return create(entity, modifier, _ -> { }); } /** * Creates tracker by entity * * @param entity entity * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { var source = RenderSource.of(entity); return source.create( pipeline(source), modifier, preUpdateConsumer ); } /** * Creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull ModelProfile profile) { return create(entity, profile.asUncompleted()); } /** * Creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull ModelProfile profile, @NotNull TrackerModifier modifier) { return create(entity, profile.asUncompleted(), modifier); } /** * Creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull ModelProfile profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return create(entity, profile.asUncompleted(), modifier, preUpdateConsumer); } /** * Creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull ModelProfile.Uncompleted profile) { return create(entity, profile, TrackerModifier.DEFAULT); } /** * Creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier) { return create(entity, profile, modifier, _ -> { }); } /** * Creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker create(@NotNull BaseEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { var source = RenderSource.of(entity, profile); return source.create( pipeline(source), modifier, preUpdateConsumer ); } /** * Gets or creates tracker by entity * * @param entity entity * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity) { return getOrCreate(entity, TrackerModifier.DEFAULT); } /** * Gets or creates tracker by entity * * @param entity entity * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, @NotNull TrackerModifier modifier) { return getOrCreate(entity, modifier, _ -> { }); } /** * Gets or creates tracker by entity * * @param entity entity * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { var source = RenderSource.of(entity); return source.getOrCreate( name(), () -> pipeline(source), modifier, preUpdateConsumer ); } /** * Gets or creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, @NotNull ModelProfile profile) { return getOrCreate(entity, profile.asUncompleted()); } /** * Gets or creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, ModelProfile profile, @NotNull TrackerModifier modifier) { return getOrCreate(entity, profile.asUncompleted(), modifier); } /** * Gets or creates tracker by entity and completed profile * * @param entity entity * @param profile profile * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, @NotNull ModelProfile profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return getOrCreate(entity, profile.asUncompleted(), modifier, preUpdateConsumer); } /** * Gets or creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, @NotNull ModelProfile.Uncompleted profile) { return getOrCreate(entity, profile, TrackerModifier.DEFAULT); } /** * Gets or creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @param modifier modifier * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier) { return getOrCreate(entity, profile, modifier, _ -> { }); } /** * Gets or creates tracker by entity and uncompleted profile * * @param entity entity * @param profile profile * @param modifier modifier * @param preUpdateConsumer task on pre-update * @return entity tracker */ public @NotNull EntityTracker getOrCreate(@NotNull BaseEntity entity, @NotNull ModelProfile.Uncompleted profile, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { var source = RenderSource.of(entity, profile); return source.getOrCreate( name(), () -> pipeline(source), modifier, preUpdateConsumer ); } private @NotNull RenderPipeline pipeline(@NotNull RenderSource source) { return new RenderPipeline( this, source, rendererGroups.values().stream().map(value -> value.create(source)).toArray(RenderedBone[]::new) ); } /** * Renderer type */ @RequiredArgsConstructor @Getter public enum Type { /** * General */ GENERAL(true), /** * Player */ PLAYER(false) ; private final boolean canBeSaved; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/renderer/RenderPipeline.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.renderer; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.animation.AnimationOverrideState; import kr.toxicity.model.api.animation.RunningAnimation; import kr.toxicity.model.api.bone.*; import kr.toxicity.model.api.nms.AnimationBundler; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.nms.PacketBundler; import kr.toxicity.model.api.nms.PlayerChannelHandler; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.ModelRotation; import kr.toxicity.model.api.util.FunctionUtil; import kr.toxicity.model.api.util.function.BonePredicate; import kr.toxicity.model.api.util.function.FloatSupplier; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import static kr.toxicity.model.api.util.CollectionUtil.associate; import static kr.toxicity.model.api.util.CollectionUtil.associateSequenced; /** * Represents the rendering pipeline for a specific model instance. *

* This class manages the hierarchy of {@link RenderedBone}s, handles player visibility and packet bundling, * and coordinates animation updates and inverse kinematics (IK) solving. *

* * @since 1.15.2 */ public final class RenderPipeline implements BoneEventHandler, Iterable { @Getter private final ModelRenderer parent; @Getter private final RenderSource source; private final RenderedBone[] bones; private final RenderedBone[] flattenBones; private final SequencedMap byIdMap; private final int displayAmount; private final Map playerMap = new ConcurrentHashMap<>(); private final Set hidePlayerSet = ConcurrentHashMap.newKeySet(); private final BoneEventDispatcher eventDispatcher = new BoneEventDispatcher(); private final BoneIKSolver ikSolver; private Predicate viewFilter = _ -> true; private Predicate hideFilter = p -> hidePlayerSet.contains(p.uuid()); private Consumer spawnPacketHandler = _ -> {}; private Consumer despawnPacketHandler = _ -> {}; private Consumer hidePacketHandler = _ -> {}; private Consumer showPacketHandler = _ -> {}; @Getter private ModelRotation rotation = ModelRotation.INVALID; /** * Creates a new render pipeline. * * @param parent the parent model renderer * @param source the source of the rendering (entity or location) * @param bones the array of root bones * @since 1.15.2 */ public RenderPipeline( @NotNull ModelRenderer parent, @NotNull RenderSource source, @NotNull RenderedBone[] bones ) { this.parent = parent; this.source = source; this.bones = bones; // Bone flattenBones = Arrays.stream(bones).flatMap(RenderedBone::flatten).toArray(RenderedBone[]::new); byIdMap = associateSequenced(flattenBones, RenderedBone::name); ikSolver = new BoneIKSolver(associate(flattenBones, RenderedBone::uuid)); // Setup displayAmount = (int) Arrays.stream(flattenBones) .peek(bone -> bone.extend(this)) .peek(bone -> bone.locator(ikSolver)) .filter(rb -> rb.getDisplay() != null) .count(); } /** * Creates a packet bundler for this pipeline. * * @return a new packet bundler * @since 1.15.2 */ public @NotNull PacketBundler createBundler() { return BetterModel.nms().createBundler(displayAmount + 1); } /** * Retrieves the channel handler for a specific player. * * @param uuid the UUID of the player * @return the channel handler, or null if not found * @since 1.15.2 */ public @Nullable PlayerChannelHandler channel(@NotNull UUID uuid) { var get = playerMap.get(uuid); return get != null ? get.handler : null; } /** * Creates an animation packet bundler based on configuration. * * @return a new animation packet bundler * @since 2.2.1 */ public @NotNull AnimationBundler createAnimationBundler() { var size = BetterModel.config().packetBundlingSize(); var nms = BetterModel.nms(); return new AnimationBundler( size <= 0 ? createBundler() : nms.createParallelBundler(size), nms.createModAnimationBuilder(displayAmount) ); } @Override public @NotNull BoneEventDispatcher eventDispatcher() { return eventDispatcher; } /** * Adds a filter to restrict which players can view the model. * * @param filter the predicate to filter players * @since 1.15.2 */ public void viewFilter(@NotNull Predicate filter) { this.viewFilter = this.viewFilter.and(Objects.requireNonNull(filter)); } /** * Adds a filter to determine if a player should be hidden from the model. * * @param filter the predicate to hide players * @since 1.15.2 */ public void hideFilter(@NotNull Predicate filter) { this.hideFilter = this.hideFilter.or(Objects.requireNonNull(filter)); } /** * Adds a handler for spawn packets. * * @param spawnPacketHandler the consumer to handle spawn packets * @since 1.15.2 */ public void spawnPacketHandler(@NotNull Consumer spawnPacketHandler) { this.spawnPacketHandler = this.spawnPacketHandler.andThen(Objects.requireNonNull(spawnPacketHandler)); } /** * Adds a handler for despawn packets. * * @param despawnPacketHandler the consumer to handle despawn packets * @since 1.15.2 */ public void despawnPacketHandler(@NotNull Consumer despawnPacketHandler) { this.despawnPacketHandler = this.despawnPacketHandler.andThen(Objects.requireNonNull(despawnPacketHandler)); } /** * Adds a handler for hide packets. * * @param despawnPacketHandler the consumer to handle hide packets * @since 1.15.2 */ public void hidePacketHandler(@NotNull Consumer despawnPacketHandler) { this.hidePacketHandler = this.hidePacketHandler.andThen(Objects.requireNonNull(despawnPacketHandler)); } /** * Adds a handler for show packets. * * @param despawnPacketHandler the consumer to handle show packets * @since 1.15.2 */ public void showPacketHandler(@NotNull Consumer despawnPacketHandler) { this.showPacketHandler = this.showPacketHandler.andThen(Objects.requireNonNull(despawnPacketHandler)); } /** * Checks if the model is spawned for a specific player. * * @param uuid the UUID of the player * @return true if spawned, false otherwise * @since 1.15.2 */ public boolean isSpawned(@NotNull UUID uuid) { return playerMap.containsKey(uuid); } /** * Retrieves the currently running animation, if any. * * @return the running animation, or null if none * @since 1.15.2 */ public @Nullable RunningAnimation runningAnimation() { return firstNotNull(RenderedBone::runningAnimation); } /** * Returns the name of the model. * * @return the model name * @since 1.15.2 */ public @NotNull String name() { return parent.name(); } /** * Despawns the model for all players and clears internal state. * * @since 1.15.2 */ public void despawn() { hitboxes().forEach(HitBox::removeHitBox); var bundler = createBundler(); remove0(bundler); if (bundler.isNotEmpty()) allPlayer().map(PlayerChannelHandler::player).forEach(bundler::send); playerMap.clear(); } /** * Rotates the model to a new orientation. * * @param rotation the new rotation * @param bundler the packet bundler to use * @return true if the rotation changed, false otherwise * @since 1.15.2 */ public boolean rotate(@NotNull ModelRotation rotation, @NotNull PacketBundler bundler) { if (rotation.equals(this.rotation)) return false; this.rotation = rotation; return matchTree(b -> b.rotate(rotation, bundler)); } /** * Ticks the model, updating animations and IK. * * @param bundler the packet bundler to use * @return true if any updates occurred * @since 1.15.2 */ public boolean tick(@NotNull AnimationBundler bundler) { var match = matchTree(RenderedBone::tick); if (match) { ikSolver.solve(); forEach(b -> b.sendTransformation(null, bundler)); } return match; } /** * Ticks the model for a specific player (e.g., for per-player animations). * * @param uuid the UUID of the player * @param bundler the packet bundler to use * @return true if any updates occurred * @since 1.15.2 */ public boolean tick(@NotNull UUID uuid, @NotNull AnimationBundler bundler) { var match = matchTree(b -> b.tick(uuid)); if (match) { ikSolver.solve(uuid); forEach(b -> b.sendTransformation(uuid, bundler)); } return match; } /** * Sets the default position modifier for all bones. * * @param movement the movement function * @since 1.15.2 */ public void defaultPosition(@NotNull Function movement) { var vec = new Vector3f(); var supplier = FunctionUtil.throttleTick(() -> movement.apply(vec)); forEach(b -> b.defaultPosition(supplier)); } /** * Scales the model. * * @param scale the scale supplier * @since 1.15.2 */ public void scale(@NotNull FloatSupplier scale) { forEach(b -> b.scale(scale)); } /** * Adds a local rotation modifier to matching bones. * * @param predicate the predicate to select bones * @param mapper the rotation mapping function * @return true if any bones were modified * @since 3.0.0 */ public boolean addLocalRotModifier(@NotNull BonePredicate predicate, @NotNull Function mapper) { return matchTree(predicate, (b, p) -> b.addLocalRotModifier(p, mapper)); } /** * Adds a global rotation modifier to matching bones. * * @param predicate the predicate to select bones * @param mapper the rotation mapping function * @return true if any bones were modified * @since 3.0.0 */ public boolean addGlobalRotModifier(@NotNull BonePredicate predicate, @NotNull Function mapper) { return matchTree(predicate, (b, p) -> b.addGlobalRotModifier(p, mapper)); } /** * Adds a position modifier to matching bones. * * @param predicate the predicate to select bones * @param mapper the position mapping function * @return true if any bones were modified * @since 1.15.2 */ public boolean addPositionModifier(@NotNull BonePredicate predicate, @NotNull Function mapper) { return matchTree(predicate, (b, p) -> b.addPositionModifier(p, mapper)); } /** * Returns a collection of all bones in this pipeline. * * @return the collection of bones * @since 1.15.2 */ public @NotNull @Unmodifiable Collection bones() { return byIdMap.values(); } /** * Returns a stream of all hitboxes associated with this model. * * @return the stream of hitboxes * @since 1.15.2 */ public @NotNull Stream hitboxes() { return stream() .map(RenderedBone::getHitBox) .filter(Objects::nonNull); } /** * Retrieves a bone by its name. * * @param name the name of the bone * @return the rendered bone, or null if not found * @since 1.15.2 */ public @Nullable RenderedBone boneOf(@NotNull BoneName name) { return byIdMap.get(name); } /** * Spawns the model for a player. * * @param player the player to spawn for * @param bundler the packet bundler to use * @param consumer a consumer for the spawned player object * @return true if spawned successfully * @since 1.15.2 */ @ApiStatus.Internal public boolean spawn(@NotNull PlatformPlayer player, @NotNull PacketBundler bundler, @NotNull Consumer consumer) { var get = BetterModel.platform().playerManager().player(player.uuid()); if (get == null) return false; var spawnedPlayer = new SpawnedPlayer(get); playerMap.put(player.uuid(), spawnedPlayer); spawnPacketHandler.accept(bundler); var hided = isHide(player); forEach(b -> b.spawn(hided, bundler)); consumer.accept(spawnedPlayer); return true; } /** * Removes the model for a player. * * @param player the player to remove for * @return true if removed successfully * @since 1.15.2 */ @ApiStatus.Internal public boolean remove(@NotNull PlatformPlayer player) { if (playerMap.remove(player.uuid()) == null) return false; var bundler = createBundler(); remove0(bundler); bundler.send(player); return true; } @ApiStatus.Internal private void remove0(@NotNull PacketBundler bundler) { despawnPacketHandler.accept(bundler); forEach(b -> b.remove(bundler)); } /** * Applies a mapper to bones matching a predicate. * * @param predicate the bone predicate * @param mapper the mapper function * @return true if any bones matched * @since 1.15.2 */ public boolean matchTree(@NotNull BonePredicate predicate, BiPredicate mapper) { Objects.requireNonNull(predicate); Objects.requireNonNull(mapper); if (predicate == BonePredicate.FALSE) return false; if (predicate == BonePredicate.TRUE || predicate.applyAtChildren() == BonePredicate.State.NOT_SET) return matchTree(b -> mapper.test(b, predicate)); var result = false; for (RenderedBone value : bones) { if (value.matchTree(predicate, mapper)) result = true; } return result; } /** * Applies a mapper to bones matching an animation predicate. * * @param mapper the mapper function * @return true if any bones matched * @since 1.15.2 */ public boolean matchAnimation(@NotNull BiPredicate mapper) { Objects.requireNonNull(mapper); var result = false; for (RenderedBone value : bones) { if (value.matchAnimation(AnimationOverrideState.NOT_MATCHED, mapper)) result = true; } return result; } /** * Checks if any bones match a predicate. * * @param predicate the predicate * @return true if any bones matched * @since 1.15.2 */ public boolean matchTree(@NotNull Predicate predicate) { Objects.requireNonNull(predicate); var result = false; for (RenderedBone value : flattenBones) { if (predicate.test(value)) result = true; } return result; } /** * Finds the first non-null result of applying a mapper to all bones. * * @param mapper the mapper function * @param the result type * @return the first non-null result, or null * @since 1.15.2 */ public @Nullable T firstNotNull(@NotNull Function mapper) { Objects.requireNonNull(mapper); for (RenderedBone value : flattenBones) { var t = mapper.apply(value); if (t != null) return t; } return null; } /** * Returns the number of players currently viewing this model. * * @return the player count * @since 1.15.2 */ public int playerCount() { return playerMap.size(); } /** * Returns a stream of all players viewing this model. * * @return the stream of players * @since 1.15.2 */ public @NotNull Stream allPlayer() { return playerMap.values() .stream() .map(spawned -> spawned.handler); } /** * Returns a stream of players who are not hidden and pass the view filter. * * @return the stream of visible players * @since 1.15.2 */ public @NotNull Stream nonHidePlayer() { return playerMap.values() .stream() .filter(spawned -> spawned.initialLoad) .map(spawned -> spawned.handler) .filter(p -> !hideFilter.test(p.player())); } /** * Returns a stream of players who pass the view filter (regardless of hidden status). * * @return the stream of viewed players * @since 1.15.2 */ public @NotNull Stream viewedPlayer() { return allPlayer().filter(channel -> viewFilter.test(channel.player())); } /** * Hides the model from a specific player. * * @param player the player to hide from * @return true if the player was successfully hidden * @since 1.15.2 */ public boolean hide(@NotNull PlatformPlayer player) { if (isHide(player) || !hidePlayerSet.add(player.uuid())) return false; if (isSpawned(player.uuid())) { var bundler = createBundler(); forEach(b -> b.forceUpdate(false, bundler)); hidePacketHandler.accept(bundler); if (bundler.isNotEmpty()) bundler.send(player); } player.task(() -> hitboxes().forEach(hb -> hb.hide(player))); return true; } /** * Checks if the model is hidden from a specific player. * * @param player the player to check * @return true if hidden * @since 1.15.2 */ public boolean isHide(@NotNull PlatformPlayer player) { return hideFilter.test(player); } /** * Shows the model to a specific player (if previously hidden). * * @param player the player to show to * @return true if the player was successfully shown * @since 1.15.2 */ public boolean show(@NotNull PlatformPlayer player) { if (!isHide(player) || !hidePlayerSet.remove(player.uuid())) return false; if (isSpawned(player.uuid())) { var bundler = createBundler(); forEach(b -> b.forceUpdate(true, bundler)); showPacketHandler.accept(bundler); if (bundler.isNotEmpty()) bundler.send(player); } player.task(() -> hitboxes().forEach(hb -> hb.show(player))); return true; } @Override public void forEach(@NotNull Consumer action) { for (RenderedBone bone : flattenBones) { action.accept(bone); } } @Override @NotNull public Iterator iterator() { return bones().iterator(); } @Override @NotNull public Spliterator spliterator() { return Arrays.spliterator(flattenBones); } /** * Returns a sequential {@code Stream} with the flattened bones as its source. * * @return a stream of all bones in this pipeline * @since 2.2.0 */ public @NotNull Stream stream() { return Arrays.stream(flattenBones); } /** * Represents a player for whom the model has been spawned. * * @since 1.15.2 */ @RequiredArgsConstructor public class SpawnedPlayer { private final PlayerChannelHandler handler; private boolean initialLoad; /** * Loads the model for this player, sending initial packets. * * @since 1.15.2 */ public void load() { initialLoad = true; if (isHide(handler.player())) return; var b = createBundler(); forEach(bone -> bone.forceUpdate(b)); if (b.isNotEmpty()) b.send(handler.player()); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/renderer/RenderSource.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.renderer; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.armor.PlayerArmor; import kr.toxicity.model.api.bone.BoneRenderContext; import kr.toxicity.model.api.nms.Profiled; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.player.PlayerSkinParts; import kr.toxicity.model.api.profile.ModelProfile; import kr.toxicity.model.api.tracker.*; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Supplier; /** * Represents a source for rendering models, providing necessary context such as location and entity data. *

* This interface serves as the entry point for creating {@link Tracker} instances, which manage the lifecycle of a rendered model. * It supports both entity-based and location-based (dummy) rendering sources. *

* * @param the type of tracker created by this source * @since 1.15.2 */ public sealed interface RenderSource { /** * Creates a dummy render source at the specified location. * * @param location the location where the model will be rendered * @return a new dummy render source * @since 1.15.2 */ @ApiStatus.Internal static @NotNull RenderSource.Dummy of(@NotNull PlatformLocation location) { return new BaseDummy(location); } /** * Creates a dummy render source at the specified location with a specific model profile. * * @param location the location where the model will be rendered * @param profile the uncompleted model profile to use * @return a new profiled dummy render source * @since 1.15.2 */ @ApiStatus.Internal static @NotNull RenderSource.Dummy of(@NotNull PlatformLocation location, @NotNull ModelProfile.Uncompleted profile) { return new ProfiledDummy(location, profile); } /** * Creates an entity render source for the given entity with a specific model profile. * * @param entity the entity to attach the model to * @param profile the uncompleted model profile to use * @return a new entity render source * @since 1.15.2 */ @ApiStatus.Internal static @NotNull RenderSource.Entity of(@NotNull kr.toxicity.model.api.entity.BaseEntity entity, @NotNull ModelProfile.Uncompleted profile) { return entity instanceof kr.toxicity.model.api.entity.BasePlayer player ? new ProfiledPlayer(player, profile) : new ProfiledEntity(entity, profile); } /** * Creates an entity render source for the given entity. * * @param entity the entity to attach the model to * @return a new entity render source * @since 1.15.2 */ @ApiStatus.Internal static @NotNull RenderSource.Entity of(@NotNull kr.toxicity.model.api.entity.BaseEntity entity) { return entity instanceof kr.toxicity.model.api.entity.BasePlayer player ? new BasePlayer(player) : new BaseEntity(entity); } /** * Returns the location of this render source. * * @return the location * @since 1.15.2 */ @NotNull PlatformLocation location(); /** * Creates a new tracker for this render source. * * @param pipeline the render pipeline to use * @param modifier the tracker modifier * @param preUpdateConsumer a consumer to run before updates * @return the created tracker * @since 1.15.2 */ T create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer); /** * Asynchronously completes the bone render context for this source. *

* This method may involve fetching skin data or other resources. *

* * @return a future completing with the bone render context * @since 1.15.2 */ @NotNull CompletableFuture completeContext(); /** * Returns a fallback bone render context. *

* This context is used when the complete context cannot be resolved or is not yet available. *

* * @return the fallback bone render context * @since 1.15.2 */ default BoneRenderContext fallbackContext() { return new BoneRenderContext(this); } /** * Represents a render source attached to an entity. * * @since 1.15.2 */ sealed interface Entity extends RenderSource { /** * Returns the entity associated with this source. * * @return the entity * @since 1.15.2 */ @NotNull kr.toxicity.model.api.entity.BaseEntity entity(); /** * Gets or creates an entity tracker for this source. * * @param name the name of the tracker * @param supplier a supplier for the render pipeline * @param modifier the tracker modifier * @param preUpdateConsumer a consumer to run before updates * @return the entity tracker * @since 1.15.2 */ @NotNull EntityTracker getOrCreate(@NotNull String name, @NotNull Supplier supplier, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer); } /** * Represents a render source at a fixed location, not attached to an entity. * * @since 1.15.2 */ sealed interface Dummy extends RenderSource { } /** * A basic implementation of {@link Dummy} with a location. * * @param location the location * @since 1.15.2 */ record BaseDummy(@NotNull PlatformLocation location) implements Dummy { @NotNull @Override public DummyTracker create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return new DummyTracker(location, pipeline, modifier, preUpdateConsumer); } @Override public @NotNull CompletableFuture completeContext() { return CompletableFuture.completedFuture(fallbackContext()); } } /** * A profiled implementation of {@link Dummy} with a location and a model profile. * * @param location the location * @param profile the model profile * @since 1.15.2 */ record ProfiledDummy(@NotNull PlatformLocation location, @NotNull ModelProfile.Uncompleted profile) implements Dummy { @NotNull @Override public DummyTracker create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return new DummyTracker(location, pipeline, modifier, preUpdateConsumer); } @Override public @NotNull CompletableFuture completeContext() { return BetterModel.platform().skinManager().complete(profile).thenApply(skin -> new BoneRenderContext(this, skin)); } } /** * A basic implementation of {@link Entity} wrapping a {@link kr.toxicity.model.api.entity.BaseEntity}. * * @param entity the entity * @since 1.15.2 */ record BaseEntity(@NotNull kr.toxicity.model.api.entity.BaseEntity entity) implements Entity { @NotNull @Override public EntityTracker create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).create(pipeline.name(), r -> new EntityTracker(r, pipeline, modifier, preUpdateConsumer)); } @Override public @NotNull EntityTracker getOrCreate(@NotNull String name, @NotNull Supplier supplier, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).getOrCreate(name, r -> new EntityTracker(r, supplier.get(), modifier, preUpdateConsumer)); } @Override public @NotNull PlatformLocation location() { return entity.location(); } @Override public @NotNull CompletableFuture completeContext() { return CompletableFuture.completedFuture(fallbackContext()); } } /** * A profiled implementation of {@link Entity} wrapping a {@link kr.toxicity.model.api.entity.BaseEntity} and a model profile. * * @param entity the entity * @param profile the model profile * @since 1.15.2 */ record ProfiledEntity(@NotNull kr.toxicity.model.api.entity.BaseEntity entity, @NotNull ModelProfile.Uncompleted profile) implements Entity { @NotNull @Override public EntityTracker create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).create(pipeline.name(), r -> new EntityTracker(r, pipeline, modifier, preUpdateConsumer)); } @Override public @NotNull EntityTracker getOrCreate(@NotNull String name, @NotNull Supplier supplier, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).getOrCreate(name, r -> new EntityTracker(r, supplier.get(), modifier, preUpdateConsumer)); } @Override public @NotNull PlatformLocation location() { return entity.location(); } @Override public @NotNull CompletableFuture completeContext() { return BetterModel.platform().skinManager().complete(profile).thenApply(skin -> new BoneRenderContext(this, skin)); } } /** * A basic implementation of {@link Entity} wrapping a {@link kr.toxicity.model.api.entity.BasePlayer}. * * @param entity the player entity * @since 1.15.2 */ record BasePlayer(@NotNull kr.toxicity.model.api.entity.BasePlayer entity) implements Entity, Profiled { @NotNull @Override public EntityTracker create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).create(pipeline.name(), r -> new PlayerTracker(r, pipeline, modifier, preUpdateConsumer)); } @Override public @NotNull EntityTracker getOrCreate(@NotNull String name, @NotNull Supplier supplier, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).getOrCreate(name, r -> new PlayerTracker(r, supplier.get(), modifier, preUpdateConsumer)); } @Override public @NotNull PlatformLocation location() { return entity.location(); } @Override public @NotNull CompletableFuture completeContext() { return BetterModel.platform().skinManager().complete(profile().asUncompleted()).thenApply(skin -> new BoneRenderContext(this, skin)); } @Override public @NotNull ModelProfile profile() { return entity.profile(); } @Override public @NotNull PlayerArmor armors() { return entity.armors(); } @Override public @NotNull PlayerSkinParts skinParts() { return entity.skinParts(); } } /** * A profiled implementation of {@link Entity} wrapping a {@link kr.toxicity.model.api.entity.BasePlayer} and a model profile. * * @param entity the player entity * @param externalProfile the external model profile * @since 1.15.2 */ record ProfiledPlayer(@NotNull kr.toxicity.model.api.entity.BasePlayer entity, @NotNull ModelProfile.Uncompleted externalProfile) implements Entity, Profiled { @NotNull @Override public EntityTracker create(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).create(pipeline.name(), r -> new PlayerTracker(r, pipeline, modifier, preUpdateConsumer)); } @Override public @NotNull EntityTracker getOrCreate(@NotNull String name, @NotNull Supplier supplier, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { return EntityTrackerRegistry.getOrCreate(entity).getOrCreate(name, r -> new PlayerTracker(r, supplier.get(), modifier, preUpdateConsumer)); } @Override public @NotNull PlatformLocation location() { return entity.location(); } @Override public @NotNull CompletableFuture completeContext() { return BetterModel.platform().skinManager().complete(externalProfile).thenApply(skin -> new BoneRenderContext(this, skin)); } @Override public @NotNull ModelProfile profile() { return entity.profile(); } @Override public @NotNull PlayerArmor armors() { return entity.armors(); } @Override public @NotNull PlayerSkinParts skinParts() { return entity.skinParts(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/data/renderer/RendererGroup.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.data.renderer; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.bone.*; import kr.toxicity.model.api.data.blueprint.BlueprintElement; import kr.toxicity.model.api.data.blueprint.ModelBoundingBox; import kr.toxicity.model.api.mount.MountController; import kr.toxicity.model.api.mount.MountControllers; import kr.toxicity.model.api.platform.PlatformItemStack; import kr.toxicity.model.api.util.MathUtil; import kr.toxicity.model.api.util.TransformedItemStack; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.joml.Vector3f; import java.util.SequencedMap; import java.util.UUID; import java.util.stream.Stream; /** * A group of models. */ @RequiredArgsConstructor public final class RendererGroup { private static final Vector3f DEFAULT_SCALE = new Vector3f(1); @Getter private final BlueprintElement.Bone parent; @Getter private final Vector3f position; private final Vector3f rotation; private final TransformedItemStack itemStack; @Getter @Unmodifiable private final SequencedMap children; @Getter private final @Nullable ModelBoundingBox hitBox; @Getter private final @NotNull Vector3f hitBoxPoint; @Getter private final @NotNull BoneItemMapper itemMapper; @Getter private final @NotNull MountController mountController; /** * Creates group instance. * @param scale scale * @param itemStack item * @param group parent * @param children children * @param box hit-box */ public RendererGroup( float scale, @Nullable PlatformItemStack itemStack, @NotNull BlueprintElement.Bone group, @NotNull SequencedMap children, @Nullable ModelBoundingBox box ) { this.parent = group; this.children = children; this.itemStack = TransformedItemStack.of( new Vector3f(), new Vector3f(), new Vector3f(scale), itemStack != null ? itemStack : BetterModel.platform().adapter().air() ); this.itemMapper = name().toItemMapper(); this.position = group.origin().toBlockScale().toVector(); this.hitBox = box; this.hitBoxPoint = box == null ? new Vector3f() : box.centerPoint(); this.rotation = group.rotation().toVector(); if (name().tagged(BoneTags.SEAT)) { mountController = BetterModel.config().defaultMountController(); } else if (name().tagged(BoneTags.SUB_SEAT)) { mountController = MountControllers.NONE; } else mountController = MountControllers.INVALID; } public @NotNull Stream flatten() { return children.isEmpty() ? Stream.of(this) : Stream.concat( Stream.of(this), children.values().stream().flatMap(RendererGroup::flatten) ); } /** * Gets name * @return name */ public @NotNull BoneName name() { return parent.name(); } /** * Gets uuid * @return uuid */ public @NotNull UUID uuid() { return parent.uuid(); } /** * Creates entity. * @param source source * @return entity */ public @NotNull RenderedBone create(@NotNull RenderSource source) { return create(source.fallbackContext(), null); } private @NotNull RenderedBone create(@NotNull BoneRenderContext context, @Nullable RenderedBone parentBone) { return new RenderedBone( this, parentBone, context, new BoneMovement( parentBone != null ? position.sub(parentBone.getGroup().position, new Vector3f()) : new Vector3f(), DEFAULT_SCALE, MathUtil.toQuaternion(rotation), rotation ), parent -> children.values().stream().map(value -> value.create(context, parent)).toArray(RenderedBone[]::new) ); } /** * Gets display item. * @return item */ public @NotNull TransformedItemStack getItemStack() { return itemStack.copy(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof RendererGroup that)) return false; return uuid().equals(that.uuid()); } @Override public int hashCode() { return uuid().hashCode(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/entity/BaseEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.entity; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.nms.Identifiable; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.EntityTrackerRegistry; import kr.toxicity.model.api.util.TransformedItemStack; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; import java.util.Optional; import java.util.stream.Stream; /** * An adapter of entity */ public interface BaseEntity extends Identifiable { /** * Gets base entity * @param entity platform entity * @return base entity */ static @NotNull BaseEntity of(@NotNull PlatformEntity entity) { if (entity instanceof PlatformPlayer player) { var channel = BetterModel.platform().playerManager().player(player.uuid()); return channel != null ? channel.base() : BetterModel.nms().adapt(player); } return BetterModel.nms().adapt(entity); } /** * Gets the platform-specific entity object. * @since 2.0.0 * @return The platform entity. */ @NotNull PlatformEntity platform(); /** * Gets the current location of the entity. * @since 2.0.0 * @return The entity's location. */ default @NotNull PlatformLocation location() { return platform().location(); } /** * Gets custom name of this entity * @return custom name */ @Nullable Component customName(); /** * Gets vanilla entity * @return vanilla entity */ @NotNull Object handle(); /** * Gets entity id * @return entity id */ int id(); /** * Checks source entity is dead * @return dead */ boolean dead(); /** * Checks source entity is on the ground * @return on the ground */ boolean ground(); /** * Checks source entity is invisible * @return invisible */ boolean invisible(); /** * Check source entity is on a glow * @return glow */ boolean glow(); /** * Check source entity is on a walk * @return walk */ boolean onWalk(); /** * Check source entity is on the fly * @return fly */ boolean fly(); /** * Gets entity's scale * @return scale */ double scale(); /** * Gets entity's pitch (x-rot) * @return pitch */ float pitch(); /** * Gets entity's body yaw (y-rot) * @return body yaw */ float bodyYaw(); /** * Gets entity's yaw (y-rot) * @return yaw */ float yaw(); /** * Gets entity's head yaw (y-rot) * @return head yaw */ float headYaw(); /** * Gets entity's damage tick * @return damage tick */ float damageTick(); /** * Gets entity's walk speed * @return walk speed */ float walkSpeed(); /** * Gets entity's passenger point * @param dest destination vector * @return passenger point */ @NotNull Vector3f passengerPosition(@NotNull Vector3f dest); /** * Gets tracked player set * @return tracked player set */ @NotNull Stream trackedBy(); /** * Gets main hand item * @return main hand */ @NotNull TransformedItemStack mainHand(); /** * Gets offhand item * @return offhand */ @NotNull TransformedItemStack offHand(); /** * Gets tracker registry of this adapter * @return optional tracker registry */ default @NotNull Optional registry() { return BetterModel.registry(uuid()); } /** * Checks this entity has controlling passenger * @return has controlling passenger */ default boolean hasControllingPassenger() { var registry = registry().orElse(null); return registry != null && registry.hasControllingPassenger(); } /** * Checks this entity has model data * @return has model data */ default boolean hasModelData() { return modelData() != null; } /** * Gets this entity's model data * @return model data */ @Nullable String modelData(); /** * Sets this entity's model data * @param modelData model data */ void modelData(@Nullable String modelData); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/entity/BasePlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.entity; import kr.toxicity.model.api.nms.Profiled; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; /** * An adapter of player */ public interface BasePlayer extends BaseEntity, Profiled { /** * Updates current inventory */ void updateInventory(); @Override @NotNull PlatformPlayer platform(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/AnimationSignalEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; /** * Triggered when an animation script emits a signal. *

* This event allows plugins/mods to react to custom signals defined within BlockBench animations. *

* * @param player the player associated with the animation * @param signal the signal * @since 2.0.0 */ public record AnimationSignalEvent( @NotNull PlatformPlayer player, @NotNull String signal ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/CancellableEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; /** * Represents an event that can be canceled. *

* Cancelling an event typically prevents the associated action from completing. *

* * @since 2.0.0 */ public interface CancellableEvent extends ModelEvent { /** * Checks if the event has been canceled. * * @return true if canceled, false otherwise * @since 2.0.0 */ boolean isCancelled(); /** * Sets the cancellation state of the event. * * @param cancel true to cancel the event, false to allow it * @since 2.0.0 */ void setCancelled(boolean cancel); @Override default boolean call() { return ModelEvent.super.call() && !isCancelled(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/CloseTrackerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.NotNull; /** * Triggered when a tracker is closed. *

* This event provides information about the tracker being closed and the reason for closure. *

* * @param tracker the tracker being closed * @param reason the reason for closing the tracker * @since 2.0.0 */ public record CloseTrackerEvent( @NotNull Tracker tracker, @NotNull Tracker.CloseReason reason ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/CreateDummyTrackerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.tracker.DummyTracker; import org.jetbrains.annotations.NotNull; /** * Triggered when a new {@link DummyTracker} is created. *

* This event allows plugins/mods to perform initialization or tracking logic for dummy trackers. *

* * @param tracker the newly created dummy tracker * @since 2.0.0 */ public record CreateDummyTrackerEvent( @NotNull DummyTracker tracker ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/CreateEntityTrackerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.tracker.EntityTracker; import org.jetbrains.annotations.NotNull; /** * Triggered when a new {@link EntityTracker} is created. *

* This event allows plugins/mods to perform initialization or tracking logic for entity trackers. *

* * @param tracker the newly created entity tracker * @since 2.0.0 */ public record CreateEntityTrackerEvent( @NotNull EntityTracker tracker ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/CreatePlayerSkinEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.profile.ModelProfile; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when a player's skin data is created or loaded. *

* This event allows modifying the player's model profile before it is used. *

* * @since 2.0.0 */ @Getter @Setter public final class CreatePlayerSkinEvent implements ModelEvent { private ModelProfile modelProfile; /** * Creates a new CreatePlayerSkinEvent. * * @param modelProfile the model profile being created * @since 2.0.0 */ @ApiStatus.Internal public CreatePlayerSkinEvent(@NotNull ModelProfile modelProfile) { this.modelProfile = modelProfile; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/DismountModelEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.tracker.EntityTracker; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when an entity dismounts from a model's hitbox. *

* This event allows plugins/mods to intercept and potentially cancel the dismounting process. *

* * @since 2.0.0 */ public final class DismountModelEvent implements CancellableEvent { private final EntityTracker tracker; private final RenderedBone bone; private final HitBox hitbox; private final PlatformEntity entity; @Getter @Setter private boolean cancelled; /** * Creates a new DismountModelEvent. * * @param tracker the entity tracker associated with the model * @param bone the bone associated with the hitbox * @param hitbox the hitbox being dismounted * @param entity the entity dismounting * @since 2.0.0 */ @ApiStatus.Internal public DismountModelEvent(@NotNull EntityTracker tracker, @NotNull RenderedBone bone, @NotNull HitBox hitbox, @NotNull PlatformEntity entity) { this.tracker = tracker; this.bone = bone; this.hitbox = hitbox; this.entity = entity; } /** * Returns the entity tracker associated with the model. * * @return the entity tracker * @since 2.0.0 */ public @NotNull EntityTracker tracker() { return tracker; } /** * Returns the bone associated with the hitbox. * * @return the rendered bone * @since 2.0.0 */ public @NotNull RenderedBone bone() { return bone; } /** * Returns the hitbox being dismounted. * * @return the hitbox * @since 2.0.0 */ public @NotNull HitBox hitbox() { return hitbox; } /** * Returns the entity dismounting the hitbox. * * @return the passenger entity * @since 2.0.0 */ public PlatformEntity entity() { return entity; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelAssetsEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.data.ModelAsset; import kr.toxicity.model.api.data.renderer.ModelRenderer; import org.jetbrains.annotations.NotNull; import java.util.Set; /** * Triggered when model assets are being gathered. *

* This event allows plugins to register custom {@link ModelAsset}s to be loaded by the engine. *

* * @param type the renderer type * @param assets the set of assets to be loaded * @since 2.0.0 */ public record ModelAssetsEvent(@NotNull ModelRenderer.Type type, @NotNull Set assets) implements ModelEvent { /** * Adds a new model asset to the loading queue. * * @param asset the asset to add * @throws IllegalArgumentException if an asset with the same name already exists * @since 2.0.0 */ public void addAsset(@NotNull ModelAsset asset) { if (!assets.add(asset)) throw new IllegalArgumentException("Asset " + asset.name() + " already exists."); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelDamageSource.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformLocation; import org.jetbrains.annotations.Nullable; /** * Represents the source of damage inflicted on a model's hitbox. *

* This interface abstracts the platform-specific damage source details. *

* * @since 2.0.0 */ public interface ModelDamageSource { /** * Returns the entity that caused the damage (e.g., the shooter of an arrow). * * @return the causing entity, or null if none * @see kr.toxicity.model.api.platform.PlatformLivingEntity * @since 2.0.0 */ @Nullable PlatformEntity getCausingEntity(); /** * Returns the entity that directly inflicted the damage (e.g., the arrow itself). * * @return the direct entity, or null if none * @since 2.0.0 */ @Nullable PlatformEntity getDirectEntity(); /** * Returns the location where the damage occurred. * * @return the damage location, or null if unknown * @since 2.0.0 */ @Nullable PlatformLocation getDamageLocation(); /** * Returns the location of the damage source. * * @return the source location, or null if unknown * @since 2.0.0 */ @Nullable PlatformLocation getSourceLocation(); /** * Checks if the damage was indirect (e.g., projectile). * * @return true if indirect, false otherwise * @since 2.0.0 */ boolean isIndirect(); /** * Returns the amount of food exhaustion caused by this damage. * * @return the food exhaustion * @since 2.0.0 */ float getFoodExhaustion(); /** * Checks if this damage should be scaled based on difficulty. * * @return true if scalable, false otherwise * @since 2.0.0 */ boolean scalesWithDifficulty(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelDespawnAtPlayerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.NotNull; /** * Triggered when a model tracker is despawned for a specific player. *

* This event notifies plugins/mods that a model is no longer visible to a player. *

* * @param player the player for whom the model is despawning * @param tracker the tracker being despawned * @since 2.0.0 */ public record ModelDespawnAtPlayerEvent( @NotNull PlatformPlayer player, @NotNull Tracker tracker ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.BetterModel; /** * Represents a base event in the BetterModel system. *

* All events related to model lifecycle, interaction, and animation implement this interface. * Events can be dispatched using the {@link #call()} method. *

* * @since 2.0.0 */ public interface ModelEvent { /** * Dispatches this event to the global event bus. * * @return the event is successfully triggered * @since 2.0.0 */ default boolean call() { return BetterModel.eventBus().call(this).triggered(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelEventApplication.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; /** * Represents an application or plugin that subscribes to model events. *

* This interface is used to check if the subscribing application is still enabled, * allowing the event bus to automatically unregister listeners from disabled plugins. *

* * @since 2.0.0 */ public interface ModelEventApplication { /** * Checks if the application is currently enabled. * * @return true if enabled, false otherwise * @since 2.0.0 */ boolean isEnabled(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelEventListener.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; /** * Represents a listener for model events. *

* This interface provides a mechanism to unregister the listener when it is no longer needed. *

* * @since 2.0.0 */ public interface ModelEventListener { /** * A no-op listener implementation. * @since 2.0.0 */ ModelEventListener NONE = () -> {}; /** * Unregisters this listener, stopping it from receiving further events. * * @since 2.0.0 */ void unregister(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelImportedEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.data.blueprint.ModelBlueprint; import kr.toxicity.model.api.data.renderer.ModelRenderer; import org.jetbrains.annotations.NotNull; /** * Triggered when a model is successfully imported and registered. *

* This event provides access to the raw blueprint and the created renderer. *

* * @param blueprint the model blueprint * @param renderer the model renderer * @since 2.0.0 */ public record ModelImportedEvent( @NotNull ModelBlueprint blueprint, @NotNull ModelRenderer renderer ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/ModelSpawnAtPlayerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.Tracker; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when a model tracker is about to be spawned for a specific player. *

* This event allows preventing the model from spawning for that player. *

* * @since 2.0.0 */ @Getter @Setter public final class ModelSpawnAtPlayerEvent implements CancellableEvent { private final Tracker tracker; private final PlatformPlayer player; private boolean cancelled; /** * Creates a new ModelSpawnAtPlayerEvent. * * @param player the player for whom the model is spawning * @param tracker the tracker being spawned * @since 2.0.0 */ @ApiStatus.Internal public ModelSpawnAtPlayerEvent(@NotNull PlatformPlayer player, @NotNull Tracker tracker) { this.tracker = tracker; this.player = player; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/MountModelEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.tracker.EntityTracker; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when an entity mounts a model's hitbox. *

* This event allows plugins/mods to intercept and potentially cancel the mounting process. *

* * @since 2.0.0 */ public final class MountModelEvent implements CancellableEvent { private final EntityTracker tracker; private final RenderedBone bone; private final HitBox hitBox; private final PlatformEntity entity; @Getter @Setter private boolean cancelled; /** * Creates a new MountModelEvent. * * @param tracker the entity tracker associated with the model * @param bone the bone associated with the hitbox * @param hitBox the hitbox being mounted * @param entity the entity attempting to mount * @since 2.0.0 */ @ApiStatus.Internal public MountModelEvent(@NotNull EntityTracker tracker, @NotNull RenderedBone bone, @NotNull HitBox hitBox, @NotNull PlatformEntity entity) { this.tracker = tracker; this.bone = bone; this.hitBox = hitBox; this.entity = entity; } /** * Returns the entity tracker associated with the model. * * @return the entity tracker * @since 2.0.0 */ public @NotNull EntityTracker tracker() { return tracker; } /** * Returns the bone associated with the hitbox. * * @return the rendered bone * @since 2.0.0 */ public @NotNull RenderedBone bone() { return bone; } /** * Returns the hitbox being mounted. * * @return the hitbox * @since 2.0.0 */ public @NotNull HitBox hitbox() { return hitBox; } /** * Returns the entity attempting to mount the hitbox. * * @return the passenger entity * @since 2.0.0 */ public PlatformEntity entity() { return entity; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/PlayerHideTrackerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.Tracker; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when a tracker is about to be hidden from a specific player. *

* This event allows preventing the tracker from being hidden. *

* * @since 2.0.0 */ @Getter @Setter public final class PlayerHideTrackerEvent implements CancellableEvent { private final Tracker tracker; private final PlatformPlayer player; private boolean cancelled; /** * Creates a new PlayerHideTrackerEvent. * * @param tracker the tracker being hidden * @param player the player from whom the tracker is being hidden * @since 2.0.0 */ @ApiStatus.Internal public PlayerHideTrackerEvent(@NotNull Tracker tracker, @NotNull PlatformPlayer player) { this.tracker = tracker; this.player = player; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/PlayerPerAnimationEndEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.NotNull; /** * Triggered when a per-player animation sequence ends. *

* This event signifies that a specific animation playing only for one player has finished. *

* * @param tracker the tracker playing the animation * @param player the player who viewed the animation * @since 2.0.0 */ public record PlayerPerAnimationEndEvent( @NotNull Tracker tracker, @NotNull PlatformPlayer player ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/PlayerPerAnimationStartEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.NotNull; /** * Triggered when a per-player animation sequence starts. *

* This event signifies that a specific animation is beginning to play for only one player. *

* * @param tracker the tracker playing the animation * @param player the player viewing the animation * @since 2.0.0 */ public record PlayerPerAnimationStartEvent( @NotNull Tracker tracker, @NotNull PlatformPlayer player ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/PlayerShowTrackerEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.Tracker; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when a tracker is about to be shown to a specific player. *

* This event allows preventing the tracker from being shown. *

* * @since 2.0.0 */ @Getter @Setter public final class PlayerShowTrackerEvent implements CancellableEvent { private final Tracker tracker; private final PlatformPlayer player; private boolean cancelled; /** * Creates a new PlayerShowTrackerEvent. * * @param tracker the tracker being shown * @param player the player to whom the tracker is being shown * @since 2.0.0 */ @ApiStatus.Internal public PlayerShowTrackerEvent(@NotNull Tracker tracker, @NotNull PlatformPlayer player) { this.tracker = tracker; this.player = player; } /** * Returns the tracker being shown. * * @return the tracker * @since 2.0.0 */ public @NotNull Tracker tracker() { return tracker; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/PluginEndReloadEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.BetterModelPlatform; import org.jetbrains.annotations.NotNull; /** * Triggered when the BetterModel platform finishes reloading. *

* This event provides the result of the reload operation. *

* * @param result the result of the reload * @since 2.0.0 */ public record PluginEndReloadEvent( @NotNull BetterModelPlatform.ReloadResult result ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/PluginStartReloadEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.pack.PackZipper; import org.jetbrains.annotations.NotNull; /** * Triggered when the BetterModel platform starts reloading. *

* This event provides access to the {@link PackZipper}, allowing other plugins/mods to inject custom assets * into the resource pack before it is generated. *

* * @param zipper the pack zipper for adding assets * @since 2.0.0 */ public record PluginStartReloadEvent( @NotNull PackZipper zipper ) implements ModelEvent { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/RemovePlayerSkinEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event; import kr.toxicity.model.api.profile.ModelProfile; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.NotNull; /** * Triggered when a player's skin data is about to be removed from the cache. *

* This event allows cancelling the removal to keep the skin data cached. *

* * @since 2.0.0 */ @Getter @Setter public final class RemovePlayerSkinEvent implements CancellableEvent { private final ModelProfile modelProfile; private boolean cancelled; /** * Creates a new RemovePlayerSkinEvent. * * @param modelProfile the model profile being removed * @since 2.0.0 */ public RemovePlayerSkinEvent(@NotNull ModelProfile modelProfile) { this.modelProfile = modelProfile; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxCreateEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.nms.HitBox; import org.jetbrains.annotations.NotNull; /** * Triggered when a hitbox is created. * * @param hitBox created hitbox * @since 2.1.0 */ public record HitBoxCreateEvent(@NotNull HitBox hitBox) implements HitBoxEvent { /** * Returns the created hitbox. * * @return created hitbox * @since 2.2.0 */ @Override public @NotNull HitBox getHitBox() { return hitBox; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxDamagedEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.event.CancellableEvent; import kr.toxicity.model.api.event.ModelDamageSource; import kr.toxicity.model.api.nms.HitBox; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Triggered when a model's hitbox is damaged. *

* This event allows modifying the damage amount or cancelling the damage entirely. *

* * @since 2.0.0 */ @Getter @Setter public final class HitBoxDamagedEvent implements CancellableEvent, HitBoxEvent { private final @NotNull HitBox hitBox; private final ModelDamageSource source; private float damage; private boolean cancelled; /** * Creates a new ModelDamagedEvent. * * @param hitBox the hitbox being damaged * @param source the source of the damage * @param damage the amount of damage * @since 2.0.0 */ @ApiStatus.Internal public HitBoxDamagedEvent(@NotNull HitBox hitBox, @NotNull ModelDamageSource source, float damage) { this.hitBox = hitBox; this.source = source; this.damage = damage; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxDismountEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.platform.PlatformEntity; import org.jetbrains.annotations.NotNull; /** * An event called when an entity dismounts from a hit box. * * @param hitBox the hit box * @param entity the entity * @since 2.1.0 */ public record HitBoxDismountEvent(@NotNull HitBox hitBox, @NotNull PlatformEntity entity) implements HitBoxEvent { @Override public @NotNull HitBox getHitBox() { return hitBox; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.event.ModelEvent; import kr.toxicity.model.api.nms.HitBox; import org.jetbrains.annotations.NotNull; /** * Base contract for events associated with a {@link HitBox}. * * @since 2.1.0 */ public interface HitBoxEvent extends ModelEvent { /** * Returns the target hitbox of this event. * * @return target hitbox * @since 2.1.0 */ @NotNull HitBox getHitBox(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxInteractAtEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.event.CancellableEvent; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.nms.ModelInteractionHand; import kr.toxicity.model.api.platform.PlatformPlayer; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; /** * Triggered when a player interacts with a model's hitbox at some position. *

* This event corresponds to a right-click interaction. *

* * @since 2.0.0 */ @Getter public final class HitBoxInteractAtEvent implements CancellableEvent, HitBoxEvent { @Setter private boolean cancelled; private final PlatformPlayer who; private final @NotNull HitBox hitBox; private final @NotNull ModelInteractionHand hand; private final @NotNull Vector3f position; /** * Creates a new HitBoxInteractAtEvent. * * @param who the player interacting * @param hitBox the hitbox being interacted with * @param hand the hand used for interaction * @param position position * @since 2.0.0 */ @ApiStatus.Internal public HitBoxInteractAtEvent(@NotNull PlatformPlayer who, @NotNull HitBox hitBox, @NotNull ModelInteractionHand hand, @NotNull Vector3f position) { this.who = who; this.hitBox = hitBox; this.hand = hand; this.position = position; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxMountEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.platform.PlatformEntity; import org.jetbrains.annotations.NotNull; /** * An event called when an entity mounts to a hit box. * * @param hitBox the hit box * @param entity the entity * @since 2.1.0 */ public record HitBoxMountEvent(@NotNull HitBox hitBox, @NotNull PlatformEntity entity) implements HitBoxEvent { @Override public @NotNull HitBox getHitBox() { return hitBox; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/event/hitbox/HitBoxRemoveEvent.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.event.hitbox; import kr.toxicity.model.api.nms.HitBox; import org.jetbrains.annotations.NotNull; /** * Triggered when a hitbox is removed. * * @param hitBox removed hitbox * @since 2.1.0 */ public record HitBoxRemoveEvent(@NotNull HitBox hitBox) implements HitBoxEvent { /** * Returns the removed hitbox. * * @return removed hitbox * @since 2.1.0 */ @Override public @NotNull HitBox getHitBox() { return hitBox; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/manager/ModelManager.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.manager; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.EntityTracker; import kr.toxicity.model.api.tracker.TrackerModifier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.Collection; import java.util.Set; import java.util.function.Consumer; /** * Manages all loaded models and provides access to them. *

* This manager is the primary entry point for retrieving {@link ModelRenderer} instances, * which are used to create and control model trackers. *

* * @since 1.15.2 */ public interface ModelManager { /** * Retrieves a model renderer by its name. * * @param name the name of the model * @return the model renderer, or null if not found * @since 1.15.2 */ @Nullable ModelRenderer model(@NotNull String name); /** * @deprecated Use {@link #model(String)} instead. * @param name the name of the model * @return the model renderer, or null if not found * @since 1.15.2 */ @Deprecated @Nullable default ModelRenderer renderer(@NotNull String name) { return model(name); } /** * Returns a collection of all loaded model renderers. * * @return an unmodifiable collection of models * @since 1.15.2 */ @NotNull @Unmodifiable Collection models(); /** * Returns a set of all loaded model names. * * @return an unmodifiable set of model keys * @since 1.15.2 */ @NotNull @Unmodifiable Set modelKeys(); /** * Returns a collection of all renderers designated for player limb animations. * * @return an unmodifiable collection of limb models * @since 1.15.2 */ @NotNull @Unmodifiable Collection limbs(); /** * Retrieves a player limb renderer by its name. * * @param name the name of the limb model * @return the limb renderer, or null if not found * @since 1.15.2 */ @Nullable ModelRenderer limb(@NotNull String name); /** * Returns a set of all loaded player limb model names. * * @return an unmodifiable set of limb keys * @since 1.15.2 */ @NotNull @Unmodifiable Set limbKeys(); /** * Plays an animation on a player. * * @param player the target player * @param model the name of the limb model * @param animation the name of the animation * @return true if the animation started successfully * @since 1.15.2 */ default boolean animate(@NotNull PlatformPlayer player, @NotNull String model, @NotNull String animation) { return animate(player, model, animation, AnimationModifier.DEFAULT_WITH_PLAY_ONCE); } /** * Plays an animation on a player with a specific modifier. * * @param player the target player * @param model the name of the limb model * @param animation the name of the animation * @param modifier the animation modifier * @return true if the animation started successfully * @since 1.15.2 */ default boolean animate(@NotNull PlatformPlayer player, @NotNull String model, @NotNull String animation, @NotNull AnimationModifier modifier) { return animate(player, model, animation, modifier, _ -> {}); } /** * Plays an animation on a player with a modifier and a configuration consumer. * * @param player the target player * @param model the name of the limb model * @param animation the name of the animation * @param modifier the animation modifier * @param consumer a consumer to configure the created tracker * @return true if the animation started successfully * @since 1.15.2 */ default boolean animate(@NotNull PlatformPlayer player, @NotNull String model, @NotNull String animation, @NotNull AnimationModifier modifier, @NotNull Consumer consumer) { var get = limb(model); if (get == null) return false; var create = get.getOrCreate(player, TrackerModifier.DEFAULT, consumer); if (!create.animate(animation, modifier, create::close)) { create.close(); return false; } return true; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/manager/PlayerManager.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.manager; import kr.toxicity.model.api.nms.PlayerChannelHandler; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Manages player-specific data and network channels. *

* This manager is responsible for injecting and retrieving {@link PlayerChannelHandler} instances, * which are essential for sending custom packets to players. *

* * @since 1.15.2 */ public interface PlayerManager { /** * Retrieves the channel handler for a player by their UUID. * * @param uuid the player's UUID * @return the channel handler, or null if not found * @since 1.15.2 */ @Nullable PlayerChannelHandler player(@NotNull UUID uuid); /** * Gets or creates the channel handler for a player. *

* Note: This should not be used with fake players. Use {@link #player(UUID)} instead for those cases. *

* * @param player the player * @return the channel handler * @since 1.15.2 */ @NotNull PlayerChannelHandler player(@NotNull PlatformPlayer player); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/manager/ProfileManager.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.manager; import kr.toxicity.model.api.profile.ModelProfileSkin; import kr.toxicity.model.api.profile.ModelProfileSupplier; import org.jetbrains.annotations.NotNull; /** * Manages the resolution and creation of model profiles (skins). *

* This manager allows configuring the strategy for fetching profiles (e.g., from Mojang API, cache, or custom sources) * and creating skin instances from raw texture data. *

* * @since 1.15.2 */ public interface ProfileManager { /** * Returns the current profile supplier strategy. * * @return the profile supplier * @since 1.15.2 */ @NotNull ModelProfileSupplier supplier(); /** * Creates a {@link ModelProfileSkin} from a raw texture string (Base64 encoded). * * @param rawTextures the raw texture data * @return the created skin profile * @since 1.15.2 */ @NotNull ModelProfileSkin skin(@NotNull String rawTextures); /** * Sets the profile supplier strategy. * * @param supplier the new profile supplier * @since 1.15.2 */ void supplier(@NotNull ModelProfileSupplier supplier); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/manager/ReloadInfo.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.manager; import lombok.Builder; import net.kyori.adventure.audience.Audience; import org.jetbrains.annotations.NotNull; /** * Represents the context for a platform reload operation. *

* This record holds information about who initiated the reload and whether certain parts of the reload should be skipped. *

* * @param skipConfig whether to skip reloading the main configuration file * @param sender the command sender who initiated the reload * @since 1.15.2 */ @Builder public record ReloadInfo(boolean skipConfig, @NotNull Audience sender) { /** * The default reload info, representing a standard reload initiated from the console. * @since 1.15.2 */ public static final ReloadInfo DEFAULT = ReloadInfo.builder() .skipConfig(false) .sender(Audience.empty()) .build(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/manager/ScriptManager.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.manager; import kr.toxicity.model.api.script.AnimationScript; import kr.toxicity.model.api.script.ScriptBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Manages the parsing and registration of animation scripts. *

* This manager allows for the creation of custom script logic that can be embedded within animations. *

* * @since 1.15.2 */ public interface ScriptManager { /** * Parses a raw script string into an {@link AnimationScript}. * * @param script the raw script string * @return the parsed script, or null if parsing failed * @since 1.15.2 */ @Nullable AnimationScript build(@NotNull String script); /** * Registers a new script builder. * * @param name the name of the parser/builder * @param script the script builder instance * @since 1.15.2 */ void addBuilder(@NotNull String name, @NotNull ScriptBuilder script); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/manager/SkinManager.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.manager; import kr.toxicity.model.api.profile.ModelProfile; import kr.toxicity.model.api.skin.SkinData; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; /** * Manages the retrieval and caching of player skins. *

* This manager handles fetching skin data from various sources (e.g., Mojang, SkinsRestorer) * and provides a fallback skin when a profile cannot be resolved. *

* * @since 1.15.2 */ public interface SkinManager { /** * Returns the fallback skin data used when a skin cannot be resolved. * * @return the fallback skin data * @since 1.15.2 */ @NotNull SkinData fallback(); /** * Asynchronously completes an uncompleted model profile to retrieve its skin data. * * @param profile the uncompleted profile * @return a future that completes with the skin data * @since 1.15.2 */ @NotNull CompletableFuture complete(@NotNull ModelProfile.Uncompleted profile); /** * Removes a specific profile from the skin cache. * * @param profile the profile to remove * @since 1.15.2 */ void removeCache(@NotNull ModelProfile profile); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/mount/MountController.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mount; import kr.toxicity.model.api.platform.PlatformLivingEntity; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; /** * A mount controller of hit-box */ public interface MountController { /** * Moves entity by player's input * @param player passenger * @param entity target * @param input input vector * @param travelVector travel vector * @return movement */ @NotNull Vector3f move(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector); /** * Moves entity by player's input on the fly * @param player passenger * @param entity target * @param input input vector * @param travelVector travel vector * @return movement */ default @NotNull Vector3f moveOnFly(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { return move(player, entity, input, travelVector).mul(1.5F); } /** * Moves entity by player's input by type * @param type type * @param player passenger * @param entity target * @param input input vector * @param travelVector travel vector * @return movement */ default Vector3f move(@NotNull MoveType type, @NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { return switch (type) { case DEFAULT -> move(player, entity, input, travelVector); case FLY -> moveOnFly(player, entity, input, travelVector); }; } /** * Checks some player can mount * @return can mount */ default boolean canMount() { return true; } /** * Checks some player can dismount by self (right-click or sneak) * @return can dismount by self */ default boolean canDismountBySelf() { return true; } /** * Checks some player can control * @return can control */ default boolean canControl() { return true; } /** * Checks some player can jump * @return can jump */ default boolean canJump() { return true; } /** * Checks some player can fly * @return can fly */ default boolean canFly() { return false; } /** * Checks can rider damage this entity * @return can damage */ default boolean canBeDamagedByRider() { return false; } /** * Movement type */ enum MoveType { /** * On the ground */ DEFAULT, /** * On fly */ FLY } /** * Creates modifier of this controller * @return modifier */ default @NotNull Modifier modifier() { return new Modifier(this); } /** * Modifier */ class Modifier { private final MountController source; private boolean canMount; private boolean canDismountBySelf; private boolean canControl; private boolean canJump; private boolean canFly; private boolean canBeDamagedByRider; /** * Modifier of a source * @param controller source */ private Modifier(@NotNull MountController controller) { this.source = controller; canMount = controller.canMount(); canDismountBySelf = controller.canDismountBySelf(); canControl = controller.canControl(); canJump = controller.canJump(); canFly = controller.canFly(); } /** * Sets some player can dismount by self (right-click or sneak) * @param canDismountBySelf can dismount * @return self */ public @NotNull Modifier canDismountBySelf(boolean canDismountBySelf) { this.canDismountBySelf = canDismountBySelf; return this; } /** * Sets some player can mount * @param canMount can mount * @return self */ public @NotNull Modifier canMount(boolean canMount) { this.canMount = canMount; return this; } /** * Sets some player can control * @param canControl can control * @return self */ public @NotNull Modifier canControl(boolean canControl) { this.canControl = canControl; return this; } /** * Sets some player can fly * @param canFly can fly * @return self */ public @NotNull Modifier canFly(boolean canFly) { this.canFly = canFly; return this; } /** * Sets some player can jump * @param canJump can jump * @return self */ public @NotNull Modifier canJump(boolean canJump) { this.canJump = canJump; return this; } /** * Sets can rider damage this entity * @param canBeDamagedByRider can be damaged by rider * @return self */ public @NotNull Modifier canBeDamagedByRider(boolean canBeDamagedByRider) { this.canBeDamagedByRider = canBeDamagedByRider; return this; } /** * Builds controller with modified value * @return modified controller */ public @NotNull MountController build() { return new MountController() { @NotNull @Override public Vector3f move(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { return source.move(player, entity, input, travelVector); } @Override public boolean canDismountBySelf() { return canDismountBySelf; } @Override public boolean canControl() { return canControl; } @Override public boolean canFly() { return canFly; } @Override public boolean canJump() { return canJump; } @Override public boolean canMount() { return canMount; } @Override public boolean canBeDamagedByRider() { return canBeDamagedByRider; } }; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/mount/MountControllers.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.mount; import kr.toxicity.model.api.platform.PlatformLivingEntity; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; /** * Builtin mount controllers */ public enum MountControllers implements MountController { /** * Invalid */ INVALID { @NotNull @Override public Vector3f move(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { return new Vector3f(); } @Override public boolean canMount() { return false; } }, /** * None */ NONE { @NotNull @Override public Vector3f move(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { return new Vector3f(); } @Override public boolean canControl() { return false; } }, /** * Walk */ WALK { @NotNull @Override public Vector3f move(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { input.normalize(); input.y = 0; input.x = input.x * 0.5F; if (input.z <= 0.0F) { input.z *= 0.25F; } return input; } }, /** * Fly */ FLY { @NotNull @Override public Vector3f move(@NotNull PlatformPlayer player, @NotNull PlatformLivingEntity entity, @NotNull Vector3f input, @NotNull Vector3f travelVector) { input.normalize(); input.x = input.x * 0.5F; if (input.z <= 0.0F) { input.z *= 0.25F; } return input; } @Override public boolean canFly() { return true; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/AnimationBundler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import org.jetbrains.annotations.NotNull; /** * A record that bundles animation packets for both standard clients and modded clients. * * @since 2.2.1 * @param standard the packet bundler for standard Minecraft clients * @param mod the packet bundler for clients with the specific mod enabled */ public record AnimationBundler( @NotNull PacketBundler standard, @NotNull ModAnimationBundler mod ) { /** * Checks if there are any animation packets to be sent. * * @since 2.2.1 * @return true if the standard packet bundler is not empty */ public boolean isNotEmpty() { return standard.isNotEmpty(); } /** * Sends the appropriate animation packets to the player based on their client type. * * @since 2.2.1 * @param handler the player's channel handler used to determine mod status and send packets */ public void send(@NotNull PlayerChannelHandler handler) { if (handler.isModEnabled()) mod.send(handler.player()); else standard.send(handler.player()); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/DisplayTransformer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionf; import org.joml.Vector3f; /** * Handles the transformation (position, scale, rotation) of a display entity. *

* This interface abstracts the interpolation logic for smooth animations. *

* * @since 1.15.2 */ public interface DisplayTransformer { /** * Applies a transformation to the display. * * @param duration the interpolation duration in ticks * @param position the target position * @param scale the target scale * @param rotation the target rotation * @param bundler the packet bundler to use * @since 1.15.2 */ void transform(int duration, @NotNull Vector3f position, @NotNull Vector3f scale, @NotNull Quaternionf rotation, @NotNull AnimationBundler bundler); /** * Sends the current transformation state to clients. * * @param bundler the packet bundler to use * @since 1.15.2 */ void sendTransformation(@NotNull PacketBundler bundler); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/HitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.mount.MountController; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.EntityTrackerRegistry; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import java.util.Optional; import java.util.function.Function; /** * Represents a hitbox for a model part, allowing for interaction and collision detection. *

* Hitboxes are often implemented using invisible entities (like Slimes or Interaction entities) * and are linked to specific bones in the model. *

* * @since 1.15.2 */ public interface HitBox extends Identifiable { /** * Hides this hitbox from a specific player. * * @param player the target player * @since 1.15.2 */ @ApiStatus.Internal void hide(@NotNull PlatformPlayer player); /** * Shows this hitbox to a specific player. * * @param player the target player * @since 1.15.2 */ @ApiStatus.Internal void show(@NotNull PlatformPlayer player); /** * Returns the name of the bone group associated with this hitbox. * * @return the group name * @since 1.15.2 */ default @NotNull BoneName groupName() { return positionSource().name(); } /** * Returns the mount controller for this hitbox. * * @return the mount controller * @since 1.15.2 */ @NotNull MountController mountController(); /** * Sets the mount controller for this hitbox. * * @param controller the new mount controller * @since 1.15.2 */ void mountController(@NotNull MountController controller); /** * Checks if the passenger of this hitbox is walking. * * @return true if walking, false otherwise * @since 1.15.2 */ boolean onWalk(); /** * Returns the source entity of this hitbox. * * @return the source entity * @since 1.15.2 */ @NotNull PlatformEntity source(); /** * Mounts an entity onto this hitbox. * * @param entity the entity to mount * @since 1.15.2 */ void mount(@NotNull PlatformEntity entity); /** * Checks if this hitbox has a mount driver. * * @return true if it has a driver, false otherwise * @since 1.15.2 */ boolean hasMountDriver(); /** * Checks if this hitbox is being controlled by another entity. * * @return true if controlled, false otherwise * @since 1.15.2 */ default boolean hasBeenControlled() { return mountController().canControl() && hasMountDriver(); } /** * Dismounts an entity from this hitbox. * * @param entity the entity to dismount * @since 1.15.2 */ void dismount(@NotNull PlatformEntity entity); /** * Dismounts all passengers from this hitbox. * * @since 1.15.2 */ void dismountAll(); /** * Checks if a dismount operation is forced. * * @return true if forced, false otherwise * @since 1.15.2 */ boolean forceDismount(); /** * Returns the relative position of this hitbox to its source entity. * * @return the relative position * @since 1.15.2 */ @NotNull Vector3f relativePosition(); /** * Removes this hitbox safely. * * @since 1.15.2 */ void removeHitBox(); /** * Returns the listener associated with this hitbox. * * @return the listener * @since 1.15.2 */ @NotNull HitBoxListener listener(); /** * Sets the listener for this hitbox. * * @param listener the new listener * @since 3.0.2 */ @ApiStatus.Internal void listener(@NotNull HitBoxListener listener); /** * Updates the listener for this hitbox using a builder function. *

* This method retrieves the current listener, converts it to a builder, * applies the provided function, and sets the resulting listener. *

* * @param function the function to apply to the builder * @since 3.0.2 */ default void listener(@NotNull Function function) { listener(function.apply(listener().toBuilder()).build()); } /** * Returns the rendered bone that acts as the position source for this hitbox. * * @return the position source bone * @since 1.15.2 */ @NotNull RenderedBone positionSource(); /** * Returns the entity tracker registry for this hitbox's source entity. * * @return an optional containing the registry, or empty if not found * @since 1.15.2 */ default @NotNull Optional registry() { return BetterModel.registry(source().uuid()); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/HitBoxListener.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import com.google.common.collect.ImmutableMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import kr.toxicity.model.api.event.hitbox.*; import kr.toxicity.model.api.platform.PlatformEntity; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import static kr.toxicity.model.api.util.CollectionUtil.newAddressingMap; /** * Listens for events related to a {@link HitBox}, such as damage, interaction, and mounting. *

* This interface allows for custom behavior when a hitbox is interacted with. *

* * @since 1.15.2 */ public interface HitBoxListener { /** * An empty listener that does nothing. * @since 1.15.2 */ HitBoxListener EMPTY = builder().build(); /** * Creates a new builder for {@link HitBoxListener}. * * @return a new builder * @since 1.15.2 */ static @NotNull Builder builder() { return new Builder(newAddressingMap(), null); } /** * Builder for {@link HitBoxListener}. * * @since 1.15.2 */ final class Builder { private final Map, Consumer> listeners; private Consumer syncConsumer; /** * Private initializer. */ private Builder( @NotNull Map, Consumer> listeners, @Nullable Consumer syncConsumer ) { this.listeners = listeners; this.syncConsumer = syncConsumer; } /** * Adds a handler for the specified hitbox event class. * * @param eventClass event class * @param consumer event consumer * @param event type * @return this builder * @since 2.1.0 */ @SuppressWarnings("unchecked") public @NotNull Builder listen(@NotNull Class eventClass, @NotNull Consumer consumer) { listeners.compute(eventClass, (_, old) -> old == null ? consumer : ((Consumer) old).andThen(consumer)); return this; } /** * Adds a sync handler. * * @param sync the sync consumer * @return this builder * @since 1.15.2 */ public @NotNull Builder sync(@NotNull Consumer sync) { var previous = syncConsumer; syncConsumer = previous != null ? previous.andThen(sync) : sync; return this; } /** * Adds a damage handler. * * @param damage the damage handler * @return this builder * @since 2.1.0 */ public @NotNull Builder damage(@NotNull Consumer damage) { return listen(HitBoxDamagedEvent.class, damage); } /** * Adds an interact-at handler. * * @param interactAt the interact-at handler * @return this builder * @since 2.1.0 */ public @NotNull Builder interactAt(@NotNull Consumer interactAt) { return listen(HitBoxInteractAtEvent.class, interactAt); } /** * Adds a remove handler. * * @param remove the remove consumer * @return this builder * @since 1.15.2 */ public @NotNull Builder remove(@NotNull Consumer remove) { return listen(HitBoxRemoveEvent.class, event -> remove.accept(event.getHitBox())); } /** * Adds a creation handler. * * @param create the creation consumer * @return this builder * @since 2.2.0 */ public @NotNull Builder create(@NotNull Consumer create) { return listen(HitBoxCreateEvent.class, event -> create.accept(event.getHitBox())); } /** * Adds a mount handler. * * @param mount the mount consumer * @return this builder * @since 1.15.2 */ public @NotNull Builder mount(@NotNull BiConsumer mount) { return listen(HitBoxMountEvent.class, event -> mount.accept(event.getHitBox(), event.entity())); } /** * Adds a dismount handler. * * @param dismount the dismount consumer * @return this builder * @since 1.15.2 */ public @NotNull Builder dismount(@NotNull BiConsumer dismount) { return listen(HitBoxDismountEvent.class, event -> dismount.accept(event.getHitBox(), event.entity())); } /** * Builds the listener. * * @return the created listener * @since 1.15.2 */ @SuppressWarnings("unchecked") public @NotNull HitBoxListener build() { var copied = ImmutableMap.copyOf(listeners); var sync = syncConsumer; return new HitBoxListener() { @Override @SuppressWarnings("unchecked") public boolean handle(@NotNull HitBoxEvent event) { var consumer = (Consumer) copied.get(event.getClass()); if (consumer != null) { consumer.accept(event); } return event.call(); } @Override public void sync(@NotNull HitBox hitBox) { if (sync != null) sync.accept(hitBox); } @Override public @NotNull Builder toBuilder() { return new Builder(new Object2ObjectOpenHashMap<>(copied), sync); } }; } } /** * Handles a hitbox event. * * @param event target event * @return whether target event is triggered * @since 2.1.0 */ @ApiStatus.Internal boolean handle(@NotNull HitBoxEvent event); /** * Handles tick method. * * @param hitBox target hitbox * @since 2.1.0 */ @ApiStatus.Internal void sync(@NotNull HitBox hitBox); /** * Creates a builder initialized with this listener's current handlers. * * @return a new builder * @since 1.15.2 */ @NotNull Builder toBuilder(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/Identifiable.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import org.jetbrains.annotations.NotNull; import java.util.UUID; /** * Represents an object that can be identified by a unique ID and a UUID. *

* This is commonly used for entities and other tracked objects in the game. *

* * @since 1.15.2 */ public interface Identifiable { /** * Returns the integer ID of the object (e.g., entity ID). * * @return the ID * @since 1.15.2 */ int id(); /** * Returns the UUID of the object. * * @return the UUID * @since 1.15.2 */ @NotNull UUID uuid(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/ModAnimationBundler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; /** * A bundler that sends animation data to a player. * @since 2.2.1 */ public interface ModAnimationBundler { /** * Sends the bundled animation data to the specified player. * * @since 2.2.1 * @param player the player to receive the animation */ void send(@NotNull PlatformPlayer player); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/ModelDisplay.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.platform.PlatformBillboard; import kr.toxicity.model.api.platform.PlatformItemStack; import kr.toxicity.model.api.platform.PlatformItemTransform; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.tracker.ModelRotation; import org.jetbrains.annotations.NotNull; /** * Represents an item display entity used for rendering model parts. *

* This interface abstracts the NMS implementation of item displays, allowing for manipulation of * position, rotation, scale, and other display properties. *

* * @since 1.15.2 */ public interface ModelDisplay extends Identifiable { /** * Checks if this display is currently invisible. * * @return true if invisible, false otherwise * @since 1.15.2 */ boolean invisible(); /** * Sets the visibility of this display. * * @param invisible true to make invisible, false to make visible * @since 1.15.2 */ void invisible(boolean invisible); /** * Rotates this display to a specific orientation. * * @param rotation the target rotation * @param bundler the packet bundler to use * @since 1.15.2 */ void rotate(@NotNull ModelRotation rotation, @NotNull PacketBundler bundler); /** * Synchronizes the potion effect (glowing, etc.) from the base entity to this display. * * @param entity the source entity * @since 2.2.0 */ void syncPotionEffect(@NotNull BaseEntity entity); /** * Synchronizes this display's position with a location. * * @param location the target location * @since 1.15.2 */ void syncPosition(@NotNull PlatformLocation location); /** * Sets the duration for position/rotation interpolation. * * @param duration the duration in ticks * @since 1.15.2 */ void moveDuration(int duration); /** * Sets the item display transform type. * * @param transform the transform type * @since 1.15.2 */ void display(@NotNull PlatformItemTransform transform); /** * Spawns this display using the provided packet bundler. * * @param bundler the packet bundler * @since 1.15.2 */ default void spawn(@NotNull PacketBundler bundler) { spawn(!invisible(), bundler); } /** * Spawns this display and sends initial entity data. * * @param bundler the packet bundler * @since 1.15.2 */ default void spawnWithEntityData(@NotNull PacketBundler bundler) { var visible = !invisible(); spawn(visible, bundler); sendEntityData(visible, bundler); } /** * Spawns this display, optionally showing the item. * * @param showItem true to show the item, false otherwise * @param bundler the packet bundler * @since 1.15.2 */ void spawn(boolean showItem, @NotNull PacketBundler bundler); /** * Removes this display. * * @param bundler the packet bundler * @since 1.15.2 */ void remove(@NotNull PacketBundler bundler); /** * Teleports this display to a new location. * * @param location the target location * @param bundler the packet bundler * @since 1.15.2 */ void teleport(@NotNull PlatformLocation location, @NotNull PacketBundler bundler); /** * Sets the item stack to be displayed. * * @param itemStack the item stack * @since 1.15.2 */ void item(@NotNull PlatformItemStack itemStack); /** * Creates a transformer for animating this display. * * @return the display transformer * @since 1.15.2 */ @NotNull DisplayTransformer createTransformer(); /** * Sends updated entity data if it has changed. * * @param bundler the packet bundler * @since 1.15.2 */ void sendDirtyEntityData(@NotNull PacketBundler bundler); /** * Sends entity data, optionally showing the item. * * @param showItem true to show the item, false otherwise * @param bundler the packet bundler * @since 1.15.2 */ void sendEntityData(boolean showItem, @NotNull PacketBundler bundler); /** * Sets the brightness override for the display. * * @param block the block light level * @param sky the skylight level * @since 1.15.2 */ void brightness(int block, int sky); /** * Sets the view range of the display. * * @param range the view range * @since 1.15.2 */ void viewRange(float range); /** * Sets the shadow radius of the display. * * @param radius the shadow radius * @since 1.15.2 */ void shadowRadius(float radius); /** * Sends the position of the display relative to an entity adapter. * * @param adapter the entity adapter * @param bundler the packet bundler * @since 1.15.2 */ void sendPosition(@NotNull BaseEntity adapter, @NotNull PacketBundler bundler); /** * Toggles the glowing effect. * * @param glow true to enable glow, false to disable * @since 1.15.2 */ void glow(boolean glow); /** * Sets the glow color. * * @param glowColor the RGB glow color * @since 1.15.2 */ void glowColor(int glowColor); /** * Sets the billboard constraint for the display. * * @param billboard the billboard type * @since 1.15.2 */ void billboard(@NotNull PlatformBillboard billboard); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/ModelInteractionHand.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; /** * Represents the hand used during an interaction with a model hitbox. * * @since 1.15.2 */ public enum ModelInteractionHand { /** * The main hand (usually right). * @since 1.15.2 */ LEFT, /** * The off-hand (usually left). * @since 1.15.2 */ RIGHT } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/ModelNametag.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents a nametag associated with a model part. *

* Nametags are typically implemented as invisible armor stands or text displays * that float above a specific bone. *

* * @since 1.15.2 */ public interface ModelNametag { /** * Sets whether the nametag should always be visible (even through blocks). * * @param alwaysVisible true for always visible, false otherwise * @since 1.15.2 */ void alwaysVisible(boolean alwaysVisible); /** * Sets the text component of the nametag. * * @param component the text component, or null to clear * @since 1.15.2 */ void component(@Nullable Component component); /** * Teleports the nametag to a new location. * * @param location the target location * @since 1.15.2 */ void teleport(@NotNull PlatformLocation location); /** * Sends the nametag packet to a specific player. * * @param player the target player * @since 1.15.2 */ void send(@NotNull PlatformPlayer player); /** * Removes the nametag. * * @param bundler the packet bundler to use * @since 1.15.2 */ void remove(@NotNull PacketBundler bundler); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/NMS.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.data.blueprint.ModelBoundingBox; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.entity.BasePlayer; import kr.toxicity.model.api.mount.MountController; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformItemStack; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.profile.ModelProfile; import kr.toxicity.model.api.tracker.EntityTrackerRegistry; import kr.toxicity.model.api.util.TransformedItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.function.BooleanSupplier; import java.util.function.Consumer; /** * Handles direct interactions with Minecraft's internal server code (NMS). *

* This interface provides methods for creating displays, managing packets, handling hitboxes, * and adapting entities for different server environments (e.g., Folia). *

* * @since 1.15.2 */ public interface NMS { /** * Creates a model display at the specified location. * * @param location the starting location * @return the created model display * @since 1.15.2 */ default @NotNull ModelDisplay create(@NotNull PlatformLocation location) { return create(location, 0, _ -> {}); } /** * Creates a model display at the specified location with an initial configuration. * * @param location the starting location * @param initialConsumer a consumer to configure the display upon creation * @return the created model display * @since 1.15.2 */ default @NotNull ModelDisplay create(@NotNull PlatformLocation location, @NotNull Consumer initialConsumer) { return create(location, 0, initialConsumer); } /** * Creates a model display at the specified location with a Y-offset and initial configuration. * * @param location the starting location * @param yOffset the vertical offset * @param initialConsumer a consumer to configure the display upon creation * @return the created model display * @since 1.15.2 */ @NotNull ModelDisplay create(@NotNull PlatformLocation location, double yOffset, @NotNull Consumer initialConsumer); /** * Creates a nametag for a rendered bone. * * @param bone the bone to attach the nametag to * @return the created nametag * @since 1.15.2 */ @NotNull ModelNametag createNametag(@NotNull RenderedBone bone); /** * Creates a nametag for a rendered bone with configuration. * * @param bone the bone to attach the nametag to * @param consumer a consumer to configure the nametag * @return the created nametag * @since 1.15.2 */ default @NotNull ModelNametag createNametag(@NotNull RenderedBone bone, @NotNull Consumer consumer) { var created = createNametag(bone); consumer.accept(created); return created; } /** * Injects a Netty channel handler into a player's connection. * * @param player the player to inject * @return the created channel handler * @since 1.15.2 */ @NotNull PlayerChannelHandler inject(@NotNull PlatformPlayer player); /** * Creates a packet bundler with an initial capacity. * * @param initialCapacity the initial capacity * @return the packet bundler * @since 1.15.2 */ @NotNull PacketBundler createBundler(int initialCapacity); /** * Creates a parallel packet bundler with a size threshold. * * @param threshold the size threshold for parallel processing * @return the packet bundler * @since 1.15.2 */ @NotNull PacketBundler createParallelBundler(int threshold); /** * Creates a mod animation bundler. * * @param initialCapacity the initial capacity * @return mod animation bundler. * @since 2.2.1 */ @NotNull ModAnimationBundler createModAnimationBuilder(int initialCapacity); /** * Applies a tint color to an item stack. * * @param itemStack the item to tint * @param rgb the RGB color value * @return the tinted item stack * @since 1.15.2 */ @NotNull PlatformItemStack tint(@NotNull PlatformItemStack itemStack, int rgb); /** * Adds a mount packet for an entity tracker to a bundler. * * @param registry the entity tracker registry * @param bundler the packet bundler * @since 1.15.2 */ void mount(@NotNull EntityTrackerRegistry registry, @NotNull PacketBundler bundler); /** * Sends a hide packet for an entity to a player via their channel handler. * * @param channel the player's channel handler * @param registry the entity tracker registry * @since 1.15.2 */ void hide(@NotNull PlayerChannelHandler channel, @NotNull EntityTrackerRegistry registry); /** * Sends a hide packet for an entity to a player if a condition is met. *

* For players, the hide operation is delayed based on configuration. *

* * @param channel the player's channel handler * @param registry the entity tracker registry * @param condition the condition to check * @since 1.15.2 */ default void hide(@NotNull PlayerChannelHandler channel, @NotNull EntityTrackerRegistry registry, @NotNull BooleanSupplier condition) { if (registry.entity() instanceof BasePlayer) { var plugin = BetterModel.platform(); plugin.scheduler().asyncTaskLater(plugin.config().playerHideDelay(), () -> { if (condition.getAsBoolean()) hide(channel, registry); }); } else hide(channel, registry); } /** * Creates a delegate hitbox for a target entity. * * @param entity the target entity * @param bone the bone associated with the hitbox * @param boundingBox the bounding box definition * @param controller the mount controller * @param listener the hitbox listener * @return the created hitbox, or null if creation failed * @since 1.15.2 */ @Nullable HitBox createHitBox(@NotNull BaseEntity entity, @NotNull RenderedBone bone, @NotNull ModelBoundingBox boundingBox, @NotNull MountController controller, @NotNull HitBoxListener listener); /** * Returns the NMS version of the server. * * @return the version * @since 1.15.2 */ @NotNull NMSVersion version(); /** * Adapts a Bukkit entity to a {@link BaseEntity}, handling Folia compatibility. * * @param entity the Bukkit entity * @return the adapted entity * @since 1.15.2 */ @NotNull BaseEntity adapt(@NotNull PlatformEntity entity); /** * Adapts a Bukkit player to a {@link BasePlayer}, handling Folia compatibility. * * @param player the Bukkit player * @return the adapted player * @since 1.15.2 */ @NotNull BasePlayer adapt(@NotNull PlatformPlayer player); /** * Retrieves the model profile (skin) for a player. * * @param player the player * @return the model profile * @since 1.15.2 */ @NotNull ModelProfile profile(@NotNull PlatformPlayer player); /** * Creates a custom skin item stack. * * @param model the model name * @param floats a list of floats * @param flags a list of flags * @param strings a list of strings * @param colors a list of colors * @return the transformed item stack * @since 1.15.2 */ default @NotNull TransformedItemStack createSkinItem(@NotNull String model, @NotNull List floats, @NotNull List flags, @NotNull List strings, @NotNull List colors) { return TransformedItemStack.empty(); } /** * Checks if the server is in online mode (either natively or via proxy). * * @return true if online mode, false otherwise * @since 1.15.2 */ boolean isProxyOnlineMode(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/NMSVersion.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; /** * Enumerates supported Minecraft server versions and their associated metadata. *

* This enum maps internal version identifiers to Minecraft versions and resource pack formats. *

* * @since 1.15.2 */ @RequiredArgsConstructor @Getter public enum NMSVersion { /** * Minecraft 1.21.4 * @since 1.15.2 */ V1_21_R3(46), /** * Minecraft 1.21.5 * @since 1.15.2 */ V1_21_R4(55), /** * Minecraft 1.21.6 - 1.21.8 * @since 1.15.2 */ V1_21_R5(64), /** * Minecraft 1.21.9 - 1.21.10 * @since 1.15.2 */ V1_21_R6(69), /** * Minecraft 1.21.11 * @since 1.15.2 */ V1_21_R7(75), /** * Minecraft 26.1.x * @since 3.0.0 */ V26_R1(84) ; /** * The resource pack format version (pack.mcmeta). */ private final int metaVersion; /** * Returns the oldest supported version. * * @return the first version enum * @since 1.15.2 */ public static @NotNull NMSVersion first() { return values()[0]; } /** * Returns the latest supported version. * * @return the last version enum * @since 1.15.2 */ public static @NotNull NMSVersion latest() { var entries = values(); return entries[entries.length - 1]; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/PacketBundler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; /** * Collects multiple packets to be sent together to a player. *

* This helps optimize network traffic by grouping related updates (e.g., bone movements) * into a single bundle or batch. *

* * @since 1.15.2 */ public interface PacketBundler { /** * Checks if the bundler contains no packets. * * @return true if empty, false otherwise * @since 1.15.2 */ boolean isEmpty(); /** * Checks if the bundler contains at least one packet. * * @return true if not empty, false otherwise * @since 1.15.2 */ default boolean isNotEmpty() { return !isEmpty(); } /** * Returns the number of packets in the bundler. * * @return the packet count * @since 1.15.2 */ int size(); /** * Sends all collected packets to the specified player. * * @param player the target player * @since 1.15.2 */ default void send(@NotNull PlatformPlayer player) { send(player, () -> {}); } /** * Sends all collected packets to the specified player and executes a callback on success. * * @param player the target player * @param onSuccess the callback to run after sending * @since 1.15.2 */ void send(@NotNull PlatformPlayer player, @NotNull Runnable onSuccess); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/PlayerChannelHandler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.entity.BasePlayer; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.tracker.EntityTrackerRegistry; import org.jetbrains.annotations.NotNull; import java.util.UUID; /** * Manages the network channel for a player, allowing for packet interception and injection. *

* This is crucial for handling custom packets and entity tracking. *

* * @since 1.15.2 */ public interface PlayerChannelHandler extends Identifiable, AutoCloseable { /** * Returns the Bukkit player associated with this handler. * * @return the player * @since 1.15.2 */ default @NotNull PlatformPlayer player() { return base().platform(); } @Override default @NotNull UUID uuid() { return base().uuid(); } @Override default int id() { return base().id(); } /** * Returns the base player adapter. * * @return the base player * @since 1.15.2 */ @NotNull BasePlayer base(); /** * Sends the correct entity data for a specific tracker to the player. * * @param registry the entity tracker registry * @since 1.15.2 */ void sendEntityData(@NotNull EntityTrackerRegistry registry); /** * Closes the channel handler, cleaning up resources. * * @since 1.15.2 */ @Override void close(); /** * Checks if the meg-mod is enabled. * @return meg-mod is enabled. */ boolean isModEnabled(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/nms/Profiled.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.nms; import kr.toxicity.model.api.armor.PlayerArmor; import kr.toxicity.model.api.player.PlayerSkinParts; import kr.toxicity.model.api.profile.ModelProfile; import org.jetbrains.annotations.NotNull; /** * Represents an entity that has a player profile, armor, and skin customization settings. *

* This interface is typically implemented by player-like entities or trackers that need to render player skins and equipment. *

* * @since 1.15.2 */ public interface Profiled { /** * Returns the model profile (skin) of the entity. * * @return the model profile * @since 1.15.2 */ @NotNull ModelProfile profile(); /** * Returns the armor equipment of the entity. * * @return the player armor * @since 1.15.2 */ @NotNull PlayerArmor armors(); /** * Returns the skin customization parts (e.g., jacket, hat) of the entity. * * @return the skin parts * @since 1.15.2 */ @NotNull PlayerSkinParts skinParts(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackAssets.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import kr.toxicity.model.api.BetterModel; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; /** * Manages assets within a specific pack overlay. *

* This class provides access to namespaces (like 'bettermodel' and 'minecraft') and allows adding resources to the pack. *

* * @since 1.15.2 */ public final class PackAssets { final PackPath path; final PackOverlay overlay; final Map resourceMap = new ConcurrentHashMap<>(); private final PackNamespace bettermodel, minecraft; PackAssets(@NotNull PackOverlay overlay) { this.overlay = overlay; this.path = overlay.path(BetterModel.config().namespace()); bettermodel = new PackNamespace(this, BetterModel.config().namespace()); minecraft = new PackNamespace(this, "minecraft"); } /** * Returns the 'bettermodel' namespace (or the configured namespace). * * @return the namespace * @since 1.15.2 */ public @NotNull PackNamespace bettermodel() { return bettermodel; } /** * Returns the 'minecraft' namespace. * * @return the namespace * @since 1.15.2 */ public @NotNull PackNamespace minecraft() { return minecraft; } int size() { return resourceMap.size(); } boolean dirty() { return size() > 0; } /** * Adds a resource to the pack. * * @param path the path of the resource * @param size the estimated size of the resource * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String path, long size, @NotNull Supplier supplier) { add(new String[] { path }, size, supplier); } /** * Adds a resource to the pack using multiple path components. * * @param paths the path components * @param size the estimated size of the resource * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String[] paths, long size, @NotNull Supplier supplier) { var resolve = path.resolve(paths); resourceMap.putIfAbsent(resolve, PackResource.of(overlay, resolve, size, supplier)); } /** * Adds a resource to the pack with unknown size. * * @param path the path of the resource * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String path, @NotNull Supplier supplier) { add(path, -1, supplier); } /** * Adds a resource to the pack using multiple path components with unknown size. * * @param paths the path components * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String[] paths, @NotNull Supplier supplier) { add(paths, -1, supplier); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackBuilder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * A builder for constructing resource pack contents within a specific path context. *

* This class simplifies adding resources to a pack by managing the current path and providing methods to resolve sub-paths. * It also integrates with {@link PackObfuscator} for resource name obfuscation. *

* * @since 1.15.2 */ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) public final class PackBuilder { private final PackAssets assets; private final PackPath path; private final PackObfuscator obfuscator = PackObfuscator.order(); /** * Resolves a sub-path and returns a new builder for that path. * * @param paths the sub-path components * @return a new PackBuilder for the resolved path * @since 1.15.2 */ public @NotNull PackBuilder resolve(@NotNull String... paths) { return new PackBuilder(assets, path.resolve(paths)); } /** * Adds a resource to the pack at the current path. * * @param path the relative path of the resource * @param estimatedSize the estimated size of the resource * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String path, long estimatedSize, @NotNull Supplier supplier) { add(new String[] { path }, estimatedSize, supplier); } /** * Adds a resource to the pack at the current path using multiple path components. * * @param paths the relative path components * @param size the estimated size of the resource * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String[] paths, long size, @NotNull Supplier supplier) { var resolve = path.resolve(paths); assets.resourceMap.putIfAbsent(resolve, PackResource.of(assets.overlay, resolve, size, supplier)); } /** * Returns the obfuscator associated with this builder. * * @return the obfuscator * @since 1.15.2 */ public @NotNull PackObfuscator obfuscator() { return obfuscator; } /** * Adds a resource to the pack at the current path with unknown size. * * @param path the relative path of the resource * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String path, @NotNull Supplier supplier) { add(path, -1, supplier); } /** * Adds a resource to the pack at the current path using multiple path components with unknown size. * * @param paths the relative path components * @param supplier the supplier for the resource content * @since 1.15.2 */ public void add(@NotNull String[] paths, @NotNull Supplier supplier) { add(paths, -1, supplier); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackBuiltInAssets.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import com.google.gson.JsonObject; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.data.Float3; import kr.toxicity.model.api.data.Float4; import kr.toxicity.model.api.data.blueprint.BlueprintElement; import kr.toxicity.model.api.util.json.JsonObjectBuilder; import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; record PackBuiltInAssets( @NotNull String path, @NotNull Function builderFunction, @NotNull Supplier supplier ) { private static final byte[] MESH_PIXEL_IMAGE; static { var image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB); Arrays.fill(((DataBufferInt) image.getRaster().getDataBuffer()).getData(), 0xFFFFFF); try (var output = new ByteArrayOutputStream()) { ImageIO.write(image, "png", output); MESH_PIXEL_IMAGE = output.toByteArray(); } catch (IOException ex) { throw new RuntimeException(ex); } } private static final Set ASSETS = Set.of( new PackBuiltInAssets( BlueprintElement.MESH_TRIANGLE_SINGLE + ".json", zipper -> zipper.modern().bettermodel().models(), () -> PackMeta.GSON.toJson(meshTriangle(faces -> faces .jsonObject("north", north -> north .jsonArray("uv", Float4.MAX_UV.toJson()) .property("texture", "#0") .property("tintindex", 0)) )).getBytes(StandardCharsets.UTF_8) ), new PackBuiltInAssets( BlueprintElement.MESH_TRIANGLE_DUPLEX + ".json", zipper -> zipper.modern().bettermodel().models(), () -> PackMeta.GSON.toJson(meshTriangle(faces -> faces .jsonObject("north", north -> north .jsonArray("uv", Float4.MAX_UV.toJson()) .property("texture", "#0") .property("tintindex", 0)) .jsonObject("south", north -> north .jsonArray("uv", Float4.MAX_UV.toJson()) .property("texture", "#0") .property("tintindex", 0)) )).getBytes(StandardCharsets.UTF_8) ), new PackBuiltInAssets( BlueprintElement.MESH_PIXEL + ".png", zipper -> zipper.modern().bettermodel().textures(), () -> MESH_PIXEL_IMAGE ) ); static void applyAs(@NotNull PackZipper zipper) { for (PackBuiltInAssets asset : ASSETS) { asset.builderFunction.apply(zipper).add(asset.path, asset.supplier); } } private static @NotNull JsonObject meshTriangle(@NotNull Function faceBuilder) { var pixel = BetterModel.config().namespace() + ":item/" + BlueprintElement.MESH_PIXEL; return JsonObjectBuilder.builder() .jsonObject("textures", textures -> textures .property("0", pixel) .property("particle", pixel)) .jsonArray("elements", elements -> elements .jsonObject(element -> element .jsonArray("from", Float3.MESH_TRIANGLE_FROM.toJson()) .jsonArray("to", Float3.MESH_TRIANGLE_TO.toJson()) .jsonObject("faces", faceBuilder::apply))) .build(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackByte.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import org.jetbrains.annotations.NotNull; /** * Represents a raw byte array associated with a specific pack path. *

* This record is used to store the binary content of a resource within a resource pack. * It implements {@link Comparable} to allow sorting based on the path. *

* * @param path the path of the resource * @param bytes the binary content of the resource * @since 1.15.2 */ public record PackByte(@NotNull PackPath path, byte[] bytes) implements Comparable { @Override public boolean equals(Object o) { if (!(o instanceof PackByte packByte)) return false; return path.equals(packByte.path); } @Override public int hashCode() { return path.hashCode(); } @Override public int compareTo(@NotNull PackByte o) { return path.compareTo(o.path); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackMeta.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import com.google.gson.*; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.nms.NMSVersion; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Represents the metadata (pack.mcmeta) of a resource pack. *

* This record handles the serialization and deserialization of the pack metadata, including * pack format, description, and supported formats. It also supports overlays for multi-version support. *

* * @param pack the main pack configuration * @param overlays the optional overlays configuration * @since 1.15.2 */ public record PackMeta( @NotNull Pack pack, @Nullable Overlay overlays ) { /** * The standard path for the pack metadata file. * @since 1.15.2 */ public static final PackPath PATH = new PackPath("pack.mcmeta"); static final Gson GSON = new GsonBuilder() .registerTypeAdapter(PackVersion.class, (JsonDeserializer) (json, _, _) -> { if (json.isJsonPrimitive()) return new PackVersion(json.getAsInt()); else if (json.isJsonArray()) { var array = json.getAsJsonArray(); return new PackVersion(array.get(0).getAsInt(), array.size() < 2 ? 0 : array.get(1).getAsInt()); } else return null; }) .registerTypeAdapter(PackVersion.class, (JsonSerializer) (src, _, _) -> src.toJson()) .registerTypeAdapter(VersionRange.class, (JsonDeserializer) (json, _, context) -> { if (json.isJsonPrimitive()) return new VersionRange(json.getAsInt()); else if (json.isJsonArray()) { var array = json.getAsJsonArray(); return new VersionRange( array.get(0).getAsInt(), array.get(1).getAsInt() ); } else if (json.isJsonObject()) { return context.deserialize(json, VersionRange.class); } else return null; }) .registerTypeAdapter(VersionRange.class, (JsonSerializer) (src, _, _) -> src.toJson()) .create(); /** * Creates a new builder for {@link PackMeta}. * * @return a new builder instance * @since 1.15.2 */ public static @NotNull Builder builder() { return new Builder(); } /** * Converts this metadata to a JSON element. * * @return the JSON representation * @since 1.15.2 */ public @NotNull JsonElement toJson() { return GSON.toJsonTree(this); } /** * Converts this metadata to a pack resource. * * @return the pack resource containing the JSON data * @since 1.15.2 */ public @NotNull PackResource toResource() { var json = GSON.toJson(this); return PackResource.of(PATH, 2L * json.length(), () -> json.getBytes(StandardCharsets.UTF_8)); } /** * Represents the 'pack' section of the metadata. * * @param packFormat the pack format version * @param description the pack description * @param supportedFormats the range of supported formats * @param minFormat the minimum supported format (>= 1.21.9) * @param maxFormat the maximum supported format * @since 1.15.2 */ public record Pack( @SerializedName("pack_format") int packFormat, @SerializedName("description") @NotNull String description, @SerializedName("supported_formats") @NotNull VersionRange supportedFormats, @SerializedName("min_format") @NotNull PackVersion minFormat, //>=1.21.9 @SerializedName("max_format") @NotNull PackVersion maxFormat ) { } /** * Represents a pack version, consisting of major and minor numbers. * * @param major the major version * @param minor the minor version * @since 1.15.2 */ public record PackVersion( int major, int minor ) { /** * Creates a pack version with only a major number. * * @param major the major version * @since 1.15.2 */ public PackVersion(int major) { this(major, 0); } /** * Converts this version to a JSON element. * * @return the JSON representation (primitive int or array) * @since 1.15.2 */ public @NotNull JsonElement toJson() { if (minor <= 0) { return new JsonPrimitive(major); } else { var array = new JsonArray(2); array.add(major); array.add(minor); return array; } } } /** * Represents the 'overlays' section of the metadata. * * @param entries the list of overlay entries * @since 1.15.2 */ public record Overlay(@Nullable List entries) { } /** * Represents a single entry in the overlays list. * * @param formats the range of formats this overlay applies to (removed in 1.21.9) * @param directory the directory name for the overlay * @param minFormat the minimum format (>= 1.21.9) * @param maxFormat the maximum format * @since 1.15.2 */ public record OverlayEntry( @NotNull VersionRange formats, //Removed in 1.21.9 @NotNull String directory, @SerializedName("min_format") @NotNull PackVersion minFormat, //>=1.21.9 @SerializedName("max_format") @NotNull PackVersion maxFormat ) { /** * Creates an overlay entry with a format range and directory. * * @param formats the format range * @param directory the directory * @since 1.15.2 */ public OverlayEntry( @NotNull VersionRange formats, //Removed in 1.21.9 @NotNull String directory ) { this( formats, directory, new PackVersion(formats.minInclusive), new PackVersion(formats.maxInclusive) ); } } /** * Represents a range of versions. * * @param minInclusive the minimum version (inclusive) * @param maxInclusive the maximum version (inclusive) * @since 1.15.2 */ public record VersionRange( @SerializedName("min_inclusive") int minInclusive, @SerializedName("max_inclusive") int maxInclusive ) { /** * Creates a version range for a single value. * * @param value the version value * @since 1.15.2 */ public VersionRange(int value) { this(value, value); } /** * Converts this range to a JSON element. * * @return the JSON representation (primitive int or array) * @since 1.15.2 */ public @NotNull JsonElement toJson() { if (minInclusive == maxInclusive) return new JsonPrimitive(minInclusive); var arr = new JsonArray(2); arr.add(minInclusive); arr.add(maxInclusive); return arr; } } /** * Builder for {@link PackMeta}. * * @since 1.15.2 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public static final class Builder { private int format = BetterModel.nms().version().getMetaVersion(); private String description = "BetterModel's default pack."; private final List entries = new ArrayList<>(); private VersionRange supportedFormats = new VersionRange( NMSVersion.first().getMetaVersion(), NMSVersion.latest().getMetaVersion() //<=1.21.8 ); private PackVersion minFormat = new PackVersion(NMSVersion.first().getMetaVersion()); private PackVersion maxFormat = new PackVersion(NMSVersion.latest().getMetaVersion()); /** * Sets the pack description. * * @param description the description * @return this builder * @since 1.15.2 */ public @NotNull Builder description(@NotNull String description) { this.description = Objects.requireNonNull(description, "description"); return this; } /** * Sets the pack format. * * @param format the format version * @return this builder * @since 1.15.2 */ public @NotNull Builder format(int format) { this.format = format; return this; } /** * Adds an overlay entry. * * @param overlayEntry the overlay entry to add * @return this builder * @since 1.15.2 */ public @NotNull Builder overlayEntry(@NotNull OverlayEntry overlayEntry) { entries.add(overlayEntry); return this; } /** * Sets the supported formats range. * * @param range the version range * @return this builder * @since 1.15.2 */ public @NotNull Builder supportedFormats(@NotNull VersionRange range) { supportedFormats = Objects.requireNonNull(range); return this; } /** * Sets the minimum supported format. * * @param version the minimum version * @return this builder * @since 1.15.2 */ public @NotNull Builder minFormat(@NotNull PackVersion version) { minFormat = Objects.requireNonNull(version); return this; } /** * Sets the maximum supported format. * * @param version the maximum version * @return this builder * @since 1.15.2 */ public @NotNull Builder maxFormat(@NotNull PackVersion version) { maxFormat = Objects.requireNonNull(version); return this; } /** * Builds the {@link PackMeta} instance. * * @return the created pack meta * @since 1.15.2 */ public @NotNull PackMeta build() { return new PackMeta( new Pack( format, description, supportedFormats, minFormat, maxFormat ), entries.isEmpty() ? null : new Overlay(entries) ); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackNamespace.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import org.jetbrains.annotations.NotNull; /** * Represents a namespace within a resource pack, providing access to standard subdirectories. *

* This class organizes resources into 'items', 'models', and 'textures' categories. *

* * @since 1.15.2 */ public final class PackNamespace { private final PackBuilder items, models, textures; PackNamespace(@NotNull PackAssets assets, @NotNull String namespace) { var subPath = assets.path.resolve("assets", namespace); items = new PackBuilder(assets, subPath.resolve("items")); models = new PackBuilder(assets, subPath.resolve("models")); textures = new PackBuilder(assets, subPath.resolve("textures", "item")); } /** * Returns the builder for the 'items' directory. * * @return the items builder * @since 1.15.2 */ public @NotNull PackBuilder items() { return items; } /** * Returns the builder for the 'models' directory. * * @return the models builder * @since 1.15.2 */ public @NotNull PackBuilder models() { return models; } /** * Returns the builder for the 'textures/item' directory. * * @return the textures builder * @since 1.15.2 */ public @NotNull PackBuilder textures() { return textures; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackObfuscator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import kr.toxicity.model.api.BetterModel; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Defines a strategy for obfuscating resource names in the pack. *

* Obfuscation can help reduce file path lengths and protect asset names. *

* * @since 1.15.2 */ public interface PackObfuscator { /** * A no-op obfuscator that returns the name as-is. * @since 1.15.2 */ PackObfuscator NONE = name -> name; /** * Obfuscates the given raw name. * * @param rawName the original name * @return the obfuscated name * @since 1.15.2 */ @NotNull String obfuscate(@NotNull String rawName); /** * Creates an order-based obfuscator if obfuscation is enabled in the configuration. * * @return the obfuscator * @since 1.15.2 */ static @NotNull PackObfuscator order() { return BetterModel.config().pack().useObfuscation() ? new Order() : NONE; } /** * Creates a pair obfuscator, combining this obfuscator (as textures) with another (as models). * * @param models the models obfuscator * @return the pair obfuscator * @since 1.15.2 */ default @NotNull Pair withModels(@NotNull PackObfuscator models) { return pair(models, this); } /** * Creates a pair obfuscator from two separate obfuscators. * * @param models the models obfuscator * @param textures the textures obfuscator * @return the pair obfuscator * @since 1.15.2 */ static @NotNull Pair pair(@NotNull PackObfuscator models, @NotNull PackObfuscator textures) { return new Pair(models, textures); } /** * An obfuscator that generates short names based on the order of appearance. * * @since 1.15.2 */ final class Order implements PackObfuscator { private static final char[] AVAILABLE_NAME = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'l', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6' ,'7', '8', '9' }; private static final int NAME_LENGTH = AVAILABLE_NAME.length; private final Map nameMap = new ConcurrentHashMap<>(); private final StringBuilder builder = new StringBuilder(); /** * Private initializer */ private Order() { } public @NotNull String obfuscate(@NotNull String rawName) { return nameMap.computeIfAbsent(rawName, _ -> { var size = nameMap.size(); builder.setLength(0); while (size >= NAME_LENGTH) { builder.append(AVAILABLE_NAME[size % NAME_LENGTH]); size /= NAME_LENGTH; } builder.append(AVAILABLE_NAME[size % NAME_LENGTH]); return builder.toString(); }); } } /** * A pair of obfuscators for models and textures. * * @param models the models obfuscator * @param textures the textures obfuscator * @since 1.15.2 */ record Pair(@NotNull PackObfuscator models, @NotNull PackObfuscator textures) {} } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackOverlay.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.util.function.BooleanConstantSupplier; import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.function.BooleanSupplier; /** * Represents a resource pack overlay, allowing for version-specific resources. *

* Overlays are used to support multiple Minecraft versions within a single resource pack. *

* * @param packName the name of the overlay (e.g., "legacy", "modern") * @param range the version range this overlay applies to * @param tester a supplier to determine if this overlay should be active * @since 1.15.2 */ public record PackOverlay( @NotNull String packName, @NotNull Optional range, @NotNull BooleanSupplier tester ) implements Comparable { /** * The default overlay (base pack). * @since 1.15.2 */ public static final PackOverlay DEFAULT = new PackOverlay( "", Optional.empty(), BooleanConstantSupplier.TRUE ); /** * The legacy overlay (for older versions). * @since 1.15.2 */ public static final PackOverlay LEGACY = new PackOverlay( "legacy", Optional.of(new PackMeta.VersionRange(22, 45)), () -> BetterModel.config().pack().generateLegacyModel() ); /** * The modern overlay (for newer versions). * @since 1.15.2 */ public static final PackOverlay MODERN = new PackOverlay( "modern", Optional.of(new PackMeta.VersionRange(46, 99)), () -> BetterModel.config().pack().generateModernModel() ); /** * Generates the root path for this overlay. * * @param namespace the namespace prefix * @return the pack path * @since 1.15.2 */ public @NotNull PackPath path(@NotNull String namespace) { return packName.isEmpty() ? PackPath.EMPTY : new PackPath(namespace + "_" + packName); } /** * Checks if this overlay is active. * * @return true if active, false otherwise * @since 1.15.2 */ public boolean test() { return tester.getAsBoolean(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PackOverlay that)) return false; return packName.equals(that.packName); } @Override public int hashCode() { return packName.hashCode(); } @Override public int compareTo(@NotNull PackOverlay o) { return packName.compareTo(o.packName); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackPath.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import org.jetbrains.annotations.NotNull; import static java.lang.String.join; /** * Represents a path within a resource pack. *

* This record encapsulates a string path and provides utility methods for resolving sub-paths. * It implements {@link Comparable} for sorting purposes. *

* * @param path the string representation of the path * @since 1.15.2 */ public record PackPath(@NotNull String path) implements Comparable { /** * The delimiter used for path separation. * @since 1.15.2 */ public static final String DELIMITER = "/"; /** * An empty pack path. * @since 1.15.2 */ public static final PackPath EMPTY = new PackPath(""); /** * Resolves a sub-path relative to this path. * * @param subPaths the sub-path components to resolve * @return the resolved pack path * @since 1.15.2 */ public @NotNull PackPath resolve(@NotNull String... subPaths) { if (subPaths.length == 0) return this; return new PackPath(path.isEmpty() ? join(DELIMITER, subPaths) : path + DELIMITER + join(DELIMITER, subPaths)); } @Override public @NotNull String toString() { return path; } @Override public int compareTo(@NotNull PackPath o) { return path.compareTo(o.path); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackResource.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.function.Supplier; /** * Represents a resource within a resource pack. *

* A resource consists of its content (as a byte array), its path, and optionally the overlay it belongs to. *

* * @since 1.15.2 */ public interface PackResource extends Supplier { /** * Returns the overlay this resource belongs to. * * @return the overlay, or null if it belongs to the base pack * @since 1.15.2 */ @Nullable PackOverlay overlay(); /** * Returns the path of this resource. * * @return the pack path * @since 1.15.2 */ @NotNull PackPath path(); /** * Returns the estimated size of this resource in bytes. * * @return the estimated size * @since 1.15.2 */ long estimatedSize(); /** * Creates a new pack resource for the base pack. * * @param path the path of the resource * @param size the estimated size * @param supplier the content supplier * @return the created resource * @since 1.15.2 */ static @NotNull PackResource of(@NotNull PackPath path, long size, @NotNull Supplier supplier) { return of(null, path, size, supplier); } /** * Creates a new pack resource for a specific overlay. * * @param overlay the overlay (or null for base pack) * @param path the path of the resource * @param size the estimated size * @param supplier the content supplier * @return the created resource * @since 1.15.2 */ static @NotNull PackResource of(@Nullable PackOverlay overlay, @NotNull PackPath path, long size, @NotNull Supplier supplier) { Objects.requireNonNull(path, "path"); Objects.requireNonNull(supplier, "supplier"); return new Packed(overlay, path, size, supplier); } /** * A simple implementation of {@link PackResource}. * * @param overlay the overlay * @param path the path * @param estimatedSize the estimated size * @param supplier the content supplier * @since 1.15.2 */ record Packed( @Nullable PackOverlay overlay, @NotNull PackPath path, long estimatedSize, @NotNull Supplier supplier ) implements PackResource { @Override public byte[] get() { return supplier.get(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackResult.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.io.File; import java.security.MessageDigest; import java.util.*; import java.util.stream.Stream; /** * Represents the result of a pack building process. *

* This class holds the generated pack metadata, the output directory, and the collection of generated resources (assets and overlays). * It also provides methods to calculate the pack hash and check for changes. *

* * @since 1.15.2 */ @RequiredArgsConstructor public final class PackResult { private final PackMeta meta; private final File directory; private final SortedMap> overlays = new TreeMap<>(); private final SortedSet assets = new TreeSet<>(); private final SortedSet assetsView = Collections.unmodifiableSortedSet(assets); private final long creationTime = System.currentTimeMillis(); private boolean frozen = false; private boolean changed = false; private UUID uuid; /** * Adds a resource to the result. * * @param overlay the overlay the resource belongs to (or null for base assets) * @param packByte the resource data * @throws IllegalStateException if the result is frozen * @since 1.15.2 */ @ApiStatus.Internal public void set(@Nullable PackOverlay overlay, @NotNull PackByte packByte) { if (frozen) throw new IllegalStateException("result is frozen."); if (overlay == null) { synchronized (assets) { assets.add(packByte); } return; } synchronized (overlays) { overlays.computeIfAbsent(overlay, _ -> new TreeSet<>()).add(packByte); } } /** * Freezes the result, preventing further modifications. * * @since 1.15.2 */ public void freeze() { freeze(false); } /** * Checks if the pack content has changed. * * @return true if changed, false otherwise * @since 1.15.2 */ public boolean changed() { return changed; } /** * Freezes the result and sets the changed status. * * @param changed whether the pack content has changed * @throws IllegalStateException if the result is already frozen * @since 1.15.2 */ public void freeze(boolean changed) { if (frozen) throw new IllegalStateException("result is frozen."); frozen = true; this.changed = changed; } /** * Returns the pack metadata. * * @return the pack metadata * @since 1.15.2 */ @NotNull public PackMeta meta() { return meta; } /** * Returns the output directory of the pack. * * @return the directory, or null if not applicable * @since 1.15.2 */ public @Nullable File directory() { return directory; } /** * Calculates and returns the SHA-256 hash of the pack content as a UUID. * * @return the hash UUID * @since 1.15.2 */ public @NotNull UUID hash() { if (uuid != null) return uuid; synchronized (this) { if (uuid != null) return uuid; try { var sha = MessageDigest.getInstance("SHA-256"); stream().map(PackByte::bytes).forEach(sha::update); return uuid = UUID.nameUUIDFromBytes(sha.digest()); } catch (Exception e) { return uuid = UUID.randomUUID(); } } } /** * Returns the total number of resources in the pack. * * @return the size * @since 1.15.2 */ public int size() { return assets.size() + overlays.values().stream().mapToInt(Set::size).sum(); } /** * Returns the time elapsed since the result was created. * * @return the elapsed time in milliseconds * @since 1.15.2 */ public long time() { return System.currentTimeMillis() - creationTime; } /** * Returns the resources for a specific overlay. * * @param overlay the overlay * @return the set of resources * @since 1.15.2 */ @NotNull @Unmodifiable public SortedSet overlays(@NotNull PackOverlay overlay) { var get = overlays.get(overlay); return get != null ? Collections.unmodifiableSortedSet(get) : Collections.emptySortedSet(); } /** * Returns a stream of all resources in the pack. * * @return the stream of resources * @since 1.15.2 */ public @NotNull Stream stream() { return Stream.concat( overlays.values().stream().flatMap(Collection::stream), assets.stream() ); } /** * Returns the base assets of the pack. * * @return the set of assets * @since 1.15.2 */ @NotNull @Unmodifiable public SortedSet assets() { return assetsView; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/pack/PackZipper.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.pack; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.util.LogUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Manages the assembly and zipping of resource pack contents. *

* This class coordinates the collection of assets across different overlays (default, legacy, modern) * and prepares the data for final pack generation. *

* * @since 1.15.2 */ public final class PackZipper { private PackZipper() { PackBuiltInAssets.applyAs(this); } private static final PackPath PACK_ICON = new PackPath("pack.png"); /** * Creates a new PackZipper instance. * * @return a new PackZipper * @since 1.15.2 */ public static @NotNull PackZipper zipper() { return new PackZipper(); } private final PackMeta.Builder metaBuilder = PackMeta.builder(); private final Map overlayMap = new ConcurrentHashMap<>(); /** * Retrieves the default assets collection. * * @return the default assets * @since 1.15.2 */ public @NotNull PackAssets assets() { return overlay(PackOverlay.DEFAULT); } /** * Retrieves the legacy assets collection. * * @return the legacy assets * @since 1.15.2 */ public @NotNull PackAssets legacy() { return overlay(PackOverlay.LEGACY); } /** * Retrieves the modern assets' collection. * * @return the modern assets * @since 1.15.2 */ public @NotNull PackAssets modern() { return overlay(PackOverlay.MODERN); } /** * Retrieves the assets collection for a specific overlay. * * @param overlay the overlay * @return the assets collection * @since 1.15.2 */ public @NotNull PackAssets overlay(@NotNull PackOverlay overlay) { return overlayMap.computeIfAbsent(overlay, PackAssets::new); } /** * Returns the builder for the pack metadata. * * @return the metadata builder * @since 1.15.2 */ public @NotNull PackMeta.Builder metaBuilder() { return metaBuilder; } /** * Builds the final pack data, including metadata and all resources. * * @return the build data * @since 1.15.2 */ @ApiStatus.Internal public @NotNull BuildData build() { var resources = new ArrayList(size()); for (Map.Entry entry : overlayMap.entrySet()) { var overlay = entry.getKey(); var value = entry.getValue(); if (overlay.test() && value.dirty()) { resources.addAll(value.resourceMap.values()); overlay.range().ifPresent(range -> metaBuilder.overlayEntry(new PackMeta.OverlayEntry(range, value.path.path()))); } value.resourceMap.clear(); } var meta = metaBuilder.build(); resources.add(meta.toResource()); var icon = loadIcon(); if (icon != null) resources.add(icon); return new BuildData(meta, resources); } /** * Returns the total estimated number of resources. * * @return the size * @since 1.15.2 */ public int size() { return overlayMap.values().stream().mapToInt(PackAssets::size).sum() + 2; } private static @Nullable PackResource loadIcon() { try ( var icon = BetterModel.platform().getResource("icon.png") ) { if (icon == null) return null; var read = icon.readAllBytes(); return PackResource.of(PACK_ICON, read.length, () -> read); } catch (IOException e) { LogUtil.handleException("Unable to get icon.png", e); return null; } } /** * Holds the result of a build operation. * * @param meta the generated pack metadata * @param resources the list of generated resources * @since 1.15.2 */ public record BuildData(@NotNull PackMeta meta, @NotNull List resources) { } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformAdapter.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Adapts platform-specific objects and operations to the BetterModel API. *

* This interface provides methods for retrieving players, creating items, and checking server state, * abstracting away the differences between platforms like Bukkit and Fabric. *

* * @since 2.0.0 */ public interface PlatformAdapter { /** * Returns the server's default view distance. * * @return the view distance in chunks * @since 2.0.0 */ int serverViewDistance(); /** * Checks if the current thread is the main server tick thread. * * @return true if on the tick thread, false otherwise * @since 2.0.0 */ boolean isTickThread(); /** * Checks if it is safe to access region data from the current thread. * * @return true if safe, false otherwise * @since 2.0.0 */ boolean isRegionSafe(); /** * Retrieves an online player by their UUID. * * @param uuid the player's UUID * @return the player, or null if not found/offline * @since 2.0.0 */ @Nullable PlatformPlayer player(@NotNull UUID uuid); /** * Retrieves an offline player by their UUID. * * @param uuid the player's UUID * @return the offline player * @since 2.0.0 */ @NotNull PlatformOfflinePlayer offlinePlayer(@NotNull UUID uuid); /** * Returns a platform-specific representation of an empty item stack (air). * * @return the air item stack * @since 2.0.0 */ @NotNull PlatformItemStack air(); /** * Returns a location at coordinates (0, 0, 0) in a default or null world. * * @return the zero location * @since 2.0.0 */ @NotNull PlatformLocation zero(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformBillboard.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; /** * Defines the billboard constraints for a display entity. *

* Billboard settings control how the display rotates to face the player. *

* * @since 2.0.0 */ public enum PlatformBillboard { /** * No rotation (default). */ FIXED, /** * Can pivot around vertical axis. */ VERTICAL, /** * Can pivot around horizontal axis. */ HORIZONTAL, /** * Can pivot around center point. */ CENTER } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.scheduler.ModelTask; import kr.toxicity.model.api.tracker.EntityTracker; import kr.toxicity.model.api.tracker.EntityTrackerRegistry; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Optional; import java.util.UUID; /** * Represents an entity in the underlying platform. *

* This interface provides access to basic entity properties like UUID and location, * as well as integration with the {@link EntityTrackerRegistry}. *

* * @since 2.0.0 */ public interface PlatformEntity extends PlatformRegionHolder { /** * Returns the unique identifier of the entity. * * @return the UUID * @since 2.0.0 */ @NotNull UUID uuid(); /** * Returns the current location of the entity. * * @return the location * @since 2.0.0 */ @NotNull PlatformLocation location(); @Override default @Nullable ModelTask task(@NotNull Runnable runnable) { return location().task(runnable); } @Override default @Nullable ModelTask taskLater(long delay, @NotNull Runnable runnable) { return location().taskLater(delay, runnable); } /** * Retrieves the tracker registry associated with this entity. * * @return an optional containing the registry if it exists * @since 2.0.0 */ default @NotNull Optional registry() { return BetterModel.registry(uuid()); } /** * Retrieves a specific tracker by name from this entity's registry. * * @param name the name of the tracker * @return an optional containing the tracker if found * @since 2.0.0 */ default @NotNull Optional tracker(@NotNull String name) { return registry().map(registry -> registry.tracker(name)); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformItemStack.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents an item stack in the underlying platform. *

* This interface provides methods for manipulating item properties like custom model data, * enchantment glint, and cloning. *

* * @since 2.0.0 */ public interface PlatformItemStack { /** * Checks if the item stack is empty or air. * * @return true if air, false otherwise * @since 2.0.0 */ boolean isAir(); /** * Sets the enchantment glint override for the item. * * @param enchant true to enable glint, false to disable * @return this item stack * @since 2.0.0 */ @NotNull PlatformItemStack enchant(boolean enchant); /** * Sets the custom model data and item model namespace for the item. * * @param customModelData the custom model data integer * @param namespace the item model namespace (optional) * @return this item stack * @since 2.0.0 */ @NotNull PlatformItemStack modelData(int customModelData, @Nullable PlatformNamespace namespace); /** * Creates a copy of this item stack. * * @return the cloned item stack * @since 2.0.0 */ @NotNull PlatformItemStack clone(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformItemTransform.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; /** * Defines the display context for an item model. *

* These values correspond to the display settings in a Minecraft item model file. *

* * @since 2.0.0 */ public enum PlatformItemTransform { /** * No specific transform. * @since 2.0.0 */ NONE, /** * Displayed in the left hand in third-person view. * @since 2.0.0 */ THIRDPERSON_LEFTHAND, /** * Displayed in the right hand in third-person view. * @since 2.0.0 */ THIRDPERSON_RIGHTHAND, /** * Displayed in the left hand in first-person view. * @since 2.0.0 */ FIRSTPERSON_LEFTHAND, /** * Displayed in the right hand in first-person view. * @since 2.0.0 */ FIRSTPERSON_RIGHTHAND, /** * Displayed on the head (e.g., helmet). * @since 2.0.0 */ HEAD, /** * Displayed in a GUI slot. * @since 2.0.0 */ GUI, /** * Displayed on the ground as an item entity. * @since 2.0.0 */ GROUND, /** * Displayed in an item frame. * @since 2.0.0 */ FIXED } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformLivingEntity.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; /** * Represents a living entity in the underlying platform. *

* This interface extends {@link PlatformEntity} to provide access to living entity-specific properties, * such as eye location. *

* * @since 2.0.0 */ public interface PlatformLivingEntity extends PlatformEntity { /** * Returns the eye location of the living entity. * * @return the eye location * @since 2.0.0 */ @NotNull PlatformLocation eyeLocation(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformLocation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; import static java.lang.Math.fma; import static java.lang.Math.sqrt; /** * Represents a location in the underlying platform. *

* This interface provides access to coordinates, rotation, and the world, * as well as methods for manipulating the location. *

* * @since 2.0.0 */ public interface PlatformLocation extends PlatformRegionHolder { /** * Returns the world associated with this location. * * @return the world * @since 2.0.0 */ PlatformWorld world(); /** * Returns the X coordinate. * * @return the X coordinate * @since 2.0.0 */ double x(); /** * Returns the Y coordinate. * * @return the Y coordinate * @since 2.0.0 */ double y(); /** * Returns the Z coordinate. * * @return the Z coordinate * @since 2.0.0 */ double z(); /** * Returns the pitch (vertical rotation). * * @return the pitch * @since 2.0.0 */ float pitch(); /** * Returns the yaw (horizontal rotation). * * @return the yaw * @since 2.0.0 */ float yaw(); /** * Creates a new location by adding the specified coordinates to this location. * * @param x the X offset * @param y the Y offset * @param z the Z offset * @return the new location * @since 2.0.0 */ @NotNull PlatformLocation add(double x, double y, double z); /** * Calculates the distance between this location and another location. * * @param other the other location * @return the distance * @since 2.1.0 */ default double distance(@NotNull PlatformLocation other) { return sqrt(distanceSquared(other)); } /** * Calculates the squared distance between this location and another location. * * @param other the other location * @return the squared distance * @since 2.1.0 */ default double distanceSquared(@NotNull PlatformLocation other) { var x = x() - other.x(); var y = y() - other.y(); var z = z() - other.z(); return fma(x, x, fma(y, y, z * z)); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformNamespace.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; /** * Represents a namespaced key (e.g., "minecraft:apple"). * * @param namespace the namespace (e.g., "minecraft") * @param path the path (e.g., "apple") * @since 2.0.0 */ public record PlatformNamespace(@NotNull String namespace, @NotNull String path) { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformOfflinePlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Represents an offline player in the underlying platform. *

* This interface provides access to basic player identification data, such as UUID and name, * without requiring the player to be online. *

* * @since 2.0.0 */ public interface PlatformOfflinePlayer { /** * Returns the unique identifier of the player. * * @return the UUID * @since 2.0.0 */ @NotNull UUID uuid(); /** * Returns the name of the player, if known. * * @return the player name, or null if unknown * @since 2.0.0 */ @Nullable String name(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformPlayer.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import org.jetbrains.annotations.NotNull; /** * Represents a player in the underlying platform. *

* This interface combines the properties of a living entity and an offline player, * providing access to player-specific data like name and online status. *

* * @since 2.0.0 */ public interface PlatformPlayer extends PlatformLivingEntity, PlatformOfflinePlayer { /** * Returns the name of the player. * * @return the player name * @since 2.0.0 */ @Override @NotNull String name(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformRegionHolder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; import kr.toxicity.model.api.scheduler.ModelTask; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Represents an object that holds a region context (e.g., an entity or location) for task scheduling. *

* This interface is crucial for platforms like Folia where tasks must be scheduled relative to a specific region. *

* * @since 2.0.0 */ public interface PlatformRegionHolder { /** * Schedules a task to run on the next tick, synchronized with this region holder. * * @param runnable the task to run * @return the scheduled task, or null if scheduling failed * @since 2.0.0 */ @Nullable ModelTask task(@NotNull Runnable runnable); /** * Schedules a task to run after a delay, synchronized with this region holder. * * @param delay the delay in ticks * @param runnable the task to run * @return the scheduled task, or null if scheduling failed * @since 2.0.0 */ @Nullable ModelTask taskLater(long delay, @NotNull Runnable runnable); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/platform/PlatformWorld.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.platform; /** * Represents a world in the underlying platform (Bukkit, Fabric, etc.). *

* This interface serves as an abstraction layer for world-related operations, * allowing the core engine to interact with worlds without direct dependencies on platform-specific APIs. *

* * @since 2.0.0 */ public interface PlatformWorld { } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/player/PlayerLimb.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.player; import kr.toxicity.model.api.armor.PlayerArmor; import kr.toxicity.model.api.bone.BoneItemMapper; import kr.toxicity.model.api.bone.BoneRenderContext; import kr.toxicity.model.api.nms.Profiled; import kr.toxicity.model.api.platform.PlatformItemTransform; import kr.toxicity.model.api.skin.SkinData; import kr.toxicity.model.api.util.TransformedItemStack; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import java.util.function.BiFunction; import java.util.function.Function; /** * Player limb data */ @RequiredArgsConstructor @Getter public enum PlayerLimb { /** * Head */ HEAD( SkinData::head, PlatformItemTransform.FIXED ), /** * Right arm */ RIGHT_ARM( SkinData::rightArm, PlatformItemTransform.FIXED ), /** * Right forearm */ RIGHT_FOREARM( (d, _) -> d.rightForeArm(), PlatformItemTransform.FIXED ), /** * Left arm */ LEFT_ARM( SkinData::leftArm, PlatformItemTransform.FIXED ), /** * Left forearm */ LEFT_FOREARM( (d, _) -> d.leftForeArm(), PlatformItemTransform.FIXED ), /** * Hip */ HIP( SkinData::hip, PlatformItemTransform.FIXED ), /** * Waist */ WAIST( SkinData::waist, PlatformItemTransform.FIXED ), /** * Chest */ CHEST( SkinData::chest, PlatformItemTransform.FIXED ), /** * Right leg */ RIGHT_LEG( SkinData::rightLeg, PlatformItemTransform.FIXED ), /** * Right foreleg */ RIGHT_FORELEG( SkinData::rightForeLeg, PlatformItemTransform.FIXED ), /** * Left leg */ LEFT_LEG( SkinData::leftLeg, PlatformItemTransform.FIXED ), /** * Left foreleg */ LEFT_FORELEG( SkinData::leftForeLeg, PlatformItemTransform.FIXED ), ; private final @NotNull BiFunction skinMapper; private final @NotNull PlatformItemTransform transform; @Getter private final @NotNull LimbItemMapper itemMapper = new LimbItemMapper(this::createItem); /** * Generates transformed item from player * @param context context * @return item */ public @NotNull TransformedItemStack createItem(@NotNull BoneRenderContext context) { return skinMapper.apply(context.skin(), context.source() instanceof Profiled profiled ? profiled.armors() : PlayerArmor.EMPTY); } /** * Limb item mapper */ @RequiredArgsConstructor public class LimbItemMapper implements BoneItemMapper { private final Function playerMapper; @NotNull @Override public PlatformItemTransform transform() { return transform; } @Override public @NotNull TransformedItemStack apply(@NotNull BoneRenderContext context, @NotNull TransformedItemStack transformedItemStack) { return playerMapper.apply(context); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/player/PlayerSkinParts.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.player; /** * Player skin parts * @param bitmask bit mask */ public record PlayerSkinParts(int bitmask) { /** * The default skin parts configuration where all parts (Cape, Jacket, Sleeves, Pants, Hat) are visible. */ public static final PlayerSkinParts DEFAULT = new PlayerSkinParts(0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40); /** * Checks if the 'Cape' part of the player's skin is enabled (visible). * * @return {@code true} if the cape is enabled, {@code false} otherwise. */ public boolean isCapeEnabled() { return (bitmask & 0x01) != 0; } /** * Checks if the 'Jacket' part of the player's skin is enabled (visible). * * @return {@code true} if the jacket is enabled, {@code false} otherwise. */ public boolean isJacketEnabled() { return (bitmask & 0x02) != 0; } /** * Checks if the 'Left Sleeve' part of the player's skin is enabled (visible). * * @return {@code true} if the left sleeve is enabled, {@code false} otherwise. */ public boolean isLeftSleeveEnabled() { return (bitmask & 0x04) != 0; } /** * Checks if the 'Right Sleeve' part of the player's skin is enabled (visible). * * @return {@code true} if the right sleeve is enabled, {@code false} otherwise. */ public boolean isRightSleeveEnabled() { return (bitmask & 0x08) != 0; } /** * Checks if the 'Left Pants Leg' part of the player's skin is enabled (visible). * * @return {@code true} if the left pants leg is enabled, {@code false} otherwise. */ public boolean isLeftPantsEnabled() { return (bitmask & 0x10) != 0; } /** * Checks if the 'Right Pants Leg' part of the player's skin is enabled (visible). * * @return {@code true} if the right pants leg is enabled, {@code false} otherwise. */ public boolean isRightPantsEnabled() { return (bitmask & 0x20) != 0; } /** * Checks if the 'Hat' (or head overlay) part of the player's skin is enabled (visible). * * @return {@code true} if the hat is enabled, {@code false} otherwise. */ public boolean isHatEnabled() { return (bitmask & 0x40) != 0; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/profile/ModelProfile.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.profile; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.platform.PlatformOfflinePlayer; import kr.toxicity.model.api.platform.PlatformPlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; import java.util.concurrent.CompletableFuture; /** * Model skin */ public interface ModelProfile { /** * Unknown skin */ ModelProfile UNKNOWN = of(ModelProfileInfo.UNKNOWN); /** * Creates profile * @param info info * @return profile */ static @NotNull ModelProfile of(@NotNull ModelProfileInfo info) { return new Simple(info, ModelProfileSkin.EMPTY); } /** * Creates profile * @param info info * @param skin skin * @return profile */ static @NotNull ModelProfile of(@NotNull ModelProfileInfo info, @NotNull ModelProfileSkin skin) { return new Simple(info, skin); } /** * Gets skin by player * @param player player * @return model profile */ static @NotNull ModelProfile of(@NotNull PlatformPlayer player) { var channel = BetterModel.platform().playerManager().player(player.uuid()); return channel != null ? channel.base().profile() : BetterModel.nms().profile(player); } /** * Gets uncompleted profile by offline player * @param offlinePlayer offline player * @return uncompleted profile */ static @NotNull Uncompleted of(@NotNull PlatformOfflinePlayer offlinePlayer) { return BetterModel.platform().profileManager().supplier().supply(offlinePlayer); } /** * Gets uncompleted profile by offline player's uuid * @param uuid offline player's uuid * @return uncompleted profile */ static @NotNull Uncompleted of(@NotNull UUID uuid) { return of(BetterModel.platform().adapter().offlinePlayer(uuid)); } /** * Gets info * @return info */ @NotNull ModelProfileInfo info(); /** * Gets skin * @return skin */ @NotNull ModelProfileSkin skin(); /** * Makes this profile as uncompleted * @return uncompleted profile */ default @NotNull Uncompleted asUncompleted() { return new Uncompleted() { @Override public @NotNull ModelProfileInfo info() { return ModelProfile.this.info(); } @Override public @NotNull CompletableFuture complete() { return CompletableFuture.completedFuture(ModelProfile.this); } }; } /** * Gets player * @return player */ default @Nullable PlatformPlayer player() { return BetterModel.platform().adapter().player(info().id()); } /** * Simple profile * @param info info * @param skin skin */ record Simple(@NotNull ModelProfileInfo info, @NotNull ModelProfileSkin skin) implements ModelProfile { } /** * Uncompleted profile */ interface Uncompleted { /** * Gets info * @return info */ @NotNull ModelProfileInfo info(); /** * Completes profile * @return completed profile */ @NotNull CompletableFuture complete(); /** * Gets fallback profile * @return profile */ default @NotNull ModelProfile fallback() { return of(info()); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/profile/ModelProfileInfo.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.profile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; /** * Profile info * @param id id * @param name name */ public record ModelProfileInfo(@NotNull UUID id, @Nullable String name) { /** * Unknown info */ public static final ModelProfileInfo UNKNOWN = new ModelProfileInfo( UUID.fromString("00000000-0000-0000-0000-000000000000"), null ); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/profile/ModelProfileSkin.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.profile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.net.URI; /** * Profile skin * @param skin skin * @param cape cape * @param slim is slim model * @param raw raw textures */ public record ModelProfileSkin( @Nullable URI skin, @Nullable URI cape, boolean slim, @NotNull String raw ) { /** * Empty skin */ public static final ModelProfileSkin EMPTY = new ModelProfileSkin(null, null, false, ""); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/profile/ModelProfileSupplier.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.profile; import kr.toxicity.model.api.platform.PlatformOfflinePlayer; import org.jetbrains.annotations.NotNull; /** * Profile supplier */ public interface ModelProfileSupplier { /** * Supplies profile * @param info info * @return uncompleted profile */ @NotNull ModelProfile.Uncompleted supply(@NotNull ModelProfileInfo info); /** * Supplies profile by player * @param player player * @return uncompleted profile */ default @NotNull ModelProfile.Uncompleted supply(@NotNull PlatformOfflinePlayer player) { return supply(new ModelProfileInfo(player.uuid(), player.name())); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/scheduler/ModelScheduler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.scheduler; import org.jetbrains.annotations.NotNull; /** * A scheduler of BetterModel */ public interface ModelScheduler { /** * Runs async task * @param runnable task * @return scheduled task */ @NotNull ModelTask asyncTask(@NotNull Runnable runnable); /** * Runs async task * @param delay delay * @param runnable task * @return scheduled task */ @NotNull ModelTask asyncTaskLater(long delay, @NotNull Runnable runnable); /** * Runs async task * @param delay delay * @param period period * @param runnable task * @return scheduled task */ @NotNull ModelTask asyncTaskTimer(long delay, long period, @NotNull Runnable runnable); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/scheduler/ModelTask.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.scheduler; /** * A scheduled task of BetterModel */ public interface ModelTask { /** * Checks this task is canceled * @return whether to cancel */ boolean isCancelled(); /** * Cancels this task */ void cancel(); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/script/AnimationScript.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.script; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Objects; import java.util.function.Consumer; /** * Animation script */ public interface AnimationScript extends Consumer { /** * Empty script */ AnimationScript EMPTY = of(_ -> {}); @Override void accept(@NotNull Tracker tracker); /** * Checks this script should be called in tick thread * @return requires tick thread */ boolean isSync(); /** * Creates a timed script * @param time time * @return timed script */ default @NotNull TimeScript time(float time) { return new TimeScript(time, this); } /** * Creates script * @param source consumer * @return script */ static @NotNull AnimationScript of(@NotNull Consumer source) { return of(false, source); } /** * Creates script * @param isSync should be called in tick thread * @param source consumer * @return script */ static @NotNull AnimationScript of(boolean isSync, @NotNull Consumer source) { Objects.requireNonNull(source); return new AnimationScript() { @Override public boolean isSync() { return isSync; } @Override public void accept(@NotNull Tracker tracker) { source.accept(tracker); } }; } /** * Sums a script list to one * @param scriptList list of a script * @return merged script */ static @NotNull AnimationScript of(@NotNull List scriptList) { return switch (scriptList.size()) { case 0 -> EMPTY; case 1 -> scriptList.getFirst(); default -> { var sync = false; Consumer consumer = _ -> {}; for (AnimationScript entityScript : scriptList) { sync |= entityScript.isSync(); consumer = consumer.andThen(entityScript); } yield of(sync, consumer); } }; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/script/BlueprintScript.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.script; import kr.toxicity.model.api.animation.AnimationIterator; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.animation.TimedStorage; import kr.toxicity.model.api.data.raw.ModelAnimation; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.List; /** * A script data of blueprint. * @param name script name * @param type type * @param length playtime * @param scripts scripts */ @ApiStatus.Internal public record BlueprintScript(@NotNull String name, @NotNull AnimationIterator.Type type, float length, @NotNull List scripts) { /** * Creates empty script * @param animation animation * @return empty script */ public static @NotNull BlueprintScript fromEmpty(@NotNull ModelAnimation animation) { return new BlueprintScript( animation.name(), animation.loop(), animation.length(), List.of(TimeScript.EMPTY, AnimationScript.EMPTY.time(animation.length())) ); } /** * Creates animation iterator of this script * @param modifier modifier * @return animation iterator */ public @NotNull AnimationIterator iterator(@NotNull AnimationModifier modifier) { return modifier.type(type).create(TimedStorage.listOf(scripts)); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/script/ScriptBuilder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.script; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.math.BigDecimal; import java.util.Map; /** * Script parser */ @FunctionalInterface public interface ScriptBuilder { /** * Build an entity script by data * @param data script data * @return script */ @NotNull AnimationScript build(@NotNull ScriptData data); record ScriptData( @Nullable String args, @NotNull ScriptMetaData metadata ) {} interface ScriptMetaData { @NotNull @Unmodifiable Map toMap(); default @Nullable Boolean asBoolean(@NotNull String key) { var get = toMap().get(key); if (get == null) return null; return switch (get) { case "true" -> true; case "false" -> false; default -> null; }; } default @Nullable Number asNumber(@NotNull String key) { var get = toMap().get(key); if (get == null) return null; try { return new BigDecimal(get); } catch (NumberFormatException e) { return null; } } default @Nullable String asString(@NotNull String key) { return toMap().get(key); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/script/TimeScript.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.script; import kr.toxicity.model.api.animation.Timed; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.NotNull; /** * Script with time * @param time time * @param script source script */ public record TimeScript(float time, @NotNull AnimationScript script) implements AnimationScript, Timed { public static final TimeScript EMPTY = AnimationScript.EMPTY.time(0); @Override public boolean isSync() { return script.isSync(); } @Override public void accept(@NotNull Tracker tracker) { script.accept(tracker); } public @NotNull TimeScript time(float newTime) { if (time == newTime) return this; return new TimeScript(newTime, script); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/skin/SkinData.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.skin; import kr.toxicity.model.api.armor.PlayerArmor; import kr.toxicity.model.api.profile.ModelProfile; import kr.toxicity.model.api.util.TransformedItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Skin data of player. */ public interface SkinData { /** * Gets model skin * @return skin */ @NotNull ModelProfile profile(); /** * Gets head part * @param armor armor * @return head */ @NotNull TransformedItemStack head(@NotNull PlayerArmor armor); /** * Gets hip part * @param armor armor * @return hip */ @NotNull TransformedItemStack hip(@NotNull PlayerArmor armor); /** * Gets waist part * @param armor armor * @return waist */ @NotNull TransformedItemStack waist(@NotNull PlayerArmor armor); /** * Gets chest part * @param armor armor * @return chest */ @NotNull TransformedItemStack chest(@NotNull PlayerArmor armor); /** * Gets left arm part * @param armor armor * @return left arm */ @NotNull TransformedItemStack leftArm(@NotNull PlayerArmor armor); /** * Gets left forearm part * @return left forearm */ @NotNull TransformedItemStack leftForeArm(); /** * Gets right arm part * @param armor armor * @return right arm */ @NotNull TransformedItemStack rightArm(@NotNull PlayerArmor armor); /** * Gets right forearm part * @return right forearm */ @NotNull TransformedItemStack rightForeArm(); /** * Gets left leg part * @param armor armor * @return left leg */ @NotNull TransformedItemStack leftLeg(@NotNull PlayerArmor armor); /** * Gets left foreleg part * @param armor armor * @return left foreleg */ @NotNull TransformedItemStack leftForeLeg(@NotNull PlayerArmor armor); /** * Gets right leg part * @param armor armor * @return right leg */ @NotNull TransformedItemStack rightLeg(@NotNull PlayerArmor armor); /** * Gets right foreleg part * @param armor armor * @return right foreleg */ @NotNull TransformedItemStack rightForeLeg(@NotNull PlayerArmor armor); /** * Gets cape * @param armor armor * @return cape */ @Nullable TransformedItemStack cape(@NotNull PlayerArmor armor); } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/DummyTracker.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.data.renderer.RenderPipeline; import kr.toxicity.model.api.event.CreateDummyTrackerEvent; import kr.toxicity.model.api.nms.PlayerChannelHandler; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.util.EventUtil; import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.function.Consumer; /** * A tracker implementation that is not attached to any entity. *

* Dummy trackers are positioned at a fixed location in the world and can be moved manually. * They are useful for static models or models controlled entirely by scripts/plugins/mods. *

* * @since 1.15.2 */ public final class DummyTracker extends Tracker { private volatile PlatformLocation location; /** * Creates a new dummy tracker. * * @param location the initial location * @param pipeline the render pipeline * @param modifier the tracker modifier * @param preUpdateConsumer a consumer to run before the first update * @since 1.15.2 */ public DummyTracker(@NotNull PlatformLocation location, @NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { super(pipeline, modifier); this.location = location; animate("spawn", AnimationModifier.DEFAULT_WITH_PLAY_ONCE); pipeline.scale(() -> scaler().scale(this)); rotation(() -> new ModelRotation(this.location.pitch(), this.location.yaw())); preUpdateConsumer.accept(this); EventUtil.call(CreateDummyTrackerEvent.class, () -> new CreateDummyTrackerEvent(this)); } /** * Moves the model to a new location. * * @param location the new location * @since 1.15.2 */ public void location(@NotNull PlatformLocation location) { Objects.requireNonNull(location, "location"); if (this.location.equals(location)) return; synchronized (this) { this.location = location; var bundler = pipeline.createBundler(); pipeline.forEach(b -> b.teleport(location, bundler)); if (bundler.isNotEmpty()) pipeline.allPlayer().map(PlayerChannelHandler::player).forEach(bundler::send); } } /** * Returns the current location of the tracker. * * @return the location * @since 1.15.2 */ @Override public @NotNull PlatformLocation location() { return location; } /** * Spawns the model for a specific player. * * @param player the target player * @since 1.15.2 */ public void spawn(@NotNull PlatformPlayer player) { var bundler = pipeline.createBundler(); spawn(player, bundler); bundler.send(player); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/EntityBodyRotator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.util.FunctionUtil; import kr.toxicity.model.api.util.MathUtil; import kr.toxicity.model.api.util.lazy.LazyFloatProvider; import lombok.AllArgsConstructor; import lombok.Setter; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Supplier; /** * Manages the body and head rotation logic for an entity tracker. *

* This class handles the complex interactions between body yaw, head yaw, and pitch, * including smoothing, clamping, and player-specific behaviors. *

* * @since 1.15.2 */ public final class EntityBodyRotator { private static final float DEGREE_EPSILON = 1 / MathUtil.DEGREES_TO_PACKED_BYTE; private final EntityTrackerRegistry registry; private final BaseEntity entity; private final LazyFloatProvider provider; private final Supplier headSupplier; private final Supplier bodySupplier; private final AtomicBoolean rotationLock = new AtomicBoolean(); private int tick; private final Quaternionf headRotation = new Quaternionf(); private ModelRotation rotation; private volatile boolean headUneven; private volatile boolean bodyUneven; private volatile boolean playerMode; private volatile float minBody; private volatile float maxBody; private volatile float minHead; private volatile float maxHead; private volatile float stable; private volatile int rotationDuration; private volatile int rotationDelay; static @NotNull RotatorData defaultData() { return new RotatorData( false, false, false, -75, 75, -75, 75, 15, 10, 10 ); } EntityBodyRotator(@NotNull EntityTrackerRegistry registry) { this.registry = registry; this.entity = registry.entity(); this.rotation = new ModelRotation( entity.pitch(), entity.yaw() ); this.provider = new LazyFloatProvider(entity.yaw(), () -> rotationDuration * MathUtil.MINECRAFT_TICK_MILLS); var vector = new Vector3f(); var vectorSupplier = LazyFloatProvider.ofVector(() -> 3 * MathUtil.MINECRAFT_TICK_MILLS, () -> vector.set( clampHead(entity.pitch()), clampHead(wrapDegrees(bodyRotation().y() - entity.headYaw())), 0 )); headSupplier = FunctionUtil.throttleTick(Tracker.TRACKER_TICK_INTERVAL, () -> MathUtil.toQuaternion(vectorSupplier.get(), headRotation)); bodySupplier = FunctionUtil.throttleTick(() -> new ModelRotation( entity.pitch(), bodyRotation0() )); reset(); } private float clampHead(float value) { return Math.clamp(value, headUneven ? minHead : -maxHead, maxHead); } private float clampBody(float value, float compare) { return Math.clamp(value, compare + (bodyUneven ? minBody : -maxBody), compare + maxBody); } /** * Locks or unlocks the rotation updates. * * @param lock true to lock, false to unlock * @return true if the state changed * @since 1.15.2 */ public boolean lockRotation(boolean lock) { return rotationLock.compareAndSet(!lock, lock); } @NotNull ModelRotation bodyRotation() { return rotationLock.get() ? rotation : (rotation = bodySupplier.get()); } private float bodyRotation0() { if (playerMode) return entity.bodyYaw(); if (registry.hasControllingPassenger()) return entity.yaw(); if (entity.onWalk()) { tick = rotationDelay; return stableBodyYaw(); } else if (MathUtil.isSimilar(entity.headYaw(), rotation.y(), DEGREE_EPSILON)) { tick = 0; return entity.headYaw(); } else if (++tick > rotationDelay) { var headYaw = entity.headYaw(); var providedYaw = provider.updateAndGet(headYaw); return wrapDegrees(clampBody(providedYaw, headYaw)); } provider.storedValue(rotation.y()); return rotation.y(); } private float stableBodyYaw() { var bodyYaw = rotation.y(); var yaw = entity.yaw(); var minStable = yaw - stable; var maxStable = yaw + stable; return wrapDegrees(Math.clamp(bodyYaw, Math.min(minStable, maxStable), Math.max(minStable, maxStable))); } private static float wrapDegrees(float value) { var f = value % 360.0F; if (f >= 180.0F) f -= 360.0F; if (f < -180.0F) f += 360.0F; return f; } @NotNull Quaternionf headRotation() { return rotationLock.get() ? headRotation : headSupplier.get(); } /** * Configures the rotator using a consumer. * * @param consumer the configuration consumer * @since 1.15.2 */ public void setValue(@NotNull Consumer consumer) { Objects.requireNonNull(consumer); var data = createData(); consumer.accept(data); setValue(data); } synchronized void setValue(@NotNull RotatorData data) { data.set(this); } /** * Resets the rotator to default settings. * * @since 1.15.2 */ public void reset() { setValue(defaultData()); } synchronized @NotNull RotatorData createData() { return new RotatorData( headUneven, bodyUneven, playerMode, minBody, maxBody, minHead, maxHead, stable, rotationDuration, rotationDelay ); } /** * Configuration data for the entity body rotator. * * @since 1.15.2 */ @Setter @AllArgsConstructor public static final class RotatorData { @SerializedName("head_uneven") private boolean headUneven; @SerializedName("body_uneven") private boolean bodyUneven; @SerializedName("player_mode") private boolean playerMode; @SerializedName("min_body") private float minBody; @SerializedName("max_body") private float maxBody; @SerializedName("min_head") private float minHead; @SerializedName("max_head") private float maxHead; @SerializedName("stable") private float stable; @SerializedName("rotation_duration") private int rotationDuration; @SerializedName("rotation_delay") private int rotationDelay; private void set(@NotNull EntityBodyRotator rotator) { rotator.headUneven = headUneven; rotator.bodyUneven = bodyUneven; rotator.playerMode = playerMode; rotator.minBody = Math.min(minBody, maxBody); rotator.maxBody = Math.max(minBody, maxBody); rotator.minHead = Math.min(minHead, maxHead); rotator.maxHead = Math.max(minHead, maxHead); rotator.stable = Math.max(stable, 0); rotator.rotationDuration = Math.max(rotationDuration, 0); rotator.rotationDelay = Math.max(rotationDelay, 0); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/EntityHideOption.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.gson.JsonArray; import org.jetbrains.annotations.NotNull; import java.util.stream.Stream; /** * Configuration for hiding various visual aspects of an entity. *

* This record allows selective hiding of equipment, fire effects, the entity body itself, and glowing effects. *

* * @param equipment whether to hide equipment * @param fire whether to hide burning state * @param visibility whether to hide entity's body * @param glowing whether to hide entity's glowing state * @since 1.15.2 */ public record EntityHideOption( boolean equipment, boolean fire, boolean visibility, boolean glowing ) { /** * Default option (hides everything). * @since 1.15.2 */ public static final EntityHideOption DEFAULT = new EntityHideOption( true, true, true, true ); /** * Disabled option (hides nothing). * @since 1.15.2 */ public static final EntityHideOption FALSE = builder().build(); /** * Composites multiple options into a single one using OR logic. * * @param options the stream of options * @return the composited option * @since 1.15.2 */ public static @NotNull EntityHideOption composite(@NotNull Stream options) { return builder() .composite(options) .build(); } /** * Deserializes hide option from a JSON array. * * @param array the JSON array * @return the option * @since 1.15.2 */ public static @NotNull EntityHideOption deserialize(@NotNull JsonArray array) { return new EntityHideOption( array.get(0).getAsBoolean(), array.get(1).getAsBoolean(), array.get(2).getAsBoolean(), array.get(3).getAsBoolean() ); } /** * Serializes hide option to a JSON array. * * @return the JSON array * @since 1.15.2 */ public @NotNull JsonArray serialize() { var array = new JsonArray(4); array.add(equipment); array.add(fire); array.add(visibility); array.add(glowing); return array; } /** * Creates a new builder for {@link EntityHideOption}. * * @return the builder * @since 1.15.2 */ public static @NotNull Builder builder() { return new Builder(); } /** * Builder for {@link EntityHideOption}. * * @since 1.15.2 */ public static final class Builder { private boolean equipment; private boolean fire; private boolean visibility; private boolean glowing; /** * Private initializer */ private Builder() { } /** * Composites multiple options into this builder. * * @param options the stream of options * @return this builder * @since 1.15.2 */ public @NotNull Builder composite(@NotNull Stream options) { options.forEach(this::or); return this; } /** * Merges another hide option using OR logic. * * @param option the option to merge * @return this builder * @since 1.15.2 */ public @NotNull Builder or(@NotNull EntityHideOption option) { equipment |= option.equipment; fire |= option.fire; visibility |= option.visibility; glowing |= option.glowing; return this; } /** * Builds the {@link EntityHideOption}. * * @return the created option * @since 1.15.2 */ public @NotNull EntityHideOption build() { return new EntityHideOption( equipment, fire, visibility, glowing ); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/EntityTracker.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.bone.BoneMovement; import kr.toxicity.model.api.bone.BoneTags; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.data.renderer.RenderPipeline; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.entity.BasePlayer; import kr.toxicity.model.api.event.CreateEntityTrackerEvent; import kr.toxicity.model.api.event.DismountModelEvent; import kr.toxicity.model.api.event.MountModelEvent; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.nms.HitBoxListener; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.util.EventUtil; import kr.toxicity.model.api.util.FunctionUtil; import kr.toxicity.model.api.util.MathUtil; import kr.toxicity.model.api.util.function.BonePredicate; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; /** * A tracker implementation that is attached to a living entity. *

* This tracker synchronizes the model's position, rotation, and animations with the target entity. * It handles hitboxes, nametags, damage tinting, and mounting mechanics. *

* * @since 1.15.2 */ public class EntityTracker extends Tracker { private static final BonePredicate CREATE_HITBOX_PREDICATE = BonePredicate.name("hitbox") .or(BonePredicate.tag(BoneTags.HITBOX)) .or(b -> b.getGroup().getMountController().canMount()) .notSet(); private static final BonePredicate CREATE_NAMETAG_PREDICATE = BonePredicate.tag(BoneTags.TAG, BoneTags.MOB_TAG, BoneTags.PLAYER_TAG).notSet(); private static final BonePredicate HITBOX_REFRESH_PREDICATE = BonePredicate.from(r -> r.getHitBox() != null); private static final BonePredicate HEAD_PREDICATE = BonePredicate.tag(BoneTags.HEAD).notSet(); private static final BonePredicate HEAD_WITH_CHILDREN_PREDICATE = BonePredicate.tag(BoneTags.HEAD_WITH_CHILDREN).withChildren(); private final EntityTrackerRegistry registry; private final AtomicInteger damageTintValue = new AtomicInteger(0xFF8080); private final AtomicLong damageTint = new AtomicLong(-1); private final Set markForSpawn = ConcurrentHashMap.newKeySet(); private final EntityBodyRotator bodyRotator; private EntityHideOption hideOption = EntityHideOption.DEFAULT; private volatile PlatformLocation location; /** * Creates a new entity tracker. * * @param registry the entity tracker registry * @param pipeline the render pipeline * @param modifier the tracker modifier * @param preUpdateConsumer a consumer to run before the first update * @since 1.15.2 */ @ApiStatus.Internal public EntityTracker(@NotNull EntityTrackerRegistry registry, @NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { super(pipeline, modifier); this.registry = registry; this.location = registry.entity().location(); bodyRotator = new EntityBodyRotator(registry); var entity = registry.entity(); var scale = FunctionUtil.throttleTickFloat(() -> scaler().scale(this)); //Shadow Optional.ofNullable(bone("shadow")) .ifPresent(bone -> { var box = bone.getGroup().getHitBox(); if (box == null) return; var shadow = BetterModel.nms().create(entity.location(), d -> { if (entity instanceof BasePlayer) d.moveDuration(1); }); var baseScale = (float) (box.x() + box.z()) / 4F; var posCache = new BoneMovement(); tick(((_, s) -> { var wPos = bone.hitBoxPosition(posCache); shadow.shadowRadius(scale.getAsFloat() * baseScale); shadow.syncPotionEffect(entity); shadow.syncPosition(location().add(wPos.x, wPos.y, wPos.z)); shadow.sendDirtyEntityData(s.getDataBundler()); shadow.sendPosition(entity, s.getTickBundler()); })); pipeline.spawnPacketHandler(shadow::spawnWithEntityData); pipeline.showPacketHandler(shadow::spawnWithEntityData); pipeline.despawnPacketHandler(shadow::remove); pipeline.hidePacketHandler(shadow::remove); }); //Animation pipeline.defaultPosition(vec -> entity.passengerPosition(vec).mul(-1)); pipeline.scale(scale); Function headRotator = r -> r.mul(bodyRotator.headRotation()); pipeline.addGlobalRotModifier(HEAD_PREDICATE, headRotator); pipeline.addGlobalRotModifier(HEAD_WITH_CHILDREN_PREDICATE, headRotator); createNametag(CREATE_NAMETAG_PREDICATE, (bone, tag) -> { if (bone.name().tagged(BoneTags.PLAYER_TAG)) { tag.alwaysVisible(true); } else if (bone.name().tagged(BoneTags.MOB_TAG)) { tag.alwaysVisible(false); } else tag.alwaysVisible(entity instanceof BasePlayer); tag.component(entity.customName()); }); listenHitBox((b, l) -> l .create(h -> registry.hitBoxCache.put(h.uuid(), h)) .remove(h -> registry.hitBoxCache.remove(h.uuid())) .mount((h, e) -> { registry.mountedHitBoxCache.put(e.uuid(), new EntityTrackerRegistry.MountedHitBox(e, h)); EventUtil.call(MountModelEvent.class, () -> new MountModelEvent(this, b, h, e)); }) .dismount((h, e) -> { registry.mountedHitBoxCache.remove(e.uuid()); EventUtil.call(DismountModelEvent.class, () -> new DismountModelEvent(this, b, h, e)); })); entity.platform().task(() -> { if (isClosed()) return; createHitBox(null, CREATE_HITBOX_PREDICATE); }); tick((_, _) -> updateLocation()); tick((_, _) -> { if (damageTint.getAndDecrement() == 0) update(TrackerUpdateAction.previousTint()); }); rotation(bodyRotator::bodyRotation); preUpdateConsumer.accept(this); EventUtil.call(CreateEntityTrackerEvent.class, () -> new CreateEntityTrackerEvent(this)); } @Override public @NotNull ModelRotation rotation() { return sourceEntity().dead() ? pipeline.getRotation() : super.rotation(); } /** * Synchronizes the tracker with the base entity's data asynchronously. * * @since 1.15.2 */ public void updateBaseEntity() { if (sourceEntity().dead() || isClosed()) return; BetterModel.platform().scheduler().asyncTaskLater(1, () -> { var entity = sourceEntity(); pipeline.forEach(bone -> bone.applyAtDisplay(BonePredicate.TRUE, display -> display.syncPotionEffect(entity))); updateLocation(); forceUpdate(true); }); } private void updateLocation() { var loc = sourceEntity().location(); if (this.location.distanceSquared(loc) < MathUtil.VECTOR_COMPARISON_EPSILON_SQ) return; synchronized (this) { this.location = loc; } pipeline.forEach(bone -> bone.applyAtDisplay(BonePredicate.TRUE, display -> display.syncPosition(loc))); } /** * Returns the entity tracker registry associated with this tracker. * * @return the registry * @since 1.15.2 */ public @NotNull EntityTrackerRegistry registry() { return registry; } /** * Creates hitboxes for the entity based on a predicate. * * @param listener the hitbox listener * @param predicate the bone predicate * @return true if any hitboxes were created * @since 1.15.2 */ public boolean createHitBox(@Nullable HitBoxListener listener, @NotNull BonePredicate predicate) { return createHitBox(registry.entity(), listener, predicate); } /** * Retrieves or creates a hitbox for the entity. * * @param listener the hitbox listener * @param predicate the bone predicate * @return the hitbox, or null if not found/created * @since 1.15.2 */ public @Nullable HitBox hitbox(@Nullable HitBoxListener listener, @NotNull Predicate predicate) { return hitbox(registry.entity(), listener, predicate); } /** * Returns the current damage tint color value. * * @return the hex color value * @since 1.15.2 */ public int damageTintValue() { return damageTintValue.get(); } /** * Sets the damage tint color value. * * @param tint the hex color value * @since 1.15.2 */ public void damageTintValue(int tint) { damageTintValue.set(tint); } /** * Triggers the damage tint effect if enabled. * * @since 1.15.2 */ public void damageTint() { if (!modifier().damageTint()) return; var get = damageTint.get(); if (get < 0 && damageTint.compareAndSet(get, 10)) task(() -> update(TrackerUpdateAction.tint(damageTintValue()))); } @Override public void despawn() { if (sourceEntity().dead()) { close(CloseReason.DESPAWN); return; } super.despawn(); } @Override public @NotNull PlatformLocation location() { return location; } /** * Returns the source entity being tracked. * * @return the source entity * @since 1.15.2 */ public @NotNull BaseEntity sourceEntity() { return registry.entity(); } /** * Cancels the active damage tint effect. * * @since 1.15.2 */ public void cancelDamageTint() { damageTint.set(-1); } /** * Refreshes the tracker, updating entity data and hitboxes. * * @since 1.15.2 */ @ApiStatus.Internal public void refresh() { updateLocation(); registry.entity().platform().task(() -> createHitBox(null, HITBOX_REFRESH_PREDICATE)); } /** * Marks a player for spawning the model. * * @param player the player * @return true if the player was added * @since 1.15.2 */ public boolean markPlayerForSpawn(@NotNull PlatformPlayer player) { return markForSpawn.add(player.uuid()); } /** * Marks a set of players for spawning the model. * * @param uuids the set of player UUIDs * @return true if any players were added * @since 1.15.2 */ public boolean markPlayerForSpawn(@NotNull Set uuids) { return markForSpawn.addAll(uuids); } /** * Unmarks a player for spawning the model. * * @param player the player * @return true if the player was removed * @since 1.15.2 */ public boolean unmarkPlayerForSpawn(@NotNull PlatformPlayer player) { return markForSpawn.remove(player.uuid()); } /** * Converts the current tracker state to a {@link TrackerData} object. * * @return the tracker data * @since 1.15.2 */ public @NotNull TrackerData asTrackerData() { return new TrackerData( name(), scaler, rotator, modifier, bodyRotator.createData(), hideOption, markForSpawn ); } /** * Returns the entity body rotator. * * @return the body rotator * @since 1.15.2 */ public @NotNull EntityBodyRotator bodyRotator() { return bodyRotator; } /** * Checks if the model can be spawned for a specific player. * * @param player the player * @return true if allowed * @since 1.15.2 */ public boolean canBeSpawnedAt(@NotNull PlatformPlayer player) { return markForSpawn.isEmpty() || markForSpawn.contains(player.uuid()); } /** * Returns the hide option for this tracker. * * @return the hide option * @since 1.15.2 */ public @NotNull EntityHideOption hideOption() { return hideOption; } /** * Sets the hide option for this tracker. * * @param hideOption the new hide option * @since 1.15.2 */ public void hideOption(@NotNull EntityHideOption hideOption) { this.hideOption = Objects.requireNonNull(hideOption); } /** * Checks if this tracker's data can be saved. * * @return true if saveable * @since 1.15.2 */ public boolean canBeSaved() { return pipeline.getParent().type().isCanBeSaved(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/EntityTrackerRegistry.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ReferenceMap; import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.config.DebugConfig; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.entity.BasePlayer; import kr.toxicity.model.api.nms.HitBox; import kr.toxicity.model.api.nms.ModelDisplay; import kr.toxicity.model.api.nms.PacketBundler; import kr.toxicity.model.api.nms.PlayerChannelHandler; import kr.toxicity.model.api.platform.PlatformEntity; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.util.CollectionUtil; import kr.toxicity.model.api.util.FunctionUtil; import kr.toxicity.model.api.util.LogUtil; import kr.toxicity.model.api.util.function.FloatSupplier; import kr.toxicity.model.api.util.lock.DuplexLock; import lombok.RequiredArgsConstructor; import lombok.ToString; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; /** * Manages all entity trackers for a specific entity. *

* This registry handles the lifecycle of trackers attached to an entity, including loading, saving, * spawning, and despawning. It acts as a central hub for accessing and manipulating models on an entity. *

* * @since 1.15.2 */ @ToString(onlyExplicitlyIncluded = true) public final class EntityTrackerRegistry { private static final Object2ReferenceMap UUID_REGISTRY_MAP = new Object2ReferenceOpenHashMap<>(); private static final Int2ReferenceMap ID_REGISTRY_MAP = new Int2ReferenceOpenHashMap<>(); private static final DuplexLock REGISTRY_LOCK = new DuplexLock(); @ToString.Include private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicBoolean loaded = new AtomicBoolean(); @ToString.Include private final BaseEntity entity; private final int id; private final UUID uuid; private final ConcurrentNavigableMap trackerMap = new ConcurrentSkipListMap<>(); @ToString.Include private final Collection trackers = Collections.unmodifiableCollection(trackerMap.values()); private final Map viewedPlayerMap = new ConcurrentHashMap<>(); final AnimationProperty animationProperty; final Map hitBoxCache = new ConcurrentHashMap<>(); private final Collection hitBox = Collections.unmodifiableCollection(hitBoxCache.values()); final Map mountedHitBoxCache = new ConcurrentHashMap<>(); private final Map mountedHitBox = Collections.unmodifiableMap(mountedHitBoxCache); /** * Retrieves a registry by entity UUID. * * @param uuid the entity UUID * @return the registry, or null if not found * @since 1.15.2 */ public static @Nullable EntityTrackerRegistry registry(@NotNull UUID uuid) { return REGISTRY_LOCK.accessToReadLock(() -> UUID_REGISTRY_MAP.get(uuid)); } /** * Retrieves a registry by entity ID. * * @param id the entity ID * @return the registry, or null if not found * @since 1.15.2 */ public static @Nullable EntityTrackerRegistry registry(int id) { return REGISTRY_LOCK.accessToReadLock(() -> ID_REGISTRY_MAP.get(id)); } /** * Retrieves a registry for a base entity. * * @param entity the base entity * @return the registry, or null if the entity has no model data * @since 1.15.2 */ public static @Nullable EntityTrackerRegistry registry(@NotNull BaseEntity entity) { var get = registry(entity.uuid()); if (get != null) return get; return entity.hasModelData() ? create(entity) : null; } /** * Iterates over all active registries. * * @param consumer the consumer to apply * @since 1.15.2 */ public static void registries(@NotNull Consumer consumer) { for (EntityTrackerRegistry registry : registries()) { consumer.accept(registry); } } /** * Returns a list of all active registries. * * @return the list of registries * @since 1.15.2 */ public static @NotNull @Unmodifiable List registries() { return REGISTRY_LOCK.accessToReadLock(() -> ImmutableList.copyOf(UUID_REGISTRY_MAP.values())); } /** * Gets or creates a registry for a base entity. * * @param entity the base entity * @return the registry * @since 1.15.2 */ @ApiStatus.Internal public static @NotNull EntityTrackerRegistry getOrCreate(@NotNull BaseEntity entity) { var get = registry(entity.uuid()); return get != null ? get : create(entity); } private static @NotNull EntityTrackerRegistry create(@NotNull BaseEntity entity) { var uuid = entity.uuid(); EntityTrackerRegistry registry; synchronized (uuid) { var get2 = registry(uuid); if (get2 != null) return get2; registry = new EntityTrackerRegistry(entity); REGISTRY_LOCK.accessToWriteLock(() -> { UUID_REGISTRY_MAP.put(registry.uuid, registry); ID_REGISTRY_MAP.put(registry.id, registry); return null; }); } registry.initialLoad(); return registry; } private static @NotNull Collection deserialize(@Nullable String raw) { if (raw == null) return Collections.emptyList(); var json = JsonParser.parseString(raw); return json.isJsonArray() ? json.getAsJsonArray().asList() : Collections.singletonList(json); } private EntityTrackerRegistry(@NotNull BaseEntity entity) { this.entity = entity; this.uuid = entity.uuid(); this.id = entity.id(); animationProperty = new AnimationProperty(); } /** * Returns the source entity. * * @return the entity * @since 1.15.2 */ public @NotNull BaseEntity entity() { return entity; } /** * Returns the entity UUID. * * @return the UUID * @since 1.15.2 */ public @NotNull UUID uuid() { return uuid; } /** * Returns the entity ID. * * @return the ID * @since 1.15.2 */ public int id() { return id; } /** * Returns all trackers in this registry. * * @return the trackers * @since 1.15.2 */ public @NotNull @Unmodifiable Collection trackers() { return trackers; } /** * Retrieves a tracker by key. * * @param key the key (model ID), or null for the first tracker * @return the tracker, or null if not found * @since 1.15.2 */ public @Nullable EntityTracker tracker(@Nullable String key) { return key == null ? first() : trackerMap.get(key); } /** * Returns the first tracker in the registry. * * @return the first tracker, or null if empty * @since 1.15.2 */ public @Nullable EntityTracker first() { var entry = trackerMap.firstEntry(); return entry != null ? entry.getValue() : null; } /** * Creates a new tracker in this registry. * * @param key the key (model ID) * @param supplier the supplier to create the tracker * @return the created tracker * @since 1.15.2 */ @ApiStatus.Internal public @NotNull EntityTracker create(@NotNull String key, @NotNull Function supplier) { var created = supplier.apply(this); if (putTracker(key, created)) { refreshSpawn(); save(); } return created; } /** * Gets or creates a tracker in this registry. * * @param key the key (model ID) * @param supplier the supplier to create the tracker * @return the tracker * @since 1.15.2 */ @ApiStatus.Internal public @NotNull EntityTracker getOrCreate(@NotNull String key, @NotNull Function supplier) { var get = trackerMap.get(key); return get != null ? get : create(key, supplier); } private boolean putTracker(@NotNull String key, @NotNull EntityTracker created) { if (isClosed() || created.isClosed()) return false; created.handleCloseEvent((_, r) -> { if (isClosed()) return; if (trackerMap.compute(key, (_, v) -> v == created ? null : v) == null) { LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> uuid + "'s tracker " + key + " has been removed. (" + trackerMap.size() + ")"); } if (trackerMap.isEmpty()) close(r); else refreshRemove(); }); var previous = trackerMap.put(key, created); if (previous != null) previous.close(); return true; } private void refreshSpawn() { viewedPlayer().forEach(value -> spawnIfNotSpawned(value.player())); } private void refreshRemove() { for (PlayerChannelCache value : viewedPlayerMap.values()) { value.hide(); } } private void initialLoad() { if (BetterModel.platform().adapter().isRegionSafe() && loaded.compareAndSet(false, true)) { load(); refreshPlayer(); } } private void refreshPlayer() { entity.trackedBy() .map(p -> BetterModel.player(p.uuid()).orElse(null)) .filter(Objects::nonNull) .forEach(this::registerPlayer); } /** * Removes a tracker from the registry. * * @param key the key (model ID) * @return true if removed successfully * @since 1.15.2 */ public boolean remove(@NotNull String key) { try (var removed = trackerMap.remove(key)) { save(); return removed != null; } } /** * Checks if the registry is closed. * * @return true if closed * @since 1.15.2 */ public boolean isClosed() { return closed.get(); } /** * Closes the registry. * * @return true if closed successfully * @since 1.15.2 */ public boolean close() { return close(Tracker.CloseReason.REMOVE); } /** * Closes the registry with a specific reason. * * @param reason the close reason * @return true if closed successfully * @since 1.15.2 */ public boolean close(@NotNull Tracker.CloseReason reason) { if (!closed.compareAndSet(false, true)) return false; viewedPlayer().forEach(value -> value.sendEntityData(this)); viewedPlayerMap.clear(); for (EntityTracker value : trackers()) { value.close(reason); } if (!reason.shouldBeSave()) runSync(() -> entity.modelData(null)); REGISTRY_LOCK.accessToWriteLock(() -> { UUID_REGISTRY_MAP.remove(uuid); ID_REGISTRY_MAP.remove(id); if (entity instanceof BasePlayer player) player.updateInventory(); return null; }); LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> uuid + "'s tracker registry has been removed. (" + UUID_REGISTRY_MAP.size() + ")"); return true; } /** * Reloads the registry, refreshing all trackers. * * @since 1.15.2 */ public void reload() { closed.set(true); var data = new ArrayList(trackerMap.size()); for (EntityTracker value : trackers()) { value.close(); if (value.canBeSaved()) data.add(value.asTrackerData()); } trackerMap.clear(); closed.set(false); load(data.stream()); } /** * Refreshes the registry state. * * @since 1.15.2 */ public void refresh() { if (entity.dead()) return; for (EntityTracker value : trackers()) { value.refresh(); } refreshPlayer(); refreshSpawn(); } /** * Despawns all trackers in the registry. * * @since 1.15.2 */ public void despawn() { for (EntityTracker value : trackers()) { if (!value.forRemoval()) value.despawn(); } viewedPlayerMap.clear(); } /** * Loads trackers from a stream of data. * * @param stream the data stream * @since 1.15.2 */ public void load(@NotNull Stream stream) { stream.forEach(parsed -> BetterModel.model(parsed.id()).ifPresent(model -> model.create(entity, parsed.modifier(), parsed::applyAs))); save(); } /** * Loads trackers from the entity's persistent data. * * @since 1.15.2 */ public void load() { load(deserialize(entity.modelData()) .stream() .map(TrackerData::deserialize)); } /** * Saves the current tracker state to the entity's persistent data. * * @since 1.15.2 */ public void save() { var data = serialize(); if (!data.isEmpty()) runSync(() -> entity.modelData(data.toString())); } private void runSync(@NotNull Runnable runnable) { if (BetterModel.platform().adapter().isTickThread()) { runnable.run(); } else entity.platform().task(runnable); } /** * Returns a stream of all displays from all trackers. * * @return the displays * @since 1.15.2 */ public @NotNull Stream displays() { return trackers() .stream() .flatMap(Tracker::displays); } /** * Serializes the registry state to a JSON array. * * @return the JSON array * @since 1.15.2 */ public @NotNull JsonArray serialize() { return CollectionUtil.mapToJson(trackers().stream().filter(EntityTracker::canBeSaved), value -> value.asTrackerData().serialize()); } /** * Checks if any tracker is spawned for a player. * * @param player the player * @return true if spawned * @since 1.15.2 */ public boolean isSpawned(@NotNull PlatformPlayer player) { return isSpawned(player.uuid()); } /** * Checks if any tracker is spawned for a player UUID. * * @param uuid the player UUID * @return true if spawned * @since 1.15.2 */ public boolean isSpawned(@NotNull UUID uuid) { return viewedPlayerMap.containsKey(uuid) && trackers() .stream() .anyMatch(t -> t.isSpawned(uuid)); } /** * Spawns trackers for a player. * * @param player the player * @return true if spawned successfully * @since 1.15.2 */ public boolean spawn(@NotNull PlatformPlayer player) { initialLoad(); return spawn(player, false); } /** * Spawns trackers for a player only if not already spawned. * * @param player the player * @return true if spawned successfully * @since 1.15.2 */ public boolean spawnIfNotSpawned(@NotNull PlatformPlayer player) { initialLoad(); return spawn(player, true); } private boolean spawn(@NotNull PlatformPlayer player, boolean shouldNotSpawned) { var handler = BetterModel.platform() .playerManager() .player(player.uuid()); if (handler == null) return false; var cache = registerPlayer(handler); if (trackerMap.isEmpty()) return false; var bundler = BetterModel.nms().createBundler(10); for (EntityTracker value : trackers()) { if (shouldNotSpawned && value.isSpawned(player)) continue; if (value.canBeSpawnedAt(player)) value.spawn(player, bundler); } if (bundler.isEmpty()) return false; BetterModel.nms().mount(this, bundler); cache.spawn(bundler); return true; } private @NotNull PlayerChannelCache registerPlayer(@NotNull PlayerChannelHandler handler) { return viewedPlayerMap.computeIfAbsent(handler.uuid(), _ -> new PlayerChannelCache(handler)); } /** * Returns a stream of all players viewing this registry. * * @return the players * @since 1.15.2 */ public @NotNull Stream viewedPlayer() { return viewedPlayerMap.values().stream().map(c -> c.channelHandler); } /** * Removes a player from viewing this registry. * * @param player the player * @return true if removed successfully * @since 1.15.2 */ public boolean remove(@NotNull PlatformPlayer player) { var cache = viewedPlayerMap.remove(player.uuid()); if (cache == null) return false; var handler = cache.channelHandler; handler.sendEntityData(this); for (EntityTracker value : trackers()) { if (!value.forRemoval() && value.isSpawned(player)) value.remove(handler.player()); } return true; } /** * Returns the hide option for a specific player. * * @param uuid the player UUID * @return the hide option * @since 1.15.2 */ public @NotNull EntityHideOption hideOption(@NotNull UUID uuid) { var cache = viewedPlayerMap.get(uuid); return cache != null ? cache.hideOption : EntityHideOption.FALSE; } /** * Returns the map of currently mounted hitboxes. * * @return the mounted hitboxes * @since 1.15.2 */ @NotNull @Unmodifiable public Map mountedHitBox() { return mountedHitBox; } /** * Returns a collection of all active hitboxes for this registry. * * @return the hitboxes * @since 2.2.0 */ @NotNull @Unmodifiable public Collection hitBoxes() { return hitBox; } /** * Checks if any hitbox has a passenger. * * @return true if there is a passenger * @since 1.15.2 */ public boolean hasPassenger() { return !mountedHitBox().isEmpty(); } /** * Checks if any hitbox has a controlling passenger. * * @return true if there is a controlling passenger * @since 1.15.2 */ public boolean hasControllingPassenger() { return mountedHitBox() .values() .stream() .map(MountedHitBox::hitBox) .anyMatch(HitBox::hasBeenControlled); } /** * Represents a hitbox that has an entity mounted on it. * * @param entity the mounted entity * @param hitBox the hitbox itself * @since 1.15.2 */ public record MountedHitBox(@NotNull PlatformEntity entity, @NotNull HitBox hitBox) { /** * Dismounts the entity from the hitbox. * @since 1.15.2 */ public void dismount() { hitBox.dismount(entity); } /** * Dismounts all entities from the hitbox. * @since 1.15.2 */ public void dismountAll() { hitBox.dismountAll(); } } final class AnimationProperty { final FloatSupplier damageTick = FunctionUtil.throttleTickFloat(entity::damageTick); final FloatSupplier walkSpeed = FunctionUtil.throttleTickFloat(() -> entity.walkSpeed() + (float) Math.sqrt(damageTick.getAsFloat())); final BooleanSupplier onWalk = FunctionUtil.throttleTickBoolean(() -> entity.onWalk() || damageTick.getAsFloat() > 0.25 || hitBoxes().stream().anyMatch(HitBox::onWalk)); final BooleanSupplier onFly = FunctionUtil.throttleTickBoolean(entity::fly); } @RequiredArgsConstructor private class PlayerChannelCache { private final PlayerChannelHandler channelHandler; private volatile EntityHideOption hideOption = EntityHideOption.DEFAULT; private void hide() { reapplyHideOption(); BetterModel.nms().hide(channelHandler, EntityTrackerRegistry.this); } private void spawn(@NotNull PacketBundler bundler) { reapplyHideOption(); bundler.send(channelHandler.player(), () -> BetterModel.nms().hide(channelHandler, EntityTrackerRegistry.this, () -> viewedPlayerMap.containsKey(channelHandler.uuid()))); } private synchronized void reapplyHideOption() { hideOption = EntityHideOption.composite(trackers() .stream() .filter(t -> t.isSpawned(channelHandler.uuid())) .map(EntityTracker::hideOption)); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/ModelRotation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.NotNull; /** * Represents the rotation of a model in degrees. *

* This record stores pitch (x) and yaw (y) values and provides utility methods for conversion. *

* * @param x the pitch (x-rotation) in degrees * @param y the yaw (y-rotation) in degrees * @since 1.15.2 */ public record ModelRotation(float x, float y) { /** * A rotation of (0, 0). * @since 1.15.2 */ public static final ModelRotation EMPTY = new ModelRotation(0, 0); /** * An invalid rotation value used for initialization or error states. * @since 1.15.2 */ public static final ModelRotation INVALID = new ModelRotation(Float.MAX_VALUE, Float.MAX_VALUE); @Override public boolean equals(Object o) { if (o == this) return true; return o instanceof ModelRotation other && packedX() == other.packedX() && packedY() == other.packedY(); } @Override public int hashCode() { return ((Byte.hashCode(packedX()) & 0xFF) << 8) | (Byte.hashCode(packedY()) & 0xFF); } /** * Returns a new rotation with only the pitch component. * * @return the pitch-only rotation * @since 1.15.2 */ public @NotNull ModelRotation pitch() { return new ModelRotation(x, 0); } /** * Returns a new rotation with only the yaw component. * * @return the yaw-only rotation * @since 1.15.2 */ public @NotNull ModelRotation yaw() { return new ModelRotation(0, y); } /** * Returns the pitch in radians. * * @return the pitch in radians * @since 1.15.2 */ public float radianX() { return x * MathUtil.DEGREES_TO_RADIANS; } /** * Returns the yaw in radians. * * @return the yaw in radians * @since 1.15.2 */ public float radianY() { return y * MathUtil.DEGREES_TO_RADIANS; } /** * Returns the pitch packed as a byte (Minecraft protocol format). * * @return the packed pitch * @since 1.15.2 */ public byte packedX() { return (byte) (x * MathUtil.DEGREES_TO_PACKED_BYTE); } /** * Returns the yaw packed as a byte (Minecraft protocol format). * * @return the packed yaw * @since 1.15.2 */ public byte packedY() { return (byte) (y * MathUtil.DEGREES_TO_PACKED_BYTE); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/ModelRotator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import kr.toxicity.model.api.util.CollectionUtil; import kr.toxicity.model.api.util.lazy.LazyFloatProvider; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; /** * Defines how a model's rotation is calculated and applied. *

* Rotators can modify the base rotation (e.g., only applying yaw, smoothing rotation) * and can be chained together. *

* * @since 1.15.2 */ public sealed interface ModelRotator extends BiFunction { /** * The global deserializer instance for rotators. * @since 1.15.2 */ Deserializer DESERIALIZER = new Deserializer(); /** * Default rotator (applies rotation as-is). * @since 1.15.2 */ @NotNull ModelRotator DEFAULT = Objects.requireNonNull(DESERIALIZER._default.apply()); /** * Empty rotator (returns zero rotation). * @since 1.15.2 */ @NotNull ModelRotator EMPTY = Objects.requireNonNull(DESERIALIZER.empty.apply()); /** * Pitch-only rotator. * @since 1.15.2 */ @NotNull ModelRotator PITCH = Objects.requireNonNull(DESERIALIZER.pitch.apply()); /** * Yaw-only rotator. * @since 1.15.2 */ @NotNull ModelRotator YAW = Objects.requireNonNull(DESERIALIZER.yaw.apply()); /** * Deserializes a rotator from a JSON object. * * @param object the JSON object * @return the deserialized rotator, or EMPTY if invalid * @since 1.15.2 */ static @NotNull ModelRotator deserialize(@NotNull JsonObject object) { var result = DESERIALIZER.deserialize(object); return result != null ? result : EMPTY; } /** * Creates a lazy rotator that smooths rotation over time. * * @param mills the smoothing duration in milliseconds * @return the lazy rotator * @since 1.15.2 */ static @NotNull ModelRotator lazy(long mills) { return Objects.requireNonNull(DESERIALIZER.lazy.apply(mills)); } /** * Returns the name of this rotator type. * * @return the name * @since 1.15.2 */ @NotNull String name(); /** * Returns the source rotator if this is a chained rotator. * * @return the source rotator, or null * @since 1.15.2 */ @Nullable ModelRotator source(); /** * Returns the configuration data for this rotator. * * @return the data, or null * @since 1.15.2 */ @Nullable JsonElement data(); /** * Returns the root rotator in the chain. * * @return the root rotator * @since 1.15.2 */ default @NotNull ModelRotator root() { var source = source(); return source != null ? source.root() : this; } /** * Serializes this rotator to a JSON object. * * @return the JSON object * @since 1.15.2 */ default @NotNull JsonObject serialize() { var json = new JsonObject(); json.addProperty("name", name()); var d = data(); if (d != null) json.add("data", d); var s = source(); if (s != null) json.add("source", s.serialize()); return json; } /** * Applies the rotator to a tracker with default rotation. * * @param tracker the tracker * @return the calculated rotation * @since 1.15.2 */ default @NotNull ModelRotation apply(@NotNull Tracker tracker) { return apply(tracker, ModelRotation.EMPTY); } /** * Applies the rotator to a tracker with a base rotation. * * @param tracker the tracker * @param rotation the base rotation * @return the calculated rotation * @since 1.15.2 */ @Override @NotNull ModelRotation apply(@NotNull Tracker tracker, @NotNull ModelRotation rotation); /** * Chains this rotator with another one. * * @param rotator the next rotator in the chain * @return the chained rotator * @since 1.15.2 */ default @NotNull ModelRotator then(@NotNull ModelRotator rotator) { return new SourcedRotator(this, rotator); } /** * Implementation of a chained rotator. * * @param source source rotator * @param delegate delegated rotator * @since 1.15.2 */ record SourcedRotator(@NotNull ModelRotator source, @NotNull ModelRotator delegate) implements ModelRotator { @Override public @NotNull String name() { return delegate.name(); } @Override public @Nullable JsonElement data() { return delegate.data(); } @Override public @NotNull ModelRotation apply(@NotNull Tracker tracker, @NotNull ModelRotation rotation) { return delegate.apply(tracker, source.apply(tracker, rotation)); } } /** * Functional interface for calculating rotation. * * @since 1.15.2 */ interface Getter { /** * Default getter returning the input rotation. * @since 1.15.2 */ Getter DEFAULT = of(r -> r); /** * Calculates the rotation. * * @param tracker the tracker * @param modelRotation the base rotation * @return the calculated rotation * @since 1.15.2 */ @NotNull ModelRotation apply(@NotNull Tracker tracker, @NotNull ModelRotation modelRotation); /** * Creates a constant rotation getter. * * @param rotator the rotation * @return the getter * @since 1.15.2 */ static @NotNull Getter of(@NotNull ModelRotation rotator) { return (_, _) -> rotator; } /** * Creates a supplier-based rotation getter. * * @param rotator the supplier * @return the getter * @since 1.15.2 */ static @NotNull Getter of(@NotNull Supplier rotator) { return (_, _) -> rotator.get(); } /** * Creates a function-based rotation getter. * * @param rotator the function * @return the getter * @since 1.15.2 */ static @NotNull Getter of(@NotNull Function rotator) { return (_, r) -> rotator.apply(r); } } /** * Builder interface for creating Getters from JSON. * * @since 1.15.2 */ interface Builder { /** * Builds a getter from JSON data. * * @param element the JSON data * @return the getter, or null if invalid * @since 1.15.2 */ @Nullable Getter build(@NotNull JsonElement element); } /** * Helper interface for built-in deserializers. * * @since 1.15.2 */ interface BuiltInDeserializer extends Function { @Override @Nullable ModelRotator apply(@NotNull JsonElement element); /** * Deserializes a default instance. * * @return the rotator * @since 1.15.2 */ default @Nullable ModelRotator apply() { return apply(JsonNull.INSTANCE); } /** * Deserializes from a long value. * * @param value the value * @return the rotator * @since 1.15.2 */ default @Nullable ModelRotator apply(long value) { return apply(new JsonPrimitive(value)); } } /** * Registry and factory for rotators. * * @since 1.15.2 */ final class Deserializer { private final Map builderMap = CollectionUtil.newAddressingMap(); private final BuiltInDeserializer _default = register("default", _ -> Getter.of(r -> r)); private final BuiltInDeserializer empty = register("empty", _ -> Getter.of(ModelRotation.EMPTY)); private final BuiltInDeserializer yaw = register("yaw", _ -> Getter.of(ModelRotation::yaw)); private final BuiltInDeserializer pitch = register("pitch", _ -> Getter.of(ModelRotation::pitch)); private final BuiltInDeserializer lazy = register("lazy", j -> { if (j.isJsonPrimitive()) { var f = j.getAsLong(); var xLazy = new LazyFloatProvider(f); var yLazy = new LazyFloatProvider(f); return Getter.of(r -> new ModelRotation(xLazy.updateAndGet(r.x()), yLazy.updateAndGet(r.y()))); } else return null; }); private Deserializer() { } /** * Registers a new rotator type. * * @param name the rotator name * @param builder the builder * @return a built-in deserializer helper * @since 1.15.2 */ public @NotNull BuiltInDeserializer register(@NotNull String name, @NotNull Builder builder) { var get = builderMap.putIfAbsent(name, builder); var selected = get != null ? get : builder; return e -> { var build = selected.build(e); var source = e.isJsonObject() ? e.getAsJsonObject().get("source") : null; return build != null ? pack(name, source != null && source.isJsonObject() ? deserialize(source.getAsJsonObject()) : null, e, build) : null; }; } /** * Deserializes a rotator from a JSON object. * * @param object the JSON object * @return the rotator, or null if invalid * @since 1.15.2 */ public @Nullable ModelRotator deserialize(@NotNull JsonObject object) { var rawName = object.getAsJsonPrimitive("name"); if (rawName == null) return null; var name = rawName.getAsString(); var get = builderMap.get(name); if (get == null) return null; var data = object.get("data"); var source = object.getAsJsonObject().get("source"); var build = get.build(data == null ? JsonNull.INSTANCE : data); return build != null ? pack( name, source != null && source.isJsonObject() ? deserialize(source.getAsJsonObject()) : null, data, build ) : null; } private @NotNull Pack pack(@NotNull String name, @Nullable ModelRotator source, @Nullable JsonElement data, @NotNull Getter getter) { return new Pack(name, source, data, getter); } private record Pack(@NotNull String name, @Nullable ModelRotator source, @Nullable JsonElement data, @NotNull Getter delegate) implements ModelRotator { @Override public @NotNull ModelRotation apply(@NotNull Tracker tracker, @NotNull ModelRotation modelRotation) { return delegate.apply(tracker, modelRotation); } } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/ModelScaler.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.gson.*; import kr.toxicity.model.api.util.CollectionUtil; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Function; /** * Defines how a model's scale is calculated. *

* Scalers can be constant values, derived from entity attributes, or composites of multiple scalers. * They are serializable to JSON for configuration purposes. *

* * @since 1.15.2 */ public sealed interface ModelScaler { /** * The global deserializer instance for scalers. * @since 1.15.2 */ Deserializer DESERIALIZER = new Deserializer(); /** * Returns the name of this scaler type. * * @return the name * @since 1.15.2 */ @NotNull String name(); /** * Calculates the scale for a given tracker. * * @param tracker the tracker * @return the calculated scale factor * @since 1.15.2 */ float scale(@NotNull Tracker tracker); /** * Returns the configuration data for this scaler as a JSON element. * * @return the data, or null if none * @since 1.15.2 */ @Nullable JsonElement data(); /** * Deserializes a scaler from a JSON object. * * @param element the JSON object * @return the deserialized scaler, or the default scaler if invalid * @since 1.15.2 */ static @NotNull ModelScaler deserialize(@NotNull JsonObject element) { var scaler = DESERIALIZER.buildScaler(element); return scaler != null ? scaler : defaultScaler(); } /** * Returns the default scaler (constant 1.0). * * @return the default scaler * @since 1.15.2 */ static @NotNull ModelScaler defaultScaler() { return DESERIALIZER.defaultScaler(); } /** * Returns a scaler that uses the entity's scale attribute. * * @return the entity scaler * @since 1.15.2 */ static @NotNull ModelScaler entity() { return DESERIALIZER.entity.deserialize(); } /** * Returns a constant value scaler. * * @param value the scale value * @return the value scaler * @since 1.15.2 */ static @NotNull ModelScaler value(float value) { return DESERIALIZER.value.deserialize(value); } /** * Creates a composite scaler that multiplies the results of multiple scalers. * * @param scalers the scalers to combine * @return the composite scaler * @since 1.15.2 */ static @NotNull ModelScaler composite(@NotNull ModelScaler... scalers) { return new Composite(new Composite.CompositeGetter(Arrays.asList(scalers))); } /** * Multiplies this scaler by a constant value. * * @param value the multiplier * @return the new composite scaler * @since 1.15.2 */ default @NotNull ModelScaler multiply(float value) { return composite(value(value)); } /** * Multiplies this scaler by another scaler. * * @param scaler the other scaler * @return the new composite scaler * @since 1.15.2 */ default @NotNull ModelScaler composite(@NotNull ModelScaler scaler) { var list = new ArrayList(); if (this instanceof Composite composite) { list.addAll(composite.getter.list); } else list.add(this); if (scaler instanceof Composite composite) { list.addAll(composite.getter.list); } else list.add(scaler); return new Composite(new Composite.CompositeGetter(list)); } /** * Serializes this scaler to a JSON object. * * @return the JSON object * @since 1.15.2 */ default @NotNull JsonObject serialize() { var json = new JsonObject(); json.addProperty("name", name()); var d = data(); if (d != null) json.add("data", d); return json; } /** * Functional interface for calculating scale. * * @since 1.15.2 */ interface Getter { /** * Default getter returning 1.0. * @since 1.15.2 */ Getter DEFAULT = _ -> 1F; /** * Getter using entity scale. * @since 1.15.2 */ Getter ENTITY = t -> t instanceof EntityTracker entityTracker ? (float) entityTracker.registry().entity().scale() : 1F; /** * Calculates the scale. * * @param tracker the tracker * @return the scale * @since 1.15.2 */ float get(@NotNull Tracker tracker); /** * Creates a constant value getter. * * @param value the value * @return the getter * @since 1.15.2 */ static @NotNull Getter value(float value) { return _ -> value; } } /** * Builder interface for creating Getters from JSON. * * @since 1.15.2 */ interface Builder { /** * Builds a getter from JSON data. * * @param data the JSON data * @return the getter, or null if invalid * @since 1.15.2 */ @Nullable Getter build(@NotNull JsonElement data); } /** * Implementation of a composite scaler. * * @since 1.15.2 */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) final class Composite implements ModelScaler { private final CompositeGetter getter; private record CompositeGetter(@NotNull List list) implements Getter { @Override public float get(@NotNull Tracker tracker) { var f = 1F; for (ModelScaler modelScaler : list) { f *= modelScaler.scale(tracker); } return f; } } @NotNull @Override public String name() { return "composite"; } @Override public float scale(@NotNull Tracker tracker) { return getter.get(tracker); } private void add(@NotNull JsonArray array, @NotNull ModelScaler scaler) { if (scaler instanceof Composite composite) { for (ModelScaler childScaler : composite.getter.list) { add(array, childScaler); } } else array.add(scaler.serialize()); } @Override public JsonElement data() { var arr = new JsonArray(); for (ModelScaler modelScaler : getter.list) { add(arr, modelScaler); } return arr.isEmpty() ? null : arr; } } /** * Helper interface for built-in deserializers. * * @since 1.15.2 */ interface BuiltInDeserializer extends Function { /** * Deserializes from a float value. * * @param value the value * @return the scaler * @since 1.15.2 */ default @NotNull ModelScaler deserialize(float value) { return apply(new JsonPrimitive(value)); } /** * Deserializes a default instance. * * @return the scaler * @since 1.15.2 */ default @NotNull ModelScaler deserialize() { return apply(JsonNull.INSTANCE); } } /** * Registry and factory for scalers. * * @since 1.15.2 */ final class Deserializer { private final Map getterMap = CollectionUtil.newAddressingMap(); private final BuiltInDeserializer def = addScaler("default", _ -> Getter.DEFAULT); private final BuiltInDeserializer entity = addScaler("entity", _ -> Getter.ENTITY); private final BuiltInDeserializer value = addScaler("value", d -> d.isJsonPrimitive() ? Getter.value(d.getAsFloat()) : Getter.DEFAULT); private Deserializer() { getterMap.put("composite", d -> { if (d.isJsonArray()) { return new Composite.CompositeGetter(d.getAsJsonArray() .asList() .stream() .filter(JsonElement::isJsonObject) .map(element -> buildScaler(element.getAsJsonObject())) .filter(Objects::nonNull) .toList()); } else return Getter.DEFAULT; }); } private @NotNull ModelScaler defaultScaler() { return def.deserialize(); } /** * Registers a new scaler type. * * @param name the scaler name * @param builder the builder * @return a built-in deserializer helper * @since 1.15.2 */ public @NotNull BuiltInDeserializer addScaler(@NotNull String name, @NotNull Builder builder) { var put = getterMap.putIfAbsent(name, builder); var target = put != null ? put : builder; return element -> pack(name, target, element); } /** * Builds a scaler from a JSON object. * * @param rawData the JSON object * @return the scaler, or null if invalid * @since 1.15.2 */ public @Nullable ModelScaler buildScaler(@NotNull JsonObject rawData) { var n = rawData.getAsJsonPrimitive("name"); if (n == null) return null; var name = n.getAsString(); var get = getterMap.get(name); if (get == null) return null; var d = rawData.get("data"); return pack(name, get, d); } private @NotNull ModelScaler pack(@NotNull String name, @NotNull Builder builder, @Nullable JsonElement data) { var build = Optional.ofNullable(builder.build(data != null ? data : JsonNull.INSTANCE)) .orElse(Getter.DEFAULT); return build instanceof Composite.CompositeGetter compositeGetter ? new Composite(compositeGetter) : new Pack(name, build, data); } private record Pack(@NotNull String name, @NotNull Getter getter, @Nullable JsonElement data) implements ModelScaler { @Override public float scale(@NotNull Tracker tracker) { return getter.get(tracker); } } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/PlayerTracker.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.data.renderer.RenderPipeline; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; /** * A specialized {@link EntityTracker} for tracking players. *

* This tracker automatically configures the body rotator to player mode, ensuring correct * head and body rotation synchronization for player entities. *

* * @since 1.15.2 */ public final class PlayerTracker extends EntityTracker { /** * Creates a new player tracker. * * @param registry the entity tracker registry * @param pipeline the render pipeline * @param modifier the tracker modifier * @param preUpdateConsumer a consumer to run before the first update * @since 1.15.2 */ @ApiStatus.Internal public PlayerTracker(@NotNull EntityTrackerRegistry registry, @NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier, @NotNull Consumer preUpdateConsumer) { super(registry, pipeline, modifier, preUpdateConsumer); bodyRotator().setValue(setter -> setter.setPlayerMode(true)); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/Tracker.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.animation.AnimationStateHandler; import kr.toxicity.model.api.bone.BoneMovement; import kr.toxicity.model.api.bone.BoneName; import kr.toxicity.model.api.bone.BoneTags; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.config.DebugConfig; import kr.toxicity.model.api.data.blueprint.BlueprintAnimation; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.data.renderer.RenderPipeline; import kr.toxicity.model.api.data.renderer.RenderSource; import kr.toxicity.model.api.entity.BaseEntity; import kr.toxicity.model.api.event.*; import kr.toxicity.model.api.event.hitbox.HitBoxEvent; import kr.toxicity.model.api.nms.*; import kr.toxicity.model.api.platform.PlatformLocation; import kr.toxicity.model.api.platform.PlatformPlayer; import kr.toxicity.model.api.script.TimeScript; import kr.toxicity.model.api.util.*; import kr.toxicity.model.api.util.function.BonePredicate; import kr.toxicity.model.api.util.function.FloatSupplier; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.*; import java.util.stream.Stream; /** * Represents the core controller for a specific model instance. *

* A Tracker manages the lifecycle, rendering, animation, and player interaction of a model. * It coordinates with the {@link RenderPipeline} to update bone positions and send packets to players. *

* * @since 1.15.2 */ public abstract class Tracker implements AutoCloseable { private static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors() * 2, new ThreadFactory() { private final AtomicInteger integer = new AtomicInteger(); @Override public Thread newThread(@NotNull Runnable r) { var thread = new Thread(r); thread.setDaemon(true); thread.setName("BetterModel-Worker-" + integer.getAndIncrement()); thread.setUncaughtExceptionHandler((t, e) -> LogUtil.handleException("Exception has occurred in " + t.getName(), e)); return thread; } }); /** * The interval in milliseconds between tracker ticks. * @since 1.15.2 */ public static final int TRACKER_TICK_INTERVAL = 25; /** * The multiplier to convert tracker ticks to Minecraft ticks (50ms). * @since 1.15.2 */ public static final int MINECRAFT_TICK_MULTIPLIER = MathUtil.MINECRAFT_TICK_MILLS / TRACKER_TICK_INTERVAL; @Getter protected final RenderPipeline pipeline; private long frame = 0; private final Queue queuedTask = new ConcurrentLinkedQueue<>(); private final AtomicBoolean tickPause = new AtomicBoolean(); private final AtomicBoolean isClosed = new AtomicBoolean(); private final AtomicBoolean readyForForceUpdate = new AtomicBoolean(); private final AtomicBoolean forRemoval = new AtomicBoolean(); private final AtomicBoolean firstStart = new AtomicBoolean(); protected final TrackerModifier modifier; private final Runnable updater; private final BundlerSet bundlerSet; private final FloatSupplier heightSupplier = FunctionUtil.throttleTickFloat(TRACKER_TICK_INTERVAL, new FloatSupplier() { private final BoneMovement heightCache = new BoneMovement(); @Override public float getAsFloat() { return (float) pipeline .stream() .filter(bone -> bone.name().tagged(BoneTags.HEAD, BoneTags.HEAD_WITH_CHILDREN)) .mapToDouble(bone -> bone.hitBoxPosition(heightCache).y) .max() .orElse(0F); } }); private final AnimationStateHandler scriptProcessor = new AnimationStateHandler<>( TimeScript.EMPTY, (b, _) -> { if (b == null) return; if (b.isSync()) { location().task(() -> b.accept(this)); } else b.accept(this); } ); private volatile ScheduledFuture task; protected ModelRotator rotator = ModelRotator.YAW; protected ModelScaler scaler = ModelScaler.entity(); private Supplier rotationSupplier = () -> ModelRotation.EMPTY; private BiConsumer closeEventHandler = (t, r) -> EventUtil.call(CloseTrackerEvent.class, () -> new CloseTrackerEvent(t, r)); private ScheduledPacketHandler handler = (t, s) -> { if (!tickPause.get()) { scriptProcessor.tick(); t.pipeline.tick(s.getViewBundler()); } }; private BiConsumer perPlayerHandler = null; /** * Creates a new tracker. * * @param pipeline the render pipeline * @param modifier the tracker modifier * @since 1.15.2 */ public Tracker(@NotNull RenderPipeline pipeline, @NotNull TrackerModifier modifier) { this.pipeline = pipeline; this.modifier = modifier; bundlerSet = new BundlerSet(); updater = () -> { try { if (frame % MINECRAFT_TICK_MULTIPLIER == 0) { Runnable task; while ((task = queuedTask.poll()) != null) task.run(); } handler.handle(this, bundlerSet); bundlerSet.send(); } catch (Throwable throwable) { LogUtil.handleException("Ticking this tracker has been failed: " + name(), throwable); } }; if (modifier.sightTrace()) pipeline.viewFilter(p -> EntityUtil.canSee(p.eyeLocation(), location())); frame((t, s) -> { if (readyForForceUpdate.compareAndSet(true, false)) t.pipeline.forEach(b -> b.dirtyUpdate(s.dataBundler)); }); tick((t, s) -> pipeline.rotate( t.rotation(), s.tickBundler )); tick((t, _) -> { var perPlayer = perPlayerHandler; if (perPlayer != null) pipeline.nonHidePlayer().forEach(p -> perPlayer.accept(t, p.player())); }); pipeline.spawnPacketHandler(_ -> start()); pipeline.eventDispatcher().handleStateCreate((_, uuid) -> bundlerSet.perPlayerViewBundler .computeIfAbsent(uuid, PerPlayerCache::new) .add()); pipeline.eventDispatcher().handleStateRemove((_, uuid) -> { var get = bundlerSet.perPlayerViewBundler.get(uuid); if (get != null) get.remove(); }); LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " tracker created: " + name()); pipeline.getSource().completeContext().thenAccept(context -> { if (pipeline.matchTree(bone -> bone.updateItem(context))) forceUpdate(true); }); } /** * Checks if the tracker's update task is currently scheduled. * * @return true if scheduled, false otherwise * @since 1.15.2 */ public boolean isScheduled() { var currentTask = task; return currentTask != null && !currentTask.isCancelled(); } private void start() { if (isScheduled()) return; synchronized (this) { if (isScheduled()) return; if (firstStart.compareAndSet(false, true)) { TrackerBuiltInAnimation.play(this); } updater.run(); task = EXECUTOR.scheduleAtFixedRate(() -> { if (playerCount() == 0 && !forRemoval.get()) { shutdown(); return; } frame++; updater.run(); }, TRACKER_TICK_INTERVAL, TRACKER_TICK_INTERVAL, TimeUnit.MILLISECONDS); LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " scheduler started: " + name()); } } private void shutdown() { if (!isScheduled()) return; synchronized (this) { if (!isScheduled()) return; task.cancel(true); task = null; frame = 0; LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " scheduler shutdown: " + name()); } } /** * Returns the current rotation of the model. * * @return the model rotation * @since 1.15.2 */ public @NotNull ModelRotation rotation() { return rotator.apply(this, rotationSupplier.get()); } /** * Sets the supplier for the base model rotation. * * @param supplier the rotation supplier * @since 1.15.2 */ public final void rotation(@NotNull Supplier supplier) { this.rotationSupplier = Objects.requireNonNull(supplier); } /** * Sets the model rotator strategy. * * @param rotator the rotator strategy * @since 1.15.2 */ public final void rotator(@NotNull ModelRotator rotator) { this.rotator = Objects.requireNonNull(rotator); } /** * Returns the model scaler. * * @return the scaler * @since 1.15.2 */ public @NotNull ModelScaler scaler() { return scaler; } /** * Sets the model scaler. * * @param scaler the new scaler * @since 1.15.2 */ public void scaler(@NotNull ModelScaler scaler) { this.scaler = Objects.requireNonNull(scaler); } /** * Schedules a task to run on the next tracker tick. * * @param runnable the task to run * @since 1.15.2 */ public void task(@NotNull Runnable runnable) { queuedTask.add(Objects.requireNonNull(runnable)); } /** * Registers a handler to run every frame (tracker tick). * * @param handler the packet handler * @since 1.15.2 */ public synchronized void frame(@NotNull ScheduledPacketHandler handler) { this.handler = this.handler.then(Objects.requireNonNull(handler)); } /** * Registers a handler to run every Minecraft tick (50ms). * * @param handler the packet handler * @since 1.15.2 */ public void tick(@NotNull ScheduledPacketHandler handler) { tick(1, handler); } /** * Registers a handler to run every N Minecraft ticks. * * @param tick the interval in Minecraft ticks * @param handler the packet handler * @since 1.15.2 */ public void tick(long tick, @NotNull ScheduledPacketHandler handler) { schedule(MINECRAFT_TICK_MULTIPLIER * tick, handler); } /** * Registers a handler to run every tick for each visible player. * * @param perPlayerHandler the per-player handler * @since 1.15.2 */ public synchronized void perPlayerTick(@NotNull BiConsumer perPlayerHandler) { var previous = this.perPlayerHandler; this.perPlayerHandler = previous == null ? perPlayerHandler : previous.andThen(perPlayerHandler); } /** * Schedules a handler to run periodically. * * @param period the period in tracker ticks * @param handler the packet handler * @since 1.15.2 */ public void schedule(long period, @NotNull ScheduledPacketHandler handler) { Objects.requireNonNull(handler); if (period <= 0) throw new RuntimeException("period cannot be <= 0"); frame(period == 1 ? handler : (t, s) -> { if (frame % period == 0) handler.handle(t, s); }); } /** * Returns the name of the model being tracked. * * @return the model name * @since 1.15.2 */ public @NotNull String name() { return pipeline.name(); } /** * Calculates the height of the model based on its head bone position. * * @return the height * @since 1.15.2 */ public double height() { return heightSupplier.getAsFloat(); } /** * Checks if the tracker has been closed. * * @return true if closed, false otherwise * @since 1.15.2 */ public boolean isClosed() { return isClosed.get(); } @Override public void close() { close(CloseReason.REMOVE); } protected void close(@NotNull CloseReason reason) { if (isClosed.compareAndSet(false, true)) { closeEventHandler.accept(this, reason); shutdown(); pipeline.despawn(); LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " closed: " + name()); } } /** * Despawns the model for all players without closing the tracker completely. * * @since 1.15.2 */ public void despawn() { if (!isClosed()) { pipeline.despawn(); LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " despawned: " + name()); } } /** * Returns the tracker modifier. * * @return the modifier * @since 1.15.2 */ public @NotNull TrackerModifier modifier() { return modifier; } /** * Pauses or resumes the tracker's ticking. * * @param pause true to pause, false to resume * @return true if the state changed, false otherwise * @since 1.15.2 */ public boolean pause(boolean pause) { return tickPause.compareAndSet(!pause, pause); } /** * Flags the tracker for a forced update on the next tick. * * @param force true to force update * @return true if the state changed * @since 1.15.2 */ public boolean forceUpdate(boolean force) { return readyForForceUpdate.compareAndSet(!force, force); } /** * Spawns the model for a specific player. * * @param player the target player * @param bundler the packet bundler * @return true if spawned successfully * @since 1.15.2 */ protected boolean spawn(@NotNull PlatformPlayer player, @NotNull PacketBundler bundler) { if (isClosed()) return false; if (!EventUtil.call(ModelSpawnAtPlayerEvent.class, () -> new ModelSpawnAtPlayerEvent(player, this)).triggered()) return false; return pipeline.spawn(player, bundler, spawned -> { LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " is spawned at player " + player.name() + ": " + name()); task(spawned::load); }); } /** * Removes the model for a specific player. * * @param player the target player * @return true if removed successfully * @since 1.15.2 */ public boolean remove(@NotNull PlatformPlayer player) { if (isClosed()) return false; EventUtil.call(ModelDespawnAtPlayerEvent.class, () -> new ModelDespawnAtPlayerEvent(player, this)); var result = pipeline.remove(player); if (result) LogUtil.debug(DebugConfig.DebugOption.TRACKER, () -> getClass().getSimpleName() + " is despawned at player " + player.name() + ": " + name()); return result; } /** * Returns the number of players currently viewing the model. * * @return the player count * @since 1.15.2 */ public int playerCount() { return pipeline.playerCount(); } /** * Returns the current location of the model. * * @return the location * @since 1.15.2 */ public abstract @NotNull PlatformLocation location(); /** * Plays an animation by name with default settings. * * @param animation the animation name * @return true if the animation started * @since 1.15.2 */ public boolean animate(@NotNull String animation) { return animate(animation, AnimationModifier.DEFAULT); } /** * Plays an animation by name with a modifier. * * @param animation the animation name * @param modifier the animation modifier * @return true if the animation started * @since 1.15.2 */ public boolean animate(@NotNull String animation, @NotNull AnimationModifier modifier) { return animate(animation, modifier, () -> {}); } /** * Plays an animation by name with a modifier and a completion task. * * @param animation the animation name * @param modifier the animation modifier * @param removeTask the task to run when the animation ends * @return true if the animation started * @since 1.15.2 */ public boolean animate(@NotNull String animation, @NotNull AnimationModifier modifier, @NotNull Runnable removeTask) { return renderer().animation(animation) .map(get -> animate(get, modifier, removeTask)) .orElse(false); } /** * Plays a blueprint animation with a modifier. * * @param animation the blueprint animation * @param modifier the animation modifier * @return true if the animation started * @since 1.15.2 */ public boolean animate(@NotNull BlueprintAnimation animation, @NotNull AnimationModifier modifier) { return animate(animation, modifier, () -> {}); } public boolean animate(@NotNull TrackerAnimation animation) { return animation.play(this); } public boolean animate(@NotNull TrackerAnimation animation, @NotNull Runnable removeTask) { return animation.play(this, removeTask); } /** * Plays a blueprint animation on filtered bones. * * @param animation the blueprint animation * @param modifier the animation modifier * @param removeTask the task to run when the animation ends * @return true if the animation started * @since 1.15.2 */ public boolean animate(@NotNull BlueprintAnimation animation, @NotNull AnimationModifier modifier, @NotNull Runnable removeTask) { var script = animation.script(modifier); if (script != null) scriptProcessor.addAnimation(animation.name(), script.iterator(modifier), modifier, () -> {}); return pipeline.matchAnimation((b, a) -> b.addAnimation(a, animation, modifier, removeTask)); } /** * Stops an animation by name. * * @param animation the animation name * @return true if the animation was stopped * @since 1.15.2 */ public boolean stopAnimation(@NotNull String animation) { return stopAnimation(_ -> true, animation); } /** * Stops an animation on filtered bones. * * @param filter the bone filter * @param animation the animation name * @return true if the animation was stopped * @since 1.15.2 */ public boolean stopAnimation(@NotNull Predicate filter, @NotNull String animation) { return stopAnimation(filter, animation, null); } /** * Stops an animation on filtered bones for a specific player (optional). * * @param filter the bone filter * @param animation the animation name * @param player the player (can be null) * @return true if the animation was stopped * @since 1.15.2 */ public boolean stopAnimation(@NotNull Predicate filter, @NotNull String animation, @Nullable PlatformPlayer player) { var script = scriptProcessor.stopAnimation(animation); return pipeline.matchTree(b -> b.stopAnimation(filter, animation, player)) || script; } /** * Replaces a running animation on filtered bones. * * @param target the name of the animation to replace * @param animation the name of the new animation * @param modifier the modifier for the new animation * @return true if the replacement occurred * @since 1.15.2 */ public boolean replace(@NotNull String target, @NotNull String animation, @NotNull AnimationModifier modifier) { return renderer().animation(animation) .map(get -> replace(target, get, modifier)) .orElse(false); } /** * Replaces a running animation on filtered bones with a blueprint animation. * * @param target the name of the animation to replace * @param animation the new blueprint animation * @param modifier the modifier for the new animation * @return true if the replacement occurred * @since 1.15.2 */ public boolean replace(@NotNull String target, @NotNull BlueprintAnimation animation, @NotNull AnimationModifier modifier) { var script = animation.script(modifier); if (script != null) scriptProcessor.replaceAnimation(target, script.iterator(modifier), modifier); return pipeline.matchAnimation((b, a) -> b.replaceAnimation(a, target, animation, modifier)); } //--- Listener --- /** * Registers a hitbox-listener builder hook that is applied when hitboxes are created. *

* This delegates to {@link kr.toxicity.model.api.bone.BoneEventDispatcher#handleCreateHitBox(BiFunction)} * in this tracker's render pipeline event dispatcher. *

* *
{@code
     * tracker.listenHitBox((bone, builder) -> builder.interact(event -> {
     *     // custom interaction handling
     * }));
     * }
* * @param function the hitbox listener builder transformer * @since 2.1.0 */ public void listenHitBox(@NotNull BiFunction function) { pipeline.hitboxes().forEach(hb -> { var bone = hb.positionSource(); hb.listener(b -> function.apply(bone, b)); }); pipeline.eventDispatcher().handleCreateHitBox(function); } /** * Registers a hitbox event listener for newly created hitboxes. *

* This is a convenience wrapper over {@link #listenHitBox(BiFunction)}. *

* *
{@code
     * tracker.listenHitBox(HitBoxInteractEvent.class, event -> {
     *     // custom interaction handling
     * });
     * }
* * @param eventClass target hitbox event class * @param consumer event consumer * @param event type * @since 2.1.0 */ public void listenHitBox(@NotNull Class eventClass, @NotNull Consumer consumer) { listenHitBox((_, builder) -> builder.listen(eventClass, consumer)); } /** * Creates a hitbox for bones matching a predicate. * * @param entity the source entity for the hitbox * @param listener the hitbox listener * @param predicate the bone predicate * @return true if any hitboxes were created * @since 1.15.2 */ public boolean createHitBox(@NotNull BaseEntity entity, @Nullable HitBoxListener listener, @NotNull BonePredicate predicate) { return tryUpdate((b, p) -> b.createHitBox(entity, p, listener), predicate); } /** * Retrieves or creates a hitbox for a specific bone. * * @param entity the source entity * @param listener the hitbox listener * @param predicate the bone predicate * @return the hitbox, or null if not found/created * @since 1.15.2 */ public @Nullable HitBox hitbox(@NotNull BaseEntity entity, @Nullable HitBoxListener listener, @NotNull Predicate predicate) { return pipeline.firstNotNull(bone -> { if (predicate.test(bone)) { if (bone.getHitBox() == null) bone.createHitBox(entity, BonePredicate.TRUE, listener); return bone.getHitBox(); } else return null; }); } /** * Creates a nametag for bones matching a predicate. * * @param predicate the bone predicate * @param consumer a consumer to configure the nametag * @return true if any nametags were created * @since 1.15.2 */ public boolean createNametag(@NotNull BonePredicate predicate, @NotNull BiConsumer consumer) { return tryUpdate((b, p) -> b.createNametag(p, tag -> { consumer.accept(b, tag); perPlayerTick((tracker, player) -> { if (pipeline.getSource() instanceof RenderSource.Entity entity && entity.entity().uuid().equals(player.uuid())) return; tag.teleport(tracker.location()); tag.send(player); }); }), predicate); } //--- Update action --- /** * Forces an update action on all bones. * * @param action the update action * @param the action type * @since 1.15.2 */ public void update(@NotNull T action) { update(action, BonePredicate.TRUE); } /** * Forces an update action on filtered bones. * * @param action the update action * @param predicate the bone predicate * @param the action type * @since 1.15.2 */ public void update(@NotNull T action, @NotNull Predicate predicate) { update(action, BonePredicate.from(predicate)); } /** * Forces an update action on filtered bones. * * @param action the update action * @param predicate the bone predicate * @param the action type * @since 1.15.2 */ public void update(@NotNull T action, @NotNull BonePredicate predicate) { if (tryUpdate(action, predicate)) forceUpdate(true); } /** * Tries to apply an update action to bones matching a predicate. * * @param action the update action * @param predicate the bone predicate * @return true if any bones were updated * @since 1.15.2 */ public boolean tryUpdate(@NotNull BiPredicate action, @NotNull BonePredicate predicate) { return pipeline.matchTree(predicate, action); } /** * Retrieves a bone by name. * * @param name the bone name * @return the bone, or null if not found * @since 1.15.2 */ public @Nullable RenderedBone bone(@NotNull BoneName name) { return pipeline.boneOf(name); } /** * Retrieves a bone by name string. * * @param name the bone name * @return the bone, or null if not found * @since 1.15.2 */ public @Nullable RenderedBone bone(@NotNull String name) { return bone(BonePredicate.name(name)); } /** * Retrieves the first bone matching a predicate. * * @param predicate the bone predicate * @return the bone, or null if not found * @since 1.15.2 */ public @Nullable RenderedBone bone(@NotNull Predicate predicate) { return pipeline.stream() .filter(predicate) .findFirst() .orElse(null); } /** * Returns a collection of all bones in the model. * * @return the bones * @since 1.15.2 */ public @NotNull @Unmodifiable Collection bones() { return pipeline.bones(); } /** * Returns a stream of all model displays. * * @return the displays * @since 1.15.2 */ public @NotNull Stream displays() { return pipeline.stream() .map(RenderedBone::getDisplay) .filter(Objects::nonNull); } /** * Hides the tracker from a specific player. * * @param player the target player * @return true if hidden successfully * @since 1.15.2 */ public boolean hide(@NotNull PlatformPlayer player) { return EventUtil.call(PlayerHideTrackerEvent.class, () -> new PlayerHideTrackerEvent(this, player)).triggered() && pipeline.hide(player); } /** * Checks if the tracker is hidden from a specific player. * * @param player the target player * @return true if hidden * @since 1.15.2 */ public boolean isHide(@NotNull PlatformPlayer player) { return pipeline.isHide(player); } /** * Shows the tracker to a specific player. * * @param player the target player * @return true if shown successfully * @since 1.15.2 */ public boolean show(@NotNull PlatformPlayer player) { return EventUtil.call(PlayerShowTrackerEvent.class, () -> new PlayerShowTrackerEvent(this, player)).triggered() && pipeline.show(player); } /** * Registers a handler for the tracker close event. * * @param consumer the handler * @since 1.15.2 */ public void handleCloseEvent(@NotNull BiConsumer consumer) { closeEventHandler = closeEventHandler.andThen(Objects.requireNonNull(consumer)); } /** * Checks if the model is spawned for a player (by UUID). * * @param uuid the player UUID * @return true if spawned * @since 1.15.2 */ public boolean isSpawned(@NotNull UUID uuid) { return pipeline.isSpawned(uuid); } /** * Checks if the model is spawned for a player. * * @param player the player * @return true if spawned * @since 1.15.2 */ public boolean isSpawned(@NotNull PlatformPlayer player) { return isSpawned(player.uuid()); } /** * Returns the renderer associated with this tracker. * * @return the renderer * @since 1.15.2 */ public @NotNull ModelRenderer renderer() { return pipeline.getParent(); } /** * Marks the tracker for removal. * * @param removal true to mark for removal * @since 1.15.2 */ @ApiStatus.Internal public void forRemoval(boolean removal) { forRemoval.set(removal); } /** * Checks if the tracker is marked for removal. * * @return true if marked for removal * @since 1.15.2 */ @ApiStatus.Internal public boolean forRemoval() { return forRemoval.get(); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Tracker tracker)) return false; return name().equals(tracker.name()); } @Override public int hashCode() { return name().hashCode(); } /** * Functional interface for handling scheduled packets. * * @since 1.15.2 */ @FunctionalInterface public interface ScheduledPacketHandler { /** * Handles packets for a tracker. * * @param tracker the tracker * @param bundlerSet the set of packet bundlers * @since 1.15.2 */ void handle(@NotNull Tracker tracker, @NotNull BundlerSet bundlerSet); /** * Chains this handler with another. * * @param other the other handler * @return the combined handler * @since 1.15.2 */ default @NotNull ScheduledPacketHandler then(@NotNull ScheduledPacketHandler other) { return (t, s) -> { handle(t, s); other.handle(t, s); }; } } /** * Holds different types of packet bundlers for a tracker tick. * * @since 1.15.2 */ public final class BundlerSet { @Getter private PacketBundler tickBundler = pipeline.createBundler(); @Getter private PacketBundler dataBundler = pipeline.createBundler(); @Getter private AnimationBundler viewBundler = pipeline.createAnimationBundler(); private final Map perPlayerViewBundler = new ConcurrentHashMap<>(); /** * Private initializer */ private BundlerSet() { } private void send() { globalSend(); perPlayerSend(); } private void perPlayerSend() { if (perPlayerViewBundler.isEmpty()) return; perPlayerViewBundler.values().forEach(PerPlayerCache::send); } private void globalSend() { if (tickBundler.isNotEmpty()) { pipeline.allPlayer().map(PlayerChannelHandler::player).forEach(tickBundler::send); tickBundler = pipeline.createBundler(); } if (dataBundler.isNotEmpty()) { pipeline.nonHidePlayer().map(PlayerChannelHandler::player).forEach(dataBundler::send); dataBundler = pipeline.createBundler(); } if (viewBundler.isNotEmpty()) { pipeline.viewedPlayer().filter(p -> !perPlayerViewBundler.containsKey(p.uuid())).forEach(viewBundler::send); viewBundler = pipeline.createAnimationBundler(); } } } @RequiredArgsConstructor private final class PerPlayerCache { private final UUID uuid; private final AtomicInteger counter = new AtomicInteger(); private AnimationBundler bundler = pipeline.createAnimationBundler(); private @NotNull Optional channel() { return Optional.ofNullable(pipeline.channel(uuid)); } public void add() { if (counter.getAndIncrement() == 0) { channel().ifPresent(handler -> EventUtil.call(PlayerPerAnimationStartEvent.class, () -> new PlayerPerAnimationStartEvent(Tracker.this, handler.player()))); } } public void remove() { if (counter.decrementAndGet() == 0) { bundlerSet.perPlayerViewBundler.remove(uuid); channel().ifPresent(handler -> { var bundler = pipeline.createBundler(); pipeline.forEach(bone -> bone.forceTransformation(bundler)); bundler.send(handler.player()); EventUtil.call(PlayerPerAnimationEndEvent.class, () -> new PlayerPerAnimationEndEvent(Tracker.this, handler.player())); }); } } private void send() { if (pipeline.tick(uuid, bundler) && bundler.isNotEmpty()) { channel().ifPresent(handler -> bundler.send(handler)); bundler = pipeline.createAnimationBundler(); } } } @Override public String toString() { return name(); } /** * Reason for closing a tracker. * * @since 1.15.2 */ @RequiredArgsConstructor public enum CloseReason { /** * The tracker was manually removed. * @since 1.15.2 */ REMOVE(false), /** * The plugin is being disabled. * @since 1.15.2 */ PLUGIN_DISABLE(true), /** * The entity or tracker was despawned. * @since 1.15.2 */ DESPAWN(true) ; private final boolean save; /** * Checks if the tracker state should be saved. * * @return true if it should be saved * @since 1.15.2 */ public boolean shouldBeSave() { return save; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/TrackerAnimation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.animation.AnimationModifier; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.NonNull; import java.util.Comparator; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; /** * A record representing an animation configuration for a {@link Tracker}. *

* This class defines how an animation should be applied, its priority, and the tasks to run * during different lifecycle stages of the animation. * * @param the type of tracker this animation applies to * @param name the unique name of the animation * @param priority the priority of the animation (higher values usually take precedence) * @param targetClass the class type of the tracker * @param applyCondition a condition to check if the animation can be played * @param modifierBuilder a function that provides an {@link AnimationModifier} * @param removeTask the task to run when the animation is removed * @param successTask the task to run when the animation starts successfully * @param fallbackTask the task to run when the animation fails to start * @since 2.2.0 */ public record TrackerAnimation( @NotNull String name, int priority, @NotNull Class targetClass, @NotNull Predicate applyCondition, @NotNull Function modifierBuilder, @NotNull Consumer removeTask, @NotNull Consumer successTask, @NotNull Consumer fallbackTask ) implements Comparable> { private static final Comparator> COMPARATOR = Comparator.comparing((TrackerAnimation animation) -> animation.priority) .thenComparing(animation -> animation.name); /** * Creates a new builder for a {@link TrackerAnimation}. * * @param name the name of the animation * @return a new builder instance * @since 2.2.0 */ public static @NotNull Builder builder(@NotNull String name) { return new Builder<>(name, Tracker.class); } @ApiStatus.Internal public TrackerAnimation { } @Override public int compareTo(@NonNull TrackerAnimation o) { return COMPARATOR.compare(this, o); } boolean play(@NotNull Tracker tracker) { return play(tracker, () -> {}); } boolean play(@NotNull Tracker tracker, @NotNull Runnable removeTask) { if (!targetClass.isInstance(tracker)) return false; var cast = targetClass.cast(tracker); if (!applyCondition.test(cast)) return false; var result = cast.animate( name, modifierBuilder.apply(cast), () -> { this.removeTask.accept(cast); removeTask.run(); } ); if (result) successTask.accept(cast); else fallbackTask.accept(cast); return result; } /** * A builder class for creating instances of {@link TrackerAnimation}. * * @param the type of tracker * @since 2.2.0 */ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) public static final class Builder { private final String name; private final Class targetClass; private int priority = 0; private @NotNull Predicate applyCondition = _ -> true; private @NotNull Function modifierBuilder = _ -> AnimationModifier.DEFAULT; private @NotNull Consumer removeTask = _ -> {}; private @NotNull Consumer successTask = _ -> {}; private @NotNull Consumer fallbackTask = _ -> {}; /** * Changes the target tracker type for this builder. * * @param newTargetClass the new target class * @param the new tracker type * @return a new builder for the specified type * @since 2.2.0 */ public Builder type(@NotNull Class newTargetClass) { return new Builder<>(name, newTargetClass) .priority(priority) .check(applyCondition) .modifier(modifierBuilder) .onRemove(removeTask) .onSuccess(successTask) .onFallback(fallbackTask); } /** * Sets the priority of the animation. * * @param priority the priority * @return this builder * @since 2.2.0 */ public @NotNull Builder priority(int priority) { this.priority = priority; return this; } /** * Sets the condition that must be met for the animation to play. * * @param applyCondition the condition predicate * @return this builder * @since 2.2.0 */ public @NotNull Builder check(@NotNull Predicate applyCondition) { this.applyCondition = Objects.requireNonNull(applyCondition, "applyCondition cannot be null"); return this; } /** * Sets the modifier builder for the animation. * * @param modifierBuilder a function that provides an {@link AnimationModifier} * @return this builder * @since 2.2.0 */ public @NotNull Builder modifier(@NotNull Function modifierBuilder) { this.modifierBuilder = Objects.requireNonNull(modifierBuilder, "modifierBuilder cannot be null"); return this; } /** * Sets the task to run when the animation is removed. * * @param removeTask the removal task * @return this builder * @since 2.2.0 */ public @NotNull Builder onRemove(@NotNull Consumer removeTask) { this.removeTask = Objects.requireNonNull(removeTask, "removeTask cannot be null"); return this; } /** * Sets the task to run when the animation starts successfully. * * @param successTask the success task * @return this builder * @since 2.2.0 */ public @NotNull Builder onSuccess(@NotNull Consumer successTask) { this.successTask = Objects.requireNonNull(successTask, "successTask cannot be null"); return this; } /** * Sets the task to run when the animation fails to start. * * @param fallbackTask the fallback task * @return this builder * @since 2.2.0 */ public @NotNull Builder onFallback(@NotNull Consumer fallbackTask) { this.fallbackTask = Objects.requireNonNull(fallbackTask, "fallbackTask cannot be null"); return this; } /** * Builds the {@link TrackerAnimation} instance. * * @return the constructed animation * @since 2.2.0 */ public @NotNull TrackerAnimation build() { return new TrackerAnimation<>( name, priority, targetClass, applyCondition, modifierBuilder, removeTask, successTask, fallbackTask ); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/TrackerBuiltInAnimation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.animation.AnimationIterator; import kr.toxicity.model.api.animation.AnimationModifier; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.function.Function; import static kr.toxicity.model.api.util.CollectionUtil.newAddressingMap; /** * A utility class for managing built-in animations for trackers. * * @since 2.2.0 */ public final class TrackerBuiltInAnimation { private static final Map> BY_NAME = newAddressingMap(); private static final SortedSet> BY_PRIORITY = new TreeSet<>(); /** * Registers a new tracker animation. * * @param name the name of the animation * @param builderFunction the function to build the animation * @param the tracker type * @return the registered animation */ public static @NotNull TrackerAnimation register(@NotNull String name, @NotNull Function, TrackerAnimation.Builder> builderFunction) { var animation = builderFunction.apply(TrackerAnimation.builder(name)).build(); register(animation); return animation; } private static void register(@NotNull TrackerAnimation animation) { synchronized (BY_NAME) { if (BY_NAME.put(animation.name(), animation) != null) throw new IllegalStateException("Duplicate animation name: " + animation.name()); BY_PRIORITY.add(animation); } } static void play(@NotNull Tracker tracker) { BY_PRIORITY.forEach(animation -> animation.play(tracker)); } /** * The default idle animation. * * @since 2.2.0 */ public static final TrackerAnimation IDLE = register("idle", b -> b.modifier(_ -> AnimationModifier.builder() .start(6) .type(AnimationIterator.Type.LOOP) .build() )); /** * The default walk animation for entity trackers. * * @since 2.2.0 */ public static final TrackerAnimation WALK = register("walk", b -> b.type(EntityTracker.class) .modifier(tracker -> { var property = tracker.registry().animationProperty; return AnimationModifier.builder() .start(6) .predicate(property.onWalk) .speed(tracker.modifier.damageAnimation() ? property.walkSpeed : null) .type(AnimationIterator.Type.LOOP) .build(); })); /** * The default flying idle animation for entity trackers. * * @since 2.2.0 */ public static final TrackerAnimation IDLE_FLY = register("idle_fly", b -> b.type(EntityTracker.class) .modifier(tracker -> { var property = tracker.registry().animationProperty; return AnimationModifier.builder() .start(6) .predicate(property.onFly) .type(AnimationIterator.Type.LOOP) .build(); })); /** * The default flying walk animation for entity trackers. * * @since 2.2.0 */ public static final TrackerAnimation WALK_FLY = register("walk_fly", b -> b.type(EntityTracker.class) .modifier(tracker -> { var property = tracker.registry().animationProperty; return AnimationModifier.builder() .start(6) .predicate(() -> property.onFly.getAsBoolean() && property.onWalk.getAsBoolean()) .type(AnimationIterator.Type.LOOP) .build(); })); /** * The default spawn animation for entity trackers. * * @since 2.2.0 */ public static final TrackerAnimation SPAWN = register("spawn", b -> b.type(EntityTracker.class) .modifier(_ -> AnimationModifier.DEFAULT_WITH_PLAY_ONCE)); private TrackerBuiltInAnimation() { throw new IllegalStateException("Utility class"); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/TrackerData.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.gson.*; import com.google.gson.annotations.SerializedName; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.Set; import java.util.UUID; /** * Represents the persistent data state of a tracker. *

* This record holds configuration and state information such as model ID, scaling, rotation, * and visibility options, which can be serialized to JSON. *

* * @param id the model ID * @param scaler the model scaler * @param rotator the model rotator * @param modifier the tracker modifier * @param bodyRotator the body rotation data * @param hideOption the entity hide options * @param markForSpawn the set of player UUIDs marked for spawning * @since 1.15.2 */ public record TrackerData( @NotNull String id, @Nullable ModelScaler scaler, @Nullable ModelRotator rotator, @NotNull TrackerModifier modifier, @Nullable @SerializedName("body-rotator") EntityBodyRotator.RotatorData bodyRotator, @Nullable @SerializedName("hide-option") EntityHideOption hideOption, @Nullable @SerializedName("mark-for-spawn") Set markForSpawn ) { /** * The GSON parser for serializing and deserializing tracker data. * @since 1.15.2 */ public static final Gson PARSER = new GsonBuilder() .registerTypeAdapter(ModelScaler.class, (JsonDeserializer) (json, _, _) -> json.isJsonObject() ? ModelScaler.deserialize(json.getAsJsonObject()) : ModelScaler.defaultScaler()) .registerTypeAdapter(ModelScaler.class, (JsonSerializer) (src, _, _) -> src.serialize()) .registerTypeAdapter(ModelRotator.class, (JsonDeserializer) (json, _, _) -> json.isJsonObject() ? ModelRotator.deserialize(json.getAsJsonObject()) : ModelRotator.YAW) .registerTypeAdapter(ModelRotator.class, (JsonSerializer) (src, _, _) -> src.serialize()) .registerTypeAdapter(EntityHideOption.class, (JsonDeserializer) (json, _, _) -> json.isJsonArray() ? EntityHideOption.deserialize(json.getAsJsonArray()) : EntityHideOption.DEFAULT) .registerTypeAdapter(EntityHideOption.class, (JsonSerializer) (src, _, _) -> src.serialize()) .registerTypeAdapter(UUID.class, (JsonDeserializer) (json, _, _) -> UUID.fromString(json.getAsString())) .registerTypeAdapter(UUID.class, (JsonSerializer) (src, _, _) -> new JsonPrimitive(src.toString())) .create(); /** * Applies this data to an existing entity tracker. * * @param tracker the target tracker * @since 1.15.2 */ public void applyAs(@NotNull EntityTracker tracker) { tracker.markPlayerForSpawn(markForSpawn()); tracker.hideOption(hideOption()); tracker.scaler(scaler()); tracker.rotator(rotator()); tracker.bodyRotator().setValue(bodyRotator()); } /** * Serializes this data to a JSON element. * * @return the JSON element * @since 1.15.2 */ public @NotNull JsonElement serialize() { return PARSER.toJsonTree(this); } /** * Deserializes tracker data from a JSON element. * * @param element the JSON element * @return the tracker data * @since 1.15.2 */ public static @NotNull TrackerData deserialize(@NotNull JsonElement element) { return element.isJsonPrimitive() ? new TrackerData( element.getAsString(), ModelScaler.entity(), null, TrackerModifier.DEFAULT, EntityBodyRotator.defaultData(), null, null ) : PARSER.fromJson(element, TrackerData.class); } /** * Returns the model scaler, or a default entity scaler if not specified. * * @return the model scaler * @since 1.15.2 */ @Override public @NotNull ModelScaler scaler() { return scaler != null ? scaler : ModelScaler.entity(); } /** * Returns the model rotator, or a default YAW rotator if not specified. * * @return the model rotator * @since 1.15.2 */ @Override public @NotNull ModelRotator rotator() { return rotator != null ? rotator : ModelRotator.YAW; } /** * Returns the entity hide option, or the default hide option if not specified. * * @return the entity hide option * @since 1.15.2 */ @Override public @NotNull EntityHideOption hideOption() { return hideOption != null ? hideOption : EntityHideOption.DEFAULT; } /** * Returns the set of player UUIDs marked for spawning, or an empty set if not specified. * * @return the set of player UUIDs marked for spawning * @since 1.15.2 */ @Override public @NotNull Set markForSpawn() { return markForSpawn != null ? markForSpawn : Collections.emptySet(); } /** * Returns the body rotation data, or default body rotation data if not specified. * * @return the body rotation data * @since 1.15.2 */ @Override public @NotNull EntityBodyRotator.RotatorData bodyRotator() { return bodyRotator != null ? bodyRotator : EntityBodyRotator.defaultData(); } /** * Serializes this TrackerData object to a JSON string. * * @return a JSON string representation of this object * @since 1.15.2 */ @NotNull @Override public String toString() { return serialize().toString(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/TrackerExtraAnimation.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.animation.AnimationModifier; /** * A utility class that contains predefined {@link TrackerAnimation} instances for common entity actions. * * @since 2.2.0 */ public final class TrackerExtraAnimation { /** * Animation played when an entity dies. * Closes the tracker and marks it for removal upon completion. * * @since 2.2.0 */ public static final TrackerAnimation DEATH = TrackerAnimation.builder("death") .type(EntityTracker.class) .modifier(_ -> AnimationModifier.DEFAULT_WITH_PLAY_ONCE) .onRemove(Tracker::close) .onSuccess(tracker -> tracker.forRemoval(true)) .build(); /** * Animation played when an entity takes damage. * * @since 2.2.0 */ public static final TrackerAnimation DAMAGE = TrackerAnimation.builder("damage") .type(EntityTracker.class) .modifier(_ -> AnimationModifier.DEFAULT_WITH_PLAY_ONCE) .build(); /** * Animation played when an entity jumps. * * @since 2.2.0 */ public static final TrackerAnimation JUMP = TrackerAnimation.builder("jump") .type(EntityTracker.class) .modifier(_ -> AnimationModifier.DEFAULT_WITH_PLAY_ONCE) .build(); private TrackerExtraAnimation() { throw new IllegalStateException("Utility class"); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/TrackerModifier.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import com.google.gson.annotations.SerializedName; import org.jetbrains.annotations.NotNull; /** * Configuration options for a {@link Tracker}. *

* This record controls various behaviors such as visibility checks (sight trace), * automatic damage animations, and damage tinting effects. *

* * @param sightTrace whether to perform sight tracing for visibility * @param damageAnimation whether to play automatic damage animations * @param damageTint whether to apply a red tint when damaged * @since 1.15.2 */ public record TrackerModifier( @SerializedName("sight-trace") boolean sightTrace, @SerializedName("damage-animation") boolean damageAnimation, @SerializedName("damage-tint") boolean damageTint ) { /** * The default modifier configuration (all enabled). * @since 1.15.2 */ public static final TrackerModifier DEFAULT = new TrackerModifier( true, true, true ); /** * Creates a new builder initialized with default values. * * @return a new builder * @since 1.15.2 */ public static @NotNull Builder builder() { return DEFAULT.toBuilder(); } /** * Creates a new builder initialized with this modifier's values. * * @return a new builder * @since 1.15.2 */ public @NotNull Builder toBuilder() { return new Builder(this); } /** * Builder for {@link TrackerModifier}. * * @since 1.15.2 */ public static final class Builder { private boolean sightTrace; private boolean damageAnimation; private boolean damageTint; /** * Private initializer * @param modifier modifier */ private Builder(@NotNull TrackerModifier modifier) { this.sightTrace = modifier.sightTrace; this.damageAnimation = modifier.damageAnimation; this.damageTint = modifier.damageTint; } /** * Sets whether to use sight tracing. * * @param sightTrace true to enable sight tracing * @return this builder * @since 1.15.2 */ public @NotNull Builder sightTrace(boolean sightTrace) { this.sightTrace = sightTrace; return this; } /** * Sets whether to enable damage animations. * * @param damageAnimation true to enable damage animations * @return this builder * @since 1.15.2 */ public @NotNull Builder damageAnimation(boolean damageAnimation) { this.damageAnimation = damageAnimation; return this; } /** * Sets whether to enable damage tinting. * * @param damageTint true to enable damage tinting * @return this builder * @since 1.15.2 */ public @NotNull Builder damageTint(boolean damageTint) { this.damageTint = damageTint; return this; } /** * Builds the {@link TrackerModifier}. * * @return the created modifier * @since 1.15.2 */ public @NotNull TrackerModifier build() { return new TrackerModifier( sightTrace, damageAnimation, damageTint ); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/tracker/TrackerUpdateAction.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.tracker; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.platform.PlatformBillboard; import kr.toxicity.model.api.util.TransformedItemStack; import kr.toxicity.model.api.util.function.BonePredicate; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.stream.Stream; /** * Represents an action that updates the state of a {@link RenderedBone}. *

* Actions can modify display properties like brightness, glow, item stack, and more. * They are applied to bones matching a specific predicate. *

* * @since 1.15.2 */ public sealed interface TrackerUpdateAction extends BiPredicate { /** * Creates an action to update display brightness. * * @param block the block light level * @param sky the skylight level * @return the action * @since 1.15.2 */ static @NotNull Brightness brightness(int block, int sky) { return new Brightness(block, sky); } /** * Creates an action to toggle the glowing effect. * * @param glow true to enable glow * @return the action * @since 1.15.2 */ static @NotNull Glow glow(boolean glow) { return glow ? Glow.TRUE : Glow.FALSE; } /** * Creates an action to set the glow color. * * @param glowColor the RGB glow color * @return the action * @since 1.15.2 */ static @NotNull GlowColor glowColor(int glowColor) { return new GlowColor(glowColor); } /** * Creates an action to set the view range. * * @param viewRange the view range * @return the action * @since 1.15.2 */ static @NotNull ViewRange viewRange(float viewRange) { return new ViewRange(viewRange); } /** * Creates an action to apply a tint color. * * @param rgb the RGB tint color * @return the action * @since 1.15.2 */ static @NotNull Tint tint(int rgb) { return new Tint(rgb); } /** * Creates an action to revert to the previous tint. * * @return the action * @since 1.15.2 */ static @NotNull PreviousTint previousTint() { return PreviousTint.INSTANCE; } /** * Creates an action to toggle the enchanted glint effect. * * @param enchant true to enable glint * @return the action * @since 1.15.2 */ static @NotNull Enchant enchant(boolean enchant) { return enchant ? Enchant.TRUE : Enchant.FALSE; } /** * Creates an action to toggle the visibility of a part. * * @param toggle true to show, false to hide * @return the action * @since 1.15.2 */ static @NotNull TogglePart togglePart(boolean toggle) { return toggle ? TogglePart.TRUE : TogglePart.FALSE; } /** * Creates an action to update the displayed item stack. * * @param itemStack the new item stack * @return the action * @since 1.15.2 */ static @NotNull ItemStack itemStack(@NotNull TransformedItemStack itemStack) { Objects.requireNonNull(itemStack); return new ItemStack(itemStack); } /** * Creates an action to set the billboard constraint. * * @param billboard the billboard type * @return the action * @since 1.15.2 */ static @NotNull Billboard billboard(@NotNull PlatformBillboard billboard) { Objects.requireNonNull(billboard); return new Billboard(billboard); } /** * Creates an action to update the item mapping. * * @return the action * @since 1.15.2 */ static @NotNull ItemMapping itemMapping() { return ItemMapping.INSTANCE; } /** * Creates an action to set the movement interpolation duration. * * @param moveDuration the duration in ticks * @return the action * @since 1.15.2 */ static @NotNull MoveDuration moveDuration(int moveDuration) { return new MoveDuration(moveDuration); } /** * Combines multiple actions into a single composite action. * * @param actions the actions to combine * @return the composite action * @since 1.15.2 */ static @NotNull TrackerUpdateAction composite(@NotNull TrackerUpdateAction... actions) { return switch (actions.length) { case 0 -> none(); case 1 -> actions[0]; default -> new Composite(Arrays.stream(actions).flatMap(TrackerUpdateAction::stream).toList()); }; } /** * Creates an action that generates a specific action for each bone. * * @param builder the function to generate actions * @return the per-bone action * @since 1.15.2 */ static @NotNull PerBone perBone(@NotNull Function builder) { return new PerBone(builder); } /** * Returns a no-op action. * * @return the none action * @since 1.15.2 */ static @NotNull None none() { return None.INSTANCE; } /** * Applies the action to a bone if it matches the predicate. * * @param bone the target bone * @param predicate the predicate to check against * @return true if the bone was updated * @since 1.15.2 */ @Override boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate); /** * Chains this action with another action. * * @param action the next action * @return the combined action * @since 1.15.2 */ default @NotNull TrackerUpdateAction then(@NotNull TrackerUpdateAction action) { return composite(this, action); } /** * Returns a stream of actions (useful for flattening composites). * * @return the stream * @since 1.15.2 */ default @NotNull Stream stream() { return Stream.of(this); } /** * Action to update brightness. * @param block the block light level * @param sky the skylight level * @since 1.15.2 */ record Brightness(int block, int sky) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.brightness(block, sky)); } } /** * Action to update glow status. * @since 1.15.2 */ @RequiredArgsConstructor enum Glow implements TrackerUpdateAction { /** * Enable glow. * @since 1.15.2 */ TRUE(true), /** * Disable glow. * @since 1.15.2 */ FALSE(false) ; private final boolean value; @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.glow(value)); } } /** * Action to update glow color. * @param glowColor the RGB glow color * @since 1.15.2 */ record GlowColor(int glowColor) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.glowColor(glowColor)); } } /** * Action to update view range. * @param viewRange the view range * @since 1.15.2 */ record ViewRange(float viewRange) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.viewRange(viewRange)); } } /** * Action to update enchantment glint. * @since 1.15.2 */ @RequiredArgsConstructor enum Enchant implements TrackerUpdateAction { /** * Enable glint. * @since 1.15.2 */ TRUE(true), /** * Disable glint. * @since 1.15.2 */ FALSE(false) ; private final boolean value; @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.enchant(predicate, value); } } /** * Action to apply a tint color. * @param rgb the RGB tint color * @since 1.15.2 */ record Tint(int rgb) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.tint(predicate, rgb); } } /** * Action to revert to previous tint. * @since 1.15.2 */ enum PreviousTint implements TrackerUpdateAction { /** * Instance. * @since 1.15.2 */ INSTANCE ; @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.tint(predicate); } } /** * Action to toggle part visibility. * @since 1.15.2 */ @RequiredArgsConstructor enum TogglePart implements TrackerUpdateAction { /** * Show part. * @since 1.15.2 */ TRUE(true), /** * Hide part. * @since 1.15.2 */ FALSE(false) ; private final boolean value; @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.invisible(!value)); } } /** * Action to update the item stack. * @param itemStack the new item stack * @since 1.15.2 */ record ItemStack(@NotNull TransformedItemStack itemStack) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.itemStack(predicate, itemStack); } } /** * Action to update the billboard constraint. * @param billboard the billboard type * @since 1.15.2 */ record Billboard(@NotNull PlatformBillboard billboard) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.billboard(billboard)); } } /** * Action to update item mapping. * @since 1.15.2 */ enum ItemMapping implements TrackerUpdateAction { /** * Instance. * @since 1.15.2 */ INSTANCE ; @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.updateItem(predicate); } } /** * Action to update movement duration. * @param moveDuration the duration in ticks * @since 1.15.2 */ record MoveDuration(int moveDuration) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return bone.applyAtDisplay(predicate, display -> display.moveDuration(moveDuration)); } } /** * Composite action. * @param actions the list of actions * @since 1.15.2 */ record Composite(@NotNull @Unmodifiable List actions) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { var result = false; for (TrackerUpdateAction action : actions) { if (action.test(bone, predicate)) result = true; } return result; } @Override public @NotNull Stream stream() { return actions.stream().flatMap(TrackerUpdateAction::stream); } } /** * Per-bone dynamic action. * @param builder the function to generate actions * @since 1.15.2 */ record PerBone(@NotNull Function builder) implements TrackerUpdateAction { @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return builder.apply(bone).test(bone, predicate); } } /** * No-op action. * @since 1.15.2 */ enum None implements TrackerUpdateAction { /** * Instance. * @since 1.15.2 */ INSTANCE ; @Override public boolean test(@NotNull RenderedBone bone, @NotNull BonePredicate predicate) { return false; } @Override public @NotNull Stream stream() { return Stream.empty(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/CollectionUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import it.unimi.dsi.fastutil.floats.FloatCollection; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import kr.toxicity.model.api.BetterModel; import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Collection util */ @ApiStatus.Internal public final class CollectionUtil { /** * No initializer */ private CollectionUtil() { throw new RuntimeException(); } /** * Creates a new addressing hash map. * @return new addressing map * @param key type * @param value type * @since 2.2.1 */ @NotNull public static Object2ObjectOpenHashMap newAddressingMap() { return new Object2ObjectOpenHashMap<>(); } /** * Creates a new sequenced chaining hash map. * @return new linked hash map * @param key type * @param value type * @since 2.2.1 */ @NotNull public static SequencedMap newSequencedChainingMap() { return new LinkedHashMap<>(); } /** * Creates a new sequenced addressing hash map. * @return new sequenced addressing map * @param key type * @param value type * @since 2.2.1 */ @NotNull public static Object2ObjectLinkedOpenHashMap newSequencedAddressingMap() { return new Object2ObjectLinkedOpenHashMap<>(); } /** * Creates a new chaining hash map. * @return new hash map * @param key type * @param value type * @param capacity the initial capacity * @since 2.2.1 */ @NotNull public static Map newChainingMap(int capacity) { return new HashMap<>(capacity); } /** * Creates a new addressing hash map. * @return new addressing map * @param key type * @param value type * @param capacity the initial capacity * @since 2.2.1 */ @NotNull public static Object2ObjectOpenHashMap newAddressingMap(int capacity) { return new Object2ObjectOpenHashMap<>(capacity); } /** * Creates a new sequenced chaining hash map. * @return new linked hash map * @param key type * @param value type * @param capacity the initial capacity * @since 2.2.1 */ @NotNull public static SequencedMap newSequencedChainingMap(int capacity) { return new LinkedHashMap<>(capacity); } /** * Creates a new sequenced addressing hash map. * @return new sequenced addressing map * @param key type * @param value type * @param capacity the initial capacity * @since 2.2.1 */ @NotNull public static Object2ObjectLinkedOpenHashMap newSequencedAddressingMap(int capacity) { return new Object2ObjectLinkedOpenHashMap<>(capacity); } /** * Filters stream by some instance * @param collection collection * @param rClass class of instance * @return filtered stream * @param element * @param return value */ @NotNull public static Stream filterIsInstance(@NotNull Collection collection, @NotNull Class rClass) { return collection.isEmpty() ? Stream.empty() : filterIsInstance(collection.stream(), rClass); } /** * Maps stream to list * @param collection collection * @param mapper mapper * @return unmodifiable list * @param element * @param return value */ @NotNull @Unmodifiable public static List mapToList(@NotNull Collection collection, @NotNull Function mapper) { return collection.isEmpty() ? Collections.emptyList() : mapToList(collection.stream(), mapper); } /** * Maps stream to list * @param stream stream * @param mapper mapper * @return unmodifiable list * @param element * @param return value */ @NotNull @Unmodifiable public static List mapToList(@NotNull Stream stream, @NotNull Function mapper) { return stream.map(mapper).toList(); } /** * Maps stream to set * @param collection collection * @param mapper mapper * @return unmodifiable set * @param element * @param return value */ @NotNull @Unmodifiable public static Set mapToSet(@NotNull Collection collection, @NotNull Function mapper) { return collection.isEmpty() ? Collections.emptySet() : mapToSet(collection.stream(), mapper); } /** * Maps stream to set * @param stream stream * @param mapper mapper * @return unmodifiable set * @param element * @param return value */ @NotNull @Unmodifiable public static Set mapToSet(@NotNull Stream stream, @NotNull Function mapper) { return stream.map(mapper).collect(Collectors.toUnmodifiableSet()); } /** * Maps stream to JSON * @param collection collection * @param mapper mapper * @return JSON array * @param element * @param return value */ @NotNull public static JsonArray mapToJson(@NotNull Collection collection, @NotNull Function mapper) { return collection.isEmpty() ? new JsonArray() : mapToJson(collection.size(), collection.stream(), mapper); } /** * Maps stream to JSON * @param stream stream * @param mapper mapper * @return JSON array * @param element * @param return value */ @NotNull public static JsonArray mapToJson(@NotNull Stream stream, @NotNull Function mapper) { return mapToJson(10, stream, mapper); } /** * Maps stream to JSON * @param capacity initial capacity * @param stream stream * @param mapper mapper * @return JSON array * @param element * @param return value */ @NotNull public static JsonArray mapToJson(int capacity, @NotNull Stream stream, @NotNull Function mapper) { var array = new JsonArray(capacity); stream.map(mapper).forEach(array::add); return array; } /** * Filters stream by some instance * @param stream stream * @param rClass class of instance * @return filtered stream * @param element * @param return value */ @NotNull public static Stream filterIsInstance(@NotNull Stream stream, @NotNull Class rClass) { return stream.filter(rClass::isInstance) .map(rClass::cast); } /** * Groups stream by some key * @param stream stream * @param keyMapper key mapper * @return unmodifiable grouped map * @param key * @param element */ @NotNull @Unmodifiable public static Map> group(@NotNull Stream stream, @NotNull Function keyMapper) { return Collections.unmodifiableMap(stream.collect(Collectors.groupingBy(keyMapper))); } /** * Maps some collection with map index * @param map map * @param function mapper * @return mapped stream * @param key * @param value * @param return value */ @NotNull public static Stream mapIndexed(@NotNull Map map, @NotNull IndexedFunction, R> function) { return mapIndexed(map.entrySet(), function); } /** * Maps some collection with map index * @param collection collection * @param function mapper * @return mapped stream * @param element * @param return value */ @NotNull public static Stream mapIndexed(@NotNull Collection collection, @NotNull IndexedFunction function) { return mapIndexed(collection.stream(), function); } /** * Maps some collection with map index * @param stream stream * @param function mapper * @return mapped stream * @param element * @param return value */ @NotNull public static Stream mapIndexed(@NotNull Stream stream, @NotNull IndexedFunction function) { var integer = new AtomicInteger(); return stream.map(e -> function.apply(integer.getAndIncrement(), e)); } /** * Maps some stream to a float collection * @param stream stream * @param mapper mapper * @param creator float collection creator * @return float collection * @param element * @param type */ @NotNull public static T mapFloat(@NotNull Stream stream, @NotNull FloatFunction mapper, @NotNull Supplier creator) { var collect = creator.get(); stream.forEach(e -> collect.add(mapper.apply(e))); return collect; } /** * Map some map's value. * @param original original map * @param mapper value mapper * @return unmodifiable map * @param key * @param value * @param new value */ @NotNull @Unmodifiable public static Map mapValue(@NotNull Map original, @NotNull Function mapper) { return associate(original.entrySet(), Map.Entry::getKey, e -> mapper.apply(e.getValue())); } /** * Gets filter with warning if not matched * @param predicate delegated predicate * @param lazyLogFunction log function * @return predicate * @param type */ @NotNull public static Predicate filterWithWarning(@NotNull Predicate predicate, @NotNull Function lazyLogFunction) { var logger = BetterModel.platform().logger(); return t -> { var testedValue = predicate.test(t); if (!testedValue) logger.warn(LogUtil.toLog(lazyLogFunction.apply(t), NamedTextColor.YELLOW)); return testedValue; }; } /** * Associates collection to map * @param collection collection * @param keyMapper key mapper * @param valueMapper value mapper * @return unmodifiable map * @param element * @param key * @param value */ @NotNull @Unmodifiable public static Map associate(@NotNull Collection collection, @NotNull Function keyMapper, @NotNull Function valueMapper) { return collection.isEmpty() ? Collections.emptyMap() : associate(collection.stream(), keyMapper, valueMapper); } /** * Associates stream to map * @param collection collection * @param keyMapper key mapper * @return unmodifiable map * @param element * @param key */ @NotNull @Unmodifiable public static Map associate(@NotNull Collection collection, @NotNull Function keyMapper) { return collection.isEmpty() ? Collections.emptyMap() : associate(collection.stream(), keyMapper); } /** * Associates stream to map * @param array array * @param keyMapper key mapper * @return unmodifiable map * @param element * @param key */ @NotNull @Unmodifiable public static Map associate(@NotNull E[] array, @NotNull Function keyMapper) { return array.length == 0 ? Collections.emptyMap() : associate(Arrays.stream(array), keyMapper); } /** * Associates stream to map * @param stream stream * @param keyMapper key mapper * @return unmodifiable map * @param element * @param key */ @NotNull @Unmodifiable public static Map associate(@NotNull Stream stream, @NotNull Function keyMapper) { return associate(stream, keyMapper, e -> e); } /** * Associates stream to map * @param stream stream * @param keyMapper key mapper * @param valueMapper value mapper * @return unmodifiable map * @param element * @param key * @param value */ @NotNull @Unmodifiable public static Map associate(@NotNull Stream stream, @NotNull Function keyMapper, @NotNull Function valueMapper) { return stream.collect(Collectors.toUnmodifiableMap(keyMapper, valueMapper)); } /** * Associates stream to sequenced map * @param collection collection * @param keyMapper key mapper * @return unmodifiable sequenced map * @param element * @param key */ @NotNull @Unmodifiable public static SequencedMap associateSequenced(@NotNull Collection collection, @NotNull Function keyMapper) { return associateSequenced(collection, keyMapper, v -> v); } /** * Associates collection to sequenced map * @param collection collection * @param keyMapper key mapper * @param valueMapper value mapper * @return unmodifiable sequenced map * @param element * @param key * @param value */ @NotNull @Unmodifiable public static SequencedMap associateSequenced(@NotNull Collection collection, @NotNull Function keyMapper, @NotNull Function valueMapper) { return collection.isEmpty() ? Collections.emptyNavigableMap() : associateSequenced(collection.size(), collection.stream(), keyMapper, valueMapper); } /** * Associates stream to sequenced map * @param array array * @param keyMapper key mapper * @return unmodifiable sequenced map * @param element * @param key */ @NotNull @Unmodifiable public static SequencedMap associateSequenced(@NotNull E[] array, @NotNull Function keyMapper) { return associateSequenced(array, keyMapper, v -> v); } /** * Associates collection to sequenced map * @param array array * @param keyMapper key mapper * @param valueMapper value mapper * @return unmodifiable sequenced map * @param element * @param key * @param value */ @NotNull @Unmodifiable public static SequencedMap associateSequenced(@NotNull E[] array, @NotNull Function keyMapper, @NotNull Function valueMapper) { var len = array.length; return len == 0 ? Collections.emptyNavigableMap() : associateSequenced(len, Arrays.stream(array), keyMapper, valueMapper); } /** * Associates stream to sequenced map * @param capacity the initial capacity * @param stream stream * @param keyMapper key mapper * @return unmodifiable sequenced map * @param element * @param key */ @NotNull @Unmodifiable public static SequencedMap associateSequenced(int capacity, @NotNull Stream stream, @NotNull Function keyMapper) { return associateSequenced(capacity, stream, keyMapper, e -> e); } /** * Associates stream to sequenced map * @param capacity the initial capacity * @param stream stream * @param keyMapper key mapper * @param valueMapper value mapper * @return unmodifiable sequenced map * @param element * @param key * @param value */ @NotNull @Unmodifiable public static SequencedMap associateSequenced(int capacity, @NotNull Stream stream, @NotNull Function keyMapper, @NotNull Function valueMapper) { return stream.collect(Collectors.collectingAndThen( Collectors.toMap( keyMapper, valueMapper, (oldV, newV) -> { throw new IllegalStateException("Duplicate key: " + oldV + " and " + newV); }, () -> newSequencedAddressingMap(capacity) ), Collections::unmodifiableSequencedMap )); } /** * Function with index * @param type * @param return value */ @FunctionalInterface public interface IndexedFunction { /** * Maps to new value * @param index index * @param t input * @return mapped value */ R apply(int index, T t); } /** * Function of float * @param type */ @FunctionalInterface public interface FloatFunction { /** * Maps to new value * @param t input * @return mapped float */ float apply(T t); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/EntityUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.platform.PlatformLocation; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import static java.lang.Math.*; /** * Utility class for entity-related calculations, primarily visibility checks. *

* This class provides methods to determine if an entity is within a player's field of view * or render distance, optimizing client-side rendering performance. *

* * @since 2.0.0 */ @ApiStatus.Internal public final class EntityUtil { /** * Private initializer to prevent instantiation. */ private EntityUtil() { throw new RuntimeException(); } /** * Y-axis threshold of user screen. */ private static final double Y_RENDER_THRESHOLD = toRadians(45); /** * In point threshold of user screen. */ private static final double IN_POINT_THRESHOLD = toRadians(10); /** * X-axis threshold of user screen. */ private static final double X_RENDER_THRESHOLD = Y_RENDER_THRESHOLD * 1.78; /** * Calculates the render distance in blocks based on the server's view distance. * * @return the render distance * @since 2.0.0 */ public static double renderDistance() { return BetterModel.platform().adapter().serverViewDistance() << 3; } /** * Calculates the entity model view radius based on the server's view distance. * * @return the view radius * @since 2.0.0 */ public static float entityModelViewRadius() { return (float) BetterModel.platform().adapter().serverViewDistance() / 4; } /** * Checks if a player can see a target entity based on sight tracing configuration. * * @param player the player's location * @param target the target entity's location * @return true if the target is visible, false otherwise * @since 2.0.0 */ public static boolean canSee(@NotNull PlatformLocation player, @NotNull PlatformLocation target) { var manager = BetterModel.config(); if (!manager.sightTrace()) return true; else if (!player.world().equals(target.world())) return false; var d = player.distance(target); if (d > manager.maxSight()) return false; else if (d <= manager.minSight()) return true; var t = fma(-abs(atan(d)), 2, PI); var ty = t + Y_RENDER_THRESHOLD; var tz = t + X_RENDER_THRESHOLD; return isInDegree(player, target, ty, tz); } /** * Checks if a target entity's custom name is visible to a player. * * @param player the player's location * @param target the target entity's location * @return true if the custom name is visible, false otherwise * @since 2.0.0 */ public static boolean isCustomNameVisible(@NotNull PlatformLocation player, @NotNull PlatformLocation target) { if (!player.world().equals(target.world())) return false; if (player.distanceSquared(target) > 25) return false; // 5 ^ 2 return isInPoint(player, target); } /** * Checks if a target entity is directly in the player's crosshair (point of view). * * @param player the player's location * @param target the target entity's location * @return true if the target is in the player's point of view * @since 2.0.0 */ public static boolean isInPoint(@NotNull PlatformLocation player, @NotNull PlatformLocation target) { return isInDegree(player, target, IN_POINT_THRESHOLD, IN_POINT_THRESHOLD); } private static boolean isInDegree(@NotNull PlatformLocation player, @NotNull PlatformLocation target, double ty, double tz) { var playerYaw = toRadians(player.yaw()); var playerPitch = -toRadians(player.pitch()); var dz = target.z() - player.z(); var dy = target.y() - player.y(); var dx = target.x() - player.x(); var ry = abs(atan2(dy, sqrt(MathUtil.fma(dz, dz, dx * dx))) - playerPitch); var rz = abs(atan2(-dx, dz) - playerYaw); return (ry <= ty || ry >= TAU - ty) && (rz <= tz || rz >= TAU - tz); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/EventUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.BetterModelEventBus; import kr.toxicity.model.api.event.ModelEvent; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.function.Supplier; /** * Utility class for dispatching model events. *

* This class provides helper methods to call events on the {@link BetterModelEventBus} * and handle cancellable events conveniently. *

* * @since 2.0.0 */ @ApiStatus.Internal public final class EventUtil { /** * Private initializer to prevent instantiation. */ private EventUtil() { throw new RuntimeException(); } /** * Calls an event on the global event bus. * * @param eventClass the class of the event * @param eventSupplier a supplier that creates the event * @param the type of the event * @return the result of the event call * @since 2.0.0 */ @NotNull public static BetterModelEventBus.Result call(@NotNull Class eventClass, @NotNull Supplier eventSupplier) { return BetterModel.eventBus().call(eventClass, eventSupplier); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/FunctionUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import kr.toxicity.model.api.util.function.BooleanConstantSupplier; import kr.toxicity.model.api.util.function.FloatConstantSupplier; import kr.toxicity.model.api.util.function.FloatSupplier; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; /** * Function util */ @ApiStatus.Internal public final class FunctionUtil { /** * No initializer */ private FunctionUtil() { throw new RuntimeException(); } /** * Makes constant value as supplier. * @param t value * @return supplier that returns always the same object. * @param supplier */ public static @NotNull Supplier asSupplier(@NotNull T t) { return () -> t; } /** * Throttles this function by tick * @param type * @param supplier target * @return throttled function */ public static @NotNull Supplier throttleTick(@NotNull Supplier supplier) { return throttleTick(MathUtil.MINECRAFT_TICK_MILLS, supplier); } /** * Throttles this function by tick * @param type * @param tick tick * @param supplier target * @return throttled function */ public static @NotNull Supplier throttleTick(long tick, @NotNull Supplier supplier) { return supplier instanceof TickThrottledSupplier throttledSupplier ? new TickThrottledSupplier<>(tick, throttledSupplier.delegate) : new TickThrottledSupplier<>(tick, supplier); } /** * Throttles this function by tick * @param supplier target * @return throttled function */ public static @NotNull FloatSupplier throttleTickFloat(@NotNull FloatSupplier supplier) { return throttleTickFloat(MathUtil.MINECRAFT_TICK_MILLS, supplier); } /** * Throttles this function by tick * @param tick tick * @param supplier target * @return throttled function */ public static @NotNull FloatSupplier throttleTickFloat(long tick, @NotNull FloatSupplier supplier) { return switch (supplier) { case TickThrottledFloatSupplier throttledSupplier -> new TickThrottledFloatSupplier(tick, throttledSupplier.delegate); case FloatConstantSupplier constantSupplier -> constantSupplier; default -> new TickThrottledFloatSupplier(tick, supplier); }; } /** * Throttles this function by tick * @param supplier target * @return throttled function */ public static @NotNull BooleanSupplier throttleTickBoolean(@NotNull BooleanSupplier supplier) { return switch (supplier) { case TickThrottledBooleanSupplier throttledSupplier -> throttledSupplier; case BooleanConstantSupplier booleanConstantSupplier -> booleanConstantSupplier; default -> new TickThrottledBooleanSupplier(supplier); }; } /** * Throttles this function by tick * @param type * @param predicate target * @return throttled function */ public static @NotNull Predicate throttleTick(@NotNull Predicate predicate) { return predicate instanceof TickThrottledPredicate throttledSupplier ? throttledSupplier : new TickThrottledPredicate<>(predicate); } /** * Throttles this function by tick * @param from * @param return * @param function target * @return throttled function */ public static @NotNull Function throttleTick(@NotNull Function function) { return throttleTick(MathUtil.MINECRAFT_TICK_MILLS, function); } /** * Throttles this function by tick * @param from * @param return * @param tick tick * @param function target * @return throttled function */ public static @NotNull Function throttleTick(long tick, @NotNull Function function) { return function instanceof TickThrottledFunction throttledFunction ? new TickThrottledFunction<>(tick, throttledFunction.delegate) : new TickThrottledFunction<>(tick, function); } private static class TickThrottledSupplier implements Supplier { private final long tick; private final Supplier delegate; private final AtomicLong time; private volatile T cache; public TickThrottledSupplier(long tick, @NotNull Supplier delegate) { this.tick = tick; this.delegate = delegate; time = new AtomicLong(-tick - 1); } @Override public T get() { var old = time.get(); var current = System.currentTimeMillis(); if (current - old >= tick && time.compareAndSet(old, current)) cache = delegate.get(); return cache; } } @RequiredArgsConstructor private static class TickThrottledFloatSupplier implements FloatSupplier { private final long tick; private final FloatSupplier delegate; private final AtomicLong time; private volatile float cache; public TickThrottledFloatSupplier(long tick, @NotNull FloatSupplier delegate) { this.tick = tick; this.delegate = delegate; time = new AtomicLong(-tick - 1); } @Override public float getAsFloat() { var old = time.get(); var current = System.currentTimeMillis(); if (current - old >= tick && time.compareAndSet(old, current)) cache = delegate.getAsFloat(); return cache; } } @RequiredArgsConstructor private static class TickThrottledBooleanSupplier implements BooleanSupplier { private final @NotNull BooleanSupplier delegate; private final AtomicLong time = new AtomicLong(-51); private volatile boolean cache; @Override public boolean getAsBoolean() { var old = time.get(); var current = System.currentTimeMillis(); if (current - old >= 50 && time.compareAndSet(old, current)) cache = delegate.getAsBoolean(); return cache; } } @RequiredArgsConstructor private static class TickThrottledPredicate implements Predicate { private final @NotNull Predicate delegate; private final AtomicLong time = new AtomicLong(-51); private volatile boolean cache; @Override public boolean test(T t) { var old = time.get(); var current = System.currentTimeMillis(); if (current - old >= 50 && time.compareAndSet(old, current)) cache = delegate.test(t); return cache; } } @RequiredArgsConstructor private static class TickThrottledFunction implements Function { private final long tick; private final @NotNull Function delegate; private final AtomicLong time; private volatile R cache; public TickThrottledFunction(long tick, @NotNull Function delegate) { this.tick = tick; this.delegate = delegate; time = new AtomicLong(-tick - 1); } @Override public R apply(T t) { var old = time.get(); var current = System.currentTimeMillis(); if (current - old >= tick && time.compareAndSet(old, current)) cache = delegate.apply(t); return cache; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/HttpUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializer; import com.google.gson.JsonParser; import com.google.gson.annotations.SerializedName; import com.google.gson.stream.JsonReader; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.version.MinecraftVersion; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.semver4j.Semver; import java.io.InputStreamReader; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.function.Function; /** * Http util */ @ApiStatus.Internal public final class HttpUtil { private static final HttpClient CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.of(5, ChronoUnit.SECONDS)) .executor(Executors.newVirtualThreadPerTaskExecutor()) .build(); private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(MinecraftVersion.class, (JsonDeserializer) (json, _, _) -> MinecraftVersion.parse(json.getAsString())) .registerTypeAdapter(Semver.class, (JsonDeserializer) (json, _, _) -> Semver.coerce(json.getAsString())) .create(); /** * No initializer */ private HttpUtil() { throw new RuntimeException(); } /** * Searches BetterModel's latest version * @return latest version */ public static @NotNull LatestVersion versionList() { return versionList(BetterModel.platform().version()); } /** * Searches BetterModel's latest version compatible with current server * @param version server version * @return latest version */ public static @NotNull LatestVersion versionList(@NotNull MinecraftVersion version) { return client(client -> { try (var stream = client.send(HttpRequest.newBuilder() .GET() .uri(URI.create("https://api.modrinth.com/v2/project/bettermodel/version")) .build(), HttpResponse.BodyHandlers.ofInputStream()).body(); var reader = new InputStreamReader(stream); var jsonReader = new JsonReader(reader) ) { return latestOf(JsonParser.parseReader(jsonReader) .getAsJsonArray() .asList() .stream() .map(e -> GSON.fromJson(e, PluginVersion.class)) .filter(PluginVersion::isSamePlatform) .filter(v -> v.versions.contains(version)) .sorted(Comparator.comparing((PluginVersion v) -> v.versionNumber).reversed()) .toList()); } }).orElse(e -> { LogUtil.handleException("Unable to get BetterModel's version info.", e); return new LatestVersion(null, null); }); } /** * Gets the latest version from a version list * @param versions versions * @return latest version */ public static @NotNull LatestVersion latestOf(@NotNull List versions) { PluginVersion release = null, snapshot = null; for (PluginVersion version : versions) { if (version.versionType.equals("release")) { if (release == null) release = version; } else if (snapshot == null) snapshot = version; } return new LatestVersion(release, snapshot); } /** * Latest version * @param release release * @param snapshot snapshot */ public record LatestVersion(@Nullable PluginVersion release, @Nullable PluginVersion snapshot) { } /** * Plugin version * @param id id * @param versionNumber number * @param versionType type * @param versions game versions * @param loaders loaders */ public record PluginVersion( @NotNull String id, @NotNull @SerializedName("version_number") Semver versionNumber, @NotNull @SerializedName("version_type") String versionType, @NotNull @SerializedName("game_versions") Set versions, @NotNull Set loaders ) { /** * Creates a text component with URL * @return text component */ public @NotNull Component toURLComponent() { var url = "https://modrinth.com/plugin/bettermodel/version/" + id; return Component.text() .content(versionNumber.getVersion()) .color(NamedTextColor.AQUA) .hoverEvent( HoverEvent.showText(Component.text() .append(Component.text(url).color(NamedTextColor.DARK_AQUA)) .appendNewline() .append(Component.text("Click to open link."))) ) .clickEvent(ClickEvent.openUrl(url)) .build(); } /** * Checks this version is same platform with running platform * @return is same platform */ public boolean isSamePlatform() { return loaders.contains(BetterModel.platform().jarType().raw()); } } /** * Uses http client * @param consumer consumer * @return result * @param type */ public static @NotNull Result client(@NotNull HttpClientConsumer consumer) { try { return new Result.Success<>(consumer.accept(CLIENT)); } catch (Exception e) { return new Result.Failure<>(e); } } /** * http result * @param type */ public sealed interface Result { /** * Gets the value or handle exception * @param function exception handler * @return value */ default @NotNull T orElse(@NotNull Function function) { return switch (this) { case Failure failure -> function.apply(failure.exception); case Success success -> success.result; }; } /** * Success * @param result result value * @param type */ record Success(@NotNull T result) implements Result {} /** * Failure * @param exception exception * @param type */ record Failure(@NotNull Exception exception) implements Result {} } /** * Http client consumer * @param type */ @FunctionalInterface public interface HttpClientConsumer { /** * Accepts a task with an http client * @param client client * @return value * @throws Exception exception */ @NotNull T accept(@NotNull HttpClient client) throws Exception; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/InterpolationUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import it.unimi.dsi.fastutil.floats.FloatArrayList; import it.unimi.dsi.fastutil.floats.FloatSortedSet; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.animation.AnimationKeyframe; import kr.toxicity.model.api.animation.VectorPoint; import kr.toxicity.model.api.tracker.Tracker; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import java.util.List; import static kr.toxicity.model.api.util.MathUtil.*; /** * Interpolation util */ @ApiStatus.Internal public final class InterpolationUtil { /** * No initializer */ private InterpolationUtil() { throw new RuntimeException(); } private static final float FRAME_HASH = (float) Tracker.TRACKER_TICK_INTERVAL / 1000F; private static final float FRAME_HASH_REVERT = 1 / FRAME_HASH; /** * Builds animation keyframe * @param position position point * @param rotation rotation point * @param scale scale point * @param rotationGlobal rotation global * @param points keyframe time set * @return animation keyframe */ @NotNull public static AnimationKeyframe buildAnimation( @NotNull List position, @NotNull List rotation, @NotNull List scale, boolean rotationGlobal, @NotNull FloatSortedSet points ) { var pp = interpolatorFor(position); var sp = interpolatorFor(scale); var rp = interpolatorFor(rotation); var keyframe = AnimationKeyframe.builder(points.size(), rotationGlobal); var before = 0F; var iterator = points.iterator(); while (iterator.hasNext()) { var f = iterator.nextFloat(); var pr = pp.build(f); var sr = sp.build(f); var rr = rp.build(f); keyframe.write( roundTime(f - before), pr.vector, sr.vector, rr.vector, pr.skipInterpolation || sr.skipInterpolation || rr.skipInterpolation ); before = f; } return keyframe.build(); } /** * Creates interpolator * @param vectors target vector list * @return point builder */ public static @NotNull VectorPointBuilder interpolatorFor(@NotNull List vectors) { var last = vectors.isEmpty() ? VectorPoint.EMPTY : vectors.getLast(); return vectors.size() < 2 ? f -> new VectorResult(last.vector(f)) : new VectorPointBuilder() { private VectorPoint p1 = VectorPoint.EMPTY; private VectorPoint p2 = vectors.getFirst(); private int i = 0; private float t = p2.time(); @Override public @NotNull VectorResult build(float nextFloat) { while (i < vectors.size() - 1 && t < nextFloat) { p1 = p2; t = (p2 = vectors.get(++i)).time(); } if (nextFloat > last.time()) return new VectorResult(last.vector(nextFloat)); else return nextFloat == t ? new VectorResult(p2.vector(), !p1.isContinuous()) : new VectorResult(p1.interpolator().interpolate(vectors, i, nextFloat)); } }; } /** * Rounds float to frame time * @param time time * @return rounded time */ public static float roundTime(float time) { return (int) fma(time, FRAME_HASH_REVERT, FRAME_EPSILON) * FRAME_HASH; } /** * Inserts lerp frame to given set * @param frames target set */ public static void insertLerpFrame(@NotNull FloatSortedSet frames) { insertLerpFrame(frames, (float) BetterModel.config().lerpFrameTime() / 20F); } /** * Inserts lerp frame to given set * @param frames target set * @param frame frame */ public static void insertLerpFrame(@NotNull FloatSortedSet frames, float frame) { if (frame <= 0F) return; var first = 0F; var second = 0F; var iterator = new FloatArrayList(frames).iterator(); while (iterator.hasNext()) { first = second; second = iterator.nextFloat(); var max = (int) ((second - first) / frame); for (int i = 0; i < max; i++) { var add = fma(frame, i + 1, first); if (second - add < frame + FRAME_EPSILON) continue; frames.add(add); } } } /** * Finds alpha value * @param p0 p0 * @param p1 p1 * @param alpha target value between p0 and p1 * @return alpha (0..1) */ public static float alpha(float p0, float p1, float alpha) { var div = p1 - p0; return div == 0 ? 0 : (alpha - p0) / div; } /** * Lerps two point * @param p0 p0 * @param p1 p1 * @param alpha alpha * @return lerped vector */ public static @NotNull Vector3f lerp(@NotNull Vector3f p0, @NotNull Vector3f p1, float alpha) { return lerp(p0, p1, alpha, new Vector3f()); } /** * Lerps two point * @param p0 p0 * @param p1 p1 * @param alpha alpha * @param dest destination vector * @return lerped vector */ public static @NotNull Vector3f lerp(@NotNull Vector3f p0, @NotNull Vector3f p1, float alpha, @NotNull Vector3f dest) { dest.x = lerp(p0.x, p1.x, alpha); dest.y = lerp(p0.y, p1.y, alpha); dest.z = lerp(p0.z, p1.z, alpha); return dest; } /** * Lerps two point * @param p0 p0 * @param p1 p1 * @param alpha alpha * @return lerped point */ public static float lerp(float p0, float p1, float alpha) { return fma(p1 - p0, alpha, p0); } private static float cubicBezier(float p0, float p1, float p2, float p3, float t) { var u = 1.0F - t; var uu = u * u; var tt = t * t; var uuu = uu * u; var utt = u * tt; var uut = uu * t; var ttt = tt * t; return fma(uuu, p0, fma(3.0F * uut, p1, fma(3.0F * utt, p2, ttt * p3))); } private static float derivativeBezier(float p1, float p2, float t) { var u = 1.0F - t; var uu = u * u; var ut = u * t; var tt = t * t; return fma(3.0F * uu, p1, fma(6.0F * ut, p2 - p1, 3.0F * tt * (1 - p2))); } private static float solveBezierTForTime(float time, float h1, float h2) { var t = 0.5F; var maxIterations = 20; for (int i = 0; i < maxIterations; i++) { var bezTime = cubicBezier(0, h1, h2, 1, t); var derivative = derivativeBezier(h1, h2, t); var error = bezTime - time; if (Math.abs(error) < FLOAT_COMPARISON_EPSILON) { return t; } if (derivative != 0) { t -= error / derivative; } t = Math.clamp(t, 0F, 1F); } return t; } /** * Interpolates two vectors as bezier * @param alpha alpha time * @param start start keyframe * @param end end keyframe * @param bezierRightTime bezier right time * @param bezierRightValue bezier right value * @param bezierLeftTime bezier left time * @param bezierLeftValue bezier left value * @return interpolated vector */ public static @NotNull Vector3f bezier( float alpha, @NotNull Vector3f start, @NotNull Vector3f end, @NotNull Vector3f bezierRightTime, @NotNull Vector3f bezierRightValue, @NotNull Vector3f bezierLeftTime, @NotNull Vector3f bezierLeftValue ) { return new Vector3f( cubicBezier(start.x, start.x + bezierRightValue.x, end.x + bezierLeftValue.x, end.x, solveBezierTForTime( alpha, bezierRightTime.x, 1 + bezierLeftTime.x )), cubicBezier(start.y, start.y + bezierRightValue.y, end.y + bezierLeftValue.y, end.y, solveBezierTForTime( alpha, bezierRightTime.y, 1 + bezierLeftTime.y )), cubicBezier(start.z, start.z + bezierRightValue.z, end.z + bezierLeftValue.z, end.z, solveBezierTForTime( alpha, bezierRightTime.z, 1 + bezierLeftTime.z )) ); } /** * Interpolates four vectors as catmull-rom. * @param p0 p0 * @param p1 p1 * @param p2 p2 * @param p3 p3 * @param t alpha * @return interpolated vector */ public static @NotNull Vector3f catmull_rom(@NotNull Vector3f p0, @NotNull Vector3f p1, @NotNull Vector3f p2, @NotNull Vector3f p3, float t) { var t2 = t * t; var t3 = t2 * t; return new Vector3f( fma(t3, fma(-1F, p0.x, fma(3F, p1.x, fma(-3F, p2.x, p3.x))), fma(t2, fma(2F, p0.x, fma(-5F, p1.x, fma(4F, p2.x, -p3.x))), fma(t, -p0.x + p2.x, 2F * p1.x))), fma(t3, fma(-1F, p0.y, fma(3F, p1.y, fma(-3F, p2.y, p3.y))), fma(t2, fma(2F, p0.y, fma(-5F, p1.y, fma(4F, p2.y, -p3.y))), fma(t, -p0.y + p2.y, 2F * p1.y))), fma(t3, fma(-1F, p0.z, fma(3F, p1.z, fma(-3F, p2.z, p3.z))), fma(t2, fma(2F, p0.z, fma(-5F, p1.z, fma(4F, p2.z, -p3.z))), fma(t, -p0.z + p2.z, 2F * p1.z))) ).mul(0.5F); } /** * Vector point builder */ @FunctionalInterface public interface VectorPointBuilder { /** * Interpolates vector from given float * @param nextFloat next float value * @return interpolated vector */ @NotNull VectorResult build(float nextFloat); } /** * Vector result * @param vector vector * @param skipInterpolation skip interpolation */ public record VectorResult(@NotNull Vector3f vector, boolean skipInterpolation) { /** * Vector result * @param vector vector */ public VectorResult(@NotNull Vector3f vector) { this(vector, false); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/LogUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.config.DebugConfig; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.function.Supplier; /** * Log util */ @ApiStatus.Internal public final class LogUtil { /** * No initializer */ private LogUtil() { throw new RuntimeException(); } /** * Handles exception message * @param message message * @param throwable exception */ public static void handleException(@NotNull String message, @NotNull Throwable throwable) { var list = new ArrayList(4); list.add(Component.text(message)); list.add(toLog("Reason: " + throwable.getMessage(), NamedTextColor.YELLOW)); if (BetterModel.config().debug().has(DebugConfig.DebugOption.EXCEPTION)) { list.add(toLog("Stack trace:", NamedTextColor.RED)); try ( var byteArray = new ByteArrayOutputStream(); var print = new PrintStream(byteArray) ) { throwable.printStackTrace(print); list.add(toLog(byteArray.toString(StandardCharsets.UTF_8), NamedTextColor.RED)); } catch (IOException e) { list.add(toLog("Unknown", NamedTextColor.RED)); } } else list.add(toLog("If you want to see the stack trace, set debug.exception to true in config.yml", NamedTextColor.LIGHT_PURPLE)); BetterModel.platform().logger().warn(list.toArray(Component[]::new)); } /** * Gets log component * @param message message * @param color color * @return component */ public static @NotNull Component toLog(@NotNull String message, @NotNull TextColor color) { return Component.text().content(message).color(color).build(); } /** * Logs debug if some option is matched. * @param option option * @param log log */ public static void debug(@NotNull DebugConfig.DebugOption option, @NotNull Supplier log) { debug(option, () -> BetterModel.platform().logger().info(Component.text() .append(toLog("[DEBUG-" + option + "] ", NamedTextColor.YELLOW)) .append(Component.text(log.get())) .build()) ); } /** * Runs debug if some option is matched. * @param option option * @param runnable debug task */ public static void debug(@NotNull DebugConfig.DebugOption option, @NotNull Runnable runnable) { if (BetterModel.config().debug().has(option)) runnable.run(); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/MathUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import it.unimi.dsi.fastutil.floats.FloatComparator; import it.unimi.dsi.fastutil.floats.FloatSet; import kr.toxicity.model.api.data.Float3; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionf; import org.joml.Vector3f; import org.joml.Vector3fc; import static java.lang.Math.PI; import static java.lang.Math.abs; /** * Math */ @ApiStatus.Internal public final class MathUtil { /** * No initializer */ private MathUtil() { throw new RuntimeException(); } /** * Minecraft tick mills */ public static final int MINECRAFT_TICK_MILLS = 50; /** * Valid rotation degree */ public static final float ROTATION_DEGREE = 22.5F; /** * Degrees to radians */ public static final float DEGREES_TO_RADIANS = (float) PI / 180F; /** * Radians to degrees */ public static final float RADIANS_TO_DEGREES = 1F / DEGREES_TO_RADIANS; /** * Degrees to packed byte */ public static final float DEGREES_TO_PACKED_BYTE = 256F / 360F; /** * Multiplier value for convert model size to block size */ public static final float MODEL_TO_BLOCK_MULTIPLIER = 16; /** * Frame epsilon value */ public static final float FRAME_EPSILON = 0.001F; /** * Float comparison epsilon value */ public static final float FLOAT_COMPARISON_EPSILON = 1E-5F; /** * Squared vector comparison epsilon value */ public static final float VECTOR_COMPARISON_EPSILON_SQ = 1E-8F; /** * Quaternion comparison epsilon value */ public static final float QUATERNION_COMPARISON_EPSILON = 1E-5F; private static final Vector3f ZERO_VECTOR = new Vector3f(); /** * Float comparator */ public static final FloatComparator FRAME_COMPARATOR = (a, b) -> isSimilar(a, b, FRAME_EPSILON) ? 0 : Float.compare(a, b); private static final FloatSet VALID_ROTATION_DEGREES = FloatSet.of( 0F, ROTATION_DEGREE, ROTATION_DEGREE * 2, -ROTATION_DEGREE, -ROTATION_DEGREE * 2 ); /** * Checks two floats are similar. * @param a a * @param b b * @return similar or not */ public static boolean isSimilar(float a, float b) { return isSimilar(a, b, FLOAT_COMPARISON_EPSILON); } /** * Checks two floats are similar. * @param a a * @param b b * @param epsilon epsilon * @return similar or not */ public static boolean isSimilar(float a, float b, float epsilon) { return abs(a - b) < epsilon; } /** * Checks two vectors are similar. * @param a a * @param b b * @return similar or not */ public static boolean isSimilar(@NotNull Vector3fc a, @NotNull Vector3fc b) { return isSimilar(a, b, VECTOR_COMPARISON_EPSILON_SQ); } /** * Checks two vectors are similar. * @param a a * @param b b * @param epsilon epsilon * @return similar or not */ public static boolean isSimilar(@NotNull Vector3fc a, @NotNull Vector3fc b, float epsilon) { return a.distanceSquared(b) < epsilon; } /** * Checks two quaternion are similar. * @param a a * @param b b * @return similar or not */ public static boolean isSimilar(@NotNull Quaternionf a, @NotNull Quaternionf b) { return isSimilar(a, b, QUATERNION_COMPARISON_EPSILON); } /** * Checks two quaternion are similar. * @param a a * @param b b * @param epsilon epsilon * @return similar or not */ public static boolean isSimilar(@NotNull Quaternionf a, @NotNull Quaternionf b, float epsilon) { return abs(fma(a.x, b.x, fma(a.y, b.y, fma(a.z, b.z, a.w * b.w)))) > 1.0F - epsilon; } /** * Creates epsilon-based hashcode of given float * @param value value * @return hashcode */ public static int similarHashCode(float value) { return (int) (value / FLOAT_COMPARISON_EPSILON); } /** * Checks these floats are valid Minecraft degree * @param rotation rotation * @return is valid */ public static boolean checkValidDegree(@NotNull Float3 rotation) { var i = 0; if (rotation.x() != 0F) i++; if (rotation.y() != 0F) i++; if (rotation.z() != 0F) i++; return i < 2 && checkValidDegree(rotation.x()) && checkValidDegree(rotation.y()) && checkValidDegree(rotation.z()); } /** * Checks this float is valid Minecraft degree * @param rotation rotation * @return is valid */ public static boolean checkValidDegree(float rotation) { return VALID_ROTATION_DEGREES.contains(rotation); } /** * Creates rotation identifier * @param rotation rotation * @return identifier */ public static @NotNull Float3 identifier(@NotNull Float3 rotation) { if (checkValidDegree(rotation)) return Float3.ZERO; return rotation; } /** * Converts vector rotation to quaternion * @param vector vector * @return rotation */ public static @NotNull Quaternionf toQuaternion(@NotNull Vector3f vector) { return toQuaternion(vector, new Quaternionf()); } /** * Converts vector rotation to quaternion * @param vector vector * @param dest destination quaternion * @return rotation */ public static @NotNull Quaternionf toQuaternion(@NotNull Vector3f vector, @NotNull Quaternionf dest) { return dest .identity() .rotateZYX( vector.z * DEGREES_TO_RADIANS, vector.y * DEGREES_TO_RADIANS, vector.x * DEGREES_TO_RADIANS ); } /** * Converts zyx euler to xyz euler * @param vec zyx euler * @return xyz euler */ public static @NotNull Vector3f toXYZEuler(@NotNull Vector3f vec) { return toQuaternion(vec) .getEulerAnglesXYZ(vec) .mul(RADIANS_TO_DEGREES); } /** * Executes fused multiply add (a * b + c) * @param a a vector * @param b b vector * @param c c vector * @return added a */ public static @NotNull Vector3f fma(@NotNull Vector3f a, @NotNull Vector3f b, @NotNull Vector3f c) { a.x = fma(a.x, b.x, c.x); a.y = fma(a.y, b.y, c.y); a.z = fma(a.z, b.z, c.z); return a; } /** * Executes fused multiply add (a * b + c) * @param a a vector * @param b b scala * @param c c vector * @return added a */ public static @NotNull Vector3f fma(@NotNull Vector3f a, float b, @NotNull Vector3f c) { a.x = fma(a.x, b, c.x); a.y = fma(a.y, b, c.y); a.z = fma(a.z, b, c.z); return a; } /** * Executes fused multiply add (a * b + c) * @param a a * @param b b * @param c c * @return a * b + c */ public static float fma(float a, float b, float c) { return org.joml.Math.fma(a, b, c); } /** * Executes fused multiply add (a * b + c) * @param a a * @param b b * @param c c * @return a * b + c */ public static double fma(double a, double b, double c) { return org.joml.Math.fma(a, b, c); } /** * Checks this vector is not zero * @param vector3f vector * @return is not zero */ public static boolean isNotZero(@NotNull Vector3f vector3f) { return !isZero(vector3f); } /** * Checks this vector is zero * @param vector vector * @return is zero */ public static boolean isZero(@NotNull Vector3f vector) { return isSimilar(vector, ZERO_VECTOR); } /** * Converts a 32-bit float to IEEE 754 half-precision bits. * * @param value source float * @return half-float bit pattern stored in a short */ public static short floatToHalf(float value) { int bits = Float.floatToIntBits(value); int sign = (bits >>> 16) & 0x8000; int exp = ((bits >>> 23) & 0xFF) - 127 + 15; int mant = bits & 0x7FFFFF; if (((bits >>> 23) & 0xFF) == 0xFF) { if (mant == 0) return (short) (sign | 0x7C00); return (short) (sign | 0x7E00); } if (exp >= 0x1F) return (short) (sign | 0x7C00); if (exp <= 0) { if (exp < -10) return (short) sign; mant |= 0x800000; int shift = 14 - exp; int halfMant = mant >> shift; int roundBit = 1 << (shift - 1); if ((mant & roundBit) != 0 && ((mant & (roundBit - 1)) != 0 || (halfMant & 1) != 0)) halfMant++; return (short) (sign | halfMant); } int halfExp = exp << 10; int halfMant = mant >> 13; if ((mant & 0x00001000) != 0) { halfMant++; if ((halfMant & 0x0400) != 0) { halfMant = 0; halfExp += 0x0400; if (halfExp >= 0x7C00) return (short) (sign | 0x7C00); } } return (short) (sign | halfExp | halfMant); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/PackUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.regex.Pattern; /** * Pack util */ @ApiStatus.Internal public final class PackUtil { /** * No initializer */ private PackUtil() { throw new RuntimeException(); } private static final Pattern REPLACE_SOURCE = Pattern.compile("[^a-z0-9_.]"); /** * Asserts that the given raw string is a valid pack name. * Throws a {@link IllegalArgumentException} if the name contains illegal characters. * @param raw The raw string to validate. */ public static void assertPackName(@NotNull String raw) { if (REPLACE_SOURCE.matcher(raw).find()) throw new IllegalArgumentException("Illegal pack name: " + raw); } /** * Converts some path to compatible with Minecraft resource location * @param raw raw path * @return converted path */ public static @NotNull String toPackName(@NotNull String raw) { return REPLACE_SOURCE.matcher(raw.toLowerCase()) .replaceAll(result -> Integer.toString(result.group().hashCode(), 16).toLowerCase()); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/ReflectionUtil.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Reflection util */ @ApiStatus.Internal public final class ReflectionUtil { /** * No initializer */ private ReflectionUtil() { throw new RuntimeException(); } /** * Checks some class is existing. * @param clazz class path * @return exists */ public static boolean classExists(@NotNull String clazz) { try { Class.forName(clazz); return true; } catch (ClassNotFoundException e) { return false; } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/TransformedItemStack.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.platform.PlatformItemStack; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import java.util.function.Function; /** * ItemStack with offset and scale * @see PlatformItemStack * @param position global position (x, y, z) * @param offset offset (x, y, z) * @param scale scale (x, y, z) * @param itemStack item */ public record TransformedItemStack(@NotNull Vector3f position, @NotNull Vector3f offset, @NotNull Vector3f scale, @NotNull PlatformItemStack itemStack) { /** * Creates empty transformed item * @return empty transformed item */ public static @NotNull TransformedItemStack empty() { return of(BetterModel.platform().adapter().air()); } /** * Creates transformed item * @param itemStack item * @return transformed item */ public static @NotNull TransformedItemStack of(@NotNull PlatformItemStack itemStack) { return of(new Vector3f(), new Vector3f(), new Vector3f(1), itemStack); } /** * Creates transformed item * @param position position * @param offset offset * @param scale scale * @param itemStack item * @return transformed item */ public static @NotNull TransformedItemStack of(@NotNull Vector3f position, @NotNull Vector3f offset, @NotNull Vector3f scale, @NotNull PlatformItemStack itemStack) { return new TransformedItemStack(position, offset, scale, itemStack); } /** * Gets transformed item as air * @return air item */ public @NotNull TransformedItemStack asAir() { return of(position, offset, scale, BetterModel.platform().adapter().air()); } /** * Sets offset * @param offset offset * @return new item */ public @NotNull TransformedItemStack offset(@NotNull Vector3f offset) { return of(position, offset, scale, itemStack); } /** * Modify item * @param mapper mapper * @return modified item */ public @NotNull TransformedItemStack modify(@NotNull Function mapper) { return of(position, offset, scale, mapper.apply(itemStack.clone())); } /** * Checks this item is air * @return is air */ public boolean isAir() { return itemStack.isAir(); } /** * Copy this item * @return copied item */ public @NotNull TransformedItemStack copy() { return new TransformedItemStack( new Vector3f(position), new Vector3f(offset), new Vector3f(scale), itemStack.clone() ); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/collection/PriorityMap.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.collection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Function; /** * A map that maintains the order of values based on priority, insertion order, and key comparison. * * @param the type of keys maintained by this map * @param the type of mapped values * @since 3.0.0 */ public final class PriorityMap, V> { private final Map> keyMap = new HashMap<>(); private final TreeMap, V> valueMap = new TreeMap<>(); private long counter; /** * Returns true if this map contains no key-value mappings. * * @return true if this map contains no key-value mappings */ public boolean isEmpty() { return valueMap.isEmpty(); } /** * Internal identifier used to sort entries in the value map. */ private record Identifier>( int priority, long count, @NotNull K key ) implements Comparable> { /** * Compares this identifier with another based on priority (descending), * then insertion count (descending), and finally the key itself. * * @param o the object to be compared. * @return a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified object. */ @Override public int compareTo(@NotNull Identifier o) { int c; if ((c = Integer.compare(o.priority, priority)) != 0) return c; if ((c = Long.compare(o.count, count)) != 0) return c; return key.compareTo(o.key); } } /** * Associates the specified value with the specified key and priority in this map. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @param priority priority of the entry * @return the previous value associated with key, or null if there was no mapping for key. */ public @Nullable V put(@NotNull K key, @NotNull V value, int priority) { Objects.requireNonNull(key); Objects.requireNonNull(value); Identifier newIdentifier, oldIdentifier; if ((oldIdentifier = keyMap.put(key, newIdentifier = new Identifier<>(priority, counter++, key))) != null) valueMap.remove(oldIdentifier); return valueMap.put(newIdentifier, value); } /** * Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. * * @param key the key whose associated value is to be returned * @return the value to which the specified key is mapped, or null if this map contains no mapping for the key */ public @Nullable V get(@NotNull K key) { Identifier identifier; return (identifier = keyMap.get(Objects.requireNonNull(key))) == null ? null : valueMap.get(identifier); } /** * Removes the mapping for a key from this map if it is present. * * @param key key whose mapping is to be removed from the map * @return the previous value associated with key, or null if there was no mapping for key. */ public @Nullable V remove(@NotNull K key) { Identifier identifier; return (identifier = keyMap.remove(Objects.requireNonNull(key))) == null ? null : valueMap.remove(identifier); } /** * Replaces the entry for the specified key only if it is currently mapped to some value. * * @param Key key with which the specified value is associated * @param function the function to compute a value * @return the previous value associated with the specified key, or null if there was no mapping for the key. */ public @Nullable V replace(@NotNull K Key, @NotNull Function function) { Identifier identifier; if ((identifier = keyMap.get(Objects.requireNonNull(Key))) == null) return null; return valueMap.computeIfPresent(identifier, (_, v) -> function.apply(v)); } /** * Returns an iterator over the values in this map in priority order. * * @return a value iterator * @since 3.0.1 */ public @NotNull Iterator valueIterator() { return new ValueIterator(); } private class ValueIterator implements Iterator { private final Iterator, V>> delegate = valueMap.entrySet().iterator(); private Identifier identifier; @Override public boolean hasNext() { return delegate.hasNext(); } @Override public V next() { var next = delegate.next(); identifier = next.getKey(); return next.getValue(); } @Override public void remove() { delegate.remove(); keyMap.remove(Objects.requireNonNull(identifier).key); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/collection/SingletonSequencedSet.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.collection; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.*; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @Unmodifiable @ApiStatus.Internal public final class SingletonSequencedSet extends AbstractSet implements SequencedSet { private final E element; private final Set delegate; public static @NotNull SingletonSequencedSet of(@NotNull E element) { return new SingletonSequencedSet<>(element); } private SingletonSequencedSet(@NotNull E element) { this.element = element; this.delegate = Collections.singleton(element); } public int size() { return 1; } public boolean contains(Object o) { return element.equals(o); } @Override public int hashCode() { return element.hashCode(); } @NotNull public Iterator iterator() { return delegate.iterator(); } // Override default methods for Collection @Override @NotNull public Spliterator spliterator() { return delegate.spliterator(); } @Override public void forEach(Consumer action) { action.accept(element); } @Override public boolean removeIf(@NotNull Predicate filter) { throw new UnsupportedOperationException(); } @Override public @NotNull Stream stream() { return Stream.of(element); } @Override public @NotNull Stream parallelStream() { return stream(); } // Override default methods for SequencedCollection @Override public E getFirst() { return element; } @Override public E getLast() { return element; } @Override public E removeFirst() { throw new UnsupportedOperationException(); } @Override public E removeLast() { throw new UnsupportedOperationException(); } @Override @NotNull @Unmodifiable public SequencedSet reversed() { return this; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/BonePredicate.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; import kr.toxicity.model.api.bone.BoneTag; import kr.toxicity.model.api.bone.RenderedBone; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.function.Predicate; /** * Bone predicate */ public interface BonePredicate extends Predicate { /** * True */ BonePredicate TRUE = of(State.TRUE, _ -> true); /** * False */ BonePredicate FALSE = of(State.FALSE, _ -> false); /** * Gets builder by name * @param name name * @return builder */ static @NotNull Builder name(@NotNull String name) { return b -> b.name().name().equalsIgnoreCase(name); } /** * Gets builder by tags * @param tags tags * @return builder */ static @NotNull Builder tag(@NotNull BoneTag... tags) { if (tags.length == 0) throw new RuntimeException("tags cannot be empty."); return b -> b.name().tagged(tags); } @Override boolean test(@NotNull RenderedBone bone); @Override @NotNull BonePredicate and(@NotNull Predicate other); @Override @NotNull BonePredicate or(@NotNull Predicate other); @Override @NotNull BonePredicate negate(); /** * Should apply at children bone too * @return apply at children */ @NotNull State applyAtChildren(); /** * Gets bone predicate * @param predicate predicate * @return bone predicate */ static @NotNull BonePredicate from(@NotNull Predicate predicate) { return of(State.NOT_SET, predicate); } /** * Gets bone predicate * @param applyAtChildren apply at children * @param predicate predicate * @return bone predicate */ static @NotNull BonePredicate of(@NotNull State applyAtChildren, @NotNull Predicate predicate) { Objects.requireNonNull(predicate, "predicate cannot be null."); return new Packed(applyAtChildren, predicate); } /** * Packed value * @param applyAtChildren apply at children * @param predicate predicate */ record Packed(@NotNull State applyAtChildren, @NotNull Predicate predicate) implements BonePredicate { @Override public boolean test(@NotNull RenderedBone bone) { return predicate.test(bone); } @Override @NotNull public BonePredicate and(@NotNull Predicate other) { Objects.requireNonNull(other); return of(applyAtChildren, t -> predicate.test(t) && other.test(t)); } @Override @NotNull public BonePredicate or(@NotNull Predicate other) { Objects.requireNonNull(other); return of(applyAtChildren, t -> predicate.test(t) || other.test(t)); } @Override @NotNull public BonePredicate negate() { return of(applyAtChildren, t -> !predicate.test(t)); } } /** * children bone state */ enum State { /** * Apply with children too */ TRUE, /** * Doesn't apply children */ FALSE, /** * Ignore parent's result */ NOT_SET } /** * Gets children predicate * @param parentSuccess result at parent bone * @return bone predicate */ @ApiStatus.Internal default @NotNull BonePredicate children(boolean parentSuccess) { return parentSuccess ? switch (applyAtChildren()) { case TRUE -> BonePredicate.TRUE; case FALSE -> BonePredicate.FALSE; case NOT_SET -> this; } : this; } /** * Builder */ @FunctionalInterface interface Builder extends Predicate { /** * Builds with child state * @return bone predicate */ default @NotNull BonePredicate notSet() { return build(State.NOT_SET); } /** * Builds with applying children bone * @return bone predicate */ default @NotNull BonePredicate withChildren() { return build(State.TRUE); } /** * Builds without applying children bone * @return bone predicate */ default @NotNull BonePredicate withoutChildren() { return build(State.FALSE); } /** * Builds with child state * @param state state * @return bone predicate */ default @NotNull BonePredicate build(@NotNull State state) { return of(state, this); } @Override @NotNull default Builder and(@NotNull Predicate other) { return bone -> test(bone) && other.test(bone); } @Override @NotNull default Builder or(@NotNull Predicate other) { return bone -> test(bone) || other.test(bone); } @Override @NotNull default Builder negate() { return bone -> !test(bone); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/BooleanConstantSupplier.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import java.util.function.BooleanSupplier; /** * Boolean constant supplier */ @RequiredArgsConstructor public enum BooleanConstantSupplier implements BooleanSupplier { /** * True */ TRUE(true), /** * False */ FALSE(false) ; private final boolean value; public static @NotNull BooleanConstantSupplier of(boolean value) { return value ? TRUE : FALSE; } @Override public boolean getAsBoolean() { return value; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/Float2FloatConstantFunction.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; /** * Float to float constant function * @param value value */ public record Float2FloatConstantFunction(float value) implements Float2FloatFunction { @Override public float applyAsFloat(float value) { return this.value; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/Float2FloatFunction.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; import org.jetbrains.annotations.NotNull; /** * Float to float function */ @FunctionalInterface public interface Float2FloatFunction { /** * Zero */ Float2FloatConstantFunction ZERO = of(0); /** * Applies float value * @param value value * @return applied value */ float applyAsFloat(float value); /** * Creates constant function by given value * @param value value * @return constant function */ static @NotNull Float2FloatConstantFunction of(float value) { return new Float2FloatConstantFunction(value); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/FloatConstantFunction.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; import org.jetbrains.annotations.NotNull; import java.util.function.Function; /** * Float constant function * @param value value * @param type */ public record FloatConstantFunction(@NotNull T value) implements FloatFunction { @Override public @NotNull T apply(float value) { return this.value; } @Override public @NotNull FloatFunction map(@NotNull Function mapper) { return FloatFunction.of(mapper.apply(value)); } @Override public @NotNull FloatFunction memoize() { return this; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/FloatConstantSupplier.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; /** * Float constant supplier * @param value value */ public record FloatConstantSupplier(float value) implements FloatSupplier { /** * One */ public static final FloatConstantSupplier ONE = FloatSupplier.of(1F); @Override public float getAsFloat() { return value; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/FloatFunction.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; import kr.toxicity.model.api.util.MathUtil; import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.function.Function; /** * Float function * @param type */ @FunctionalInterface public interface FloatFunction { /** * Applies float * @param value float value * @return applied value */ @NotNull T apply(float value); /** * Creates constant function by given value * @param t value * @return constant function * @param type */ static @NotNull FloatConstantFunction of(@NotNull T t) { return new FloatConstantFunction<>(Objects.requireNonNull(t)); } /** * Maps this function to new type * @param mapper mapper * @return mapped function * @param return type */ default @NotNull FloatFunction map(@NotNull Function mapper) { return f -> mapper.apply(apply(f)); } /** * Memoize this function * @return memoized function */ default @NotNull FloatFunction memoize() { var map = new Int2ReferenceOpenHashMap(); return f -> map.computeIfAbsent(MathUtil.similarHashCode(f), _ -> apply(f)); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/function/FloatSupplier.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.function; import org.jetbrains.annotations.NotNull; /** * Float supplier */ @FunctionalInterface public interface FloatSupplier { /** * Gets float value * @return float value */ float getAsFloat(); /** * Creates supplier by given value * @param value val ue * @return supplier */ static @NotNull FloatConstantSupplier of(float value) { return new FloatConstantSupplier(value); } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/interpolator/VectorInterpolator.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.interpolator; import com.google.gson.annotations.SerializedName; import kr.toxicity.model.api.animation.VectorPoint; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import java.util.List; import static kr.toxicity.model.api.util.InterpolationUtil.*; /** * Interpolator */ @ApiStatus.Internal public enum VectorInterpolator { /** * Linear */ @SerializedName("linear") LINEAR { @NotNull @Override public Vector3f interpolate(@NotNull List points, int p2Index, float time) { var p1 = p2Index > 0 ? points.get(p2Index - 1) : points.getFirst(); var p2 = points.get(p2Index); var t1 = p1.time(); var t2 = p2.time(); var a = alpha(t1, t2, time); return lerp( p1.vector(lerp(t1, t2, a)), p2.vector(), a ); } }, /** * Catmullrom */ @SerializedName("catmullrom") CATMULLROM { private static @NotNull VectorPoint indexOf(@NotNull List list, int index, int relative) { var i = index + relative; while (i < 0) i += list.size(); return list.get(i % list.size()); } @NotNull @Override public Vector3f interpolate(@NotNull List points, int p2Index, float time) { var p0 = indexOf(points, p2Index, -2); var p1 = indexOf(points, p2Index, -1); var p2 = points.get(p2Index); var p3 = indexOf(points, p2Index, 1); var t1 = p1.time(); var t2 = p2.time(); var a = alpha(t1, t2, time); return catmull_rom( p0.vector(), p1.vector(lerp(t1, t2, a)), p2.vector(), p3.vector(), a ); } }, /** * Bezier */ @SerializedName("bezier") BEZIER { @NotNull @Override public Vector3f interpolate(@NotNull List points, int p2Index, float time) { var p1 = p2Index > 0 ? points.get(p2Index - 1) : points.getFirst(); var p2 = points.get(p2Index); var t1 = p1.time(); var t2 = p2.time(); var a = alpha(t1, t2, time); return bezier( a, p1.vector(lerp(t1, t2, a)), p2.vector(), p1.bezier().rightTime(), p1.bezier().rightValue(), p2.bezier().leftTime(), p2.bezier().leftValue() ); } }, /** * Step */ @SerializedName("step") STEP { @NotNull @Override public Vector3f interpolate(@NotNull List points, int p2Index, float time) { return (p2Index > 0 ? points.get(p2Index - 1) : points.getFirst()).vector(time); } @Override public boolean isContinuous() { return false; } } ; /** * Interpolates vector * @param points points * @param p2Index p2 index * @param time destination time * @return interpolated vector */ @NotNull public abstract Vector3f interpolate(@NotNull List points, int p2Index, float time); /** * Checks this interpolator is continuous * @return is continuous */ public boolean isContinuous() { return true; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/json/JsonArrayBuilder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.json; import com.google.gson.JsonArray; import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.function.Consumer; public final class JsonArrayBuilder { private final JsonArray array = new JsonArray(); /** * Creates builder * @return builder */ public static @NotNull JsonArrayBuilder builder() { return new JsonArrayBuilder(); } public @NotNull JsonArrayBuilder jsonObject(@NotNull Consumer consumer) { var builder = JsonObjectBuilder.builder(); Objects.requireNonNull(consumer).accept(builder); var json = builder.build(); if (!json.isEmpty()) array.add(json); return this; } public @NotNull JsonArray build() { return array; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/json/JsonObjectBuilder.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.json; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; /** * JSON object builder */ @ApiStatus.Internal public final class JsonObjectBuilder { private final JsonObject object = new JsonObject(); /** * Private initializer */ private JsonObjectBuilder() { } /** * Creates builder * @return builder */ public static @NotNull JsonObjectBuilder builder() { return new JsonObjectBuilder(); } /** * Builds JSON object * @return build */ public @NotNull JsonObject build() { return object; } /** * Adds JSON object * @param name name * @param consumer builder * @return self */ public @NotNull JsonObjectBuilder jsonObject(@NotNull String name, @NotNull Consumer consumer) { var builder = builder(); Objects.requireNonNull(consumer).accept(builder); if (builder.object.isEmpty()) return this; object.add(name, builder.build()); return this; } /** * Adds JSON object * @param name name * @param jsonObject object * @return self */ public @NotNull JsonObjectBuilder jsonObject(@NotNull String name, @Nullable JsonObject jsonObject) { if (jsonObject != null) object.add(name, jsonObject); return this; } /** * Adds JSON array * @param name name * @param array array * @return self */ public @NotNull JsonObjectBuilder jsonArray(@NotNull String name, @Nullable JsonArray array) { if (array != null) object.add(name, array); return this; } /** * Adds JSON array * @param name name * @param consumer consumer * @return self */ public @NotNull JsonObjectBuilder jsonArray(@NotNull String name, @NotNull Consumer consumer) { var builder = JsonArrayBuilder.builder(); Objects.requireNonNull(consumer).accept(builder); var json = builder.build(); if (!json.isEmpty()) object.add(name, json); return this; } /** * Adds JSON property * @param name name * @param property property * @return self */ public @NotNull JsonObjectBuilder property(@NotNull String name, @NotNull String property) { object.addProperty(name, property); return this; } /** * Adds JSON property * @param name name * @param property property * @return self */ public @NotNull JsonObjectBuilder property(@NotNull String name, @NotNull Boolean property) { object.addProperty(name, property); return this; } /** * Adds JSON property * @param name name * @param property property * @return self */ public @NotNull JsonObjectBuilder property(@NotNull String name, @Nullable Number property) { if (property != null) object.addProperty(name, property); return this; } /** * Adds JSON property * @param entries entries * @return self */ public @NotNull JsonObjectBuilder stringProperties(@NotNull Iterable> entries) { for (var entry : entries) { property(entry.getKey(), entry.getValue()); } return this; } /** * Adds JSON property * @param entries entries * @return self */ public @NotNull JsonObjectBuilder booleanProperties(@NotNull Iterable> entries) { for (var entry : entries) { property(entry.getKey(), entry.getValue()); } return this; } /** * Adds JSON property * @param entries entries * @return self */ public @NotNull JsonObjectBuilder numberProperties(@NotNull Iterable> entries) { for (var entry : entries) { property(entry.getKey(), entry.getValue()); } return this; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/lazy/LazyFloatProvider.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.lazy; import kr.toxicity.model.api.util.InterpolationUtil; import kr.toxicity.model.api.util.function.FloatSupplier; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.joml.Vector3f; import java.util.Objects; import java.util.function.Supplier; /** * Lazy float provider */ @ApiStatus.Internal public final class LazyFloatProvider { private final FloatSupplier requiredTime; private long time = System.currentTimeMillis(); private float storedValue; private boolean first = true; /** * Creates from time * @param requiredTime required time */ public LazyFloatProvider(long requiredTime) { this((float) requiredTime); } /** * Creates from time * @param requiredTime required time */ public LazyFloatProvider(float requiredTime) { this(() -> requiredTime); } /** * Creates from time supplier * @param requiredTime required time supplier */ public LazyFloatProvider(@NotNull FloatSupplier requiredTime) { this.requiredTime = requiredTime; } /** * Creates from time supplier * @param requiredTime required time supplier * @param initialValue initial value */ public LazyFloatProvider(float initialValue, @NotNull FloatSupplier requiredTime) { this(requiredTime); this.storedValue = initialValue; } /** * Updates and gets float * @param updateValue destination value * @return interpolated value */ public float updateAndGet(float updateValue) { var req = requiredTime.getAsFloat(); if (req <= 0 || first) { first = false; return storedValue = updateValue; } var current = System.currentTimeMillis(); var alpha = Math.clamp((float) (current - time) / req, 0, 1); time = current; return storedValue = InterpolationUtil.lerp( storedValue, updateValue, alpha ); } /** * Sets stored value * @param storedValue new value */ public void storedValue(float storedValue) { this.storedValue = storedValue; time = System.currentTimeMillis(); } /** * Gets lazy provider of vector * @param requiredTime required time * @param delegate source provider * @return lazy provider */ public static @NotNull Supplier ofVector(@NotNull FloatSupplier requiredTime, @NotNull Supplier delegate) { Objects.requireNonNull(requiredTime); Objects.requireNonNull(delegate); var xLazy = new LazyFloatProvider(requiredTime); var yLazy = new LazyFloatProvider(requiredTime); var zLazy = new LazyFloatProvider(requiredTime); return () -> { var get = delegate.get(); get.x = xLazy.updateAndGet(get.x); get.y = yLazy.updateAndGet(get.y); get.z = zLazy.updateAndGet(get.z); return get; }; } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/lock/DuplexLock.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.lock; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; /** * Duplex lock */ @ApiStatus.Internal public final class DuplexLock { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); /** * Access to read lock * @param supplier supplier * @return value * @param type */ public T accessToReadLock(@NotNull Supplier supplier) { var readLock = lock.readLock(); readLock.lock(); try { return supplier.get(); } finally { readLock.unlock(); } } /** * Access to write lock * @param supplier supplier * @return value * @param type */ public T accessToWriteLock(@NotNull Supplier supplier) { var writeLock = lock.writeLock(); writeLock.lock(); try { return supplier.get(); } finally { writeLock.unlock(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/util/lock/SingleLock.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.util.lock; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Supplier; @ApiStatus.Internal public final class SingleLock { private final Object lock; public SingleLock() { this(null); } public SingleLock(@Nullable Object lock) { this.lock = lock != null ? lock : this; } public T accessToLock(@NotNull Supplier supplier) { synchronized (lock) { return supplier.get(); } } } ================================================ FILE: api/src/main/java/kr/toxicity/model/api/version/MinecraftVersion.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.api.version; import org.jetbrains.annotations.NotNull; import org.semver4j.Semver; import java.util.Comparator; import java.util.Objects; /** * Minecraft version. * @param major title * @param minor main update * @param patch minor update */ public record MinecraftVersion(int major, int minor, int patch) implements Comparable { /** * 26.1.2 */ public static final MinecraftVersion V26_1_2 = of(26, 1, 2); /** * 26.1.1 */ public static final MinecraftVersion V26_1_1 = of(26, 1, 1); /** * 26.1 */ public static final MinecraftVersion V26_1 = of(26, 1, 0); /** * 1.21.11 */ public static final MinecraftVersion V1_21_11 = of(1, 21, 11); /** * 1.21.10 */ public static final MinecraftVersion V1_21_10 = of(1, 21, 10); /** * 1.21.9 */ public static final MinecraftVersion V1_21_9 = of(1, 21, 9); /** * 1.21.8 */ public static final MinecraftVersion V1_21_8 = of(1, 21, 8); /** * 1.21.7 */ public static final MinecraftVersion V1_21_7 = of(1, 21, 7); /** * 1.21.6 */ public static final MinecraftVersion V1_21_6 = of(1, 21, 6); /** * 1.21.5 */ public static final MinecraftVersion V1_21_5 = of(1, 21, 5); /** * 1.21.4 */ public static final MinecraftVersion V1_21_4 = of(1, 21, 4); /** * Comparator */ private static final Comparator COMPARATOR = Comparator.comparing(MinecraftVersion::major) .thenComparing(MinecraftVersion::minor) .thenComparing(MinecraftVersion::patch); /** * Parses version from string * @param version version like "1.21.11" * @return parsed version */ public static @NotNull MinecraftVersion parse(@NotNull String version) { var split = Objects.requireNonNull(Semver.coerce(version)); return of(split.getMajor(), split.getMinor(), split.getPatch()); } /** * Creates version * @param major major * @param minor minor * @param patch patch * @return Minecraft version */ public static @NotNull MinecraftVersion of(int major, int minor, int patch) { return new MinecraftVersion(major, minor, patch); } @Override public int compareTo(@NotNull MinecraftVersion o) { return COMPARATOR.compare(this, o); } @NotNull @Override public String toString() { return major + "." + minor + "." + patch; } } ================================================ FILE: build.gradle.kts ================================================ import io.papermc.hangarpublishplugin.model.Platforms plugins { alias(libs.plugins.convention.standard) alias(libs.plugins.hangar) id("xyz.jpenilla.run-paper") version "3.0.2" } val minecraft = property("minecraft_version").toString() val versionString = version.toString() val groupString = group.toString() val javadocJar by tasks.registering(Jar::class) { dependsOn(tasks.dokkaGenerate) archiveClassifier = "javadoc" from(layout.buildDirectory.dir("dokka/html").orNull?.asFile) } runPaper { disablePluginJarDetection() } tasks { runServer { pluginJars(fileTree("plugins")) pluginJars(project(":platform:bettermodel-paper").tasks.named("shadowJar").flatMap { it.archiveFile }) pluginJars(project(":test-plugin").tasks.jar.flatMap { it.archiveFile }) minecraftVersion(minecraft) downloadPlugins { hangar("ViaVersion", "5.9.0") hangar("ViaBackwards", "5.9.0") hangar("Skript", "2.15.0") } } build { finalizedBy( javadocJar ) } jar { enabled = false } } hangarPublish { publications.register("plugin") { version = project.version as String id = "BetterModel" apiKey = System.getenv("HANGAR_API_TOKEN") val log = System.getenv("COMMIT_MESSAGE") if (log != null) { changelog = log channel = "Snapshot" } else { changelog = rootProject.file("changelog/$versionString.md").readText() channel = "Release" } platforms { register(Platforms.PAPER) { jar = project(":platform:bettermodel-paper").tasks.named("shadowJar").flatMap { it.archiveFile } platformVersions = SUPPORTED_VERSIONS dependencies { hangar("SkinsRestorer") { required = false } } } } } } ================================================ FILE: buildSrc/build.gradle.kts ================================================ plugins { `kotlin-dsl` } repositories { mavenCentral() gradlePluginPortal() } dependencies { implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) implementation(libs.build.kotlin.jvm) implementation(libs.build.shadow) implementation(libs.build.hangarPublish) implementation(libs.build.minotaur) implementation(libs.build.resourcefactory) implementation(libs.build.paperweight) implementation("org.jetbrains.dokka:dokka-gradle-plugin:2.2.0") implementation("dev.yumi.gradle.licenser:dev.yumi.gradle.licenser.gradle.plugin:3.0.1") implementation("com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin:0.36.0") } ================================================ FILE: buildSrc/settings.gradle.kts ================================================ rootProject.name = "buildSrc" dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } ================================================ FILE: buildSrc/src/main/kotlin/Extensions.kt ================================================ import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project const val JAVA_VERSION = 25 val BUILD_NUMBER: String? = System.getenv("BUILD_NUMBER") val Project.libs get() = rootProject.extensions.getByName("libs") as LibrariesForLibs val LATEST_VERSION = listOf( "26.1", "26.1.1", "26.1.2" ) val SUPPORTED_VERSIONS = buildList { addAll(listOf( "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11", )) addAll(LATEST_VERSION) } val BUKKIT_LOADERS = listOf("spigot") val PAPER_LOADERS = listOf("paper", "purpur", "folia") ================================================ FILE: buildSrc/src/main/kotlin/bukkit-conventions.gradle.kts ================================================ plugins { id("standard-conventions") } val minecraft = property("minecraft_version").toString() dependencies { compileOnly("io.papermc.paper:paper-api:$minecraft.build.+") testImplementation("io.papermc.paper:paper-api:$minecraft.build.+") } ================================================ FILE: buildSrc/src/main/kotlin/modrinth-conventions.gradle.kts ================================================ plugins { id("com.modrinth.minotaur") } val versionString = version.toString() val classifier = project.name .substringAfterLast('-') .replaceFirstChar { it.uppercase() } modrinth { token = System.getenv("MODRINTH_API_TOKEN") projectId = "bettermodel" syncBodyFrom = rootProject.file("BANNER.md").readText() val log = System.getenv("COMMIT_MESSAGE") if (log != null) { versionType = "beta" changelog = log } else { versionType = "release" changelog = rootProject.file("changelog/$versionString.md").readText() } additionalFiles { javadocJar(rootProject.layout.buildDirectory.file("libs/${rootProject.name}-$versionString-javadoc.jar")) } versionNumber = versionString versionName = "BetterModel $versionString for $classifier" } ================================================ FILE: buildSrc/src/main/kotlin/paperweight-conventions.gradle.kts ================================================ plugins { id("standard-conventions") id("io.papermc.paperweight.userdev") } dependencies { compileOnly(project(":bettermodel-api")) compileOnly(project(":bettermodel-api:bettermodel-bukkit-api")) } ================================================ FILE: buildSrc/src/main/kotlin/plugin-conventions.gradle.kts ================================================ plugins { id("bukkit-conventions") id("modrinth-conventions") id("com.gradleup.shadow") } val shade: Configuration = configurations.getByName("shade") val versionString = version.toString() val groupString = group.toString() val classifier: String = project.name.substringAfterLast('-') dependencies { compileOnly(project(":bettermodel-api")) compileOnly(project(":bettermodel-api:bettermodel-bukkit-api")) compileOnly(project(":bettermodel-core")) shade(project(":bettermodel-core:bettermodel-bukkit-core")) { exclude("org.jetbrains.kotlin") } } tasks { jar { finalizedBy(shadowJar) } shadowJar { configurations.set(listOf(shade)) manifest { attributes(mapOf( "Dev-Build" to (BUILD_NUMBER ?: -1), "Version" to versionString, "Author" to "toxicity188", "Url" to "https://github.com/toxicity188/BetterModel", "Created-By" to "Gradle $gradle", "Build-Jdk" to "${System.getProperty("java.vendor")} ${System.getProperty("java.version")}", "Build-OS" to "${System.getProperty("os.arch")} ${System.getProperty("os.name")}" ) + libs.bundles.manifestLibrary.get().associate { "library-${it.name}" to it.version }) } archiveBaseName = rootProject.name archiveClassifier = classifier destinationDirectory = rootProject.layout.buildDirectory.dir("libs") dependencies { exclude(dependency("org.jetbrains:annotations:26.0.2")) } fun prefix(pattern: String) { relocate(pattern, "$groupString.shaded.$pattern") } prefix("kotlin") prefix("kr.toxicity.library.armormodel") prefix("org.incendo.cloud") prefix("org.bstats") prefix("net.byteflux.libby") } } modrinth { uploadFile.set(tasks.shadowJar) dependencies { optional.project( "mythicmobs", "skinsrestorer" ) } } ================================================ FILE: buildSrc/src/main/kotlin/publish-conventions.gradle.kts ================================================ import com.vanniktech.maven.publish.JavaLibrary import com.vanniktech.maven.publish.JavadocJar import com.vanniktech.maven.publish.SourcesJar import kotlin.io.encoding.Base64 plugins { id("standard-conventions") id("com.vanniktech.maven.publish") signing } val artifactBaseId = name val artifactVersion = project.version.toString().run { BUILD_NUMBER?.let { substringBeforeLast("-$it") } ?: this } signing { val key = System.getenv("SIGNING_KEY")?.let { Base64.decode(it.toByteArray()).toString(Charsets.UTF_8) } val password = System.getenv("SIGNING_PASSWORD") if (!key.isNullOrEmpty() && !password.isNullOrEmpty()) { useInMemoryPgpKeys( key, password ) } else useGpgCmd() } dependencies { api(libs.bundles.library) compileOnly(libs.lombok) annotationProcessor(libs.lombok) testCompileOnly(libs.lombok) testAnnotationProcessor(libs.lombok) } mavenPublishing { publishToMavenCentral() signAllPublications() coordinates("io.github.toxicity188", artifactBaseId, artifactVersion) configure(JavaLibrary( javadocJar = JavadocJar.Javadoc(), sourcesJar = SourcesJar.Sources(), )) pom { name = artifactBaseId description = "Modern Bedrock model engine for Minecraft Java Edition" inceptionYear = "2024" url = "https://github.com/toxicity188/BetterModel/" licenses { license { name = "MIT License" url = "https://mit-license.org/" } } developers { developer { id = "toxicity188" name = "toxicity188" url = "https://github.com/toxicity188/" } } scm { url = "https://github.com/toxicity188/BetterModel/" connection = "scm:git:git://github.com/toxicity188/BetterModel.git" developerConnection = "scm:git:ssh://git@github.com/toxicity188/BetterModel.git" } } } publishing { repositories { maven { name = "GitHubPackages" url = uri("https://maven.pkg.github.com/toxicity188/${rootProject.name}") credentials { username = "toxicity188" password = System.getenv("PACKAGES_API_TOKEN") } } } } ================================================ FILE: buildSrc/src/main/kotlin/standard-conventions.gradle.kts ================================================ plugins { java kotlin("jvm") id("org.jetbrains.dokka") id("dev.yumi.gradle.licenser") } group = "kr.toxicity.model" version = property("project_version").toString() + (BUILD_NUMBER?.let { "-SNAPSHOT-$it" } ?: "") val shade = configurations.create("shade") configurations.implementation { extendsFrom(shade) } rootProject.dependencies.dokka(project) dependencies { testImplementation(kotlin("test")) compileOnly(libs.bundles.library) testImplementation(libs.bundles.library) } tasks { test { useJUnitPlatform() } compileJava { options.encoding = Charsets.UTF_8.name() } } license { rule(rootProject.file("LICENSE_HEADER")) include("**/*.java", "**/*.kt") exclude("**/*.properties") } java { disableAutoTargetJvm() toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION) } kotlin { jvmToolchain(JAVA_VERSION) } dokka { moduleName = project.name dokkaSourceSets.configureEach { displayName = project.name } } ================================================ FILE: changelog/3.0.1.md ================================================ ## 🔧 Fixes - fix: PriorityMap ## 🧹 Chores - chore: update net.fabricmc:fabric-loader to 0.19.2 - chore: update net.fabricmc.fabric-loom-repositories to 1.16-SNAPSHOT - chore: update Skript to v2.15.0 for test server - chore: update fabric-api to v0.146.1+26.1.2 - chore: update eu.pb4:polymer-resource-pack to v0.16.3+26.1.2 [Full change log](https://github.com/toxicity188/BetterModel/compare/3.0.0...3.0.1) ================================================ FILE: changelog/3.0.2.md ================================================ [Full change log](https://github.com/toxicity188/BetterModel/compare/3.0.1...3.0.2) ================================================ FILE: changelog/v1/1.10.0.md ================================================ # BetterModel 1.10.0 ### Add - new entity rotation API - 'bodyrotation' MythicMobs mechanic ### Fix - handle equipment change of player - entity invisibility - traffic optimization - animation accuracy - parallel animation packet bundling [Full change log](https://github.com/toxicity188/BetterModel/compare/1.9.3...1.10.0) ================================================ FILE: changelog/v1/1.10.1.md ================================================ # BetterModel 1.10.1 ### Notice Now support about 1.20.4 is dropped. ### Add - passengers API in EntityTrackerRegistry ### Fix - entity body and head rotation - MythicMobs mechanics - hitbox rendering - inventory handling [Full change log](https://github.com/toxicity188/BetterModel/compare/1.10.0...1.10.1) ================================================ FILE: changelog/v1/1.10.2.md ================================================ # BetterModel 1.10.2 ### Fix - optimize animation interpolation and keyframe - an attribute packet about hitbox - Folia compatibility - death animation [Full change log](https://github.com/toxicity188/BetterModel/compare/1.10.1...1.10.2) ================================================ FILE: changelog/v1/1.10.3.md ================================================ # BetterModel 1.10.3 ### Add - initial molang support - expose id and uuid of item-display ### Fix - equipment in the left hand - animation signal - packet bundler [Full change log](https://github.com/toxicity188/BetterModel/compare/1.10.2...1.10.3) ================================================ FILE: changelog/v1/1.11.0.md ================================================ # BetterModel 1.11.0 ### Add - API for [per-player animation](https://github.com/toxicity188/BetterModel/wiki/Per%E2%80%90player-animation) - rewrite animation packet packer - 'remapmodel' MythicMobs mechanic - [custom nametag](https://github.com/toxicity188/BetterModel/wiki/Configuring-bone-tag) ### Fix - [molang support](https://github.com/toxicity188/BetterModel/wiki/Math-animation) - exception with some bone [Full change log](https://github.com/toxicity188/BetterModel/compare/1.10.3...1.11.0) * * * ![](https://github.com/user-attachments/assets/39e72652-d484-48a6-85d0-47e18d382275) * * * ================================================ FILE: changelog/v1/1.11.1.md ================================================ # BetterModel 1.11.1 ## Notice Now API repository is moved. #### Release ```kotlin repositories { mavenCentral() } dependencies { compileOnly("io.github.toxicity188:bettermodel:VERSION") } ``` #### Snapshot ```kotlin repositories { maven("https://maven.pkg.github.com/toxicity188/BetterModel") } dependencies { compileOnly("io.github.toxicity188:bettermodel:VERSION-SNAPSHOT") } ``` ## Add - support [shadow bone](https://github.com/toxicity188/BetterModel/wiki/Configuring-bone-tag#custom-shadow) to sync ModelEngine's usage ## Fix - math animation interpolation - ModelRenderer#create(Entity, GameProfile, boolean) - player nametag [Full change log](https://github.com/toxicity188/BetterModel/compare/1.11.0...1.11.1) ================================================ FILE: changelog/v1/1.11.2.md ================================================ # BetterModel 1.11.2 ## Add - Improved reload performance - Reload indicator bar ## Change - Kotlin 2.2.10 - player's left/right item bone tag (pli, pri) -> entity's left/right item bone tag (li, ri) - ModelRenderer#create(Location, Player) -> ModelRenderer#create(Location, OfflinePlayer) ## Fix - MythicMobs mechanics - Keyframe generation - Interpolation duration - Hiding player's armor when disguised [Full change log](https://github.com/toxicity188/BetterModel/compare/1.11.1...1.11.2) ================================================ FILE: changelog/v1/1.11.3.md ================================================ # BetterModel 1.11.3 ## Notice - It will be helpful to check 'template texture' docs to ignore resource pack generation. - There are some deprecated API in ModelManager and PlayerManager. ## Add - allow textures in BetterModel/players with [template texture](https://github.com/toxicity188/BetterModel/wiki/Template-texture) - add player/monster nametag (ptag_, mtag_) - ModelRenderer API - more BetterModel API - improved reload speed ``` BetterModel.modelOrNull("name"); //Gets nullable model renderer BetterModel.model("name"); //Gets optional model renderer ``` ## Fix - avoid classloader conflict of Adventure in Paper. - tracker remove refreshment - minor optimized memory cost - animation script [Full change log](https://github.com/toxicity188/BetterModel/compare/1.11.2...1.11.3) ================================================ FILE: changelog/v1/1.11.4.md ================================================ # BetterModel 1.11.4 ## Add - refresh plugin jar - use caffeine - add hide argument in /bm play (#134) - lerp-frame-time 5 -> 3 - zip hash - Auto-merge with Nexo ## Fix - animation interpolation - plugin lifecycle [Full change log](https://github.com/toxicity188/BetterModel/compare/1.11.3...1.11.4) ================================================ FILE: changelog/v1/1.12.0.md ================================================ # BetterModel 1.12.0 ## Add - billboard MythicMobs mechanic - 5 of [animation script](https://github.com/toxicity188/BetterModel/wiki/Animation-Script) - TrackerUpdateAction#composite - ModelEngine blueprints migration ``` changepart partvis enchant remap tint ``` ## Change - Kotlin 2.2.20 ## Fix - SkinsRestorer compatibility - mount hitbox cache - hitbox exception - minor keyframe optimization [Full change log](https://github.com/toxicity188/BetterModel/compare/1.11.4...1.12.0) ================================================ FILE: changelog/v1/1.12.1.md ================================================ # BetterModel 1.12.1 ## Notice This is a simple bug fix version for preparing 1.21.9 update. ## Add - More comfortable help command (/bm) - API update related to 1.21.9 ## Fix - Citizens trait - NPE in animation script - Boot issue with some version like 1.21.3 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.12.0...1.12.1) ================================================ FILE: changelog/v1/1.13.0.md ================================================ # BetterModel 1.13.0 ## Add - 1.21.9 support - component logging ## Change - optimize state handler ## Fix - animation script - 'hold on last' animation type - model resolution - step interpolation - exception handling on tick [Full change log](https://github.com/toxicity188/BetterModel/compare/1.12.1...1.13.0) ================================================ FILE: changelog/v1/1.13.1.md ================================================ # BetterModel 1.13.1 ## Feat - official 1.21.10 support - use paper-plugin.yml in Paper platform - make CommandAPI as runtime - 'pairmodel' mechanics (experimental) - 'use-obfuscation' config option ![](https://github.com/user-attachments/assets/5934c271-1397-4b5a-8f58-c5eff46c10d2) ## Fix - ignore group name's case - add minimum keyframe time [Full change log](https://github.com/toxicity188/BetterModel/compare/1.13.0...1.13.1) ================================================ FILE: changelog/v1/1.13.2.md ================================================ # BetterModel 1.13.2 ## Notice - This is a hotfix of 1.13.1 ## Feat - support obfuscation in player limb ## Fix - properly supporting minecraft 1.21.10 - plugin loading issue with some legacy Paper version - some model's loading issue - invalid hitbox bounding box [Full change log](https://github.com/toxicity188/BetterModel/compare/1.13.1...1.13.2) ================================================ FILE: changelog/v1/1.13.3.md ================================================ # BetterModel 1.13.3 ## Notice This is a version for avoid confusion with BlockBench 5 ## Feat - initial BlockBench 5 support - remove blueprint reference from renderer for gc - reuse IO buffer ## Fix - Tracker#show - animation replacement (MythicMobs mechanic 'defaultstate') [Full change log](https://github.com/toxicity188/BetterModel/compare/1.13.2...1.13.3) ================================================ FILE: changelog/v1/1.13.4.md ================================================ # BetterModel 1.13.4 ## Feat - cape support in player limb (bone tag 'cape_') - 'enable-strict-loading' config - Kotlin 2.2.21 ## Fix - UV size - 'shadow' bone tag - exception in animation script - position convert in BlockBench 5.0 - 'false' texture loading error - filter dummy data in model file - animation sync of each model part [Full change log](https://github.com/toxicity188/BetterModel/compare/1.13.3...1.13.4) ================================================ FILE: changelog/v1/1.14.0.md ================================================ # ⚡ BetterModel 1.14.0 ## 📗 Notice This update introduces significant API changes to the entity adapter. Please verify the compatibility of all plugins that depend on BetterModel. ## 🔥 Feat ### Armor in player model (experimental) ![](https://github.com/user-attachments/assets/abc277b3-b811-49e7-92e6-9847d73f39db) * * * The player model now supports vanilla armor. Armor textures can be customized in the BetterModel/armors directory. ### Global texture (global_) ![](https://github.com/user-attachments/assets/a16e066a-3f4c-46eb-a569-ed58489dc2e7) * * * Any texture prefixed with `global_` is recognized as a global texture. These textures have no namespace and can be shared between multiple models, avoiding duplicate texture creation. ### Other features - remove image process - 'loop_mode' argument in /npc animate ## 🔧 Fix - move duration - parsing bone tag - NoSuchElementException in ModelAnimation ## 🧹 Chores - deps: update dependency com.github.ben-manes.caffeine:caffeine to v3.2.3 - deps: update dependency io.github.toxicity188:dynamicuv to v1.1.0 - deps: update gradle to v9.2.0 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.13.4...1.14.0) ================================================ FILE: changelog/v1/1.14.1.md ================================================ ## 🔥 Feat ### IK rig (experimental) ![](https://github.com/user-attachments/assets/8eae5c72-9403-44af-8a1d-746b1e94f7d6) ![](https://github.com/user-attachments/assets/10d6d5af-7af7-44bc-ad62-fe1396713fa0) * * * Now BetterModel supports IK rig. (locator, null object) ### Rotation in global space ![](https://github.com/user-attachments/assets/61b04973-34fb-410d-ba7a-53936f250568) * * * Now animator option 'rotation in global space' is available. ### Other features - 'brightness' script - namespace in MythicMobs script (bm) ## 🔧 Fix - default leather armor color - animation placeholder - exception handling in missing textures - stability with SkinsRestorer [Full change log](https://github.com/toxicity188/BetterModel/compare/1.14.0...1.14.1) ================================================ FILE: changelog/v1/1.14.2.md ================================================ ## 🔥 Feat - more accurate ik rig - support client skin customisation for player limb (cape) ## 🔧 Fix - RenderedBone#worldPosition ('ModelPart' MythicMobs targeter) - 'camera' element type - 'bm test' command in Spigot platform - step interpolation - bezier interpolation [Full change log](https://github.com/toxicity188/BetterModel/compare/1.14.1...1.14.2) ================================================ FILE: changelog/v1/1.15.0.md ================================================ ## 📗 Notice Due to Mojang’s [new version numbering system](https://www.minecraft.net/en-us/article/minecraft-new-version-numbering-system) (e.g., 1.21.x → 26.x), the next BetterModel feature release will begin at `2.0.0` to avoid confusion with Minecraft versions. This also marks the completion of the BetterModel 1.x series, as its core APIs and features have now reached a stable, finalized state. Several technical questions are expected to emerge in the near future, and they may influence the evolution of BetterModel 2.0.0: - Will Mojang upgrade Minecraft’s Java requirement to [version 25](https://openjdk.org/projects/jdk/25/)? - Will Spigot platform compatibility be phased out based on [future policy decisions by the Paper team](https://github.com/PaperMC/Paper/pull/13135#issuecomment-3368037505)? - How will NMS be organized under Mojang’s new version numbering system? ## 🔥 Feat ![](https://github.com/user-attachments/assets/17aa49c1-abf9-4796-a369-9bb7600e94ff) - [Minecraft 1.21.11](https://minecraft.wiki/w/Java_Edition_1.21.11) support - new profile API to handle uncompleted profile ```java // ModelProfile#of // Can be offline player, uuid BetterModel.model("model").ifPresent(model -> model.create(location, ModelProfile.of(player))); ``` - [CommandAPI](https://github.com/CommandAPI/CommandAPI) is no longer used. Now [cloud](https://github.com/Incendo/cloud) is used to implement commands. - parallel json build - optimized IK - optimized texture load - BonePredicate builder ```java //Or chain BonePredicate.name("hitbox") .or(BonePredicate.tag(BoneTags.HITBOX)) .or(b -> b.getGroup().getMountController().canMount()) .notSet(); //Apply with children BonePredicate.tag(BoneTags.HEAD_WITH_CHILDREN).withChildren(); ``` - added 'loop_type' to '/bm limb' command - added 'scaling' to 'bm disguise' command ## ⚡ Refactor - ModelChildren -> ModelOutliner - BaseEntity - Float3 and Float4 - ModelBlueprint ## 🔧 Fix - Tracker#hide on init - resource pack load error - Citizens trait - UV issue regarding right forearm ## 🧹 Chores - fix(deps): update dependency io.lumine:mythic-dist to v5.11.1 - fix(deps): update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.3.0 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.14.2...1.15.0) ================================================ FILE: changelog/v1/1.15.1.md ================================================ ## 🔥 Feat - feat: per-player animation in MythicMobs state mechanic - feat: TrackerUpdateAction#perBone - feat: optimize memory usage ## 🔧 Fix - fix: model disappearance when plugin is reloaded - fix: mount controller strafing - fix: Kotlin 2.3.0 compatibility ## ⚡ Refactor - refactor: ModelManagerImpl.kt ## 🧹 Chores - chore: update Kotlin to 2.3.0 - chore: update citizens-main to 2.0.41-SNAPSHOT - chore: update cloud-paper to 2.0.0-beta.14 - fix(deps): update dependency net.kyori:adventure-api to v4.26.1 - fix(deps): update dependency com.nexomc:nexo to v1.16.1 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.15.0...1.15.1) ================================================ FILE: changelog/v1/1.15.2.md ================================================ ## 🔥 Feat - feat: add frame time and frame interpolate in texture ## 🔧 Fix - fix: skin cache expiration - fix: disable EntitiesLoadEvent listener (#239) - fix: equality check with v6 authlib (#238) - fix: optimize memory allocation - fix: invalid type inference of compiler (#245) - fix: empty data save ## ⚡ Refactor - refactor: simplify and rename some code - refactor: clear unnecessary hitbox code ## 🧹 Chores - chore: update license to 2026 - fix(deps): update dependency net.skinsrestorer:skinsrestorer-api to v15.9.2 - fix(deps): update dependency com.nexomc:nexo to v1.17.0 - fix(deps): update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.3.1 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.15.1...1.15.2) ================================================ FILE: changelog/v1/1.3.2.md ================================================ # BetterModel 1.3.2 ### Fix - Fix scale calculation - Fix player limb - Expand sight degree ### Add - Add 'disable-generating-legacy-models' config - Add more API ================================================ FILE: changelog/v1/1.3.3.md ================================================ # BetterModel 1.3.3 ### Fix - Fix texture rendering issue. - Fix hitbox removing. - Adds help command description. ================================================ FILE: changelog/v1/1.4.1.md ================================================ # BetterModel 1.4.1 ### Fix - Improve sight trace - Improve command - Improve walk speed calculation - Force resources name to lower case - Fix Minecraft <=1.21.3 resource - Fix entity remove/hide ### Add - Add animation script - Add .mcmeta animation - Add 'follow-mob-invisibility' option ================================================ FILE: changelog/v1/1.4.2.md ================================================ # BetterModel 1.4.2 ### Fix - Fix interpolation - Fix tick frame - Fix entity scale attribute - Fix hitbox logic to match Minecraft vanilla - Implement more accurate movement and animation - Hide player head in first person camera ### Add - Add 'module' config - Support 'smooth' interpolation - Add 'damage-effect(de)' parameter in model mechanic(boolean) ![1](https://github.com/user-attachments/assets/9b7d596a-db1a-41c7-8693-56342f0c0045) ![2](https://github.com/user-attachments/assets/128a3508-b8a0-4154-b59e-e1487a8804af) ================================================ FILE: changelog/v1/1.4.3.md ================================================ # BetterModel 1.4.3 ### Add - 1.21.5 client, server support - Support Purpur AFK - World position API - More optimization ### Fix - More smooth animation - Fake player like Citizens - Hit box - Animated texture - World change [Full change log](https://github.com/toxicity188/BetterModel/compare/1.4.2...1.4.3) ================================================ FILE: changelog/v1/1.4.md ================================================ # BetterModel 1.4 ### Add - Add texture path validator - Add @ModelPart targeter (MythicMobs) - Add changepart mechanic (MythicMobs) - Add debug config - Free cube rotation (>=1.21.4) - Kotlin 2.1.10 - Sync invisibility and glowing to tracker entity ![1](https://github.com/user-attachments/assets/aceda548-1b3f-4ed9-89ff-39ec68cfbefd) ![2](https://github.com/user-attachments/assets/a70f5049-77b0-4f47-b2ef-7bbd6896eb11) ================================================ FILE: changelog/v1/1.5.1.md ================================================ # BetterModel 1.5.1 ### Notice - This version is a hotfix of BetterModel 1.5. ### Add - config 'version-check' ### Fix - Spigot support - loading issue with legacy server - incorrect world handling ### Change - The default pack builder is now 'zip' [Full change log](https://github.com/toxicity188/BetterModel/compare/1.5...1.5.1) ================================================ FILE: changelog/v1/1.5.2.md ================================================ # BetterModel 1.5.2 ### Add - 'bindhitbox' mechanic - 'mountmodel' mechanic - 'dismountmodel' mechanic - 'dismountallmodel' mechanic - use interaction entity - 'override' property in animation - 'loop mode' property in animation - 'smooth' interpolation ### Fix - make hitbox listener entity to bukkit entity. - optimize animation update [Full change log](https://github.com/toxicity188/BetterModel/compare/1.5.1...1.5.2) ================================================ FILE: changelog/v1/1.5.3.md ================================================ # BetterModel 1.5.3 ### Fix - Bukkit hitbox event - update checker - optimize packet listener ### Add - multi tag support - new player limb tag and API ``` head (ph) right arm (pra) right forearm (prfa) left arm (pla) left forearm (plfa) hip (phip) waist (pw) chest (pc) right leg (prl) right foreleg (prfl) left leg (pll) left foreleg (plfl) left item (pli) right item (pri) ``` [Full change log](https://github.com/toxicity188/BetterModel/compare/1.5.2...1.5.3) ================================================ FILE: changelog/v1/1.5.4.md ================================================ # BetterModel 1.5.4 ### Notice this is a hotfix of 1.5.3. ### Fix - optimize packet listener and packet data update - checking entity dead - fix, clean and optimize animation logic [Full change log](https://github.com/toxicity188/BetterModel/compare/1.5.3...1.5.4) ================================================ FILE: changelog/v1/1.5.5.md ================================================ # BetterModel 1.5.5 ### Notice - Support about 1.20.2 is dropped. ### Fix - tracker view filter - legacy version support of player animation - improved animation key frame - hit-box despawn - call PlayerInteractAtEntityEvent of base entity - optimize resource pack - improved command - improved resource pack speed, size, and logging - legacy version test ### Add - HMCCosmetics backpack support (bone tag: hmc_bp) - Default model for test and learn (demon_knight.bbmodel) [Full change log](https://github.com/toxicity188/BetterModel/compare/1.5.4...1.5.5) ================================================ FILE: changelog/v1/1.5.md ================================================ # BetterModel 1.5 ### Add - 'glow' and 'enchant' mechanic - update checker - more API ### Fix - entity scale sync - entity shadow - some compatibility issues with MythicMobs - some platform support [Full change log](https://github.com/toxicity188/BetterModel/compare/c70a86c0...1.5) ================================================ FILE: changelog/v1/1.6.0.md ================================================ # BetterModel 1.6.0 ### Feat - Tracker#hide and Tracker#show API - TrackerModifier#hideOption - Now player animation is compatible with Shaderspack (requires 1.21.4 or upper client and server) ### Fix - Fix hit-box collision and interaction with 1.21.5 - Tinted item cache [Full change log](https://github.com/toxicity188/BetterModel/compare/1.5.5...1.6.0) ![0](https://github.com/user-attachments/assets/24a1e4ca-a2da-481e-ba86-14c4cc18fc3c) ================================================ FILE: changelog/v1/1.6.1.md ================================================ # BetterModel 1.6.1 ### Fix - Optimized vector calculation - Head rotation tag (h, hi) - Hitbox size and position - Entity scale attribute - Player animation model size - Animation frame quality ### Change - Simplify resource pack generation - MythicMobs 5.9.0 compatibility (bindhitbox) ### Add - Skin event API [Full change log](https://github.com/toxicity188/BetterModel/compare/1.6.0...1.6.1) ================================================ FILE: changelog/v1/1.7.0.md ================================================ # BetterModel 1.7.0 ### Notice - This update has HUGE API change and refactor, You should check all of your system related to BetterModel. ### Add - 1.21.6 server support - HUGE API refactor, change and update. - entity model data serialization - bezier, step interpolation - optimize bone item build - tint for player animation - offline-mode skin loading - support multiple models per one entity - minor optimization - prevent sending an empty packet - new player animation example (roll) [Full change log](https://github.com/toxicity188/BetterModel/compare/1.6.1...1.7.0) ================================================ FILE: changelog/v1/1.8.0.md ================================================ # BetterModel 1.8.0 ### Add - Minecraft 1.21.7 server support - Write more Javadocs - Thread lifecycle based on viewed player - Entity id tracking - More accurate spawn handling - Refactor and optimize some API - Head rotation delay ### Fix - 'smooth interpolation' with low keyframe animation - Unnecessary tick thread task allocation - Animation with only a single keyframe. - MythicMobs mechanic - Packet handling issue with 1.20.4 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.7.0...1.8.0) ================================================ FILE: changelog/v1/1.8.1.md ================================================ # BetterModel 1.8.1 ### Notice This is a simple hotfix for optimization and compatibility. ### Fix - huge packet traffic optimization - optimize packet handler with my external plugin (BetterHealthBar, BetterDamage) - compatibility with Folia - 'lockmodel' mechanic ================================================ FILE: changelog/v1/1.9.0.md ================================================ # BetterModel 1.9.0 ## Add - API BetterModelConfig - API EntityHideOption - new pack API and more detailed resource pack generation ```yaml pack: generate-modern-model: true generate-legacy-model: true ``` ### Fix - minor packet optimization - entity despawn handling - animation script - equipment packet [Full change log](https://github.com/toxicity188/BetterModel/compare/1.8.1...1.9.0) ================================================ FILE: changelog/v1/1.9.1.md ================================================ # BetterModel 1.9.1 ### Notice This is a hotfix about version 1.9.0. ### Add - API Tracker#billboard ### Fix - item model usage in 1.21.2-1.21.3 - unnecessary condition check - entity data serialization - hide option resetting - equipment hiding - entity spawning issue when quit and rejoin [Full change log](https://github.com/toxicity188/BetterModel/compare/1.9.0...1.9.1) ================================================ FILE: changelog/v1/1.9.2.md ================================================ # BetterModel 1.9.2 ### Notice It may be simple but huge update for optimization and animation quality. ### Fix - registry creation when spawning - model spawn issue with teleport - entity tracker registry optimization - large traffic optimization - implements more accurate animation [Full change log](https://github.com/toxicity188/BetterModel/compare/1.9.1...1.9.2) ================================================ FILE: changelog/v1/1.9.3.md ================================================ # BetterModel 1.9.3 ### Add - 1.21.8 server support - New API for updating tracker (Tracker#update) ### Fix - entity yaw/pitch sync - data packet synchronization - improved animation accuracy [Full change log](https://github.com/toxicity188/BetterModel/compare/1.9.2...1.9.3) ================================================ FILE: changelog/v2/2.0.0-pre1.md ================================================ ## 📚 Notices *This is a pre-release version distributed to assist in migrating to the new API.* *Content in this document is subject to change before the official 2.0.0 release.* Starting with this version, BetterModel is officially designated as v2.0.0. Please be aware that this update introduces several Breaking Changes. ## ✨ Feats ### Fabric platform port BetterModel now supports the Fabric platform as a server-side mod. - Supported: Dedicated Server, Integrated Server (Singleplayer) - Unsupported: Hybrid servers (e.g., Arclight, Mohist) ## 🚀 Breaking Changes ### Publishing & Dependencies The legacy `io.github.toxicity188:bettermodel` package is no longer published. Starting with v2.0.0, the project is split into the following modules: - `bettermodel-api`: Common API module. - `bettermodel-core`: Internal core module (Internal use only). - `bettermodel-bukkit-api`: API for Bukkit-based environments (Spigot, Paper, etc.). - `bettermodel-fabric`: Server-side mod implementation for Fabric. Please refer to `README.md` for detailed build script instructions.
Gradle (Kotlin) #### Release ```kotlin repositories { mavenCentral() maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION") // bukkit(spigot, paper, etc) api //modApi("io.github.toxicity188:bettermodel-fabric:VERSION") // mod(fabric) } ``` #### Snapshot ```kotlin repositories { maven("https://maven.pkg.github.com/toxicity188/BetterModel") { credentials { username = YOUR_GITHUB_USERNAME password = YOUR_GITHUB_TOKEN } } maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION-SNAPSHOT") // bukkit(spigot, paper, etc) api //modApi("io.github.toxicity188:bettermodel-fabric:VERSION-SNAPSHOT") // mod(fabric) } ```
### API & Event System - **Platform Adapters**: You must now use the appropriate adapter for your platform (e.g., `BukkitAdapter`, `FabricAdapter`). - **Event Bus**: `BetterModelEventBus` (accessible via BetterModel#eventBus) has been introduced. Bukkit's Event API is no longer supported.
Adapter API Usage (Bukkit) ```java EntityTracker tracker = BetterModel.model("demon_knight") .map(r -> r.create(BukkitAdapter.adapt(entity), TrackerModifier.DEFAULT, t -> t.update(TrackerUpdateAction.tint(0x0026FF)))) .orElse(null); ```
Event API Usage (Bukkit) ```java import org.bukkit.plugin.java.JavaPlugin; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.data.ModelAsset; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.event.ModelAssetsEvent; import java.util.*; public class YourPlugin extends JavaPlugin { @Override public void onEnable() { BetterModel.eventBus().subscribe(ModelAssetsEvent.class, event -> { if (event.type() == ModelRenderer.Type.PLAYER) event.addAsset(ModelAsset.of( "knight", () -> Objects.requireNonNull(getResource("knight.bbmodel")) )); }); } } ```
### Bukkit Compatibility To optimize the project, support for the following versions (which showed low usage) has been removed: - 1.20.5 ~ 1.20.6 - 1.21.2 ~ 1.21.3 ## 🧹 Chores - chore(deps): update gradle to v9.3.0 - chore(deps): update plugin com.vanniktech.maven.publish to v0.36.0 - chore: update dependency io.github.toxicity188:armormodel to v1.0.2 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.15.2...2.0.0-pre1) ================================================ FILE: changelog/v2/2.0.0-pre2.md ================================================ ## 📚 Notices *This is a pre-release version distributed to assist in migrating to the new API.* *Content in this document is subject to change before the official 2.0.0 release.* Starting with this version, BetterModel is officially designated as v2.0.0. Please be aware that this update introduces several Breaking Changes. ## ✨ Feats ### Fabric platform port BetterModel now supports the Fabric platform as a server-side mod. - Supported: Dedicated Server, Integrated Server (Singleplayer) - Unsupported: Hybrid servers (e.g., Arclight, Mohist) ### Keyframe optimization `AnimationMovement` has been deprecated and replaced by the highly optimized `AnimationKeyframe`. This change introduces a more efficient data access structure, significantly improving animation processing performance. ### ModelAssetsEvent The new `ModelAssetsEvent` allows you to inject BlockBench models from external resources directly into the BetterModel reload pipeline. This enables seamless integration of custom assets within the engine's internal build process. ## 🚀 Breaking Changes ### Publishing & Dependencies The legacy `io.github.toxicity188:bettermodel` package is no longer published. Starting with v2.0.0, the project is split into the following modules: - `bettermodel-api`: Common API module. - `bettermodel-core`: Internal core module (Internal use only). - `bettermodel-bukkit-api`: API for Bukkit-based environments (Spigot, Paper, etc.). - `bettermodel-fabric`: Server-side mod implementation for Fabric. Please refer to `README.md` for detailed build script instructions.
Gradle (Kotlin) #### Release ```kotlin repositories { mavenCentral() maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION") // bukkit(spigot, paper, etc) api //modApi("io.github.toxicity188:bettermodel-fabric:VERSION") // mod(fabric) } ``` #### Snapshot ```kotlin repositories { maven("https://maven.pkg.github.com/toxicity188/BetterModel") { credentials { username = YOUR_GITHUB_USERNAME password = YOUR_GITHUB_TOKEN } } maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION-SNAPSHOT") // bukkit(spigot, paper, etc) api //modApi("io.github.toxicity188:bettermodel-fabric:VERSION-SNAPSHOT") // mod(fabric) } ```
### API & Event System - **Platform Adapters**: You must now use the appropriate adapter for your platform (e.g., `BukkitAdapter`, `FabricAdapter`). - **Event Bus**: `BetterModelEventBus` (accessible via BetterModel#eventBus) has been introduced. - **New Entrypoints**: Dedicated entrypoints for each platform have been introduced to provide platform-specific functionalities: - `BetterModelBukkit`: Use `BetterModelBukkit#platform()` to access Bukkit-specific APIs. - `BetterModelFabric`: Use `BetterModelFabric#platform()` to access Fabric-specific APIs.
Adapter API Usage (Bukkit) ```java EntityTracker tracker = BetterModel.model("demon_knight") .map(r -> r.create(BukkitAdapter.adapt(entity), TrackerModifier.DEFAULT, t -> t.update(TrackerUpdateAction.tint(0x0026FF)))) .orElse(null); ```
Event API Usage (Bukkit) ```java import org.bukkit.plugin.java.JavaPlugin; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.data.ModelAsset; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.event.ModelAssetsEvent; import java.util.*; public class YourPlugin extends JavaPlugin { @Override public void onEnable() { BetterModelBukkit.platform().eventBus().subscribe(this, ModelAssetsEvent.class, event -> { if (event.type() == ModelRenderer.Type.PLAYER) event.addAsset(ModelAsset.of( "knight", () -> Objects.requireNonNull(getResource("knight.bbmodel")) )); }); } } ```
### Bukkit-specific Event Support While the core Event API is now platform-agnostic, you can still utilize Bukkit's Event API via **`BetterModelBukkitEvent`**. This ensures seamless integration with the existing Bukkit event lifecycle, which is particularly useful for tools like **Skript** or other plugins that rely on Bukkit-native events.
Event handling with Bukkit (Skript) ``` import: kr.toxicity.model.api.bukkit.event.BetterModelBukkitEvent kr.toxicity.model.api.event.CreateEntityTrackerEvent on BetterModelBukkitEvent: set {_original} to event.as(CreateEntityTrackerEvent.class) {_original} is set broadcast {_original}.tracker().name() ```
### Bukkit Compatibility To optimize the project, support for the following versions (which showed low usage) has been removed: - 1.20.5 ~ 1.20.6 - 1.21.2 ~ 1.21.3 ## 🧹 Chores - chore(deps): update gradle to v9.3.0 - chore(deps): update plugin com.vanniktech.maven.publish to v0.36.0 - chore: update dependency io.github.toxicity188:armormodel to v1.0.2 [Full change log](https://github.com/toxicity188/BetterModel/compare/2.0.0-pre1...2.0.0-pre2) ================================================ FILE: changelog/v2/2.0.0.md ================================================ ## 📚 Notices Starting with this version, BetterModel is officially designated as v2.0.0. Please be aware that this update introduces several Breaking Changes. ## ✨ Feats ### Fabric platform port BetterModel now supports the Fabric platform as a server-side mod. - Supported: Dedicated Server, Integrated Server (Singleplayer) - Unsupported: Hybrid servers (e.g., Arclight, Mohist) ### Keyframe optimization `AnimationMovement` has been deprecated and replaced by the highly optimized `AnimationKeyframe`. This change introduces a more efficient data access structure, significantly improving animation processing performance. ### ModelAssetsEvent The new `ModelAssetsEvent` allows you to inject BlockBench models from external resources directly into the BetterModel reload pipeline. This enables seamless integration of custom assets within the engine's internal build process. ### Others - feat: optimize IK solver ## 🚀 Breaking Changes ### Publishing & Dependencies The legacy `io.github.toxicity188:bettermodel` package is no longer published. Starting with v2.0.0, the project is split into the following modules: - `bettermodel-api`: Common API module. - `bettermodel-core`: Internal core module (Internal use only). - `bettermodel-bukkit-api`: API for Bukkit-based environments (Spigot, Paper, etc.). - `bettermodel-fabric`: Server-side mod implementation for Fabric. Please refer to `README.md` for detailed build script instructions.
Gradle (Kotlin) #### Release ```kotlin repositories { mavenCentral() maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION") // bukkit(spigot, paper, etc) api //modApi("io.github.toxicity188:bettermodel-fabric:VERSION") // mod(fabric) } ``` #### Snapshot ```kotlin repositories { maven("https://maven.pkg.github.com/toxicity188/BetterModel") { credentials { username = YOUR_GITHUB_USERNAME password = YOUR_GITHUB_TOKEN } } maven("https://maven.blamejared.com/") // For transitive dependency in bettermodel-fabric maven("https://maven.nucleoid.xyz/") // For transitive dependency in bettermodel-fabric } dependencies { compileOnly("io.github.toxicity188:bettermodel-bukkit-api:VERSION-SNAPSHOT") // bukkit(spigot, paper, etc) api //modApi("io.github.toxicity188:bettermodel-fabric:VERSION-SNAPSHOT") // mod(fabric) } ```
### API & Event System - **Platform Adapters**: You must now use the appropriate adapter for your platform (e.g., `BukkitAdapter`, `FabricAdapter`). - **Event Bus**: `BetterModelEventBus` (accessible via BetterModel#eventBus) has been introduced. - **New Entrypoints**: Dedicated entrypoints for each platform have been introduced to provide platform-specific functionalities: - `BetterModelBukkit`: Use `BetterModelBukkit#platform()` to access Bukkit-specific APIs. - `BetterModelFabric`: Use `BetterModelFabric#platform()` to access Fabric-specific APIs.
Adapter API Usage (Bukkit) ```java EntityTracker tracker = BetterModel.model("demon_knight") .map(r -> r.create(BukkitAdapter.adapt(entity), TrackerModifier.DEFAULT, t -> t.update(TrackerUpdateAction.tint(0x0026FF)))) .orElse(null); ```
Event API Usage (Bukkit) ```java import org.bukkit.plugin.java.JavaPlugin; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.data.ModelAsset; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.event.ModelAssetsEvent; import java.util.*; public class YourPlugin extends JavaPlugin { @Override public void onEnable() { BetterModelBukkit.platform().eventBus().subscribe(this, ModelAssetsEvent.class, event -> { if (event.type() == ModelRenderer.Type.PLAYER) event.addAsset(ModelAsset.of( "knight", () -> Objects.requireNonNull(getResource("knight.bbmodel")) )); }); } } ```
### Bukkit-specific Event Support While the core Event API is now platform-agnostic, you can still utilize Bukkit's Event API via **`BetterModelBukkitEvent`**. This ensures seamless integration with the existing Bukkit event lifecycle, which is particularly useful for tools like **Skript** or other plugins that rely on Bukkit-native events.
Event handling with Bukkit (Skript) ``` import: kr.toxicity.model.api.bukkit.event.BetterModelBukkitEvent kr.toxicity.model.api.event.CreateEntityTrackerEvent on BetterModelBukkitEvent: set {_original} to event.as(CreateEntityTrackerEvent.class) {_original} is set broadcast {_original}.tracker().name() ```
### Bukkit Compatibility To optimize the project, support for the following versions (which showed low usage) has been removed: - 1.20.5 ~ 1.20.6 - 1.21.2 ~ 1.21.3 ## 🧹 Chores - chore(deps): update gradle to v9.3.1 - fix(deps): update dependency com.nexomc:nexo to v1.18.0 - chore(deps): update plugin com.vanniktech.maven.publish to v0.36.0 - chore: update dependency io.github.toxicity188:armormodel to v1.0.2 [Full change log](https://github.com/toxicity188/BetterModel/compare/1.15.2...2.0.0) ================================================ FILE: changelog/v2/2.0.1.md ================================================ ## 🔧 Fixes - player animation in NPC - Fabric event bus ## 🧹 Chores - chore: update kotlin to v2.3.10 - chore: update fabric api to v0.141.3+1.21.11 - chore: update MythicMobs to v5.11.2 - chore: update fabric-language-kotlin to v1.13.9+kotlin.2.3.10 - chore: update polymer-resource-pack to v0.15.2+1.21.11 [Full change log](https://github.com/toxicity188/BetterModel/compare/2.0.0...2.0.1) ================================================ FILE: changelog/v2/2.1.0.md ================================================ ## ✨ Feats - optimize most of runtime calculation - improved body rotator - API `Tracker#listenHitBox` for handle each tracker's hitbox individually - new example model: `blue_wizard.bbmodel` - `glow_` tag for enable light emission both of group and cube (#284) ```java tracker.listenHitBox(HitBoxInteractEvent.class, event -> { HitBox hitBox = event.getHitBox(); // Do something with hitbox }); ``` ## 🔧 Fixes - ik rig - non hide player filtering ## 🧹 Chores - fix(deps): update dependency org.bstats:bstats-bukkit to v3.2.1 [Full change log](https://github.com/toxicity188/BetterModel/compare/2.0.1...2.1.0) ================================================ FILE: changelog/v2/2.2.0.md ================================================ ## ✨ Feats - optimize model realtime thread (3~5x lightweight than BM 2.0.1) - optimize `BoneName` - add `TrackerAnimation` API (`TrackerBuiltInAnimation`, `TrackerExtraAnimation`) - remove unused class (`AnimationEventHandler`) ```java public static final TrackerAnimation DEATH = TrackerAnimation.builder("death") .type(EntityTracker.class) .modifier(tracker -> AnimationModifier.DEFAULT_WITH_PLAY_ONCE) .onRemove(Tracker::close) .onSuccess(tracker -> tracker.forRemoval(true)) .build(); ``` ## 🔧 Fixes - Molang support in Spigot platform ## 🚀 Changes - config `pack.use-obfuscation` is now true by default ## 🧹 Chores - fix(deps): update dependency com.nexomc:nexo to v1.20.1 - fix(deps): update dependency net.skinsrestorer:skinsrestorer-api to v15.10.1 - fix(deps): update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.3.2 [Full change log](https://github.com/toxicity188/BetterModel/compare/2.1.0...2.2.0) ================================================ FILE: changelog/v3/3.0.0.md ================================================ ## 📚 Notices With the official release of Minecraft 26.1, BetterModel has decided to deploy a major version update to `3.0.0`. As with 2.0.0, there are several **Breaking Changes**, so please take note when using it. --- ## ✨ Feats ### [Minecraft 26.1.x](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-1-2) Support ![](https://github.com/user-attachments/assets/0153e97a-e320-41d1-8d94-ed626127b0cc) From now on, BetterModel supports running on Minecraft 26.1.x Servers. --- ### [Mesh Element](https://en.wikipedia.org/wiki/Triangle_mesh) Support (Experiment) ![](https://github.com/user-attachments/assets/facf34a2-31d3-4ebd-a7ff-9fe86c651ff8) We now support client resource pack conversion for Mesh Elements from [BlockBench](https://www.blockbench.net/). It is available on Minecraft clients version 26.1 and above. --- ### Others - Support meg-client-mod. - A `priority` property has been added to the `AnimationModifier` class, allowing you to adjust the application order of animations. - perf: minor optimization for some method - refactor: remove unused code --- ## 🚀 Breaking Changes ### [Java 25](https://openjdk.org/projects/jdk/25/) Usage BetterModel is now built in a Java 25 environment, and therefore the required Java version to run it on a server has also been increased to 25. --- ### [Deobfuscation](https://www.minecraft.net/en-us/article/removing-obfuscation-in-java-edition) Porting As obfuscation has been removed from the Minecraft jar starting from 26.1, tooling for mod platforms—which are most affected by mappings—has been changed. - `bettermodel-fabric`: Starting from 3.0.0, this is separated from the mod platform's API, `bettermodel-mod-api`.
build.gradle.kts ```kotlin // Use the following dependency when referencing only the API. // There is no need to use a separate remap configuration such as modCompileOnly. dependencies { compileOnly("io.github.toxicity188:bettermodel-mod-api:3.0.0") } ``` ```kotlin // Use this when importing all Fabric platform modules to automate test servers, etc. // There is no need to use a separate remap configuration such as modApi. dependencies { api("io.github.toxicity188:bettermodel-fabric:3.0.0") } ```
--- ### Deprecation of 1.21.3 Support To keep the project lightweight, support for versions 1.21.3 and below will be discontinued. Therefore, operation is only guaranteed on servers and clients version 1.21.4 or higher. --- ## 🔧 Fixes - fix: swap unsupported char to hashcode - fix: unnecessary decimal value (#299) - fix: global rot (#325) - fix: close limb when reloading --- ## 🧹 Chores - chore: update cloud - chore: update Purpur api - chore: com.vdurmont:semver4j -> org.semver4j:semver4j - fix(deps): update dependency com.nexomc:nexo to v1.21.0 - chore(deps): update gradle to v9.4.1 - fix(deps): update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.4.1 - fix(deps): update dependency org.jetbrains.dokka:dokka-gradle-plugin to v2.2.0 - fix(deps): update kotlin monorepo to v2.3.20 - fix(deps): update dependency net.fabricmc:fabric-language-kotlin to v1.13.10+kotlin.2.3.20 - fix(deps): update dependency net.fabricmc:fabric-loader to v0.19.1 - fix(deps): update dependency com.modrinth.minotaur:com.modrinth.minotaur.gradle.plugin to v2.9.0 - fix(deps): update dependency org.projectlombok:lombok to v1.18.44 - fix(deps): update dependency net.skinsrestorer:skinsrestorer-api to v15.12.0 - chore: update net.citizensnpcs:citizens-main to 2.0.42-SNAPSHOT - chore: update polymer-resource-pack to 0.16.2+26.1.1 - fix(deps): update dependency com.google.guava:guava to v33.6.0-jre - fix(deps): update dependency net.kyori:adventure-platform-fabric to v6.9.0 --- [Full change log](https://github.com/toxicity188/BetterModel/compare/2.2.0...3.0.0) ================================================ FILE: core/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.publish) } dependencies { api(project(":bettermodel-api")) compileOnly(libs.bundles.minecraft) compileOnly("com.mojang:authlib:7.0.61") compileOnly(libs.bundles.core) compileOnly(libs.cloud.core) } ================================================ FILE: core/bukkit-core/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.bukkit) } dependencies { shade(project(":bettermodel-api")) { isTransitive = false } shade(project(":bettermodel-api:bettermodel-bukkit-api")) { isTransitive = false } shade(project(":bettermodel-core")) { isTransitive = false } shade(project(":purpur")) rootProject.project("nms").subprojects.forEach { compileOnly(it) } shade(libs.bundles.shadedLibrary) { exclude("net.kyori") exclude("org.ow2.asm") exclude("io.leangen.geantyref") } compileOnly(libs.bundles.manifestLibrary) compileOnly("net.citizensnpcs:citizens-main:2.0.42-SNAPSHOT") { exclude("net.byteflux") } compileOnly("net.skinsrestorer:skinsrestorer-api:15.12.0") compileOnly("io.lumine:Mythic-Dist:5.11.2") compileOnly("com.nexomc:nexo:1.21.0") } ================================================ FILE: core/bukkit-core/src/main/java/kr/toxicity/model/bukkit/AbstractBetterModelPlugin.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit; import kr.toxicity.model.BetterModelPlatformImpl; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.BetterModelLogger; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.bukkit.platform.BukkitAdapter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.jar.Attributes; import java.util.jar.Manifest; public abstract class AbstractBetterModelPlugin extends JavaPlugin implements BetterModelPlatformImpl, BetterModelBukkit { protected boolean skipInitialReload; protected final AtomicBoolean onReload = new AtomicBoolean(); protected final AtomicBoolean firstLoad = new AtomicBoolean(); protected final BukkitAdapter adapter = new BukkitAdapter(); protected final BetterModelLogger logger = new BetterModelLogger() { private volatile ComponentLogger internalLogger; private @NotNull ComponentLogger logger() { ComponentLogger logger; if ((logger = internalLogger) != null) return logger; synchronized (this) { if ((logger = internalLogger) != null) return logger; return internalLogger = ComponentLogger.logger(getLogger().getName()); } } @Override public void info(@NotNull Component... message) { var log = logger(); synchronized (this) { for (Component s : message) { log.info(s); } } } @Override public void warn(@NotNull Component... message) { var log = logger(); synchronized (this) { for (Component s : message) { log.warn(s); } } } }; private @Nullable Attributes attributes; public void onLoad() { new BetterModelLibrary().load(this); BetterModel.register(this); } public void skipInitialReload() { this.skipInitialReload = true; } @Override public void saveResource(@NotNull String resourcePath) { saveResource(resourcePath, false); } @Override @NotNull public BukkitAdapter adapter() { return adapter; } public @NotNull Attributes attributes() { if (attributes != null) return attributes; synchronized (this) { if (attributes != null) return attributes; try ( var stream = Objects.requireNonNull(getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF")) ) { return attributes = new Manifest(stream).getMainAttributes(); } catch (IOException e) { throw new RuntimeException(e); } } } } ================================================ FILE: core/bukkit-core/src/main/java/kr/toxicity/model/bukkit/BetterModelLibrary.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.util.function.BooleanConstantSupplier; import net.byteflux.libby.Library; import net.byteflux.libby.relocation.Relocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.*; import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.stream.Stream; @SuppressWarnings("unused") public final class BetterModelLibrary { private static final String KOTLIN_RELOCATED = "_kotlin".substring(1); private static final List LIBRARY_DATA = new ArrayList<>(); public static final LibraryData KOTLIN = register( "org{}jetbrains{}kotlin", KOTLIN_RELOCATED + "-stdlib", builder -> builder.relocation(KOTLIN_RELOCATED) ); public static final LibraryData BSTATS = register( "org{}bstats", "bstats-bukkit", builder -> builder.relocation("org{}bstats") .subModules( "bstats-base" ) ); public static final LibraryData CLOUD = register( "org{}incendo", "cloud-paper", builder -> builder .subModules( "cloud-brigadier", "cloud-bukkit" ) .relocation("org{}incendo{}cloud") ); public static final LibraryData CLOUD_CORE = register( "org{}incendo", "cloud-core", builder -> builder .subModules( "cloud-services" ) .relocation("org{}incendo{}cloud") ); public static final LibraryData GEANTYREF = register( "io{}leangen{}geantyref", "geantyref", builder -> builder.predicate(BooleanConstantSupplier.of(!BetterModelBukkit.IS_PAPER)) ); public static final LibraryData MOLANG_COMPILER = register( "gg{}moonflower", "molang-compiler", builder -> builder ); public static final LibraryData ADVENTURE_API = register( "net{}kyori", "adventure-api", builder -> builder .subModules( "adventure-key", "adventure-text-logger-slf4j", "adventure-text-serializer-legacy", "adventure-nbt", "adventure-text-serializer-gson", "adventure-text-serializer-gson-legacy-impl", "adventure-text-serializer-json", "adventure-text-serializer-json-legacy-impl" ) .predicate(BooleanConstantSupplier.of(!BetterModelBukkit.IS_PAPER)) ); public static final LibraryData EXAMINATION_API = register( "net{}kyori", "examination-api", builder -> builder .subModules( "examination-string" ) .predicate(BooleanConstantSupplier.of(!BetterModelBukkit.IS_PAPER)) ); public static final LibraryData OPTION = register( "net{}kyori", "option", builder -> builder.predicate(BooleanConstantSupplier.of(!BetterModelBukkit.IS_PAPER)) ); public static final LibraryData ADVENTURE_PLATFORM = register( "net{}kyori", "adventure-platform-bukkit", builder -> builder .subModules( "adventure-platform-api", "adventure-platform-facet", "adventure-platform-viaversion", "adventure-text-serializer-bungeecord" ) .predicate(BooleanConstantSupplier.of(!BetterModelBukkit.IS_PAPER)) ); public static final LibraryData ASM_TREE = register( "org{}ow2{}asm", "asm-tree", builder -> builder.predicate(BooleanConstantSupplier.of(!BetterModelBukkit.IS_PAPER)) ); public void load(@NotNull AbstractBetterModelPlugin plugin) { var manager = new BetterModelLibraryManager(plugin); manager.addRepository("https://maven-central.storage-download.googleapis.com/maven2/"); manager.addRepository("https://maven.blamejared.com/"); manager.addMavenCentral(); LIBRARY_DATA.stream() .filter(LibraryData::isLoaded) .flatMap(library -> library.toLibby(plugin)) .forEach(manager::loadLibrary); } private static @NotNull LibraryData register(@NotNull String group, @NotNull String artifact, @NotNull Function function) { var build = function.apply(new LibraryData.Builder(group, artifact)).build(); LIBRARY_DATA.add(build); return build; } public record LibraryData( @NotNull String group, @NotNull String artifact, @Nullable String relocation, @NotNull String versionRef, @NotNull @Unmodifiable Set subModules, @NotNull BooleanSupplier predicate ) { private static class Builder { final String group; final String artifact; @Nullable String relocation; @NotNull String versionRef; @NotNull @Unmodifiable Set subModules = Collections.emptySet(); @NotNull BooleanSupplier predicate = BooleanConstantSupplier.TRUE; Builder(@NotNull String group, @NotNull String artifact) { this.group = group; this.artifact = this.versionRef = artifact; } @NotNull Builder predicate(@NotNull BooleanSupplier predicate) { this.predicate = Objects.requireNonNull(predicate); return this; } @NotNull Builder subModules(@NotNull String... subModules) { this.subModules = Set.of(Objects.requireNonNull(subModules)); return this; } @NotNull Builder relocation(@Nullable String relocation) { this.relocation = Objects.requireNonNull(relocation); return this; } @NotNull Builder versionRef(@NotNull String versionRef) { this.versionRef = Objects.requireNonNull(versionRef); return this; } @NotNull LibraryData build() { return new LibraryData( group, artifact, relocation, versionRef, subModules, predicate ); } } public boolean isLoaded() { return predicate.getAsBoolean(); } private @NotNull Stream toLibby(@NotNull AbstractBetterModelPlugin plugin) { var version = plugin.attributes().getValue("library-" + versionRef); return Stream.concat( Stream.of(artifact), subModules.stream() ).map(name -> { var libs = Library.builder() .groupId(group) .artifactId(name) .version(version); if (relocation != null) libs.relocate(new Relocation(relocation, "kr{}toxicity{}model{}shaded{}" + relocation)); return libs.build(); }); } } } ================================================ FILE: core/bukkit-core/src/main/java/kr/toxicity/model/bukkit/BetterModelLibraryManager.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit; import net.byteflux.libby.LibraryManager; import net.byteflux.libby.classloader.URLClassLoaderHelper; import net.byteflux.libby.logging.adapters.JDKLogAdapter; import org.bukkit.plugin.Plugin; import java.net.URLClassLoader; import java.nio.file.Path; import java.util.Objects; final class BetterModelLibraryManager extends LibraryManager { private final URLClassLoaderHelper classLoader; BetterModelLibraryManager(Plugin plugin) { super(new JDKLogAdapter(Objects.requireNonNull(plugin, "plugin").getLogger()), plugin.getDataFolder().toPath(), ".libs"); var pluginLoader = plugin.getClass().getClassLoader(); URLClassLoader loader; try { var field = pluginLoader.getClass().getDeclaredField("libraryLoader"); field.setAccessible(true); loader = (URLClassLoader) field.get(pluginLoader); } catch (Exception ignored) { loader = (URLClassLoader) pluginLoader; } this.classLoader = new URLClassLoaderHelper(loader, this); } protected void addToClasspath(Path file) { this.classLoader.addToClasspath(file); } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelConfigImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit import kr.toxicity.model.api.BetterModelConfig import kr.toxicity.model.api.bukkit.platform.BukkitAdapter import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.config.IndicatorConfig import kr.toxicity.model.api.config.ModuleConfig import kr.toxicity.model.api.config.PackConfig import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.mount.MountControllers import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.util.EntityUtil import kr.toxicity.model.util.ifNull import kr.toxicity.model.util.toPackName import org.bukkit.Material import org.bukkit.configuration.ConfigurationSection import org.bukkit.inventory.ItemStack import java.io.File import java.util.function.Supplier class BetterModelConfigImpl(yaml: ConfigurationSection) : BetterModelConfig { private val debug = yaml.getConfigurationSection("debug")?.let { DebugConfig.from(it::getBoolean) } ?: DebugConfig.DEFAULT private val indicator = yaml.getConfigurationSection("indicator")?.let { IndicatorConfig.from(it::getBoolean) } ?: IndicatorConfig.DEFAULT private val module = yaml.getConfigurationSection("module")?.let { ModuleConfig.from(it::getBoolean) } ?: ModuleConfig.DEFAULT private val pack = yaml.getConfigurationSection("pack")?.let { PackConfig.from(it::getBoolean) } ?: PackConfig.DEFAULT private val metrics = yaml.getBoolean("metrics", true) private val sightTrace = yaml.getBoolean("sight-trace", true) private val mergeWithExternalResources = yaml.getBoolean("merge-with-external-resources", true) private val itemModel = yaml.getString("item")?.let { runCatching { Material.getMaterial(it.uppercase()).ifNull { "This item doesn't exist: $it" } }.getOrDefault(Material.LEATHER_HORSE_ARMOR) } ?: Material.LEATHER_HORSE_ARMOR private val item = Supplier { BukkitAdapter.adapt(ItemStack(itemModel)) } private val itemNamespace = yaml.getString("item-namespace")?.toPackName() ?: "bm_models" private val maxSight = yaml.getDouble("max-sight", -1.0).run { if (this <= 0.0) EntityUtil.renderDistance() else this } private val minSight = yaml.getDouble("min-sight", 5.0) private val namespace = yaml.getString("namespace") ?: "bettermodel" private val packType = yaml.getString("pack-type")?.let { runCatching { BetterModelConfig.PackType.valueOf(it.uppercase()) }.getOrNull() } ?: BetterModelConfig.PackType.ZIP private val buildFolderLocation = (yaml.getString("build-folder-location") ?: "BetterModel/build").replace('/', File.separatorChar) private val followMobInvisibility = yaml.getBoolean("follow-mob-invisibility", true) private val usePurpurAfk = yaml.getBoolean("use-purpur-afk", true) private val versionCheck = yaml.getBoolean("version-check", true) private val defaultMountController = when (yaml.getString("default-mount-controller")?.lowercase()) { "invalid" -> MountControllers.INVALID "none" -> MountControllers.NONE "fly" -> MountControllers.FLY else -> MountControllers.WALK } private val lerpFrameTime = yaml.getInt("lerp-frame-time", 5) private val cancelPlayerModelInventory = yaml.getBoolean("cancel-player-model-inventory") private val playerHideDelay = yaml.getLong("player-hide-delay", 3L).coerceAtLeast(1L) private val packetBundlingSize = yaml.getInt("packet-bundling-size", 16) private val enableStrictLoading = yaml.getBoolean("enable-strict-loading") override fun debug(): DebugConfig = debug override fun indicator(): IndicatorConfig = indicator override fun module(): ModuleConfig = module override fun pack(): PackConfig = pack override fun item(): Supplier = item override fun itemModel(): String = itemModel.name override fun itemNamespace(): String = itemNamespace override fun metrics(): Boolean = metrics override fun sightTrace(): Boolean = sightTrace override fun mergeWithExternalResources(): Boolean = mergeWithExternalResources override fun maxSight(): Double = maxSight override fun minSight(): Double = minSight override fun namespace(): String = namespace override fun packType(): BetterModelConfig.PackType = packType override fun buildFolderLocation(): String = buildFolderLocation override fun followMobInvisibility(): Boolean = followMobInvisibility override fun usePurpurAfk(): Boolean = usePurpurAfk override fun versionCheck(): Boolean = versionCheck override fun defaultMountController(): MountController = defaultMountController override fun lerpFrameTime(): Int = lerpFrameTime override fun cancelPlayerModelInventory(): Boolean = cancelPlayerModelInventory override fun playerHideDelay(): Long = playerHideDelay override fun packetBundlingSize(): Int = packetBundlingSize override fun enableStrictLoading(): Boolean = enableStrictLoading } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelPlugin.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit import kr.toxicity.model.api.BetterModelConfig import kr.toxicity.model.api.BetterModelEvaluator import kr.toxicity.model.api.BetterModelLogger import kr.toxicity.model.api.BetterModelPlatform.ReloadResult import kr.toxicity.model.api.BetterModelPlatform.ReloadResult.* import kr.toxicity.model.api.bukkit.BukkitModelEventBus import kr.toxicity.model.api.bukkit.scheduler.BukkitModelScheduler import kr.toxicity.model.api.manager.* import kr.toxicity.model.api.nms.NMS import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.version.MinecraftVersion import kr.toxicity.model.bukkit.command.startBukkitCommand import kr.toxicity.model.bukkit.configuration.PluginConfiguration import kr.toxicity.model.bukkit.manager.PlayerManagerImpl import kr.toxicity.model.bukkit.util.ADVENTURE_PLATFORM import kr.toxicity.model.bukkit.util.audience import kr.toxicity.model.bukkit.util.registerListener import kr.toxicity.model.manager.* import kr.toxicity.model.util.* import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.format.NamedTextColor.* import org.bukkit.Bukkit import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.server.ServerLoadEvent import org.semver4j.Semver import java.io.File import java.io.InputStream import java.util.function.BiConsumer import java.util.function.Consumer import java.util.jar.JarEntry import java.util.jar.JarFile abstract class BetterModelPlugin : AbstractBetterModelPlugin() { private lateinit var props: BetterModelProperties override fun onLoad() { super.onLoad() props = runCatching { BetterModelProperties(this) }.getOrElse { warn( "Unable to start BetterModel.".toComponent(), "Reason: ${it.message ?: "Unknown"}".toComponent(RED), "Stack trace: ${it.stackTraceToString()}".toComponent(RED), "Plugin will be automatically disabled.".toComponent(DARK_RED) ) return Bukkit.getPluginManager().disablePlugin(this) } } override fun onEnable() { props.managers.forEach(GlobalManager::start) if (isSnapshot) warn( "This build is dev version: be careful to use it!".toComponent(), "Build number: ${props.snapshot}".toComponent(LIGHT_PURPLE) ) startBukkitCommand() registerListener(object : Listener { @EventHandler fun PlayerJoinEvent.join() { if (!player.isOp || !config().versionCheck()) return props.scheduler.asyncTask { val result = LATEST_VERSION player.audience().infoNotNull( result.release ?.takeIf { props.semver < it.versionNumber() } ?.let { version -> componentOf("New BetterModel release found: ") { append(version.toURLComponent()) } }, result.snapshot ?.takeIf { props.semver < it.versionNumber() } ?.let { version -> componentOf("New BetterModel snapshot found: ") { append(version.toURLComponent()) } } ) } } @EventHandler fun ServerLoadEvent.load() { if (skipInitialReload || type != ServerLoadEvent.LoadType.STARTUP) return when (val result = reload(ReloadInfo(true, Audience.empty()))) { is Failure -> result.throwable.handleException("Unable to load plugin properly.") is OnReload -> throw RuntimeException("Plugin load failed.") is Success -> info( "Plugin is loaded. (${result.totalTime().withComma()} ms)".toComponent(GREEN), "Minecraft version: ${props.version}, NMS version: ${props.nms.version()}".toComponent(AQUA), "Platform: ${ when { IS_FOLIA -> "Folia" IS_PURPUR -> "Purpur" IS_PAPER -> "Paper" else -> "Bukkit" } }".toComponent(AQUA) ) } } }) } override fun onDisable() { if (!firstLoad.get()) return props.managers.forEach(GlobalManager::end) ADVENTURE_PLATFORM?.close() } override fun reload(info: ReloadInfo): ReloadResult { if (!onReload.compareAndSet(false, true)) return OnReload.INSTANCE return runCatching { if (!info.skipConfig) props.config = BetterModelConfigImpl(PluginConfiguration.CONFIG.create()) val zipper = PackZipper.zipper().also(props.reloadStartTask) ReloadPipeline( config().indicator().options.toIndicator(info) ).use { pipeline -> val time = System.currentTimeMillis() props.managers.forEach { it.reload(pipeline, zipper) } Success( firstLoad.compareAndSet(false, true), System.currentTimeMillis() - time, config().packType().toGenerator().create(zipper, pipeline.apply { status = "Generating files..." goal = zipper.size() }) ) } }.getOrElse { Failure(it) }.apply { onReload.set(false) }.also(props.reloadEndTask) } override fun loadAssets(pipeline: ReloadPipeline, prefix: String, consumer: BiConsumer) { JarFile(file).use { pipeline.forEachParallel(it.entries() .asSequence() .filter { entry -> entry.name.startsWith(prefix) && entry.name.length > prefix.length + 1 && !entry.isDirectory } .toList(), JarEntry::getSize ) { entry -> it.getInputStream(entry).use { stream -> consumer.accept(entry.name.substring(prefix.length + 1), stream) } } } } override fun dataFolder(): File = dataFolder override fun logger(): BetterModelLogger = logger override fun scheduler(): BukkitModelScheduler = props.scheduler override fun evaluator(): BetterModelEvaluator = props.evaluator override fun eventBus(): BukkitModelEventBus = props.eventbus override fun modelManager(): ModelManager = ModelManagerImpl override fun playerManager(): PlayerManager = PlayerManagerImpl override fun scriptManager(): ScriptManager = ScriptManagerImpl override fun skinManager(): SkinManager = SkinManagerImpl override fun profileManager(): ProfileManager = ProfileManagerImpl override fun config(): BetterModelConfig = props.config override fun version(): MinecraftVersion = props.version override fun semver(): Semver = props.semver override fun nms(): NMS = props.nms override fun isSnapshot(): Boolean = props.snapshot > 0 @Synchronized override fun addReloadStartHandler(consumer: Consumer) { val previous = props.reloadStartTask props.reloadStartTask = { previous(it) consumer.accept(it) } } @Synchronized override fun addReloadEndHandler(consumer: Consumer) { val previous = props.reloadEndTask props.reloadEndTask = { previous(it) consumer.accept(it) } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelProperties.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit import kr.toxicity.model.BetterModelEvaluatorImpl import kr.toxicity.model.api.BetterModelConfig import kr.toxicity.model.api.BetterModelPlatform.ReloadResult import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.event.PluginEndReloadEvent import kr.toxicity.model.api.event.PluginStartReloadEvent import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.version.MinecraftVersion.* import kr.toxicity.model.bukkit.configuration.PluginConfiguration import kr.toxicity.model.bukkit.manager.CompatibilityManager import kr.toxicity.model.bukkit.manager.EntityManager import kr.toxicity.model.bukkit.manager.PlayerManagerImpl import kr.toxicity.model.bukkit.scheduler.BukkitScheduler import kr.toxicity.model.bukkit.scheduler.PaperScheduler import kr.toxicity.model.manager.* import kr.toxicity.model.util.* import org.bstats.bukkit.Metrics import org.bukkit.Bukkit import org.semver4j.Semver private typealias Latest = kr.toxicity.model.bukkit.nms.v26_R1.NMSImpl internal class BetterModelProperties( private val plugin: AbstractBetterModelPlugin ) { private lateinit var _config: BetterModelConfig private var _metrics: Metrics? = null val version = parse(Bukkit.getBukkitVersion().substringBefore('-')) val nms = when (version) { V26_1, V26_1_1, V26_1_2 -> Latest() V1_21_11 -> kr.toxicity.model.bukkit.nms.v1_21_R7.NMSImpl() V1_21_9, V1_21_10 -> kr.toxicity.model.bukkit.nms.v1_21_R6.NMSImpl() V1_21_6, V1_21_7, V1_21_8 -> kr.toxicity.model.bukkit.nms.v1_21_R5.NMSImpl() V1_21_5 -> kr.toxicity.model.bukkit.nms.v1_21_R4.NMSImpl() V1_21_4 -> kr.toxicity.model.bukkit.nms.v1_21_R3.NMSImpl() else -> { warn( "Note: this version is officially untested.".toComponent(), "So be careful to use!".toComponent() ) Latest() } } val scheduler = if (BetterModelBukkit.IS_FOLIA) PaperScheduler() else BukkitScheduler() val evaluator = BetterModelEvaluatorImpl() val eventbus = BukkitModelEventBusImpl() @Suppress("DEPRECATION") //To support Spigot :( val semver = Semver.coerce(plugin.description.version).ifNull { "Unable to load BetterModel's sermver." } val snapshot = runCatching { plugin.attributes().getValue("Dev-Build").toInt() }.getOrElse { it.handleException("Unable to parse manifest's build data") -1 } var config get() = _config set(value) { _config = value.apply { if (metrics()) { if (_metrics == null) _metrics = Metrics(plugin, 24237) } else { _metrics?.shutdown() _metrics = null } } } val managers by lazy { listOf( CompatibilityManager, ArmorManager, ProfileManagerImpl, SkinManagerImpl, ModelManagerImpl, PlayerManagerImpl, EntityManager, ScriptManagerImpl ) } var reloadStartTask: (PackZipper) -> Unit = { callEvent { PluginStartReloadEvent(it) } } var reloadEndTask: (ReloadResult) -> Unit = { callEvent { PluginEndReloadEvent(it) } } init { config = BetterModelConfigImpl(PluginConfiguration.CONFIG.create()) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BukkitModelEventBusImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit import kr.toxicity.model.BetterModelEventBusImpl import kr.toxicity.model.api.BetterModelEventBus import kr.toxicity.model.api.bukkit.BukkitModelEventBus import kr.toxicity.model.api.bukkit.event.BetterModelBukkitEvent import kr.toxicity.model.api.event.CancellableEvent import org.bukkit.Bukkit class BukkitModelEventBusImpl : BukkitModelEventBus, BetterModelEventBus by BetterModelEventBusImpl({ eventClass, supplier -> BetterModelBukkitEvent(eventClass, supplier).apply { Bukkit.getPluginManager().callEvent(this) }.source()?.let { event -> if (event !is CancellableEvent || !event.isCancelled()) BetterModelEventBus.Result.SUCCESS else BetterModelEventBus.Result.FAIL } ?: BetterModelEventBus.Result.NO_EVENT_HANDLER }) ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/audience/AudiencePlayer.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.audience import kr.toxicity.model.bukkit.util.audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.text.Component import org.bukkit.entity.Player class AudiencePlayer( override val sender: Player ) : BukkitAudience { private val audience = sender.audience() override fun sendMessage(message: Component) { audience.sendMessage(message) } override fun showBossBar(bar: BossBar) { audience.showBossBar(bar) } override fun hideBossBar(bar: BossBar) { audience.hideBossBar(bar) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/audience/AudienceSender.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.audience import kr.toxicity.model.bukkit.util.audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.text.Component import org.bukkit.command.CommandSender class AudienceSender( override val sender: CommandSender ) : BukkitAudience { private val audience = sender.audience() override fun sendMessage(message: Component) { audience.sendMessage(message) } override fun showBossBar(bar: BossBar) { audience.showBossBar(bar) } override fun hideBossBar(bar: BossBar) { audience.hideBossBar(bar) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/audience/BukkitAudience.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.audience import net.kyori.adventure.audience.Audience import org.bukkit.command.CommandSender interface BukkitAudience : Audience { val sender: CommandSender } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/command/Commands.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.command import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.BetterModelPlatform.ReloadResult.* import kr.toxicity.model.api.animation.AnimationIterator import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.tracker.EntityHideOption import kr.toxicity.model.api.tracker.ModelScaler import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerModifier import kr.toxicity.model.bukkit.audience.AudiencePlayer import kr.toxicity.model.bukkit.audience.AudienceSender import kr.toxicity.model.bukkit.audience.BukkitAudience import kr.toxicity.model.bukkit.util.PLUGIN import kr.toxicity.model.bukkit.util.toRegistry import kr.toxicity.model.bukkit.util.toTracker import kr.toxicity.model.bukkit.util.wrap import kr.toxicity.model.command.* import kr.toxicity.model.util.* import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.format.NamedTextColor.* import org.bukkit.command.CommandSender import org.bukkit.entity.EntityType import org.bukkit.entity.Player import org.bukkit.util.Vector import org.incendo.cloud.SenderMapper import org.incendo.cloud.bukkit.BukkitCommandMeta import org.incendo.cloud.bukkit.CloudBukkitCapabilities import org.incendo.cloud.bukkit.data.MultipleEntitySelector import org.incendo.cloud.bukkit.parser.PlayerParser.playerParser import org.incendo.cloud.bukkit.parser.location.LocationParser.locationParser import org.incendo.cloud.bukkit.parser.selector.MultipleEntitySelectorParser.multipleEntitySelectorParser import org.incendo.cloud.context.CommandContext import org.incendo.cloud.execution.ExecutionCoordinator import org.incendo.cloud.paper.LegacyPaperCommandManager import org.incendo.cloud.parser.standard.BooleanParser.booleanParser import org.incendo.cloud.parser.standard.DoubleParser.doubleParser import org.incendo.cloud.parser.standard.EnumParser.enumParser import org.incendo.cloud.parser.standard.StringParser.stringParser import org.incendo.cloud.suggestion.SuggestionProvider.blockingStrings private val MODEL_SUGGESTION = blockingStrings { _, _ -> BetterModel.modelKeys() } private val LIMB_SUGGESTION = blockingStrings { _, _ -> BetterModel.limbKeys() } fun startBukkitCommand() { LegacyPaperCommandManager( PLUGIN, ExecutionCoordinator.simpleCoordinator(), SenderMapper.create( { sender -> if (sender is Player) AudiencePlayer(sender) else AudienceSender(sender) }, { audience -> (audience as BukkitAudience).sender } ) ).apply { if (hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) { registerBrigadier() brigadierManager().setNativeNumberSuggestions(true) } else if (hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) registerAsynchronousCompletions() }.register( "bettermodel", "All-related command.", { it.meta(BukkitCommandMeta.BUKKIT_DESCRIPTION, info.description.textDescription()) }, "bm", "model" ) { create( "reload", "Reloads BetterModel.", "re", "rl" ) { handler(::reload) } create( "spawn", "Summons some model to given type", "s" ) { required("model", stringParser(), MODEL_SUGGESTION) .optional("type", enumParser(EntityType::class.java)) .optional("scale", doubleParser(0.0625, 16.0)) .optional("location", locationParser()) .senderType(AudiencePlayer::class.java) .handler(::spawn) } create( "test", "Tests some model's animation to specific player", "t" ) { required("model", stringParser(), MODEL_SUGGESTION) .required( "animation", stringParser(), blockingStrings { ctx, _ -> ctx.nullableString("model") { BetterModel.modelOrNull(it)?.animations()?.keys } ?: emptySet() } ) .optional("player", playerParser()) .optional("location", locationParser()) .handler(::test) } create( "disguise", "Disguises self.", "d" ) { required("model", stringParser(), MODEL_SUGGESTION) .optional("scaling", booleanParser()) .senderType(AudiencePlayer::class.java) .handler(::disguise) } create( "undisguise", "Undisguises self.", "ud" ) { senderType(AudiencePlayer::class.java) .optional("model", stringParser(), blockingStrings { ctx, _ -> ctx.sender().sender.toRegistry()?.trackers()?.map(Tracker::name) ?: emptyList() }) .handler(::undisguise) } create( "play", "Plays player animation", "p" ) { required("limb", stringParser(), LIMB_SUGGESTION) .required( "animation", stringParser(), blockingStrings { ctx, _ -> ctx.nullableString("limb") { BetterModel.limbOrNull(it)?.animations()?.keys } ?: emptySet() } ) .optional("loop_type", enumParser(AnimationIterator.Type::class.java)) .optional("hide", booleanParser()) .senderType(AudiencePlayer::class.java) .handler(::play) } create( "hide", "Hides some entities from target player." ) { required("model", stringParser(), MODEL_SUGGESTION) .required("player", playerParser()) .required("entities", multipleEntitySelectorParser()) .handler(::hide) } create( "show", "Shows some entities to target player." ) { required("model", stringParser(), MODEL_SUGGESTION) .required("player", playerParser()) .required("entities", multipleEntitySelectorParser()) .handler(::show) } create( "version", "Checks BetterModel's version", "v" ) { handler(::version) } } } private fun hide(context: CommandContext) { val sender = context.sender() val model = context.get("model") val player = context.get("player").wrap() var success = false context.get("entities").values().forEach { if (it.toRegistry()?.tracker(model)?.hide(player) == true) success = true } if (!success) sender.warn("Failed to hide any of provided entities.") } private fun show(context: CommandContext) { val sender = context.sender() val model = context.get("model") val player = context.get("player").wrap() var success = false context.get("entities").values().forEach { if (it.toRegistry()?.tracker(model)?.show(player) == true) success = true } if (!success) sender.warn("Failed to show any of provided entities.") } private fun disguise(context: CommandContext) { val audience = context.sender() val player = audience.sender val scaling = if (context.getOrDefault("scaling", true)) ModelScaler.entity() else ModelScaler.defaultScaler() context.model("model") { return audience.warn("Unable to find this model: $it") }.getOrCreate(player.wrap(), TrackerModifier.DEFAULT) { it.scaler(scaling) } } private fun undisguise(context: CommandContext) { val audience = context.sender() val player = audience.sender val model = context.nullable("model") if (model != null) { player.toTracker(model)?.close() ?: audience.warn("Cannot find this model to undisguise: $model") } else player.toRegistry()?.close() ?: audience.warn("Cannot find any model to undisguise") } private fun spawn(context: CommandContext) { val audience = context.sender() val player = audience.sender val model = context.model("model") { return audience.warn("Unable to find this model: $it") } val type = context.nullable("type", EntityType.HUSK) val scale = context.nullable("scale", 1.0) val loc = context.nullable("location") { player.location } loc.run { (world ?: player.world).spawnEntity( this, type ) }.takeIf { it.isValid }?.let { entity -> model.create(entity.wrap(), TrackerModifier.DEFAULT) { tracker -> tracker.scaler(ModelScaler.entity().multiply(scale.toFloat())) } } ?: audience.warn("Entity spawning has been blocked.") } private fun version(context: CommandContext) { val sender = context.sender() sender.info("Searching version, please wait...") PLATFORM.scheduler().asyncTask { val version = LATEST_VERSION sender.infoNotNull( emptyComponentOf(), "Current: ${PLATFORM.semver()}".toComponent(), version.release?.let { version -> componentOf("Latest release: ") { append(version.toURLComponent()) } }, version.snapshot?.let { version -> componentOf("Latest snapshot: ") { append(version.toURLComponent()) } } ) } } private fun reload(context: CommandContext) { val audience = context.sender() PLATFORM.scheduler().asyncTask { audience.info("Start reloading. please wait...") when (val result = PLATFORM.reload(audience)) { is OnReload -> audience.warn("BetterModel is still on reload!") is Success -> { audience.info( emptyComponentOf(), "Reload completed. (${result.totalTime().withComma()}ms)".toComponent(GREEN), "Assets reload time - ${result.assetsTime().withComma()}ms".toComponent { color(GRAY) hoverEvent("Reading all config and model.".toComponent().toHoverEvent()) }, "Packing time - ${result.packingTime().withComma()}ms".toComponent { color(GRAY) hoverEvent("Packing all model to resource pack.".toComponent().toHoverEvent()) }, "${BetterModel.models().size.withComma()} of models are loaded successfully. (${result.length().toByteFormat()})".toComponent(YELLOW), (if (result.packResult.changed()) "${result.packResult.size().withComma()} of files are zipped." else "Zipping is skipped due to the same result.").toComponent(YELLOW), emptyComponentOf() ) } is Failure -> { audience.warn( emptyComponentOf(), "Reload failed.".toComponent(), "Please read the log to find the problem.".toComponent(), emptyComponentOf() ) audience.warn() result.throwable.handleException("Reload failed.") } } } } private fun play(context: CommandContext) { val audience = context.sender() val player = audience.sender val limb = context.limb("limb") { return audience.warn("Unable to find this limb: $it") } val animation = context.string("animation") { limb.animation(it).orElse(null) ?: return audience.warn("Unable to find this animation: $it") } val loopType = context.nullable("loop_type", AnimationIterator.Type.PLAY_ONCE) val hide = context.nullable("hide") != false limb.getOrCreate(player.wrap(), TrackerModifier.DEFAULT) { it.hideOption(if (hide) EntityHideOption.DEFAULT else EntityHideOption.FALSE) }.run { if (!animate(animation, AnimationModifier(0, 0, loopType), ::close)) close() } } private fun test(context: CommandContext) { val audience = context.sender() val model = context.model("model") { return audience.warn("Unable to find this model: $it") } val animation = context.string("animation") { str -> model.animation(str).orElse(null) ?: return audience.warn("Unable to find this animation: $str") } val player = context.nullable("player") { (audience as? AudiencePlayer)?.sender ?: return audience.warn("Unable to find target player.") } val location = context.nullable("location") { player.location.apply { add(Vector(0, 0, 10).rotateAroundY(-Math.toRadians(yaw.toDouble()))) yaw += 180 } } model.create(location.wrap()).run { spawn(player.wrap()) animate(animation, AnimationModifier(0, 0, AnimationIterator.Type.PLAY_ONCE), ::close) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/Compatibility.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility fun interface Compatibility { fun start() } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/citizens/CitizensCompatibility.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.citizens import kr.toxicity.model.bukkit.compatibility.Compatibility import kr.toxicity.model.bukkit.compatibility.citizens.command.AnimateCommand import kr.toxicity.model.bukkit.compatibility.citizens.command.LimbCommand import kr.toxicity.model.bukkit.compatibility.citizens.command.ModelCommand import kr.toxicity.model.bukkit.compatibility.citizens.trait.ModelTrait import net.citizensnpcs.api.CitizensAPI import net.citizensnpcs.api.trait.TraitInfo class CitizensCompatibility : Compatibility { override fun start() { CitizensAPI.getTraitFactory() .registerTrait(TraitInfo.create(ModelTrait::class.java)) CitizensAPI.getCommandManager().run { register(ModelCommand::class.java) register(AnimateCommand::class.java) register(LimbCommand::class.java) } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/citizens/command/AnimateCommand.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.citizens.command import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.animation.AnimationIterator import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.util.function.FloatSupplier import kr.toxicity.model.bukkit.util.wrap import net.citizensnpcs.api.CitizensAPI import net.citizensnpcs.api.command.Arg import net.citizensnpcs.api.command.Command import net.citizensnpcs.api.command.CommandContext import net.citizensnpcs.api.npc.NPC import org.bukkit.Bukkit import org.bukkit.command.CommandSender class AnimateCommand { @Command( aliases = ["npc"], usage = "animate [loop_type] [speed] [player]", desc = "", modifiers = ["animate"], min = 3, max = 6, permission = "citizens.npc.animate" ) @Suppress("UNUSED") fun animate( args: CommandContext, sender: CommandSender, npc: NPC?, @Arg(1) id: String, @Arg(2) animation: String, @Arg(3) loopType: String?, @Arg(4) speed: String?, @Arg(5) player: String? ) { val targetNpc = CitizensAPI.getNPCRegistry().getById(id.toIntOrNull() ?: return) ?: return val modifier = AnimationModifier.builder() .player(player?.let(Bukkit::getPlayer)?.wrap()) .start(0) .end(0) .speed(speed?.toFloatOrNull()?.let(FloatSupplier::of)) .type(loopType?.runCatching { AnimationIterator.Type.valueOf(uppercase()) }?.getOrNull() ?: AnimationIterator.Type.PLAY_ONCE) .build() BetterModel.registryOrNull(targetNpc.entity.uniqueId)?.trackers()?.forEach { it.animate(animation, modifier) } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/citizens/command/LimbCommand.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.citizens.command import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.animation.AnimationIterator import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.tracker.TrackerModifier import kr.toxicity.model.bukkit.util.wrap import net.citizensnpcs.api.CitizensAPI import net.citizensnpcs.api.command.Arg import net.citizensnpcs.api.command.Command import net.citizensnpcs.api.command.CommandContext import net.citizensnpcs.api.npc.NPC import org.bukkit.Bukkit import org.bukkit.command.CommandSender import org.bukkit.entity.Player class LimbCommand { @Command( aliases = ["npc"], usage = "limb [loop_type] [player]", desc = "", modifiers = ["limb"], min = 4, max = 6, permission = "citizens.npc.animate" ) @Suppress("UNUSED") fun animate( args: CommandContext, sender: CommandSender, npc: NPC?, @Arg(1) id: String, @Arg(2) model: String, @Arg(3) animation: String, @Arg(4) type: String?, @Arg(5) player: String? ) { val targetNpc = CitizensAPI.getNPCRegistry().getById(id.toIntOrNull() ?: return) ?: return val npcEntity = (targetNpc.entity as? Player)?.wrap() ?: return val targetPlayer = player?.let(Bukkit::getPlayer)?.wrap() val animType = type ?.let { value -> runCatching { AnimationIterator.Type.valueOf(value.uppercase()) }.getOrNull() } ?: AnimationIterator.Type.PLAY_ONCE BetterModel.limb(model) .map { renderer -> renderer.getOrCreate(npcEntity, TrackerModifier.DEFAULT) { tracker -> if (targetPlayer != null) { tracker.markPlayerForSpawn(targetPlayer) } } } .ifPresent { tracker -> val success = tracker.animate( animation, AnimationModifier.builder() .start(0) .player(targetPlayer) .type(animType) .build() ) { if (targetPlayer != null) { tracker.unmarkPlayerForSpawn(targetPlayer) tracker.registry().remove(targetPlayer) if (tracker.playerCount() == 0) tracker.close() } else { tracker.close() } } if (!success) { tracker.close() return@ifPresent } if (targetPlayer != null && !tracker.isSpawned(targetPlayer)) { tracker.markPlayerForSpawn(targetPlayer) tracker.registry().spawnIfNotSpawned(targetPlayer) } } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/citizens/command/ModelCommand.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.citizens.command import kr.toxicity.model.api.BetterModel import kr.toxicity.model.bukkit.compatibility.citizens.trait.ModelTrait import net.citizensnpcs.api.command.Arg import net.citizensnpcs.api.command.Arg.CompletionsProvider import net.citizensnpcs.api.command.Command import net.citizensnpcs.api.command.CommandContext import net.citizensnpcs.api.command.CommandMessages import net.citizensnpcs.api.npc.NPC import net.citizensnpcs.api.util.Messaging import org.bukkit.command.CommandSender class ModelCommand { @Command( aliases = ["npc"], usage = "model [model]", desc = "", modifiers = ["model"], min = 1, max = 2, permission = "citizens.npc.model" ) @Suppress("UNUSED") fun model(args: CommandContext, sender: CommandSender, npc: NPC?, @Arg(1, completionsProvider = TabComplete::class) model: String?) { if (npc == null) return Messaging.sendTr(sender, CommandMessages.MUST_HAVE_SELECTED) npc.getOrAddTrait(ModelTrait::class.java).renderer = model?.let { BetterModel.modelOrNull(it) } sender.sendMessage("Set ${npc.name}'s model to $model.") } private class TabComplete : CompletionsProvider { override fun getCompletions(p0: CommandContext?, p1: CommandSender?, p2: NPC?): Collection = BetterModel.modelKeys() } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/citizens/trait/ModelTrait.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.citizens.trait import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.data.renderer.ModelRenderer import kr.toxicity.model.bukkit.util.wrap import net.citizensnpcs.api.trait.Trait import net.citizensnpcs.api.trait.TraitName import net.citizensnpcs.api.util.DataKey @TraitName("model") class ModelTrait : Trait("model") { private var _renderer: ModelRenderer? = null var renderer get() = _renderer set(value) { npc?.entity?.let { value?.create(it.wrap()) ?: BetterModel.registryOrNull(it.uniqueId)?.close() } _renderer = value } override fun load(key: DataKey) { key.getString("")?.let { BetterModel.modelOrNull(it)?.let { model -> renderer = model } } } override fun save(key: DataKey) { npc?.entity?.uniqueId?.let { uuid -> key.setString("", BetterModel.registryOrNull(uuid)?.first()?.name()) } } override fun onSpawn() { npc?.entity?.let { if (BetterModel.registryOrNull(it.uniqueId) == null) { renderer?.create(it.wrap()) } } } override fun onCopy() { onSpawn() } override fun onDespawn() { npc?.entity?.uniqueId?.let { BetterModel.registryOrNull(it)?.close() } } override fun onRemove() { npc?.entity?.uniqueId?.let { BetterModel.registryOrNull(it)?.close() } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/MythicMobsCompatibility.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs import io.lumine.mythic.bukkit.MythicBukkit import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent import io.lumine.mythic.bukkit.events.MythicMechanicLoadEvent import io.lumine.mythic.bukkit.events.MythicTargeterLoadEvent import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.EntityTracker import kr.toxicity.model.bukkit.compatibility.Compatibility import kr.toxicity.model.bukkit.compatibility.mythicmobs.condition.ModelHasPassengerCondition import kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic.* import kr.toxicity.model.bukkit.compatibility.mythicmobs.targeter.ModelPartTargeter import kr.toxicity.model.bukkit.util.registerListener import kr.toxicity.model.manager.ScriptManagerImpl import kr.toxicity.model.util.CONFIG import kr.toxicity.model.util.componentOf import kr.toxicity.model.util.toComponent import kr.toxicity.model.util.warn import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.event.EventHandler import org.bukkit.event.Listener class MythicMobsCompatibility : Compatibility { private companion object { const val NAMESPACE = "bm:" } override fun start() { ScriptManagerImpl.addBuilder("mm") { name -> val args = name.args() ?: return@addBuilder AnimationScript.EMPTY AnimationScript.of(BetterModelBukkit.IS_FOLIA) script@ { tracker -> if (!CONFIG.module().model) return@script if (tracker !is EntityTracker) return@script val entity = (tracker.registry().entity() as? BaseBukkitEntity ?: return@script).entity() if (!MythicBukkit.inst().apiHelper.castSkill( entity, args, MythicBukkit.inst().apiHelper.getMythicMobInstance(entity)?.power ?: 1F ) { name.metadata.toMap().forEach { (key, value) -> it.parameters[key] = value.toString() } }) warn(componentOf( "Unknown MythicMobs skill name: ".toComponent(), args.toComponent(NamedTextColor.RED) )) } } registerListener(object : Listener { @EventHandler fun MythicMechanicLoadEvent.load() { if (!CONFIG.module().model) return when (mechanicName.lowercase().substringAfter(NAMESPACE)) { "playlimbanim" -> register(PlayLimbAnimMechanic(config)) "model" -> register(ModelMechanic(config)) "state", "animation" -> register(StateMechanic(config)) "defaultstate", "defaultanimation" -> register(DefaultStateMechanic(config)) "partvisibility", "partvis" -> register(PartVisibilityMechanic(config)) "bindhitbox" -> register(BindHitBoxMechanic(config)) "changepart" -> register(ChangePartMechanic(config)) "tint", "color" -> register(TintMechanic(config)) "brightness", "light" -> register(BrightnessMechanic(config)) "enchant" -> register(EnchantMechanic(config)) "billboard" -> register(BillboardMechanic(config)) "glow", "glowbone" -> register(GlowMechanic(config)) "mountmodel" -> register(MountModelMechanic(config)) "dismountmodel" -> register(DismountModelMechanic(config)) "dismountallmodel", "dismountall" -> register(DismountAllModelMechanic(config)) "lockmodel", "lockrotation" -> register(LockModelMechanic(config)) "bodyrotation", "bodyclamp" -> register(BodyRotationMechanic(config)) "remapmodel", "remap" -> register(RemapModelMechanic(config)) "pairmodel" -> register(PairModelMechanic(config)) } } @EventHandler fun MythicConditionLoadEvent.load() { if (!CONFIG.module().model) return when (conditionName.lowercase().substringAfter(NAMESPACE)) { "modelhaspassenger" -> register(ModelHasPassengerCondition(config)) } } @EventHandler fun MythicTargeterLoadEvent.load() { if (!CONFIG.module().model) return when (targeterName.lowercase().substringAfter(NAMESPACE)) { "modelpart" -> register(ModelPartTargeter(config)) } } }) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/MythicMobsValue.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.SkillMetadata import kr.toxicity.model.api.util.function.BonePredicate import kr.toxicity.model.bukkit.util.toRegistry import kr.toxicity.model.bukkit.util.toTracker import kr.toxicity.model.util.boneName import kr.toxicity.model.util.toPackName val MM_MODEL_ID = arrayOf("mid", "m", "model") val MM_PART_ID = arrayOf("partid", "p", "pid", "part") val MM_CHILDREN = arrayOf("children", "child") val MM_EXACT_MATCH = arrayOf("exactmatch", "em", "exact", "match") val MM_SEAT = arrayOf("seat", "p", "pbone") fun SkillMetadata.toRegistry() = caster.entity.toRegistry() fun SkillMetadata.toTracker(model: String?) = caster.entity.toTracker(model) fun AbstractEntity.toTracker(model: String?) = bukkitEntity.toTracker(model) fun AbstractEntity.toRegistry() = bukkitEntity.toRegistry() fun MythicLineConfig.toPlaceholderString(array: Array, defaultValue: String? = null) = toPlaceholderString(array, defaultValue) { it } fun MythicLineConfig.toPlaceholderStringList(array: Array, mapper: (List) -> T) = toPlaceholderString(array) { mapper(it?.split(",") ?: emptyList()) } fun MythicLineConfig.toPlaceholderString(array: Array, defaultValue: String? = null, mapper: (String?) -> T): (PlaceholderArgument) -> T { return getPlaceholderString(array, defaultValue)?.let { { meta -> mapper(when (meta) { is PlaceholderArgument.None -> it.get() is PlaceholderArgument.SkillMeta -> it[meta.meta] is PlaceholderArgument.TargetedSkillMeta -> it.get(meta.meta, meta.target) is PlaceholderArgument.Entity -> it[meta.entity] }) } } ?: mapper(null).let { mapped -> { mapped } } } fun MythicLineConfig.toPlaceholderInteger(array: Array, defaultValue: Int = 0) = toPlaceholderInteger(array, defaultValue) { it ?: defaultValue } fun MythicLineConfig.toNullablePlaceholderInteger(array: Array) = toPlaceholderInteger(array, null) { it } fun MythicLineConfig.toPlaceholderInteger(array: Array, defaultValue: Int? = null, mapper: (Int?) -> T): (PlaceholderArgument) -> T { return getPlaceholderInteger(array, defaultValue?.toString())?.let { { meta -> mapper(when (meta) { is PlaceholderArgument.None -> it.get() is PlaceholderArgument.SkillMeta -> it[meta.meta] is PlaceholderArgument.TargetedSkillMeta -> it.get(meta.meta, meta.target) is PlaceholderArgument.Entity -> it[meta.entity] }) } } ?: mapper(null).let { mapped -> { mapped } } } fun MythicLineConfig.toPlaceholderFloat(array: Array, defaultValue: Float = 0F) = toPlaceholderFloat(array, defaultValue) { it ?: defaultValue } fun MythicLineConfig.toNullablePlaceholderFloat(array: Array) = toPlaceholderFloat(array, null) { it } fun MythicLineConfig.toPlaceholderFloat(array: Array, defaultValue: Float? = null, mapper: (Float?) -> T): (PlaceholderArgument) -> T { return getPlaceholderFloat(array, defaultValue?.toString())?.let { { meta -> mapper(when (meta) { is PlaceholderArgument.None -> it.get() is PlaceholderArgument.SkillMeta -> it[meta.meta] is PlaceholderArgument.TargetedSkillMeta -> it.get(meta.meta, meta.target) is PlaceholderArgument.Entity -> it[meta.entity] }) } } ?: mapper(null).let { mapped -> { mapped } } } fun MythicLineConfig.toPlaceholderBoolean(array: Array, defaultValue: Boolean? = null) = toPlaceholderBoolean(array, defaultValue) { it == true } fun MythicLineConfig.toNullablePlaceholderBoolean(array: Array, defaultValue: Boolean? = null) = toPlaceholderBoolean(array, defaultValue) { it } fun MythicLineConfig.toPlaceholderBoolean(array: Array, defaultValue: Boolean? = null, mapper: (Boolean?) -> T): (PlaceholderArgument) -> T { return getPlaceholderBoolean(array, defaultValue)?.let { { meta -> mapper(when (meta) { is PlaceholderArgument.None -> it.get() is PlaceholderArgument.SkillMeta -> it[meta.meta] is PlaceholderArgument.TargetedSkillMeta -> it.get(meta.meta, meta.target) is PlaceholderArgument.Entity -> it[meta.entity] }) } } ?: mapper(null).let { mapped -> { mapped } } } fun MythicLineConfig.toPlaceholderColor(array: Array, defaultValue: String = "FFFFFF") = toPlaceholderColor(array, defaultValue) { it } fun MythicLineConfig.toPlaceholderColor(array: Array, defaultValue: String = "FFFFFF", mapper: (Int?) -> T): (PlaceholderArgument) -> T { return toPlaceholderString(array, defaultValue) { mapper(it?.toIntOrNull(16)) } } val MythicLineConfig.bonePredicateNullable get() = toBonePredicate(BonePredicate.TRUE) val MythicLineConfig.bonePredicate get() = toBonePredicate(BonePredicate.FALSE) val MythicLineConfig.modelPlaceholder get() = toPlaceholderString(MM_MODEL_ID) { it?.toPackName() } fun MythicLineConfig.toBonePredicate(defaultPredicate: BonePredicate): (PlaceholderArgument) -> BonePredicate { val match = toPlaceholderBoolean(MM_EXACT_MATCH, true) val children = toPlaceholderBoolean(MM_CHILDREN, false) val partSupplier = toPlaceholderString(MM_PART_ID) { it?.boneName?.name } return { meta -> val part = partSupplier(meta) if (part == null) defaultPredicate else { BonePredicate.of(if (children(meta)) BonePredicate.State.TRUE else BonePredicate.State.FALSE, if (match(meta)) { { b -> b.name().name == part } } else { { b -> b.name().name.contains(part, ignoreCase = true) } }) } } } fun SkillMetadata.toPlaceholderArgs() = PlaceholderArgument.SkillMeta(this) fun AbstractEntity.toPlaceholderArgs() = PlaceholderArgument.Entity(this) fun toPlaceholderArgs(meta: SkillMetadata, target: AbstractEntity) = PlaceholderArgument.TargetedSkillMeta(meta, target) sealed interface PlaceholderArgument { data object None : PlaceholderArgument data class SkillMeta(val meta: SkillMetadata) : PlaceholderArgument data class TargetedSkillMeta(val meta: SkillMetadata, val target: AbstractEntity) : PlaceholderArgument data class Entity(val entity: AbstractEntity) : PlaceholderArgument } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/condition/ModelHasPassengerCondition.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.condition import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.conditions.IEntityCondition import kr.toxicity.model.bukkit.compatibility.mythicmobs.MM_SEAT import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderArgs import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderStringList import kr.toxicity.model.bukkit.compatibility.mythicmobs.toRegistry import kr.toxicity.model.util.boneName class ModelHasPassengerCondition(mlc: MythicLineConfig) : IEntityCondition { private val seat = mlc.toPlaceholderStringList(MM_SEAT) { it.map { s -> s.boneName.name }.toSet() } override fun check(p0: AbstractEntity): Boolean { val args = p0.toPlaceholderArgs() val set = seat(args) return p0.toRegistry()?.let { if (set.isEmpty()) it.hasPassenger() else set.any { seat -> it.mountedHitBox().values.any { box -> box.hitBox().positionSource().name().name == seat } } } == true } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/AbstractSkillMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.bukkit.MythicBukkit import io.lumine.mythic.core.skills.SkillMechanic import kr.toxicity.model.api.bukkit.BetterModelBukkit abstract class AbstractSkillMechanic(mlc: MythicLineConfig) : SkillMechanic(MythicBukkit.inst().skillManager, null, null, mlc) { init { isAsyncSafe = !BetterModelBukkit.IS_FOLIA } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/BillboardMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.bukkit.compatibility.mythicmobs.* class BillboardMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicateNullable private val billboard = mlc.toPlaceholderString(arrayOf("billboard", "bb"), "fixed") { it?.runCatching { PlatformBillboard.valueOf(uppercase()) }?.getOrNull() ?: PlatformBillboard.FIXED } override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { it.update( TrackerUpdateAction.billboard(billboard(args)), predicate(args) ) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/BindHitBoxMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import io.lumine.mythic.bukkit.MythicBukkit import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.bukkit.util.unwarp import org.bukkit.entity.Damageable import org.bukkit.entity.Entity class BindHitBoxMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicate private val type = mlc.toPlaceholderString(arrayOf("type", "t", "mob", "m")) { if (it != null) MythicBukkit.inst().mobManager.getMythicMob(it).orElse(null) else null } init { isAsyncSafe = false } override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { val e = type(args) ?: return SkillResult.CONDITION_FAILED val spawned = e.spawn(p0.caster.location, p0.caster.level).apply { setParent(p0.caster) setOwnerUUID(p0.caster.entity.uniqueId) }.entity.bukkitEntity it.createHitBox(HitBoxListener.builder() .sync { hitBox -> if (!spawned.isValid) hitBox.removeHitBox() else spawned.teleportAsync((hitBox as Entity).location) } .damage { event -> if (spawned is Damageable) { spawned.damage(event.damage.toDouble(), event.source.causingEntity?.unwarp()) event.isCancelled = true } } .remove { spawned.remove() } .build(), predicate(args)) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/BodyRotationMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* class BodyRotationMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val headUneven = mlc.toNullablePlaceholderBoolean(arrayOf("headuneven", "hu", "head")) private val maxHead = mlc.toNullablePlaceholderFloat(arrayOf("maxhead", "mh", "mxh")) private val minHead = mlc.toNullablePlaceholderFloat(arrayOf("minhead", "mnh")) private val bodyUneven = mlc.toNullablePlaceholderBoolean(arrayOf("bodyuneven", "bu", "body")) private val maxBody = mlc.toNullablePlaceholderFloat(arrayOf("maxbody", "mb", "mxb")) private val minBody = mlc.toNullablePlaceholderFloat(arrayOf("minbody", "mnb")) private val playerMode = mlc.toNullablePlaceholderBoolean(arrayOf("playermode", "m", "mode", "player")) private val stable = mlc.toNullablePlaceholderFloat(arrayOf("stable", "s")) private val rDelay = mlc.toNullablePlaceholderInteger(arrayOf("rdelay", "rde")) private val rDuration = mlc.toNullablePlaceholderInteger(arrayOf("rduration", "rdu")) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.bodyRotator()?.run { setValue { setter -> headUneven(args)?.let { setter.setHeadUneven(it) } maxHead(args)?.let { setter.setMaxHead(it) } minHead(args)?.let { setter.setMinHead(it) } bodyUneven(args)?.let { setter.setBodyUneven(it) } maxBody(args)?.let { setter.setMaxBody(it) } minBody(args)?.let { setter.setMinBody(it) } playerMode(args)?.let { setter.setPlayerMode(it) } stable(args)?.let { setter.setStable(it) } rDelay(args)?.let { setter.setRotationDelay(it) } rDuration(args)?.let { setter.setRotationDuration(it) } } SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/BrightnessMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.script.BrightnessScript class BrightnessMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicateNullable private val block = mlc.toPlaceholderInteger(arrayOf("block", "b")) { (it ?: -1).coerceAtLeast(-1).coerceAtMost(15) } private val sky = mlc.toPlaceholderInteger(arrayOf("sky", "s")) { (it ?: -1).coerceAtLeast(-1).coerceAtMost(15) } override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { BrightnessScript( predicate(args), block(args), sky(args) ).accept(it) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/ChangePartMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.script.ChangePartScript import kr.toxicity.model.util.boneName class ChangePartMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicate private val nmid = mlc.toPlaceholderString(arrayOf("newmodelid", "nm", "nmid", "newmodel")) private val newPart = mlc.toPlaceholderString(arrayOf("newpart", "np", "npid")) { it?.boneName } override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { ChangePartScript( predicate(args), nmid(args) ?: return SkillResult.CONDITION_FAILED, newPart(args) ?: return SkillResult.CONDITION_FAILED ).accept(it) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/DefaultStateMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.util.function.FloatSupplier import kr.toxicity.model.bukkit.compatibility.mythicmobs.* class DefaultStateMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val type = mlc.toPlaceholderString(arrayOf("t", "type")) private val state = mlc.toPlaceholderString(arrayOf("state", "s")) private val li = mlc.toNullablePlaceholderInteger(arrayOf("li")) private val lo = mlc.toNullablePlaceholderInteger(arrayOf("lo")) private val sp = mlc.toNullablePlaceholderFloat(arrayOf("speed", "sp")) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { val t = type(args) val s = state(args) if (t == null) return SkillResult.CONDITION_FAILED it.replace(t, s ?: t, AnimationModifier.builder() .start(li(args) ?: -1) .end(lo(args) ?: -1) .speed(sp(args)?.let(FloatSupplier::of)) .build()) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/DismountAllModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.ITargetedEntitySkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.MM_SEAT import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderArgs import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderStringList import kr.toxicity.model.bukkit.compatibility.mythicmobs.toRegistry import kr.toxicity.model.util.boneName class DismountAllModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), ITargetedEntitySkill { private val seat = mlc.toPlaceholderStringList(MM_SEAT) { it.map { s -> s.boneName.name }.toSet() } init { isAsyncSafe = false } override fun castAtEntity(p0: SkillMetadata, p1: AbstractEntity): SkillResult { val args = toPlaceholderArgs(p0, p1) return p0.toRegistry()?.let { registry -> val set = seat(args) registry.mountedHitBox() .values .asSequence() .filter { set.isEmpty() || set.contains(it.hitBox.positionSource().name().name) } .forEach { it.dismountAll() } SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/DismountModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.ITargetedEntitySkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.util.boneName class DismountModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), ITargetedEntitySkill { private val driver = mlc.toPlaceholderBoolean(arrayOf("driver", "d", "drive"), true) private val seat = mlc.toPlaceholderStringList(MM_SEAT) { it.map { s -> s.boneName.name }.toSet() } init { isAsyncSafe = false } override fun castAtEntity(p0: SkillMetadata, p1: AbstractEntity): SkillResult { val args = toPlaceholderArgs(p0, p1) return p0.toRegistry()?.let { registry -> val set = seat(args) val d = driver(args) registry.mountedHitBox()[p1.bukkitEntity.uniqueId]?.takeIf { set.isEmpty() || (set.contains(it.hitBox.positionSource().name().name) && (!d || it.hitBox.mountController().canControl())) }?.dismount() SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/EnchantMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.script.EnchantScript class EnchantMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicateNullable private val enchant = mlc.toPlaceholderBoolean(arrayOf("enchant", "en"), true) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { EnchantScript( predicate(args), enchant(args) ).accept(it) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/GlowMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.bukkit.compatibility.mythicmobs.* class GlowMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicateNullable private val glow = mlc.toNullablePlaceholderBoolean(arrayOf("glow", "g"), true) private val color = mlc.toPlaceholderColor(arrayOf("color", "c")) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { val predicate = predicate(args) glow(args)?.let { glow -> it.update( TrackerUpdateAction.glow(glow), predicate ) } color(args)?.let { c -> it.update( TrackerUpdateAction.glowColor(c), predicate ) } SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/LockModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.modelPlaceholder import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderArgs import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderBoolean import kr.toxicity.model.bukkit.compatibility.mythicmobs.toTracker class LockModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val lock = mlc.toPlaceholderBoolean(arrayOf("lock", "l"), true) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.bodyRotator()?.let { it.lockRotation(lock(args)) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/ModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.tracker.ModelScaler import kr.toxicity.model.api.tracker.TrackerModifier import kr.toxicity.model.bukkit.compatibility.mythicmobs.modelPlaceholder import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderArgs import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderBoolean import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderFloat import kr.toxicity.model.bukkit.util.wrap class ModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val mid = mlc.modelPlaceholder private val s = mlc.toPlaceholderFloat(arrayOf("scale", "s"), 1F) private val st = mlc.toPlaceholderBoolean(arrayOf("sight-trace", "st"), true) private val da = mlc.toPlaceholderBoolean(arrayOf("damageanimation", "da", "animation"), false) private val dt = mlc.toPlaceholderBoolean(arrayOf("damagetint", "tint", "dt"), true) private val r = mlc.toPlaceholderBoolean(arrayOf("remove", "r"), false) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() val e = p0.caster.entity.bukkitEntity return if (r(args)) { if (mid(args)?.let { BetterModel.registryOrNull(e.uniqueId)?.remove(it) } == true) SkillResult.SUCCESS else SkillResult.CONDITION_FAILED } else { BetterModel.modelOrNull(mid(args) ?: return SkillResult.CONDITION_FAILED)?.let { it.create(e.wrap(), TrackerModifier( st(args), da(args), dt(args) )) { t -> t.scaler(ModelScaler.entity().multiply(s(args))) } SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/MountModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.ITargetedEntitySkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.mount.MountControllers import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.bukkit.util.wrap class MountModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), ITargetedEntitySkill { companion object { private val dismountListener = HitBoxListener.builder() .dismount { h, _ -> h.removeHitBox() } .build() } private val model = mlc.modelPlaceholder private val driver = mlc.toPlaceholderBoolean(arrayOf("driver", "d", "drive"), true) private val damagemount = mlc.toPlaceholderBoolean(arrayOf("damagemount", "dmg"), false) private val interact = mlc.toPlaceholderString(arrayOf("mode", "m")) exec@ { when (it) { "walking" -> MountControllers.WALK.modifier() "force_walking" -> MountControllers.WALK.modifier() .canDismountBySelf(false) "flying" -> MountControllers.FLY.modifier() "force_flying" -> MountControllers.FLY.modifier() .canDismountBySelf(false) else -> null }?.canMount(false) } private val seat = mlc.toPlaceholderStringList(MM_SEAT) { it.toSet() } init { isAsyncSafe = false } override fun castAtEntity(p0: SkillMetadata, p1: AbstractEntity): SkillResult { val args = toPlaceholderArgs(p0, p1) return p0.toTracker(model(args))?.let { tracker -> val set = seat(args) tracker.hitbox(dismountListener) { (set.isEmpty() || set.contains(it.name().name)) && it.hitBox?.hasMountDriver() != true }?.let { hitBox -> hitBox.mountController(interact(args) ?.canControl(driver(args)) ?.canBeDamagedByRider(damagemount(args)) ?.build() ?: MountControllers.WALK) hitBox.mount(p1.bukkitEntity.wrap()) SkillResult.SUCCESS } } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/PairModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.ITargetedEntitySkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.modelPlaceholder import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderArgs import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderBoolean import kr.toxicity.model.bukkit.compatibility.mythicmobs.toTracker import kr.toxicity.model.bukkit.util.wrap import org.bukkit.entity.Player class PairModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), ITargetedEntitySkill { private val model = mlc.modelPlaceholder private val remove = mlc.toPlaceholderBoolean(arrayOf("remove", "r"), false) override fun castAtEntity(p0: SkillMetadata, p1: AbstractEntity): SkillResult { val target = p1.bukkitEntity as? Player ?: return SkillResult.CONDITION_FAILED val args = toPlaceholderArgs(p0, p1) p0.toTracker(model(args))?.let { if (remove(args)) { it.show(target.wrap()) } else { it.hide(target.wrap()) } } return SkillResult.SUCCESS } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/PartVisibilityMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.script.PartVisibilityScript class PartVisibilityMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicateNullable private val v = mlc.toPlaceholderBoolean(arrayOf("visibility", "visible", "v"), true) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { PartVisibilityScript( predicate(args), v(args) ).accept(it) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/PlayLimbAnimMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.ITargetedEntitySkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.animation.AnimationIterator import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.util.function.FloatSupplier import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.bukkit.util.wrap import kr.toxicity.model.util.componentOf import kr.toxicity.model.util.toComponent import kr.toxicity.model.util.warn import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.entity.Player class PlayLimbAnimMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), ITargetedEntitySkill { private val modelId = mlc.modelPlaceholder private val animationId = mlc.toPlaceholderString(arrayOf("animation", "anim", "a")) private val speed = mlc.toNullablePlaceholderFloat(arrayOf("speed", "sp")) private val remove = mlc.toPlaceholderBoolean(arrayOf("remove", "r"), false) private val mode = mlc.toPlaceholderString(arrayOf("mode", "loop"), "once") { when (it?.lowercase()) { "loop" -> AnimationIterator.Type.LOOP "hold" -> AnimationIterator.Type.HOLD_ON_LAST else -> AnimationIterator.Type.PLAY_ONCE } } override fun castAtEntity(data: SkillMetadata, target: AbstractEntity): SkillResult { val targetPlayer = target.bukkitEntity as? Player ?: return SkillResult.CONDITION_FAILED val args = toPlaceholderArgs(data, target) val removal = remove(args) val currentModelId = modelId(args) ?: return SkillResult.INVALID_CONFIG val currentAnimationId = animationId(args) ?: if (!removal) return SkillResult.INVALID_CONFIG else "" if (removal) { BetterModel.registryOrNull(targetPlayer.uniqueId)?.remove(currentModelId) } else { val renderer = BetterModel.limb(currentModelId).orElse(null) ?: return SkillResult.CONDITION_FAILED.apply { warn(componentOf( "Error: Player not found: ".toComponent(), currentModelId.toComponent(NamedTextColor.RED) )) } val loopType = mode(args) val modifier = AnimationModifier(0, 0, loopType, speed(args)?.let(FloatSupplier::of)) renderer.getOrCreate(targetPlayer.wrap()).run { if (!animate( currentAnimationId, modifier, if (loopType == AnimationIterator.Type.PLAY_ONCE) { { close() } } else { {} } )) close() } } return SkillResult.SUCCESS } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/RemapModelMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.modelPlaceholder import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderArgs import kr.toxicity.model.bukkit.compatibility.mythicmobs.toPlaceholderString import kr.toxicity.model.bukkit.compatibility.mythicmobs.toTracker import kr.toxicity.model.script.RemapScript class RemapModelMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val modelId = mlc.modelPlaceholder private val newModelId = mlc.toPlaceholderString(arrayOf("newmodelid", "n", "nid", "newmodel")) private val map = mlc.toPlaceholderString(arrayOf("map")) override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() val m = modelId(args) ?: return SkillResult.INVALID_CONFIG return p0.toTracker(m)?.let { RemapScript( newModelId(args) ?: return SkillResult.INVALID_CONFIG, map(args) ).accept(it) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/StateMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.adapters.AbstractEntity import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.ITargetedEntitySkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.util.function.FloatSupplier import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.bukkit.util.wrap import org.bukkit.entity.Player class StateMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill, ITargetedEntitySkill { private val model = mlc.modelPlaceholder private val state = mlc.toPlaceholderString(arrayOf("state", "s")) private val li = mlc.toPlaceholderInteger(arrayOf("li"), 1) private val lo = mlc.toPlaceholderInteger(arrayOf("lo")) private val sp = mlc.toNullablePlaceholderFloat(arrayOf("speed", "sp")) private val remove = mlc.toPlaceholderBoolean(arrayOf("remove", "r")) private val priority = mlc.toPlaceholderInteger(arrayOf("p", "pr", "priority")) override fun cast(p0: SkillMetadata): SkillResult { return cast(null, p0) } override fun castAtEntity(p0: SkillMetadata, p1: AbstractEntity): SkillResult { return cast(p1.bukkitEntity as? Player, p0) } private fun cast(player: Player?, meta: SkillMetadata): SkillResult { val args = meta.toPlaceholderArgs() return meta.toTracker(model(args))?.let { val s = state(args) ?: return SkillResult.CONDITION_FAILED if (remove(args)) it.stopAnimation(s) else it.animate(s, AnimationModifier.builder() .start(li(args)) .end(lo(args)) .speed(sp(args)?.let(FloatSupplier::of)) .player(player?.wrap()) .priority(priority(args)) .build()) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/mechanic/TintMechanic.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.mechanic import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.INoTargetSkill import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.SkillResult import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.script.TintScript class TintMechanic(mlc: MythicLineConfig) : AbstractSkillMechanic(mlc), INoTargetSkill { private val model = mlc.modelPlaceholder private val predicate = mlc.bonePredicateNullable private val damageTint = mlc.toPlaceholderBoolean(arrayOf("damagetint", "d", "dmg", "damage")) private val color = mlc.toPlaceholderColor(arrayOf("color", "c")) { it ?: 0xFFFFFF } override fun cast(p0: SkillMetadata): SkillResult { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.let { TintScript( predicate(args), color(args), damageTint(args) ).accept(it) SkillResult.SUCCESS } ?: SkillResult.CONDITION_FAILED } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/mythicmobs/targeter/ModelPartTargeter.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.mythicmobs.targeter import io.lumine.mythic.api.adapters.AbstractLocation import io.lumine.mythic.api.config.MythicLineConfig import io.lumine.mythic.api.skills.SkillMetadata import io.lumine.mythic.api.skills.targeters.ILocationTargeter import kr.toxicity.model.bukkit.compatibility.mythicmobs.* import kr.toxicity.model.util.boneName class ModelPartTargeter(mlc: MythicLineConfig) : ILocationTargeter { private val model = mlc.modelPlaceholder private val part = mlc.toPlaceholderString(MM_PART_ID) { it?.boneName?.name } override fun getLocations(p0: SkillMetadata): Collection { val args = p0.toPlaceholderArgs() return p0.toTracker(model(args))?.bone(part(args) ?: return emptyList())?.hitBoxPosition()?.let { listOf(p0.caster.entity.location.add( it.x.toDouble(), it.y.toDouble(), it.z.toDouble() )) } ?: emptyList() } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/nexo/NexoCompatibility.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.nexo import com.nexomc.nexo.api.events.resourcepack.NexoPrePackGenerateEvent import kr.toxicity.model.api.BetterModelPlatform import kr.toxicity.model.bukkit.compatibility.Compatibility import kr.toxicity.model.bukkit.util.PLUGIN import kr.toxicity.model.bukkit.util.registerListener import kr.toxicity.model.util.* import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.event.EventHandler import org.bukkit.event.Listener class NexoCompatibility : Compatibility { override fun start() { if (CONFIG.mergeWithExternalResources()) PLUGIN.skipInitialReload() registerListener(object : Listener { @EventHandler fun NexoPrePackGenerateEvent.generate() { if (!CONFIG.mergeWithExternalResources()) return when (val result = PLATFORM.reload()) { is BetterModelPlatform.ReloadResult.Success -> { result.packResult().directory()?.let { addResourcePack(it) info("Successfully merged with Nexo.".toComponent(NamedTextColor.GREEN)) } } is BetterModelPlatform.ReloadResult.OnReload -> { warn("BetterModel is still on reload!".toComponent(NamedTextColor.RED)) } is BetterModelPlatform.ReloadResult.Failure -> { result.throwable.handleException("Unable to merge with Nexo.") } } } }) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/compatibility/skinsrestorer/SkinsRestorerCompatibility.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.compatibility.skinsrestorer import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.bukkit.compatibility.Compatibility import kr.toxicity.model.bukkit.util.wrap import kr.toxicity.model.manager.ProfileManagerImpl import kr.toxicity.model.manager.SkinManagerImpl import kr.toxicity.model.util.PLATFORM import net.skinsrestorer.api.SkinsRestorerProvider import net.skinsrestorer.api.event.SkinApplyEvent import org.bukkit.Bukkit import org.bukkit.entity.Player import java.util.concurrent.CompletableFuture class SkinsRestorerCompatibility : Compatibility { private val manager = SkinsRestorerProvider.get() override fun start() { manager.eventBus.subscribe(PLATFORM, SkinApplyEvent::class.java) { val player = it.getPlayer(Player::class.java) SkinManagerImpl.removeCache(ModelProfile.of(player.wrap())) } ProfileManagerImpl.supplier { SkinsRestorerProfile(it) } } private inner class SkinsRestorerProfile( private val info: ModelProfileInfo ) : ModelProfile.Uncompleted { override fun info(): ModelProfileInfo = info override fun complete(): CompletableFuture = CompletableFuture.supplyAsync { manager.playerStorage .getSkinForPlayer( info.id, info.name, Bukkit.getOnlineMode() ).map { skin -> ModelProfile.of( info, ProfileManagerImpl.skin(skin.value) ) }.orElse(null) } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/configuration/PluginConfiguration.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.configuration import kr.toxicity.model.bukkit.util.toYaml import kr.toxicity.model.util.DATA_FOLDER import kr.toxicity.model.util.PLATFORM import kr.toxicity.model.util.ifNull import org.bukkit.configuration.file.YamlConfiguration import java.io.File enum class PluginConfiguration( private val dir: String ) { CONFIG("config.yml"), ; fun create(): YamlConfiguration { val file = File(DATA_FOLDER, dir) val exists = file.exists() if (!exists) PLATFORM.saveResource(dir) val yaml = file.toYaml() val newYaml = PLATFORM.getResource(dir).ifNull { "Resource '$dir' not found." }.use { it.toYaml() } yaml.getKeys(true).forEach { if (!newYaml.contains(it)) yaml.set(it, null) } newYaml.getKeys(true).forEach { if (!yaml.contains(it)) yaml.set(it, newYaml.get(it)) yaml.setComments(it ,newYaml.getComments(it)) } return yaml.apply { save(file) } } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/manager/CompatibilityManager.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.manager import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.bukkit.compatibility.citizens.CitizensCompatibility import kr.toxicity.model.bukkit.compatibility.mythicmobs.MythicMobsCompatibility import kr.toxicity.model.bukkit.compatibility.nexo.NexoCompatibility import kr.toxicity.model.bukkit.compatibility.skinsrestorer.SkinsRestorerCompatibility import kr.toxicity.model.bukkit.purpur.PurpurHook import kr.toxicity.model.bukkit.util.registerListener import kr.toxicity.model.manager.GlobalManager import kr.toxicity.model.manager.ReloadPipeline import kr.toxicity.model.util.info import kr.toxicity.model.util.toComponent import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.Bukkit import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.server.PluginEnableEvent object CompatibilityManager : GlobalManager { private val compatibilities = mutableMapOf( "MythicMobs" to { MythicMobsCompatibility() }, "Citizens" to { CitizensCompatibility() }, "SkinsRestorer" to { SkinsRestorerCompatibility() }, "Nexo" to { NexoCompatibility() } ) override fun start() { if (BetterModelBukkit.IS_PURPUR) PurpurHook.start() Bukkit.getPluginManager().run { compatibilities.entries.removeIf { (k, v) -> if (isPluginEnabled(k)) { v().start() k.hookMessage() true } else false } } registerListener(object : Listener { @EventHandler fun PluginEnableEvent.enable() { val name = plugin.name compatibilities.remove(name)?.let { it().start() name.hookMessage() } } }) } private fun String.hookMessage() = info("Plugin hooks $this".toComponent(NamedTextColor.AQUA)) override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/manager/EntityManager.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.manager import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent import com.destroystokyo.paper.event.entity.EntityJumpEvent import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent import com.destroystokyo.paper.event.player.PlayerJumpEvent import it.unimi.dsi.fastutil.objects.ReferenceSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.tracker.EntityTracker import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerExtraAnimation import kr.toxicity.model.bukkit.util.registerListener import kr.toxicity.model.bukkit.util.wrap import kr.toxicity.model.manager.GlobalManager import kr.toxicity.model.manager.ReloadPipeline import kr.toxicity.model.util.PLATFORM import org.bukkit.entity.Entity import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.entity.* import org.bukkit.event.player.PlayerChangedWorldEvent import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.world.EntitiesUnloadEvent import org.bukkit.inventory.EquipmentSlot import org.bukkit.potion.PotionEffectType object EntityManager : GlobalManager { private val effectSet = ReferenceSet.of( PotionEffectType.GLOWING, PotionEffectType.INVISIBILITY ) private class PaperListener : Listener { //More accurate world change event for Paper @EventHandler(priority = EventPriority.MONITOR) fun EntityRemoveFromWorldEvent.remove() { BetterModel.registryOrNull(entity.uniqueId)?.despawn() } @EventHandler(priority = EventPriority.MONITOR) fun EntityAddToWorldEvent.add() { BetterModel.registryOrNull(entity.wrap())?.refresh() } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun EntityJumpEvent.jump() { entity.forEachTracker { it.animate(TrackerExtraAnimation.JUMP) } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun PlayerJumpEvent.jump() { player.forEachTracker { it.animate(TrackerExtraAnimation.JUMP) } } } private class SpigotListener : Listener { //Portal event for Spigot @EventHandler(priority = EventPriority.MONITOR) fun EntityRemoveEvent.remove() { BetterModel.registryOrNull(entity.uniqueId)?.despawn() } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun EntitySpawnEvent.spawn() { BetterModel.registryOrNull(entity.wrap())?.refresh() } @EventHandler(priority = EventPriority.MONITOR) fun PlayerChangedWorldEvent.change() { BetterModel.registryOrNull(player.uniqueId)?.let { it.despawn() it.refresh() } } } //Event handlers private val standardListener = object : Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun EntityPotionEffectEvent.potion() { //Apply potion effect if (action == EntityPotionEffectEvent.Action.CHANGED) return if (oldEffect?.let { it.type in effectSet } == true || newEffect?.let { it.type in effectSet } == true) entity.forEachTracker { it.updateBaseEntity() } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun EntityDismountEvent.dismount() { //Dismount val e = dismounted isCancelled = e is HitBox && (e.mountController().canFly() || !e.mountController().canDismountBySelf()) && !e.forceDismount() } @EventHandler(priority = EventPriority.MONITOR) fun PlayerQuitEvent.quit() { //Quit val wrap = player.wrap() BetterModel.registryOrNull(wrap.uuid())?.close() PLATFORM.scheduler().asyncTask { EntityTrackerRegistry.registries { registry -> registry.remove(wrap) } } (player.vehicle as? HitBox)?.dismount(wrap) } @EventHandler(priority = EventPriority.MONITOR) fun PlayerDeathEvent.death() { BetterModel.registryOrNull(entity.uniqueId)?.despawn() } @EventHandler(priority = EventPriority.MONITOR) fun EntitiesUnloadEvent.unload() { //Chunk unload entities.forEach { entity -> BetterModel.registryOrNull(entity.uniqueId)?.despawn() } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun EntityDeathEvent.death() { //Death entity.forEachTracker { it.animate(TrackerExtraAnimation.DEATH) } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun PlayerInteractEntityEvent.interact() { //Interact base entity based on interaction entity (rightClicked as? HitBox)?.let { if (hand == EquipmentSlot.HAND && !player.triggerDismount(rightClicked)) player.triggerMount(it) } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) fun EntityDamageEvent.damage() { //Damage if (this is EntityDamageByEntityEvent) { val victim = entity.run { if (this is HitBox) source().uuid() else uniqueId } val v = damager.vehicle if (v is HitBox && !v.mountController().canBeDamagedByRider() && v.source().uuid() == victim) { isCancelled = true return } // if (cause == EntityDamageEvent.DamageCause.ENTITY_ATTACK) { // EntityTracker.tracker(damager)?.animate("attack", AnimationModifier.DEFAULT_WITH_PLAY_ONCE) // } } entity.forEachTracker { it.animate(TrackerExtraAnimation.DAMAGE) it.damageTint() } } } private val platformListener = if (BetterModelBukkit.IS_PAPER) PaperListener() else SpigotListener() //Lifecycles override fun start() { registerListener(standardListener) registerListener(platformListener) } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { EntityTrackerRegistry.registries(EntityTrackerRegistry::reload) } override fun end() { EntityTrackerRegistry.registries { it.save() it.close(Tracker.CloseReason.PLUGIN_DISABLE) } } //Extension private fun Entity.forEachTracker(block: (EntityTracker) -> Unit) { BetterModel.registryOrNull(uniqueId)?.trackers()?.forEach(block) } private fun Player.triggerDismount(e: Entity): Boolean { val previous = vehicle if (previous !is HitBox) return false val uuid = if (e is HitBox) e.source().uuid() else e.uniqueId if (previous.source().uuid() == uuid && previous.mountController().canDismountBySelf()) { previous.dismount(wrap()) return true } return false } private fun Player.triggerMount(hitBox: HitBox) { if (hitBox.mountController().canMount()) hitBox.mount(wrap()) } } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/manager/PlayerManagerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.manager import kr.toxicity.model.api.manager.PlayerManager import kr.toxicity.model.api.nms.PlayerChannelHandler import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.bukkit.util.registerListener import kr.toxicity.model.bukkit.util.wrap import kr.toxicity.model.manager.GlobalManager import kr.toxicity.model.manager.ReloadPipeline import kr.toxicity.model.manager.SkinManagerImpl import kr.toxicity.model.util.PLATFORM import kr.toxicity.model.util.handleFailure import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent import java.util.* import java.util.concurrent.ConcurrentHashMap object PlayerManagerImpl : PlayerManager, GlobalManager { private val playerMap = ConcurrentHashMap() override fun start() { registerListener(object : Listener { @EventHandler(priority = EventPriority.HIGHEST) fun PlayerJoinEvent.join() { if (player.isOnline) runCatching { //For fake player player.wrap().register() }.handleFailure { "Unable to load ${player.name}'s data." } } @EventHandler(priority = EventPriority.MONITOR) fun PlayerQuitEvent.quit() { playerMap.remove(player.uniqueId)?.use { SkinManagerImpl.removeCache(it.base().profile()) } } }) } private fun PlatformPlayer.register() = playerMap.computeIfAbsent(uuid()) { PLATFORM.nms().inject(this) }.apply { SkinManagerImpl.complete(base().profile().asUncompleted()) } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { } override fun end() { playerMap.values.removeIf { it.use { used -> SkinManagerImpl.removeCache(used.base().profile()) } true } } override fun player(uuid: UUID): PlayerChannelHandler? = playerMap[uuid] override fun player(player: PlatformPlayer): PlayerChannelHandler = player.register() } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/scheduler/BukkitScheduler.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.scheduler import kr.toxicity.model.api.bukkit.scheduler.BukkitModelScheduler import kr.toxicity.model.api.scheduler.ModelTask import kr.toxicity.model.bukkit.util.PLUGIN import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.scheduler.BukkitTask class BukkitScheduler : BukkitModelScheduler { private fun BukkitTask.wrap() = object : ModelTask { override fun isCancelled(): Boolean = this@wrap.isCancelled override fun cancel() { this@wrap.cancel() } } private fun ifEnabled(block: () -> ModelTask?): ModelTask? { return if (PLUGIN.isEnabled) block() else null } override fun task(location: Location, runnable: Runnable) = ifEnabled { Bukkit.getScheduler().runTask(PLUGIN, runnable).wrap() } override fun taskLater(location: Location, delay: Long, runnable: Runnable) = ifEnabled { Bukkit.getScheduler().runTaskLater(PLUGIN, runnable, delay).wrap() } override fun asyncTask(runnable: Runnable) = Bukkit.getScheduler().runTaskAsynchronously(PLUGIN, runnable).wrap() override fun asyncTaskLater(delay: Long, runnable: Runnable) = Bukkit.getScheduler().runTaskLaterAsynchronously(PLUGIN, runnable, delay).wrap() override fun asyncTaskTimer(delay: Long, period: Long, runnable: Runnable) = Bukkit.getScheduler().runTaskTimerAsynchronously(PLUGIN, runnable, delay, period).wrap() } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/scheduler/PaperScheduler.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.scheduler import io.papermc.paper.threadedregions.scheduler.ScheduledTask import kr.toxicity.model.api.bukkit.scheduler.BukkitModelScheduler import kr.toxicity.model.api.scheduler.ModelTask import kr.toxicity.model.bukkit.util.PLUGIN import org.bukkit.Bukkit import org.bukkit.Location import java.util.concurrent.TimeUnit class PaperScheduler : BukkitModelScheduler { private fun ScheduledTask.wrap() = object : ModelTask { override fun isCancelled(): Boolean = this@wrap.isCancelled override fun cancel() { this@wrap.cancel() } } private fun ifEnabled(block: () -> ModelTask?): ModelTask? { return if (PLUGIN.isEnabled) block() else null } override fun task(location: Location, runnable: Runnable): ModelTask? = ifEnabled { Bukkit.getRegionScheduler().run(PLUGIN, location) { runnable.run() }.wrap() } override fun taskLater(location: Location, delay: Long, runnable: Runnable): ModelTask? = ifEnabled { Bukkit.getRegionScheduler().runDelayed(PLUGIN, location, { runnable.run() }, delay).wrap() } override fun asyncTask(runnable: Runnable) = Bukkit.getAsyncScheduler().runNow(PLUGIN) { runnable.run() }.wrap() override fun asyncTaskLater(delay: Long, runnable: Runnable) = Bukkit.getAsyncScheduler().runDelayed(PLUGIN, { runnable.run() }, (delay * 50).coerceAtLeast(1), TimeUnit.MILLISECONDS).wrap() override fun asyncTaskTimer(delay: Long, period: Long, runnable: Runnable) = Bukkit.getAsyncScheduler().runAtFixedRate(PLUGIN, { runnable.run() }, (delay * 50).coerceAtLeast(1), (period * 50).coerceAtLeast(1), TimeUnit.MILLISECONDS).wrap() } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/util/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.util import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack fun Entity.wrap() = adapt(this) fun LivingEntity.wrap() = adapt(this) fun OfflinePlayer.wrap() = adapt(this) fun Player.wrap() = adapt(this) fun Location.wrap() = adapt(this) fun World.wrap() = adapt(this) fun ItemStack.wrap() = adapt(this) fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/util/Entities.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.util import kr.toxicity.model.api.BetterModel import org.bukkit.entity.Entity fun Entity.toTracker(model: String?) = toRegistry()?.tracker(model) fun Entity.toRegistry() = BetterModel.registryOrNull(uniqueId) ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/util/Events.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.util import org.bukkit.Bukkit import org.bukkit.event.Listener fun registerListener(listener: Listener) { Bukkit.getPluginManager().registerEvents(listener, PLUGIN) } ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/util/Plugins.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.util import kr.toxicity.model.bukkit.BetterModelPlugin import kr.toxicity.model.util.PLATFORM val PLUGIN get() = PLATFORM as BetterModelPlugin ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/util/Senders.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.util import kr.toxicity.model.bukkit.BetterModelLibrary import net.kyori.adventure.platform.bukkit.BukkitAudiences import org.bukkit.command.CommandSender val ADVENTURE_PLATFORM = if (BetterModelLibrary.ADVENTURE_PLATFORM.isLoaded) BukkitAudiences.create(PLUGIN) else null fun CommandSender.audience() = ADVENTURE_PLATFORM?.sender(this) ?: this ================================================ FILE: core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/util/Yamls.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.util import org.bukkit.configuration.file.YamlConfiguration import java.io.File import java.io.InputStream import java.io.InputStreamReader import java.nio.charset.StandardCharsets fun File.toYaml() = YamlConfiguration.loadConfiguration(this) fun InputStream.toYaml() = InputStreamReader(this, StandardCharsets.UTF_8).use { reader -> reader.buffered().use(YamlConfiguration::loadConfiguration) } ================================================ FILE: core/src/main/java/kr/toxicity/model/BetterModelPlatformImpl.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model; import kr.toxicity.model.api.BetterModelPlatform; import kr.toxicity.model.manager.ReloadPipeline; import org.jetbrains.annotations.NotNull; import java.io.InputStream; import java.util.function.BiConsumer; public interface BetterModelPlatformImpl extends BetterModelPlatform { void saveResource(@NotNull String resourcePath); void loadAssets(@NotNull ReloadPipeline pipeline, @NotNull String prefix, @NotNull BiConsumer consumer); } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/BetterModelEvaluatorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model import gg.moonflower.molangcompiler.api.MolangCompiler import gg.moonflower.molangcompiler.api.MolangRuntime import kr.toxicity.model.api.BetterModelEvaluator import kr.toxicity.model.api.util.function.Float2FloatFunction class BetterModelEvaluatorImpl : BetterModelEvaluator { private val molang = MolangCompiler.create(MolangCompiler.DEFAULT_FLAGS, javaClass.classLoader) private fun Float.query() = MolangRuntime.runtime() .setQuery("life_time", this) .setQuery("anim_time", this) .create() override fun compile(expression: String): Float2FloatFunction { val compiled = molang.compile(expression) return Float2FloatFunction { it.query().safeResolve(compiled) } } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/BetterModelEventBusImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model import kr.toxicity.model.api.BetterModelEventBus import kr.toxicity.model.api.event.CancellableEvent import kr.toxicity.model.api.event.ModelEvent import kr.toxicity.model.api.event.ModelEventApplication import kr.toxicity.model.api.event.ModelEventListener import kr.toxicity.model.api.util.lock.DuplexLock import kr.toxicity.model.util.handleFailure import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.function.Consumer import java.util.function.Supplier class BetterModelEventBusImpl( private val externalCallback: (Class, Supplier) -> BetterModelEventBus.Result = { _, _ -> BetterModelEventBus.Result.NO_EVENT_HANDLER } ) : BetterModelEventBus { private val subscribers = ConcurrentHashMap, BusManager>() override fun subscribe(application: ModelEventApplication, eventClass: Class, consumer: Consumer): ModelEventListener { return subscribers.computeIfAbsent(eventClass) { BusManager(it) } .register(application, consumer) } @Suppress("UNCHECKED_CAST") override fun call(eventClass: Class, eventSupplier: Supplier): BetterModelEventBus.Result { return subscribers[eventClass]?.let { manager -> eventSupplier .get() .also(manager) .also { externalCallback(eventClass) { it } } .let { event -> if (event !is CancellableEvent || !event.isCancelled()) BetterModelEventBus.Result.SUCCESS else BetterModelEventBus.Result.FAIL } } ?: run { externalCallback(eventClass) { eventSupplier.get() } } } private inner class BusManager( private val clazz: Class<*> ) : (ModelEvent) -> Unit { private val map = ConcurrentHashMap() fun register(application: ModelEventApplication, consumer: Consumer): ModelEventListener { if (!application.isEnabled) return ModelEventListener.NONE return map.compute(application) { k, v -> v?.takeIf { k.isEnabled } ?: ListenerRegistry() }?.add(consumer) ?: ModelEventListener.NONE } override fun invoke(p1: ModelEvent) { synchronized(this) { map.entries.removeIf { (application, registry) -> !application.isEnabled || runCatching { registry(p1) }.handleFailure { "Unable to pass this event: ${clazz.simpleName}" }.isFailure } if (map.isEmpty()) subscribers.remove(clazz, this) } } } private class ListenerRegistry : (ModelEvent) -> Unit { val map = IdentityHashMap>() val lock = DuplexLock() fun add(consumer: Consumer<*>): ModelEventListener = ListenerImpl(this, consumer) @Suppress("UNCHECKED_CAST") override fun invoke(p1: ModelEvent) { lock.accessToReadLock { map.values.forEach { consumer -> (consumer as Consumer).accept(p1) } } } } private class ListenerImpl( registry: ListenerRegistry, consumer: Consumer<*> ) : ModelEventListener { private val ref = WeakReference(registry) init { registry.lock.accessToWriteLock { registry.map[this] = consumer } } override fun unregister() { ref.get()?.let { it.lock.accessToWriteLock { it.map.remove(this) } } } } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/command/CommandBuildContext.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.command import net.kyori.adventure.audience.Audience import org.incendo.cloud.Command import org.incendo.cloud.CommandManager import org.incendo.cloud.description.Description class CommandBuildContext( val manager: CommandManager, val commandMapper: CommandBuilder.(Command.Builder) -> Command.Builder, name: String, description: String, vararg aliases: String, ) { val root = CommandBuilder( null, this, CommandBuilder.Info(name, Description.description(description), aliases.toList()) ) fun build() { root.build().forEach { manager.command(it) } } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/command/CommandBuilder.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.command import kr.toxicity.model.util.* import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.TextComponent import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.format.NamedTextColor.* import net.kyori.adventure.text.format.TextDecoration import org.incendo.cloud.Command import org.incendo.cloud.description.Description import org.incendo.cloud.parser.standard.IntegerParser class CommandBuilder( val parent: CommandBuilder?, val context: CommandBuildContext, val info: Info ) : CommandLike { private companion object { const val PAGE_SPLIT_INDEX = 5 val prefix = listOf( emptyComponentOf(), "------ BetterModel ${PLATFORM.semver()} ------".toComponent(GRAY), emptyComponentOf() ) val fullPrefix = listOf( prefix, listOf( componentOf { decorate(TextDecoration.BOLD) append(spaceComponentOf()) append("[Wiki]".toComponent { color(AQUA) toURLComponent("https://github.com/toxicity188/BetterModel/wiki") }) append(spaceComponentOf()) append("[Download]".toComponent { color(GREEN) toURLComponent("https://modrinth.com/plugin/bettermodel/versions") }) append(spaceComponentOf()) append("[Discord]".toComponent { color(BLUE) toURLComponent("https://discord.com/invite/rePyFESDbk") }) }, emptyComponentOf() ) ).flatten() fun TextComponent.Builder.toURLComponent(url: String) = hoverEvent(componentOf( url.toComponent(DARK_AQUA), lineComponentOf(), lineComponentOf(), "Click to open link.".toComponent() ).toHoverEvent()).clickEvent(ClickEvent.openUrl(url)) } private val root: CommandBuilder = parent?.root ?: this private val suggest: String = parent?.let { "${it.suggest} ${info.name}" } ?: info.simpleName private val permission: String = parent?.let { "${it.permission}.${info.name}" } ?: info.name private val children = mutableListOf() private val helpCommand by lazy { val maxPage = children.size / PAGE_SPLIT_INDEX + 1 val helpComponents = (1..maxPage).map { index -> (if (index == 1) fullPrefix else prefix).toMutableList() .also { list -> children.subList(PAGE_SPLIT_INDEX * (index - 1), (PAGE_SPLIT_INDEX * index).coerceAtMost(children.size)).forEach { list += it.toComponent() } list += "/$suggest [help] [page] - help command.".toComponent(LIGHT_PURPLE) list += emptyComponentOf() list += "---------< Page $index / $maxPage >---------".toComponent(GRAY) }.toTypedArray() } val builder = createBuilder() .permission("$permission.help") .handler { ctx -> val page = ctx.getOrDefault("page", 1) .coerceAtLeast(1) .coerceAtMost(maxPage) ctx.sender().info(*helpComponents[page - 1]) } listOf( builder .optional("page", IntegerParser.integerParser(1, maxPage)) .build(), builder.literal("help", "h") .optional("page", IntegerParser.integerParser(1, maxPage)) .build() ) } fun create( name: String, description: String, vararg aliases: String, builder: Command.Builder.() -> Command.Builder ) { children += CommandLike.Cloud(createBuilder() .mapInfo(Info(name, Description.description(description), aliases.toList())) .run(builder) .build()) } data class Info( val name: String, val description: Description, val aliases: List ) { val simpleName get() = if (aliases.isNotEmpty()) aliases.minBy { it.length } else name } override fun toComponent(): TextComponent { TODO("Not yet implemented") } override fun build(): List> = buildList { children.flatMapTo(this) { it.build() } addAll(helpCommand) } private fun Command.Builder.mapInfo(info: Info) = literal(info.name, *info.aliases.toTypedArray()) .commandDescription(info.description) .permission("$permission.${info.name}") private fun createBuilder(): Command.Builder = parent?.createBuilder()?.mapInfo(info) ?: context.commandMapper(this, context.manager.commandBuilder( info.name, info.description, *info.aliases.toTypedArray() )) } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/command/CommandExtensions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.command import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.data.renderer.ModelRenderer import net.kyori.adventure.audience.Audience import org.incendo.cloud.Command import org.incendo.cloud.CommandManager import org.incendo.cloud.context.CommandContext fun CommandManager.register( name: String, description: String, commandMapper: CommandBuilder.(Command.Builder) -> Command.Builder, vararg aliases: String, block: CommandBuilder.() -> Unit ) = CommandBuildContext(this, commandMapper, name, description, *aliases).run { root.block() build() } inline fun CommandContext<*>.limb(key: String, notFound: (String) -> ModelRenderer) = optional(key).flatMap { BetterModel.limb(it) }.orElse(null) ?: notFound(key) inline fun CommandContext<*>.model(key: String, notFound: (String) -> ModelRenderer) = optional(key).flatMap { BetterModel.model(it) }.orElse(null) ?: notFound(key) inline fun CommandContext<*>.string(key: String, mapper: (String) -> T) = mapper(get(key)) fun CommandContext<*>.nullableString(key: String, mapper: (String) -> T): T? = optional(key).map { mapper(it) }.orElse(null) inline fun CommandContext<*>.nullable(key: String): T? = optional(key).orElse(null) inline fun CommandContext<*>.nullable(key: String, ifNotFound: T): T = optional(key).orElse(null) ?: ifNotFound inline fun CommandContext<*>.nullable(key: String, ifNotFound: () -> T): T = optional(key).orElse(null) ?: ifNotFound() ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/command/CommandLike.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.command import kr.toxicity.model.util.* import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.TextComponent import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.format.NamedTextColor.* import net.kyori.adventure.text.format.TextDecoration import org.incendo.cloud.Command import org.incendo.cloud.component.CommandComponent import org.incendo.cloud.component.CommandComponent.ComponentType.* interface CommandLike { fun toComponent(): TextComponent fun build(): List> data class Cloud( private val command: Command ) : CommandLike { override fun toComponent(): TextComponent = command.toComponent() private fun Command.toComponent() = componentOf { append("/".toComponent()) components().forEachIndexed { i, comp -> append(comp.toComponent(i == 0)) if (i < components().size) append(spaceComponentOf()) } append(lineComponentOf()) append(" | ".toComponent { color(GREEN).decorate(TextDecoration.BOLD) }) append(" └ ".toComponent()) append(commandDescription().description().textDescription().toComponent(GRAY)) hoverEvent(componentOf( "Permission:".toComponent(DARK_AQUA), lineComponentOf(), commandPermission().permissionString().toComponent(), lineComponentOf(), lineComponentOf(), "Click to suggest command.".toComponent() ).toHoverEvent()) clickEvent(ClickEvent.suggestCommand("/" + components().filter { it.type() == LITERAL }.joinToString(" ") { it.name() })) } private fun CommandComponent.toComponent(root: Boolean): TextComponent = componentOf { val n = if (root) aliases().minBy { it.length } else name() when (type()) { LITERAL -> content(n).color(YELLOW) REQUIRED_VARIABLE -> content("<$n>").color(RED) OPTIONAL_VARIABLE -> content("[$n]").color(DARK_AQUA) FLAG -> content("-$n").color(LIGHT_PURPLE) } hoverEvent(componentOf { if (aliases().isNotEmpty()) { append(componentOf( "Aliases:".toComponent(DARK_AQUA), lineComponentOf(), componentWithLineOf(*aliases().map(String::toComponent).toTypedArray()), lineComponentOf(), lineComponentOf() )) } append("Click to suggest command.".toComponent()) }.toHoverEvent()) } override fun build(): List> = listOf(command) } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/ArmorManager.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import kr.toxicity.library.armormodel.ArmorImage import kr.toxicity.library.armormodel.ArmorModel import kr.toxicity.library.armormodel.ArmorNameMapper import kr.toxicity.library.armormodel.ArmorPaletteImage import kr.toxicity.model.api.pack.PackObfuscator import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.util.* import net.kyori.adventure.text.format.NamedTextColor import java.io.File import java.net.URI import java.net.http.HttpResponse import java.util.concurrent.CompletableFuture import java.util.jar.JarFile import java.util.zip.ZipEntry import kotlin.io.path.createTempFile object ArmorManager : GlobalManager { private val ARMOR_PATH = "assets/minecraft/textures/entity/equipment/humanoid" to "assets/minecraft/textures/entity/equipment/humanoid_leggings" private val ARMOR_TRIM_PATH = "assets/minecraft/textures/trims/entity/humanoid" to "assets/minecraft/textures/trims/entity/humanoid_leggings" private const val ARMOR_PALETTE_PATH = "assets/minecraft/textures/trims/color_palettes" private val armors = setOf( "chainmail", "copper", "diamond", "gold", "iron", "leather", "netherite" ) private val palettes = setOf( "amethyst", "copper", "copper_darker", "diamond", "diamond_darker", "emerald", "gold", "gold_darker", "iron", "iron_darker", "lapis", "netherite", "netherite_darker", "quartz", "redstone", "resin" ) private val trims = setOf( "bolt", "coast", "dune", "eye", "flow", "host", "raiser", "rib", "sentry", "shaper", "silence", "snout", "spire", "tide", "vex", "ward", "wayfinder", "wild" ) var armor: ArmorModel = ArmorModel.EMPTY private set private data class VersionManifest( val latest: ManifestLatest, val versions: List ) { val manifest get() = versions.associateBy { it.id }[latest.release]!! } private data class ManifestLatest( val release: String ) private data class ManifestVersion( val id: String, val url: String ) { fun toURI(): URI = URI.create(url) } private data class VersionHash( val downloads: Map ) { val client by downloads } private data class HashDownload( val url: String ) { fun toURI(): URI = URI.create(url) } private data class MinecraftClient( val version: String, val file: File ) private fun downloadMinecraftClient(): CompletableFuture = httpClient { val cacheFolder = DATA_FOLDER.getOrCreateDirectory(".cache") sendAsync( buildHttpRequest { GET() uri(URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")) }, HttpResponse.BodyHandlers.ofInputStream() ).thenComposeAsync { response1 -> val manifest = response1.toJson(VersionManifest::class.java).manifest val cache = File(cacheFolder, "${manifest.id}.jar") if (cache.exists() && cache.length() > 0) CompletableFuture.supplyAsync { MinecraftClient(manifest.id, cache) } else sendAsync( buildHttpRequest { GET() uri(manifest.toURI()) }, HttpResponse.BodyHandlers.ofInputStream() ).thenComposeAsync { response2 -> sendAsync( buildHttpRequest { GET() uri(response2.toJson(VersionHash::class.java).client.toURI()) }, HttpResponse.BodyHandlers.ofInputStream() ).thenComposeAsync { response3 -> val temp = createTempFile(cache.parentFile.toPath(), manifest.id, ".tmp").toFile() response3.body().use { input -> temp.outputStream().buffered().use(input::copyTo) } temp.renameTo(cache) CompletableFuture.supplyAsync { MinecraftClient(manifest.id, cache) } } } } }.orElse { CompletableFuture.completedFuture(null) } private class ArmorImageCache( val name: String, val armor: ByteArray, val leggings: ByteArray ) { val size: Long get() = armor.size.toLong() + leggings.size.toLong() fun write(target: File) { val file = File(target, name).apply { mkdirs() } File(file, "armor.png").outputStream().buffered().use { it.write(armor) } File(file, "leggings.png").outputStream().buffered().use { it.write(leggings) } } } private fun JarFile.loadArmorImage(name: String, pathPair: Pair) = ArmorImageCache( name, loadImage(pathPair.first, name), loadImage(pathPair.second, name) ) private fun JarFile.loadImage(path: String, name: String) = getInputStream(ZipEntry("$path/$name.png")).use { it.readAllBytes() } override fun reload( pipeline: ReloadPipeline, zipper: PackZipper ) { if (!CONFIG.module().playerAnimation) { armor = ArmorModel.EMPTY return } val folder = DATA_FOLDER.getOrCreateDirectory("armors") { info("Downloading client jar...".toComponent()) val armorsFile = File(it, "armors") val trimsFile = File(it, "armor_trims") val palettesFile = File(it, "palettes").apply { mkdirs() } runCatching { downloadMinecraftClient().join()?.let { client -> JarFile(client.file).use { jar -> pipeline.forEachParallel( armors.map { name -> jar.loadArmorImage(name, ARMOR_PATH) }, ArmorImageCache::size ) { image -> image.write(armorsFile) } pipeline.forEachParallel( trims.map { name -> jar.loadArmorImage(name, ARMOR_TRIM_PATH) }, ArmorImageCache::size ) { image -> image.write(trimsFile) } pipeline.forEachParallel( palettes.map { name -> name to jar.loadImage(ARMOR_PALETTE_PATH, name) }, { image -> image.second.size.toLong() }, ) { image -> File(palettesFile, "${image.first}.png").writeBytes(image.second)} } } info("Download success!".toComponent(NamedTextColor.LIGHT_PURPLE)) }.handleFailure { "Unable to download default armor assets." } } val textures = PackObfuscator.order() val models = PackObfuscator.order() armor = ArmorModel.builder() .namespace(CONFIG.namespace()) .streamLoader { path -> PLATFORM.getResource(path)!! } .armors(pipeline .mapParallel(File(folder, "armors").subFiles(), File::length) { it.toArmorImage() } .sortedBy { it.name } ) .armorTrims(pipeline .mapParallel(File(folder, "armor_trims").subFiles(), File::length) { it.toArmorImage() } .sortedBy { it.name } ) .palettes(pipeline .mapParallel(File(folder, "palettes").subFiles(), File::length) { it.toPaletteImage() } .sortedBy { it.name } ) .nameMapper(ArmorNameMapper( { textures.obfuscate(it) }, { models.obfuscate(it) } )) .flush(false) .build() armor.builders().forEach { zipper.modern().add( it.path(), 256 ) { it.get() } } } private fun File.toArmorImage() = runCatching { ArmorImage( nameWithoutExtension, File(this, "armor.png").toImage(), File(this, "leggings.png").toImage() ) }.handleFailure { "Unable to load this armor image: $path" }.getOrNull() private fun File.toPaletteImage() = runCatching { ArmorPaletteImage(nameWithoutExtension, toImage()) }.handleFailure { "Unable to load this palette image: $path" }.getOrNull() } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/GlobalManager.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import kr.toxicity.model.api.pack.PackZipper interface GlobalManager { fun start() {} fun reload(pipeline: ReloadPipeline, zipper: PackZipper) fun end() {} } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/ModelManagerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import com.google.gson.JsonArray import com.google.gson.JsonObject import kr.toxicity.model.api.bone.BoneItemMapper import kr.toxicity.model.api.data.ModelAsset import kr.toxicity.model.api.data.blueprint.BlueprintElement import kr.toxicity.model.api.data.blueprint.BlueprintJson import kr.toxicity.model.api.data.blueprint.ModelBlueprint import kr.toxicity.model.api.data.renderer.ModelRenderer import kr.toxicity.model.api.data.renderer.RendererGroup import kr.toxicity.model.api.event.ModelAssetsEvent import kr.toxicity.model.api.event.ModelImportedEvent import kr.toxicity.model.api.manager.ModelManager import kr.toxicity.model.api.pack.PackBuilder import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.platform.PlatformNamespace import kr.toxicity.model.util.* import net.kyori.adventure.text.format.NamedTextColor.* import java.io.File import java.util.concurrent.ConcurrentHashMap import kotlin.io.path.extension object ModelManagerImpl : ModelManager, GlobalManager { private lateinit var itemModelNamespace: PlatformNamespace private val generalModelMap = addressingMapOf() private val generalModelView = generalModelMap.toImmutableView() private val playerModelMap = addressingMapOf() private val playerModelView = playerModelMap.toImmutableView() private val modelExtensions = setOf("bbmodel", "ajmodel") private fun importModels( type: ModelRenderer.Type, pipeline: ReloadPipeline, dir: File ): Sequence { val targetAssets = ModelAssetsEvent(type, dir.fileTrees().use { stream -> stream.filter { it.extension.lowercase() in modelExtensions } .map(ModelAsset::of) .toMutableSet() }).apply { call() } .assets .ifEmpty { return emptySequence() } .toList() val modelFileMap = ConcurrentHashMap>(targetAssets.size) val typeName = type.name.lowercase() pipeline.apply { status = "Importing $typeName models..." goal = targetAssets.size }.forEachParallel(targetAssets, ModelAsset::sizeAssume) { val index = pipeline.progress() val load = it.toTexturedModel() ?: return@forEachParallel modelFileMap.compute(load.name) { _, v -> if (v != null) { // A model with the same name already exists from a different file warn( "Duplicate $typeName model name '${load.name}'.".toComponent(), "Duplicated file: $it".toComponent(RED), "And: ${v.first}".toComponent(RED) ) if (v.first < it) return@compute v } debugPack { componentOf( "$typeName model file successfully loaded: ".toComponent(), it.toString().toComponent(GREEN), " ($index/${pipeline.goal})".toComponent(DARK_GRAY) ) } it to load } } return modelFileMap.values .asSequence() .sortedBy { it.first } .map { ImportedModel( it.first.sizeAssume - it.second.textures.sumOf { tex -> tex.image.size }, type, it.second ) } } private fun loadModels(pipeline: ReloadPipeline, zipper: PackZipper) { ModelPipeline(zipper).use { if (CONFIG.module().model) it.addModelTo( generalModelMap, importModels(ModelRenderer.Type.GENERAL, pipeline, DATA_FOLDER.getOrCreateDirectory("models") { folder -> File(DATA_FOLDER.parent, "ModelEngine/blueprints") .takeIf(File::isDirectory) ?.run { copyRecursively(folder, overwrite = true) info("ModelEngine's models are successfully migrated.".toComponent(GREEN)) } ?: run { folder.addResource("demon_knight.bbmodel") folder.addResource("blue_wizard.bbmodel") } }) ) if (CONFIG.module().playerAnimation) it.addModelTo( playerModelMap, importModels(ModelRenderer.Type.PLAYER, pipeline, DATA_FOLDER.getOrCreateDirectory("players") { folder -> folder.addResource("steve.bbmodel") }) ) } } private data class ImportedModel( val jsonSize: Long, val type: ModelRenderer.Type, val blueprint: ModelBlueprint ) private class ModelPipeline( zipper: PackZipper ) : AutoCloseable { private var indexer = 1 private var estimatedSize = 0L private val textures = zipper.assets().bettermodel().textures() private val legacyModel = ModelBuilder( models = zipper.legacy().bettermodel().models().resolve("item"), available = CONFIG.pack().generateLegacyModel, onBuild = { blueprints, _, size -> val json = blueprints.first() entries += jsonObjectOf( "predicate" to jsonObjectOf("custom_model_data" to indexer), "model" to "${CONFIG.namespace()}:item/${json.name}" ) models.add(json.jsonName(), size) { json.buildJson().toByteArray() } }, onClose = { val itemName = CONFIG.itemModel().lowercase() jsonObjectOf( "parent" to "minecraft:item/generated", "textures" to jsonObjectOf("layer0" to "minecraft:item/$itemName"), "overrides" to entries ).run { models.add("${CONFIG.itemNamespace()}.json", estimatedSize) { toByteArray() } zipper.legacy().minecraft().models().resolve("item").add("$itemName.json", estimatedSize) { toByteArray() } } } ) private val modernModel = ModelBuilder( models = zipper.modern().bettermodel().models().resolve("modern_item"), available = CONFIG.pack().generateModernModel, onBuild = { blueprints, json, size -> entries += jsonObjectOf( "threshold" to indexer, "model" to blueprints.toModernJson(json) ) blueprints.forEach { json -> models.add(json.jsonName(), size / blueprints.size) { json.buildJson().toByteArray() } } }, onClose = { zipper.modern().bettermodel().items().add("${CONFIG.itemNamespace()}.json", estimatedSize) { jsonObjectOf("model" to jsonObjectOf( "type" to "range_dispatch", "property" to "custom_model_data", "fallback" to jsonObjectOf( "type" to "empty" ), "entries" to entries )).toByteArray() } } ) override fun close() { modernModel.close() legacyModel.close() } fun addModelTo( targetMap: MutableMap, model: Sequence ) { model.forEach { addModelTo(targetMap, it) } } private fun addModelTo( targetMap: MutableMap, importedModel: ImportedModel ) { val (size, type, blueprint) = importedModel val context = blueprint.context() targetMap[blueprint.name] = blueprint.toRenderer(type) render@ { group -> if (!context.canBeRendered()) return@render null listOfNotNull( modernModel.ifAvailable { val json = group.buildModernJson(obfuscator, context) val itemModel = group.buildMeshItemModel(context) if (json != null || itemModel != null) { build(json ?: emptyList(), itemModel, if (json != null) size / json.size else 0) } else null }, legacyModel.ifAvailable { group.buildLegacyJson(obfuscator, context) ?.let { build(listOf(it), null, size) } } ).run { if (isNotEmpty()) indexer++ else null } }.apply { debugPack { componentOf( "This model was successfully imported: ".toComponent(), blueprint.name.toComponent(GREEN) ) } callEvent { ModelImportedEvent(blueprint, this) } } context.buildImage(textures.obfuscator()).forEach { image -> textures.add(image.pngName(), image.estimatedSize()) { image.toByteArray() } image.mcmeta()?.let { meta -> textures.add(image.mcmetaName(), -1) { meta.toByteArray() } } } estimatedSize += size } inner class ModelBuilder( val models: PackBuilder, private val available: Boolean, private val onBuild: ModelBuilder.(List, JsonObject?, Long) -> Unit, private val onClose: ModelBuilder.() -> Unit ) : AutoCloseable { val entries = jsonArrayOf() val obfuscator = textures.obfuscator().withModels(models.obfuscator()) inline fun ifAvailable(block: ModelBuilder.() -> T): T? { return if (available) block() else null } fun build(list: List, json: JsonObject?, size: Long) { onBuild(list, json, size) } override fun close() { ifAvailable { if (!entries.isEmpty) onClose() } } } private fun List.toModernJson(plus: JsonObject?) = if (size == 1) first().toModernJson() else jsonObjectOf( "type" to "composite", "models" to fold(JsonArray(size + (if (plus != null) 1 else 0)).apply { plus?.run(::add) }) { array, element -> array.apply { add(element.toModernJson()) } } ) private fun BlueprintJson.toModernJson() = jsonObjectOf( "type" to "model", "model" to "${CONFIG.namespace()}:modern_item/$name", "tints" to jsonArrayOf( jsonObjectOf( "type" to "custom_model_data", "default" to 0xFFFFFF ) ) ) private fun ModelBlueprint.toRenderer(type: ModelRenderer.Type, builder: (BlueprintElement.Group) -> Int?): ModelRenderer { fun Collection.toBoneMap(mapper: (BlueprintElement.Bone) -> T) = filterIsInstance().let { bone -> bone.associateTo(sequencedAddressingMapOf(bone.size)) { it.name() to mapper(it) } }.toImmutableView() fun BlueprintElement.Bone.parse(): RendererGroup { if (this !is BlueprintElement.Group) return RendererGroup(1.0F, null, this, emptySequencedMap(), null) return RendererGroup( scale(), if (name.toItemMapper() !== BoneItemMapper.EMPTY) null else builder(this)?.let { i -> CONFIG.item().get().modelData(i, itemModelNamespace) }, this, children.toBoneMap { it.parse() }, hitBox(), ) } return ModelRenderer( name, type, elements.toBoneMap { it.parse() }, animations ) } } override fun start() { } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { itemModelNamespace = PlatformNamespace(CONFIG.namespace(), CONFIG.itemNamespace()) generalModelMap.clear() playerModelMap.clear() loadModels(pipeline, zipper) } override fun model(name: String): ModelRenderer? = generalModelView[name] override fun models(): Collection = generalModelView.values override fun modelKeys(): Set = generalModelView.keys override fun limb(name: String): ModelRenderer? = playerModelView[name] override fun limbs(): Collection = playerModelView.values override fun limbKeys(): Set = playerModelView.keys } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/ProfileManagerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import com.google.gson.GsonBuilder import com.google.gson.annotations.SerializedName import kr.toxicity.model.api.manager.ProfileManager import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.profile.ModelProfileSkin import kr.toxicity.model.api.profile.ModelProfileSupplier import kr.toxicity.model.profile.DefaultHttpModelProfileSupplier import kr.toxicity.model.profile.HttpModelProfileSupplier import kr.toxicity.model.util.PLATFORM import java.net.URI import java.util.* object ProfileManagerImpl : ProfileManager, GlobalManager { private val gson = GsonBuilder().create() private lateinit var supplier: ModelProfileSupplier override fun supplier(): ModelProfileSupplier = supplier override fun supplier(supplier: ModelProfileSupplier) { this.supplier = supplier } override fun skin(rawTextures: String): ModelProfileSkin { return gson.fromJson(Base64.getDecoder().decode(rawTextures).toString(Charsets.UTF_8), Profile::class.java).run { ModelProfileSkin( textures.skin?.toURI(), textures.cape?.toURI(), textures.skin?.metadata?.slim == true, rawTextures ) } } private data class Profile( val textures: ProfileTextures ) private data class ProfileTextures( @SerializedName("SKIN") val skin: ProfileSkin?, @SerializedName("CAPE") val cape: ProfileSkin?, ) private data class ProfileSkin( val url: String, val metadata: ProfileMetadata ) { fun toURI(): URI = URI.create(url) } private data class ProfileMetadata( val model: String ) { val slim get() = model == "slim" } override fun start() { supplier = if (PLATFORM.nms().isProxyOnlineMode) DefaultHttpModelProfileSupplier() else HttpModelProfileSupplier() } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/ReloadPipeline.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import kr.toxicity.model.manager.debug.ReloadIndicator import kr.toxicity.model.util.PLATFORM import kr.toxicity.model.util.parallelIOThreadPool import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger class ReloadPipeline( private val indicators: List ) : AutoCloseable { var status = "Starting..." private val pool = parallelIOThreadPool() private val current = AtomicInteger() var goal = 0 set(value) { field = value current.set(0) } fun progress() = current.incrementAndGet() fun forEachParallel(list: List, sizeAssume: (T) -> Long, block: (T) -> Unit) { pool.forEachParallel(list, sizeAssume, block) } fun mapParallel(list: List, sizeAssume: (T) -> Long, block: (T) -> R?): List { return CopyOnWriteArrayList().apply { forEachParallel(list, sizeAssume) { t: T -> block(t)?.let { add(it) } } } } private val task = PLATFORM.scheduler().asyncTaskTimer(1, 1) { current.get().run { Status( if (goal > 0) toFloat() / goal.toFloat() else 0F, this, goal, status ) }.run { indicators.forEach { it status this } } } data class Status( val progress: Float, val current: Int, val goal: Int, val status: String ) override fun close() { task.cancel() indicators.forEach(ReloadIndicator::close) pool.close() } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/ScriptManagerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import kr.toxicity.model.api.event.AnimationSignalEvent import kr.toxicity.model.api.manager.ScriptManager import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.script.ScriptBuilder import kr.toxicity.model.script.* import kr.toxicity.model.util.boneName import kr.toxicity.model.util.bonePredicate import java.util.regex.Matcher import java.util.regex.Pattern object ScriptManagerImpl : ScriptManager, GlobalManager { private val scriptMap = hashMapOf() private val scriptPattern = Pattern.compile("^(?[a-zA-Z]+)(:(?([\\w_\\-])+))?(\\{(?([\\w\\W])+)})?$") private val validatePattern = Pattern.compile("^[a-z]+$") init { addBuilder("signal") { val args = it.args() ?: return@addBuilder AnimationScript.EMPTY AnimationScript.of { tracker -> tracker.pipeline.allPlayer() .map { channel -> channel.player() } .forEach { player -> AnimationSignalEvent(player, args).call() } } } addBuilder("tint") { TintScript( it.metadata.bonePredicate, it.metadata.asNumber("color")?.toInt() ?: return@addBuilder AnimationScript.EMPTY, it.metadata.asBoolean("damage") == true ) } addBuilder("partvis") { PartVisibilityScript( it.metadata.bonePredicate, it.metadata.asBoolean("visible") ?: return@addBuilder AnimationScript.EMPTY, ) } addBuilder("partbright") { BrightnessScript( it.metadata.bonePredicate, it.metadata().asNumber("block")?.toInt() ?: return@addBuilder AnimationScript.EMPTY, it.metadata().asNumber("sky")?.toInt() ?: return@addBuilder AnimationScript.EMPTY, ) } addBuilder("enchant") { EnchantScript( it.metadata.bonePredicate, it.metadata.asBoolean("enchant") ?: return@addBuilder AnimationScript.EMPTY, ) } addBuilder("changepart") { ChangePartScript( it.metadata.bonePredicate, it.metadata.asString("nmodel") ?: return@addBuilder AnimationScript.EMPTY, it.metadata.asString("npart")?.boneName ?: return@addBuilder AnimationScript.EMPTY ) } addBuilder("remap") { RemapScript( it.metadata.asString("model") ?: return@addBuilder AnimationScript.EMPTY, it.metadata.asString("map") ) } } override fun build(script: String): AnimationScript? = script.toScript() override fun addBuilder(name: String, script: ScriptBuilder) { if (!validatePattern.matcher(name).find()) throw RuntimeException("name must be in [a-z]") scriptMap[name] = script } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { } private fun String.toScript(): AnimationScript? = scriptPattern.matcher(this) .takeIf(Matcher::find) ?.let { scriptMap[it.group("name").lowercase()]?.build(ScriptBuilder.ScriptData(it.group("argument"), ScriptMetaDataImpl(it.group("metadata") ?.split(';') ?.associate { pair -> pair.split('=', limit = 2).let { arr -> arr[0] to arr[1] } } ?: emptyMap() ) )) } private class ScriptMetaDataImpl( private val map: Map ) : ScriptBuilder.ScriptMetaData { override fun toMap(): Map = map } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/SkinManagerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.RemovalCause import it.unimi.dsi.fastutil.ints.IntList import kr.toxicity.library.armormodel.ArmorResource import kr.toxicity.library.dynamicuv.* import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.event.CreatePlayerSkinEvent import kr.toxicity.model.api.event.RemovePlayerSkinEvent import kr.toxicity.model.api.manager.SkinManager import kr.toxicity.model.api.pack.PackObfuscator import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.skin.SkinData import kr.toxicity.model.api.util.TransformedItemStack import kr.toxicity.model.util.* import org.joml.Vector3f import java.awt.image.BufferedImage import java.net.URI import java.net.http.HttpResponse import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import javax.imageio.ImageIO object SkinManagerImpl : SkinManager, GlobalManager { private const val DIV_FACTOR = 16F / 0.9375F private var uvNamespace = UVNamespace( CONFIG.namespace(), "player_limb" ) private val HEAD = UVModel( { uvNamespace }, "head" ).addElement( UVElement( ElementVector(8F, 8F, 8F).div(DIV_FACTOR), ElementVector(0f, 4F, 0f).div(DIV_FACTOR), UVSpace(8, 8, 8), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(8, 8), UVFace.SOUTH to UVPos(24, 8), UVFace.EAST to UVPos(0, 8), UVFace.WEST to UVPos(16, 8), UVFace.UP to UVPos(8, 0), UVFace.DOWN to UVPos(16, 0) ) ) ).addElement( UVElement( ElementVector(8F, 8F, 8F).div(DIV_FACTOR).inflate(0.5f), ElementVector(0f, 4F, 0f).div(DIV_FACTOR), UVSpace(8, 8, 8), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(8 + 32, 8), UVFace.SOUTH to UVPos(24 + 32, 8), UVFace.EAST to UVPos(32, 8), UVFace.WEST to UVPos(16 + 32, 8), UVFace.UP to UVPos(8 + 32, 0), UVFace.DOWN to UVPos(16 + 32, 0) ) ) ) private val CHEST = UVModel( { uvNamespace }, "chest" ).addElement( UVElement( ElementVector(8f, 4f, 4f).div(DIV_FACTOR), ElementVector(0f, 2f, 0f).div(DIV_FACTOR), UVSpace(8, 4, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(20, 20), UVFace.SOUTH to UVPos(32, 20), UVFace.EAST to UVPos(16, 20), UVFace.WEST to UVPos(28, 20), UVFace.UP to UVPos(20, 16) ) ) ).addElement( UVElement( ElementVector(8f, 4f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, 2f, 0f).div(DIV_FACTOR), UVSpace(8, 4, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(20, 20 + 16), UVFace.SOUTH to UVPos(32, 20 + 16), UVFace.EAST to UVPos(16, 20 + 16), UVFace.WEST to UVPos(28, 20 + 16), UVFace.UP to UVPos(20, 16 + 16) ) ) ) private val WAIST = UVModel( { uvNamespace }, "waist" ).addElement( UVElement( ElementVector(8f, 4f, 4f).div(DIV_FACTOR), ElementVector(0f, 2f, 0f).div(DIV_FACTOR), UVSpace(8, 4, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(20, 24), UVFace.SOUTH to UVPos(32, 24), UVFace.EAST to UVPos(16, 24), UVFace.WEST to UVPos(28, 24) ) ) ).addElement( UVElement( ElementVector(8f, 4f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, 2f, 0f).div(DIV_FACTOR), UVSpace(8, 4, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(20, 24 + 16), UVFace.SOUTH to UVPos(32, 24 + 16), UVFace.EAST to UVPos(16, 24 + 16), UVFace.WEST to UVPos(28, 24 + 16) ) ) ) private val HIP = UVModel( { uvNamespace }, "hip" ).addElement( UVElement( ElementVector(8f, 4f, 4f).div(DIV_FACTOR), ElementVector(0f, 2f, 0f).div(DIV_FACTOR), UVSpace(8, 4, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(20, 28), UVFace.SOUTH to UVPos(32, 28), UVFace.EAST to UVPos(16, 28), UVFace.WEST to UVPos(28, 28), UVFace.DOWN to UVPos(28, 16) ) ) ).addElement( UVElement( ElementVector(8f, 4f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, 2f, 0f).div(DIV_FACTOR), UVSpace(8, 4, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(20, 28 + 16), UVFace.SOUTH to UVPos(32, 28 + 16), UVFace.EAST to UVPos(16, 28 + 16), UVFace.WEST to UVPos(28, 28 + 16), UVFace.DOWN to UVPos(28, 16 + 16) ) ) ) private val LEFT_LEG = UVModel( { uvNamespace }, "left_leg" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(20, 52), UVFace.SOUTH to UVPos(28, 52), UVFace.EAST to UVPos(16, 52), UVFace.WEST to UVPos(24, 52), UVFace.UP to UVPos(20, 48) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(20 - 16, 52), UVFace.SOUTH to UVPos(28 - 16, 52), UVFace.EAST to UVPos(0, 52), UVFace.WEST to UVPos(24 - 16, 52), UVFace.UP to UVPos(20 - 16, 48) ) ) ) private val LEFT_FORELEG = UVModel( { uvNamespace }, "left_foreleg" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(20, 58), UVFace.SOUTH to UVPos(28, 58), UVFace.EAST to UVPos(16, 58), UVFace.WEST to UVPos(24, 58), UVFace.DOWN to UVPos(24, 48) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(20 - 16, 58), UVFace.SOUTH to UVPos(28 - 16, 58), UVFace.EAST to UVPos(0, 58), UVFace.WEST to UVPos(24 - 16, 58), UVFace.DOWN to UVPos(24 - 16, 48) ) ) ) private val RIGHT_LEG = UVModel( { uvNamespace }, "right_leg" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(4, 20), UVFace.SOUTH to UVPos(12, 20), UVFace.EAST to UVPos(0, 20), UVFace.WEST to UVPos(8, 20), UVFace.UP to UVPos(4, 16) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(4, 20 + 16), UVFace.SOUTH to UVPos(12, 20 + 16), UVFace.EAST to UVPos(0, 20 + 16), UVFace.WEST to UVPos(8, 20 + 16), UVFace.UP to UVPos(4, 16 + 16) ) ) ) private val RIGHT_FORELEG = UVModel( { uvNamespace }, "right_foreleg" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(4, 26), UVFace.SOUTH to UVPos(12, 26), UVFace.EAST to UVPos(0, 26), UVFace.WEST to UVPos(8, 26), UVFace.DOWN to UVPos(8, 16) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(4, 26 + 16), UVFace.SOUTH to UVPos(12, 26 + 16), UVFace.EAST to UVPos(0, 26 + 16), UVFace.WEST to UVPos(8, 26 + 16), UVFace.DOWN to UVPos(8, 16 + 16) ) ) ) private val LEFT_ARM = UVModel( { uvNamespace }, "left_arm" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(36, 52), UVFace.SOUTH to UVPos(44, 52), UVFace.EAST to UVPos(32, 52), UVFace.WEST to UVPos(40, 52), UVFace.UP to UVPos(36, 48) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(36 + 16, 52), UVFace.SOUTH to UVPos(44 + 16, 52), UVFace.EAST to UVPos(32 + 16, 52), UVFace.WEST to UVPos(40 + 16, 52), UVFace.UP to UVPos(36 + 16, 48) ) ) ) private val LEFT_FOREARM = UVModel( { uvNamespace }, "left_forearm" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(36, 58), UVFace.SOUTH to UVPos(44, 58), UVFace.EAST to UVPos(32, 58), UVFace.WEST to UVPos(40, 58), UVFace.DOWN to UVPos(40, 48) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(36 + 16, 58), UVFace.SOUTH to UVPos(44 + 16, 58), UVFace.EAST to UVPos(32 + 16, 58), UVFace.WEST to UVPos(40 + 16, 58), UVFace.DOWN to UVPos(40 + 16, 48) ) ) ) private val RIGHT_ARM = UVModel( { uvNamespace }, "right_arm" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(44, 20), UVFace.SOUTH to UVPos(52, 20), UVFace.EAST to UVPos(40, 20), UVFace.WEST to UVPos(48, 20), UVFace.UP to UVPos(44, 16) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(44, 20 + 16), UVFace.SOUTH to UVPos(52, 20 + 16), UVFace.EAST to UVPos(40, 20 + 16), UVFace.WEST to UVPos(48, 20 + 16), UVFace.UP to UVPos(44, 16 + 16) ) ) ) private val RIGHT_FOREARM = UVModel( { uvNamespace }, "right_forearm" ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(44, 26), UVFace.SOUTH to UVPos(52, 26), UVFace.EAST to UVPos(40, 26), UVFace.WEST to UVPos(48, 26), UVFace.DOWN to UVPos(48, 16) ) ) ).addElement( UVElement( ElementVector(4f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(4, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(44, 26 + 16), UVFace.SOUTH to UVPos(52, 26 + 16), UVFace.EAST to UVPos(40, 26 + 16), UVFace.WEST to UVPos(48, 26 + 16), UVFace.DOWN to UVPos(48, 16 + 16) ) ) ) private val SLIM_LEFT_ARM = UVModel( { uvNamespace }, "left_slim_arm" ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(36, 52), UVFace.SOUTH to UVPos(43, 52), UVFace.EAST to UVPos(32, 52), UVFace.WEST to UVPos(39, 52), UVFace.UP to UVPos(36, 48) ) ) ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(36 + 16, 52), UVFace.SOUTH to UVPos(43 + 16, 52), UVFace.EAST to UVPos(32 + 16, 52), UVFace.WEST to UVPos(39 + 16, 52), UVFace.UP to UVPos(36 + 16, 48) ) ) ) private val SLIM_LEFT_FOREARM = UVModel( { uvNamespace }, "left_slim_forearm" ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(36, 58), UVFace.SOUTH to UVPos(43, 58), UVFace.EAST to UVPos(32, 58), UVFace.WEST to UVPos(39, 58), UVFace.DOWN to UVPos(39, 48) ) ) ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(36 + 16, 58), UVFace.SOUTH to UVPos(43 + 16, 58), UVFace.EAST to UVPos(32 + 16, 58), UVFace.WEST to UVPos(39 + 16, 58), UVFace.DOWN to UVPos(39 + 16, 48) ) ) ) private val SLIM_RIGHT_ARM = UVModel( { uvNamespace }, "right_slim_arm" ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(44, 20), UVFace.SOUTH to UVPos(51, 20), UVFace.EAST to UVPos(40, 20), UVFace.WEST to UVPos(47, 20), UVFace.UP to UVPos(44, 16) ) ) ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(44, 20 + 16), UVFace.SOUTH to UVPos(51, 20 + 16), UVFace.EAST to UVPos(40, 20 + 16), UVFace.WEST to UVPos(47, 20 + 16), UVFace.UP to UVPos(44, 16 + 16) ) ) ) private val SLIM_RIGHT_FOREARM = UVModel( { uvNamespace }, "right_slim_forearm" ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(44, 26), UVFace.SOUTH to UVPos(51, 26), UVFace.EAST to UVPos(40, 26), UVFace.WEST to UVPos(47, 26), UVFace.DOWN to UVPos(47, 16) ) ) ).addElement( UVElement( ElementVector(3f, 6f, 4f).div(DIV_FACTOR).inflate(0.25f), ElementVector(0f, -3f, 0f).div(DIV_FACTOR), UVSpace(3, 6, 4), UVElement.ColorType.COMPLEX_ARGB, mapOf( UVFace.NORTH to UVPos(44, 26 + 16), UVFace.SOUTH to UVPos(51, 26 + 16), UVFace.EAST to UVPos(40, 26 + 16), UVFace.WEST to UVPos(47, 26 + 16), UVFace.DOWN to UVPos(47, 16 + 16) ) ) ) private val CAPE = UVModel( { uvNamespace }, "cape" ).addElement( UVElement( ElementVector(10f, 16f, 1f).div(DIV_FACTOR), ElementVector(0f, -8f, 0.5f).div(DIV_FACTOR), UVSpace(10, 16, 1), UVElement.ColorType.RGB, mapOf( UVFace.NORTH to UVPos(12, 1), UVFace.SOUTH to UVPos(1, 1), UVFace.EAST to UVPos(11, 1), UVFace.WEST to UVPos(0, 1), UVFace.UP to UVPos(1, 0), UVFace.DOWN to UVPos(11, 0) ) ) ) private data class SkinModelData( private val model: UVModel, val data: UVModelData ) { var namespace = model.itemModelNamespace() private set fun refresh() { namespace = model.itemModelNamespace() } } private fun UVModel.asModelData(image: BufferedImage): SkinModelData { val data = write(image) return SkinModelData( this, data ) } private val whiteList = IntList.of(*(0..8).map { 0xFFFFFF }.toIntArray()) private fun SkinModelData.asItem(colors: IntList = IntList.of()): TransformedItemStack = PLATFORM.nms().createSkinItem( namespace, data.floats, data.flags, emptyList(), colors + data.colors ) private fun SkinModelData.asItem(resource: ArmorResource, item: ArmorItem? = null): TransformedItemStack { if (item == null) return asItem(whiteList) val armorData = ArmorManager.armor.resource(resource).run { item.trim()?.let { customModelData(item.type, item.tint, it, item.palette?.let { p -> ArmorManager.armor.colors()[p] }) } ?: customModelData(item.type, item.tint) } return PLATFORM.nms().createSkinItem( namespace, data.floats, data.flags, armorData.strings, armorData.colors + data.colors ) } fun write(block: (UVByteBuilder) -> Unit) { val itemObf = PackObfuscator.order() val modelObf = PackObfuscator.order() fun UVModel.write(armorResource: ArmorResource? = null) { val model = modelName() packName(itemObf.obfuscate(model)) asJson(UVLoadContext( UVTextureName.DEFAULT, { modelObf.obfuscate("${model}_$it") }, { indexer, _, array -> armorResource?.let { ArmorManager.armor.resource(it) }?.let { array += it.toJson() indexer.shiftColor(9) } } )).forEach { block(it) } } HEAD.write(ArmorResource.HELMET) CHEST.write(ArmorResource.CHEST) WAIST.write(ArmorResource.WAIST) HIP.write(ArmorResource.HIP) LEFT_LEG.write(ArmorResource.LEFT_LEG) LEFT_FORELEG.write(ArmorResource.LEFT_FORELEG) RIGHT_LEG.write(ArmorResource.RIGHT_LEG) RIGHT_FORELEG.write(ArmorResource.RIGHT_FORELEG) LEFT_ARM.write(ArmorResource.LEFT_ARM) LEFT_FOREARM.write() RIGHT_ARM.write(ArmorResource.RIGHT_ARM) RIGHT_FOREARM.write() SLIM_LEFT_ARM.write(ArmorResource.LEFT_ARM) SLIM_LEFT_FOREARM.write() SLIM_RIGHT_ARM.write(ArmorResource.RIGHT_ARM) SLIM_RIGHT_FOREARM.write() CAPE.write() block(UVTextureName.DEFAULT.normalPixel(uvNamespace)) block(UVTextureName.DEFAULT.translucentPixel(uvNamespace)) } private val profileCache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .removalListener { key, value, cause -> if (cause == RemovalCause.EXPIRED && key != null && value != null) { handleExpiration(key, value) } } .build() private val fallback by lazy { PLATFORM.getResource("fallback_skin.png")!!.use { SkinDataImpl(ModelProfile.UNKNOWN, ImageIO.read(it), null) } } private fun handleExpiration(key: UUID, skin: SkinDataImpl) { skin.profile().let { if (!callEvent { RemovePlayerSkinEvent(it) } || it.playerEquals()) profileCache.put(key, skin) } } private fun ModelProfile.playerEquals() = player()?.let { player -> ModelProfile.of(player).info() } == info() override fun fallback(): SkinData = fallback override fun complete(profile: ModelProfile.Uncompleted): CompletableFuture { if (profile.info() == ModelProfileInfo.UNKNOWN) return CompletableFuture.completedFuture(fallback) return profileCache.getIfPresent(profile.info().id)?.let { CompletableFuture.completedFuture(it) } ?: profile.complete().thenApply { provided -> CreatePlayerSkinEvent(provided).run { call() modelProfile } }.thenComposeAsync compose@ { selected -> val skin = selected.skin().skin ?: return@compose CompletableFuture.completedFuture(fallback) val cape = selected.skin().cape httpClient { fun URI.toFuture() = sendAsync( buildHttpRequest { uri(this@toFuture) GET() }, HttpResponse.BodyHandlers.ofInputStream() ).thenComposeAsync { request -> CompletableFuture.supplyAsync { request.body().use { ImageIO.read(it) } } } skin.toFuture().thenCombine(cape?.toFuture() ?: CompletableFuture.completedFuture(null)) { skin, cape -> SkinDataImpl( selected, skin.convertLegacy(), cape ).apply { profileCache.put(profile.info().id, this) } } }.orElse { it.handleException("Unable to read this skin: ${selected.info().name}") CompletableFuture.completedFuture(fallback) } }.exceptionally { it.handleException("unable to read this skin: ${profile.info().name}") profileCache.invalidate(profile.info().id) null } } override fun removeCache(profile: ModelProfile) = profileCache.invalidate(profile.info().id) private fun BufferedImage.convertLegacy() = if (height == 64) this else BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB).also { newImage -> fun drawTo(from: UVPos, to: UVPos, xr: IntRange, zr: IntRange) { val maxX = xr.last + xr.first for (x in xr) { for (z in zr) { newImage.setRGB( to.x + maxX - x, to.z + z, getRGB(from.x + x, from.z + z) ) } } } fun drawTo(from: UVPos, to: UVPos) { drawTo(from, to, 0..<4, 4..<16) drawTo(from, to, 4..<8, 0..<16) drawTo(from, to, 8..<12, 0..<16) drawTo(from, to, 12..<16, 4..<16) } newImage.createGraphics().let { it.drawImage(this, 0, 0, null) it.dispose() } drawTo(UVPos(0, 16), UVPos(16, 48)) drawTo(UVPos(40, 16), UVPos(32, 48)) } private class SkinDataImpl( private val profile: ModelProfile, private val head: SkinModelData, private val hip: SkinModelData, private val waist: SkinModelData, private val chest: SkinModelData, private val leftArm: SkinModelData, private val rightArm: SkinModelData, private val leftLeg: SkinModelData, private val leftForeLeg: SkinModelData, private val rightLeg: SkinModelData, private val rightForeLeg: SkinModelData, private val leftForeArm: TransformedItemStack, private val rightForeArm: TransformedItemStack, private val cape: TransformedItemStack? ) : SkinData { constructor( profile: ModelProfile, skinImage: BufferedImage, capeImage: BufferedImage? ) : this( profile, HEAD.asModelData(skinImage), HIP.asModelData(skinImage), WAIST.asModelData(skinImage), CHEST.asModelData(skinImage), (if (profile.skin().slim) SLIM_LEFT_ARM else LEFT_ARM).asModelData(skinImage), (if (profile.skin().slim) SLIM_RIGHT_ARM else RIGHT_ARM).asModelData(skinImage) , LEFT_LEG.asModelData(skinImage), LEFT_FORELEG.asModelData(skinImage), RIGHT_LEG.asModelData(skinImage), RIGHT_FORELEG.asModelData(skinImage), (if (profile.skin().slim) SLIM_LEFT_FOREARM else LEFT_FOREARM).asModelData(skinImage).asItem(), (if (profile.skin().slim) SLIM_RIGHT_FOREARM else RIGHT_FOREARM).asModelData(skinImage).asItem(), capeImage?.let { CAPE.asModelData(it).asItem() } ) override fun profile(): ModelProfile = profile override fun head(armor: PlayerArmor): TransformedItemStack = head.asItem(ArmorResource.HELMET, armor.helmet()) override fun hip(armor: PlayerArmor): TransformedItemStack = hip.asItem(ArmorResource.HIP, armor.leggings()) override fun waist(armor: PlayerArmor): TransformedItemStack = waist.asItem(ArmorResource.WAIST, armor.chestplate()) override fun chest(armor: PlayerArmor): TransformedItemStack = chest.asItem(ArmorResource.CHEST, armor.chestplate()) override fun leftArm(armor: PlayerArmor): TransformedItemStack = leftArm.asItem(ArmorResource.LEFT_ARM, armor.chestplate()) override fun rightArm(armor: PlayerArmor): TransformedItemStack = rightArm.asItem(ArmorResource.RIGHT_ARM, armor.chestplate()) override fun leftLeg(armor: PlayerArmor): TransformedItemStack = leftLeg.asItem(ArmorResource.LEFT_LEG, armor.leggings()) override fun rightLeg(armor: PlayerArmor): TransformedItemStack = rightLeg.asItem(ArmorResource.RIGHT_LEG, armor.leggings()) override fun leftForeLeg(armor: PlayerArmor): TransformedItemStack = leftForeLeg.asItem(ArmorResource.LEFT_FORELEG, armor.boots()) override fun rightForeLeg(armor: PlayerArmor): TransformedItemStack = rightForeLeg.asItem(ArmorResource.RIGHT_FORELEG, armor.boots()) override fun leftForeArm(): TransformedItemStack = leftForeArm override fun rightForeArm(): TransformedItemStack = rightForeArm override fun cape(armor: PlayerArmor): TransformedItemStack? = if (armor.chestplate() != null) cape?.offset(Vector3f(0F, 0F, -1F / 16F)) else cape fun refresh() { head.refresh() hip.refresh() waist.refresh() chest.refresh() leftArm.refresh() rightArm.refresh() leftLeg.refresh() leftForeLeg.refresh() rightLeg.refresh() rightForeLeg.refresh() } } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { uvNamespace = UVNamespace( CONFIG.namespace(), "player_limb" ) if (!CONFIG.module().playerAnimation) return write { resource -> zipper.modern().add(resource.path(), resource.estimatedSize()) { resource.build() } } profileCache.asMap().entries.forEach { it.value.refresh() } } override fun end() { profileCache.cleanUp() } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/debug/BossBarIndicator.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager.debug import kr.toxicity.model.manager.ReloadPipeline import kr.toxicity.model.util.componentOf import kr.toxicity.model.util.emptyComponentOf import kr.toxicity.model.util.toComponent import kr.toxicity.model.util.withComma import net.kyori.adventure.audience.Audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.text.format.NamedTextColor class BossBarIndicator( private val audience: Audience ) : ReloadIndicator { private var showed = false private val bossBar by lazy { BossBar.bossBar( emptyComponentOf(), 0F, BossBar.Color.GREEN, BossBar.Overlay.PROGRESS ).apply { showed = true audience.showBossBar(this) } } override fun status(status: ReloadPipeline.Status) { bossBar.run { name(componentOf(status.status) { append(" (${status.current.withComma()} / ${status.goal.withComma()})".toComponent(NamedTextColor.YELLOW)) }) progress(status.progress) } } override fun close() { if (showed) audience.hideBossBar(bossBar) } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/manager/debug/ReloadIndicator.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.manager.debug import kr.toxicity.model.manager.ReloadPipeline interface ReloadIndicator { infix fun status(status: ReloadPipeline.Status) fun close() } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/profile/DefaultHttpModelProfileSupplier.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.profile import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSupplier import kr.toxicity.model.util.PLATFORM class DefaultHttpModelProfileSupplier : ModelProfileSupplier { private val http = HttpModelProfileSupplier() override fun supply(info: ModelProfileInfo): ModelProfile.Uncompleted { val player = PLATFORM.adapter().offlinePlayer(info.id) return if (player is PlatformPlayer) ModelProfile.of(player).asUncompleted() else http.supply(info) } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/profile/HttpModelProfileSupplier.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.profile import com.github.benmanes.caffeine.cache.Caffeine import com.google.gson.GsonBuilder import com.google.gson.JsonParser import com.mojang.authlib.properties.PropertyMap import com.mojang.util.UUIDTypeAdapter import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin import kr.toxicity.model.api.profile.ModelProfileSupplier import kr.toxicity.model.manager.ProfileManagerImpl import kr.toxicity.model.util.buildHttpRequest import kr.toxicity.model.util.handleException import kr.toxicity.model.util.httpClient import java.io.Reader import java.net.URI import java.net.http.HttpResponse import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit /** * This source file is part of BetterModel. * Copyright (c) 2024–2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ class HttpModelProfileSupplier : ModelProfileSupplier { private val profileCache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .build() private val serializer = GsonBuilder() .registerTypeAdapter(UUID::class.java, UUIDTypeAdapter()) .registerTypeAdapter(PropertyMap::class.java, PropertyMap.Serializer()) .create() private data class Profile( val id: UUID, val name: String, val properties: PropertyMap ) private fun read(reader: Reader) = serializer.fromJson(reader, Profile::class.java) override fun supply(info: ModelProfileInfo): ModelProfile.Uncompleted { return object : ModelProfile.Uncompleted { override fun info(): ModelProfileInfo = info override fun complete(): CompletableFuture { return profileCache.getIfPresent(info)?.let { CompletableFuture.completedFuture(it) } ?: httpClient { (info.name?.let { sendAsync( buildHttpRequest { GET() uri(URI.create("https://api.minecraftservices.com/minecraft/profile/lookup/name/${it}")) }, HttpResponse.BodyHandlers.ofInputStream() ).thenApply { body -> body.body().use { body -> body.reader().use(JsonParser::parseReader) }.asJsonObject .getAsJsonPrimitive("id") .asString } } ?: CompletableFuture.completedFuture(info.id.toString().replace("-", ""))).thenComposeAsync { sendAsync( buildHttpRequest { GET() uri(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/$it")) }, HttpResponse.BodyHandlers.ofInputStream() ) }.thenApplyAsync { it.body().use { body -> body.reader().use(::read) }.let { profile -> ModelProfile.of( ModelProfileInfo(profile.id, profile.name), profile.properties["textures"].firstOrNull()?.let { property -> ProfileManagerImpl.skin(property.value) } ?: ModelProfileSkin.EMPTY ).apply { profileCache.put(info, this) } } }.exceptionally { it.handleException("Unable to get ${info.name}'s skin data.") fallback() } }.orElse { it.handleException("Unable to get ${info.name}'s user data.") CompletableFuture.completedFuture(fallback()) } } } } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/script/BrightnessScript.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.script import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.function.BonePredicate class BrightnessScript( val predicate: BonePredicate, val block: Int, val sky: Int ) : AnimationScript { override fun accept(tracker: Tracker) { tracker.update( TrackerUpdateAction.brightness(block, sky), predicate ) } override fun isSync(): Boolean = false } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/script/ChangePartScript.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.script import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneName import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.function.BonePredicate class ChangePartScript( val predicate: BonePredicate, newModel: String, newPart: BoneName ) : AnimationScript { private val model by lazy { BetterModel.modelOrNull(newModel)?.groupByTree(newPart)?.itemStack } override fun accept(tracker: Tracker) { model?.let { tracker.update( TrackerUpdateAction.itemStack(it), predicate ) } } override fun isSync(): Boolean = false } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/script/EnchantScript.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.script import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.function.BonePredicate class EnchantScript( val predicate: BonePredicate, val enchant: Boolean ) : AnimationScript { override fun accept(tracker: Tracker) { tracker.update( TrackerUpdateAction.enchant(enchant), predicate ) } override fun isSync(): Boolean = false } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/script/PartVisibilityScript.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.script import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.function.BonePredicate class PartVisibilityScript( val predicate: BonePredicate, val visible: Boolean ) : AnimationScript { override fun accept(tracker: Tracker) { tracker.update( TrackerUpdateAction.togglePart(visible), predicate ) } override fun isSync(): Boolean = false } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/script/RemapScript.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.script import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.util.toPackName import kr.toxicity.model.util.toSet class RemapScript( model: String, map: String? ) : AnimationScript { private val newModel by lazy { model.toPackName().let { BetterModel.modelOrNull(it) } } private val filter by lazy { map?.let { BetterModel.modelOrNull(it.toPackName())?.flatten()?.map { group -> group.name() }?.toSet() } } override fun accept(tracker: Tracker) { val f = filter newModel?.run { tracker.update(TrackerUpdateAction.perBone { (if (f == null || f.contains(it.name())) { groupByTree(it.name())?.itemStack?.let { item -> TrackerUpdateAction.itemStack(item) } } else null) ?: TrackerUpdateAction.none() }) } } override fun isSync(): Boolean = false } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/script/TintScript.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.script import kr.toxicity.model.api.script.AnimationScript import kr.toxicity.model.api.tracker.EntityTracker import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.function.BonePredicate class TintScript( val predicate: BonePredicate, val color: Int, val damageTint: Boolean ) : AnimationScript { override fun accept(tracker: Tracker) { if (damageTint && tracker is EntityTracker) { tracker.damageTintValue(color) } else { if (tracker is EntityTracker) tracker.cancelDamageTint() tracker.update( TrackerUpdateAction.tint(color), predicate ) } } override fun isSync(): Boolean = false } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Buffers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import com.google.gson.JsonElement import kr.toxicity.model.api.data.blueprint.BlueprintImage import kr.toxicity.model.api.data.raw.ModelData import java.io.ByteArrayOutputStream import java.io.OutputStreamWriter import java.nio.charset.StandardCharsets private val IO_BUFFER = ThreadLocal.withInitial { ByteArrayOutputStream(1024) } fun BlueprintImage.toByteArray(): ByteArray { return image } fun JsonElement.toByteArray(): ByteArray { return IO_BUFFER.get().let { buffer -> buffer.reset() OutputStreamWriter(buffer, StandardCharsets.UTF_8).use { ModelData.GSON.toJson(this, it) } buffer.toByteArray() } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Collections.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import it.unimi.dsi.fastutil.objects.* import kr.toxicity.model.api.util.CollectionUtil import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger import java.util.stream.Collectors import java.util.stream.Stream fun addressingMapOf() = CollectionUtil.newAddressingMap() fun sequencedAddressingMapOf() = CollectionUtil.newSequencedAddressingMap() fun addressingMapOf(capacity: Int) = CollectionUtil.newAddressingMap(capacity) fun sequencedAddressingMapOf(capacity: Int) = CollectionUtil.newSequencedAddressingMap(capacity) fun emptySequencedMap(): SequencedMap = Collections.emptyNavigableMap() fun MutableMap.toImmutableView(): Map = when (this) { is Object2ObjectMap -> Object2ObjectMaps.unmodifiable(this) is Object2ReferenceMap -> Object2ReferenceMaps.unmodifiable(this) is Reference2ObjectMap -> Reference2ObjectMaps.unmodifiable(this) is Reference2ReferenceMap -> Reference2ReferenceMaps.unmodifiable(this) else -> Collections.unmodifiableMap(this) } fun SequencedMap.toImmutableView(): SequencedMap = when (this) { is Object2ObjectSortedMap -> Object2ObjectSortedMaps.unmodifiable(this) is Object2ReferenceSortedMap -> Object2ReferenceSortedMaps.unmodifiable(this) is Reference2ObjectSortedMap -> Reference2ObjectSortedMaps.unmodifiable(this) is Reference2ReferenceSortedMap -> Reference2ReferenceSortedMaps.unmodifiable(this) else -> Collections.unmodifiableSequencedMap(this) } fun Stream.toSet(): Set = collect(Collectors.toUnmodifiableSet()) fun Stream.toMutableSet(): MutableSet = collect(Collectors.toSet()) fun parallelIOThreadPool() = try { ParallelIOThreadPool() } catch (error: OutOfMemoryError) { throw RuntimeException("You have to set your Linux max thread limit!", error) } class ParallelIOThreadPool : AutoCloseable { private val available = Runtime.getRuntime().availableProcessors() * 2 private val integer = AtomicInteger() private val pool = Executors.newFixedThreadPool(available) { Thread(it).apply { isDaemon = true name = "BetterModel-IO-Worker-${integer.andIncrement}" uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { thread, exception -> exception.handleException("A error has been occurred in ${thread.name}") } } } override fun close() { pool.close() } fun forEachParallel(list: List, sizeAssume: (T) -> Long, block: (T) -> Unit) { if (list.isEmpty()) return val size = list.size val lastIndex = list.lastIndex val tasks = if (available >= size) { list.map { { block(it) } } } else { val sorted = list.sortedBy(sizeAssume) val queue = arrayListOf<() -> Unit>() var i = 0 val add = (size.toDouble() / available).toInt() while (i <= size) { val list = ArrayList(add) for (t in i..<(i + add).coerceAtMost(size)) { val ht = t / 2 list += sorted[if (t % 2 == 0) ht else lastIndex - ht] } queue += { list.forEach(block) } i += add } queue } CompletableFuture.allOf( *tasks.map { CompletableFuture.runAsync({ it() }, pool) }.toTypedArray() ).join() } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Events.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import kr.toxicity.model.api.event.ModelEvent import kr.toxicity.model.api.util.EventUtil inline fun callEvent(noinline block: () -> T): Boolean = EventUtil.call(T::class.java) { block() }.triggered() ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Files.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import java.awt.image.BufferedImage import java.io.File import java.io.InputStream import java.nio.file.Files import java.nio.file.Path import java.util.stream.Stream import javax.imageio.ImageIO inline fun File.getOrCreateDirectory(name: String, initialConsumer: (File) -> Unit = {}) = File(this, name).also { target -> if (!target.exists()) { target.mkdirs() initialConsumer(target) } } fun File.subFiles(): List = listFiles()?.toList() ?: emptyList() inline fun copyResourceAs(name: String, block: (InputStream) -> Unit) { PLATFORM.getResource(name)?.use(block) } fun File.toImage(): BufferedImage = ImageIO.read(this) fun File.fileTrees(): Stream = Files.find( toPath(), Int.MAX_VALUE, { _, attr -> !attr.isDirectory } ) fun File.addResource(name: String) { copyResourceAs(name) { input -> File(this, name).outputStream().use { it.buffered().use { output -> input.copyTo(output) } } } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import kr.toxicity.model.api.bone.BoneName import java.math.BigDecimal import java.text.DecimalFormat val BYTE_UNIT = BigDecimal("1024.000") val COMMA_FORMAT = DecimalFormat("#,###") val COMMA_DECIMAL_FORMAT = DecimalFormat("#,###.000") inline fun T?.ifNull(lazyMessage: () -> String): T & Any = this ?: throw RuntimeException(lazyMessage()) fun Number.withComma(): String = COMMA_FORMAT.format(this) val String.boneName get() = BoneName.of(this) fun Long.toByteFormat(): String { var value = BigDecimal("$this.000") for (format in LengthFormat.entries) { if (value < BYTE_UNIT) return "${COMMA_DECIMAL_FORMAT.format(value)} ${format.name}" value /= BYTE_UNIT } return "${COMMA_DECIMAL_FORMAT.format(value)} ${LengthFormat.entries.last().name}" } enum class LengthFormat { B, KB, MB, GB } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Gsons.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2024 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import kr.toxicity.model.api.data.ModelAsset import kr.toxicity.model.api.data.blueprint.ModelBlueprint fun ModelAsset.toTexturedModel(): ModelBlueprint? = runCatching { toResult().let { result -> if (result.errors.isNotEmpty()) warn( *buildList { add("Error has been occurred while parsing this model: ${result.blueprint.name}") addAll(result.errors) }.map { error -> error.toComponent() }.toTypedArray() ) result.blueprint } }.handleFailure { "Unable to load this model: $name" }.getOrNull() fun buildJsonArray(capacity: Int = 10, block: JsonArray.() -> Unit) = JsonArray(capacity).apply(block) fun buildJsonObject(block: JsonObject.() -> Unit) = JsonObject().apply(block) fun jsonArrayOf(vararg element: Any?) = buildJsonArray { element.filterNotNull().forEach { add(it.toJsonElement()) } } fun jsonObjectOf(vararg element: Pair) = buildJsonObject { element.forEach { add(it.first, it.second.toJsonElement()) } } operator fun JsonArray.plusAssign(other: JsonElement) { add(other) } fun Any.toJsonElement(): JsonElement = when (this) { is String -> JsonPrimitive(this) is Char -> JsonPrimitive(this) is Number -> JsonPrimitive(this) is Boolean -> JsonPrimitive(this) is JsonElement -> this is List<*> -> run { val map = mapNotNull { it?.toJsonElement() } buildJsonArray(map.size) { map.forEach { add(it) } } } is Map<*, *> -> buildJsonObject { forEach { add(it.key?.toString() ?: return@forEach, it.value?.toJsonElement() ?: return@forEach) } } else -> throw RuntimeException("Unsupported type: ${javaClass.name}") } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Indicators.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import kr.toxicity.model.api.config.IndicatorConfig import kr.toxicity.model.api.manager.ReloadInfo import kr.toxicity.model.manager.debug.BossBarIndicator import kr.toxicity.model.manager.debug.ReloadIndicator import java.util.* private typealias Type = IndicatorConfig.IndicatorOption private val INDICATOR_MAP = EnumMap ReloadIndicator?>(Type::class.java).apply { put(Type.PROGRESS_BAR) { BossBarIndicator(it.sender) } } fun Type.toIndicator(info: ReloadInfo) = INDICATOR_MAP[this]?.invoke(info) fun Iterable.toIndicator(info: ReloadInfo) = mapNotNull { it.toIndicator(info) } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Packs.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import kr.toxicity.model.api.BetterModelConfig import kr.toxicity.model.api.BetterModelConfig.PackType.* import kr.toxicity.model.api.pack.* import kr.toxicity.model.manager.ReloadPipeline import net.kyori.adventure.text.format.NamedTextColor import java.io.File import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.io.path.pathString fun BetterModelConfig.PackType.toGenerator() = when (this) { FOLDER -> FolderGenerator() ZIP -> ZipGenerator() NONE -> NoneGenerator() } interface PackGenerator { val exists: Boolean fun create(zipper: PackZipper, pipeline: ReloadPipeline): PackResult fun hashEquals(result: PackResult): Boolean { val hash = result.hash().toString() return File(DATA_FOLDER.getOrCreateDirectory(".cache"), "zip-hash.txt").run { if (!exists || !exists() || readText() != hash) { writeText(hash) true } else false } } } class FolderGenerator : PackGenerator { private val file = File(DATA_FOLDER.parent, CONFIG.buildFolderLocation()) override val exists: Boolean = file.exists() private val fileTree by lazy { sortedMapOf(reverseOrder()).apply { val after = CONFIG.buildFolderLocation() + File.separatorChar Files.walk(file.apply { mkdirs() }.toPath()).use { stream -> stream.forEach { put(it.pathString.substringAfter(after), it) } } } } private fun PackPath.toFile(): File { val replaced = path.replace('/', File.separatorChar) return synchronized(fileTree) { fileTree.remove(replaced)?.toFile() } ?: File(file, replaced).apply { parentFile.mkdirs() } } override fun create(zipper: PackZipper, pipeline: ReloadPipeline): PackResult { val build = zipper.build() val pack = PackResult(build.meta(), file) val changed = AtomicBoolean() pipeline.forEachParallel(build.resources(), PackResource::estimatedSize) { val bytes = it.get() pack[it.overlay()] = PackByte(it.path(), bytes) val file = it.path().toFile() val index = pipeline.progress() if (file.length() != bytes.size.toLong()) { file.writeBytes(bytes) changed.set(true) debugPack { componentOf( "This file was successfully generated: ".toComponent(), it.path().path.toComponent(NamedTextColor.GREEN), " ($index/${pipeline.goal})".toComponent(NamedTextColor.DARK_GRAY) ) } } } fileTree.values.forEach { it.toFile().delete() } return pack.apply { freeze(changed.get()) } } } class ZipGenerator : PackGenerator { private val file = File(DATA_FOLDER.parent, "${CONFIG.buildFolderLocation()}.zip") override val exists: Boolean = file.exists() override fun create(zipper: PackZipper, pipeline: ReloadPipeline): PackResult { return zipper.writeToResult(pipeline, file).apply { freeze(hashEquals(this)) }.apply { if (!changed()) return this fun zip(zip: ZipOutputStream) { zip.setLevel(Deflater.BEST_COMPRESSION) zip.setComment("BetterModel's generated resource pack.") stream().forEach { zip.putNextEntry(ZipEntry(it.path().path())) zip.write(it.bytes()) zip.closeEntry() } } file.outputStream().use { it.buffered().use { buffered -> ZipOutputStream(buffered).use(::zip) } } } } } class NoneGenerator : PackGenerator { override val exists: Boolean = false override fun create(zipper: PackZipper, pipeline: ReloadPipeline): PackResult { return zipper.writeToResult(pipeline).apply { freeze() } } } fun PackZipper.writeToResult(pipeline: ReloadPipeline, dir: File? = null): PackResult { val build = build() return PackResult(build.meta(), dir).apply { pipeline.forEachParallel(build.resources(), PackResource::estimatedSize) { set(it.overlay(), PackByte(it.path(), it.get())) val index = pipeline.progress() debugPack { componentOf( "This file was successfully zipped: ".toComponent(), it.path().path.toComponent(NamedTextColor.GREEN), " ($index/${pipeline.goal})".toComponent(NamedTextColor.DARK_GRAY) ) } } } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Platforms.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import com.github.benmanes.caffeine.cache.Caffeine import com.google.gson.GsonBuilder import kr.toxicity.model.BetterModelPlatformImpl import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.util.HttpUtil import kr.toxicity.model.api.util.LogUtil import kr.toxicity.model.api.util.PackUtil import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor import java.io.InputStream import java.io.InputStreamReader import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.charset.StandardCharsets import java.util.concurrent.TimeUnit val PLATFORM get() = BetterModel.platform() as BetterModelPlatformImpl val CONFIG get() = BetterModel.config() val DATA_FOLDER get() = PLATFORM.dataFolder() private val LATEST_VERSION_CACHE = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .build { HttpUtil.versionList() } private val GSON = GsonBuilder().disableHtmlEscaping().create() val LATEST_VERSION: HttpUtil.LatestVersion get() = LATEST_VERSION_CACHE.get(Unit) fun info(vararg message: Component) = PLATFORM.logger().info(*message) fun warn(vararg message: Component) = PLATFORM.logger().warn(*message) inline fun debugPack(lazyMessage: () -> Component) { if (CONFIG.debug().has(DebugConfig.DebugOption.PACK)) info(componentOf( "[${Thread.currentThread().name}] ".toComponent(NamedTextColor.YELLOW), lazyMessage() )) } fun Throwable.handleException(message: String) = LogUtil.handleException(message, this) inline fun Result.handleFailure(lazyMessage: () -> String) = onFailure { it.handleException(lazyMessage()) } fun String.toPackName() = PackUtil.toPackName(this) fun httpClient(block: HttpClient.() -> T): HttpUtil.Result = HttpUtil.client { it.block() } fun buildHttpRequest(builder: HttpRequest.Builder.() -> Unit): HttpRequest = HttpRequest.newBuilder().apply(builder).build() fun HttpResponse.toJson(clazz: Class): T = body().use { InputStreamReader(it, StandardCharsets.UTF_8).use { reader -> GSON.fromJson(reader, clazz) } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Scripts.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import kr.toxicity.model.api.script.ScriptBuilder import kr.toxicity.model.api.util.function.BonePredicate val ScriptBuilder.ScriptMetaData.bonePredicate get(): BonePredicate { val match = asBoolean("exact") != false val children = asBoolean("children") == true val part = asString("part")?.boneName?.name return if (part == null) BonePredicate.TRUE else { BonePredicate.of(if (children) BonePredicate.State.TRUE else BonePredicate.State.FALSE, if (match) { { b -> b.name().name == part } } else { { b -> b.name().name.contains(part, ignoreCase = true) } }) } } ================================================ FILE: core/src/main/kotlin/kr/toxicity/model/util/Senders.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.util import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import net.kyori.adventure.text.ComponentLike import net.kyori.adventure.text.TextComponent import net.kyori.adventure.text.event.HoverEvent import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextColor import net.kyori.adventure.text.format.TextDecoration val INFO = " [!] ".toComponent { decorate(TextDecoration.BOLD).color(NamedTextColor.GREEN) } val WARN = " [!] ".toComponent { decorate(TextDecoration.BOLD).color(NamedTextColor.RED) } inline fun String.toComponent(builder: TextComponent.Builder.() -> TextComponent.Builder = { this }) = componentOf(this, builder) fun String.toComponent(color: TextColor) = componentOf(this) { color(color) } fun componentOf() = Component.text() fun spaceComponentOf() = Component.space() fun emptyComponentOf() = Component.empty() fun lineComponentOf() = Component.newline() fun componentOf(vararg like: ComponentLike) = componentOf { append(*like) } fun componentWithLineOf(vararg like: ComponentLike) = componentOf { like.forEachIndexed { i, l -> append(l) if (i < like.lastIndex) append(lineComponentOf()) } this } inline fun componentOf(content: String, builder: TextComponent.Builder.() -> TextComponent.Builder) = componentOf { content(content).let(builder) } inline fun componentOf(builder: TextComponent.Builder.() -> TextComponent.Builder) = componentOf().let(builder).build() fun ComponentLike.toHoverEvent() = HoverEvent.showText(this) fun Audience.info(message: String) = info(message.toComponent()) fun Audience.warn(message: String) = warn(message.toComponent()) fun Audience.infoNotNull(vararg messages: ComponentLike?): Unit = info(*messages.filterNotNull().ifEmpty { return }.toTypedArray()) fun Audience.info(vararg messages: ComponentLike) = sendMessage(componentWithLineOf(*messages.map { componentOf(INFO, it) }.toTypedArray())) fun Audience.warn(vararg messages: ComponentLike) = sendMessage(componentWithLineOf(*messages.map { componentOf(WARN, it) }.toTypedArray())) fun Audience.info(message: ComponentLike) = sendMessage(componentOf(INFO, message)) fun Audience.warn(message: ComponentLike) = sendMessage(componentOf(WARN, message)) ================================================ FILE: core/src/main/resources/blue_wizard.bbmodel ================================================ {"meta":{"format_version":"5.0","model_format":"free","box_uv":false},"name":"blue_wizard","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":128,"height":128},"elements":[{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.66264,27.39,-1.19375],"to":[0.53736,28.51,0.84625],"autouv":0,"color":1,"origin":[-0.06264,28.19,-0.39375],"faces":{"north":{"uv":[62,33,64,34],"texture":0},"east":{"uv":[24,11,27,12],"texture":0},"south":{"uv":[62,36,64,37],"texture":0},"west":{"uv":[36,10,39,11],"texture":0},"up":{"uv":[15,20,13,17],"texture":0},"down":{"uv":[49,41,47,44],"texture":0}},"type":"cube","uuid":"3ba71253-feb2-8e7b-4548-01cd3e1df6ab"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.15736,27.63,-1.21375],"to":[2.15736,28.83,0.86625],"autouv":0,"color":1,"rotation":[0,0,22.5],"origin":[0.95736,28.43,-0.39375],"faces":{"north":{"uv":[50,29,53,31],"texture":0},"east":{"uv":[50,31,53,33],"texture":0},"south":{"uv":[32,50,35,52],"texture":0},"west":{"uv":[50,33,53,35],"texture":0},"up":{"uv":[3,44,0,41],"texture":0},"down":{"uv":[44,6,41,9],"texture":0}},"type":"cube","uuid":"d4d3e984-46f1-f6da-5c1f-cae44bfd11b3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.28264,27.63,-1.21375],"to":[-0.28264,28.83,0.86625],"autouv":0,"color":1,"rotation":[0,0,-22.5],"origin":[-1.08264,28.43,-0.39375],"faces":{"north":{"uv":[51,6,54,8],"texture":0},"east":{"uv":[10,51,13,53],"texture":0},"south":{"uv":[51,14,54,16],"texture":0},"west":{"uv":[51,16,54,18],"texture":0},"up":{"uv":[44,17,41,14],"texture":0},"down":{"uv":[44,17,41,20],"texture":0}},"type":"cube","uuid":"0f36a065-bbfb-34aa-9cd2-94c66b683a68"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.55736,28.47,-1.21375],"to":[3.19736,29.67,0.86625],"autouv":0,"color":1,"rotation":[0,0,45],"origin":[2.35736,29.27,-0.39375],"faces":{"north":{"uv":[57,4,59,6],"texture":0},"east":{"uv":[35,50,38,52],"texture":0},"south":{"uv":[8,57,10,59],"texture":0},"west":{"uv":[50,35,53,37],"texture":0},"up":{"uv":[52,40,50,37],"texture":0},"down":{"uv":[45,50,43,53],"texture":0}},"type":"cube","uuid":"de0f931c-847a-9947-b487-f6a601951d69"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.30264,28.47,-1.21375],"to":[-1.68264,29.67,0.86625],"autouv":0,"color":1,"rotation":[0,0,-45],"origin":[-2.48264,29.27,-0.39375],"faces":{"north":{"uv":[57,39,59,41],"texture":0},"east":{"uv":[45,50,48,52],"texture":0},"south":{"uv":[57,41,59,43],"texture":0},"west":{"uv":[48,50,51,52],"texture":0},"up":{"uv":[2,54,0,51],"texture":0},"down":{"uv":[10,51,8,54],"texture":0}},"type":"cube","uuid":"72a8e5fb-7b1b-994b-fc27-349d355d138b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.64264,29.29,-1.19375],"to":[-2.82264,32.91,0.40625],"autouv":0,"color":1,"origin":[2.89736,29.71,-0.39375],"faces":{"north":{"uv":[21,54,22,59],"texture":0},"east":{"uv":[34,6,36,11],"texture":0},"south":{"uv":[22,54,23,59],"texture":0},"west":{"uv":[8,34,10,39],"texture":0},"up":{"uv":[15,14,14,12],"texture":0},"down":{"uv":[13,21,12,23],"texture":0}},"type":"cube","uuid":"fdcdff03-57c1-e071-5769-588fbc314386"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.80264,28.91,0.40625],"to":[2.69736,32.91,1.20625],"autouv":0,"color":1,"origin":[2.89736,29.71,-0.39375],"faces":{"north":{"uv":[0,0,7,5],"texture":0},"east":{"uv":[2,55,3,60],"texture":0},"south":{"uv":[0,5,7,10],"texture":0},"west":{"uv":[3,55,4,60],"texture":0},"up":{"uv":[31,48,24,47],"texture":0},"down":{"uv":[54,28,47,29],"texture":0}},"type":"cube","uuid":"3072f681-6e4b-aef4-27b4-6990650a7f1b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.00264,28.71,-0.21375],"to":[1.89736,29.69,0.64625],"autouv":0,"color":1,"rotation":[22.5,0,0],"origin":[2.89736,27.89,-1.01375],"faces":{"north":{"uv":[55,25,60,26],"texture":0},"east":{"uv":[36,65,37,66],"texture":0},"south":{"uv":[26,55,31,56],"texture":0},"west":{"uv":[37,65,38,66],"texture":0},"up":{"uv":[60,27,55,26],"texture":0},"down":{"uv":[60,27,55,28],"texture":0}},"type":"cube","uuid":"9a276e96-6c77-ddd3-368c-f181c5dfb929"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.00264,29.71,0.40625],"to":[1.89736,32.91,1.20625],"autouv":0,"color":1,"origin":[2.89736,29.71,-1.19375],"faces":{"north":{"uv":[8,17,13,21],"texture":0},"east":{"uv":[13,6,14,10],"texture":0},"south":{"uv":[18,5,23,9],"texture":0},"west":{"uv":[44,56,45,60],"texture":0},"up":{"uv":[60,24,55,23],"texture":0},"down":{"uv":[60,24,55,25],"texture":0}},"type":"cube","uuid":"1812dc1c-20ad-522f-4394-729108bec75e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.55736,29.83,-1.59375],"to":[3.53736,31.55,0.84625],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[2.07736,29.15,-0.39375],"faces":{"north":{"uv":[21,50,22,52],"texture":0},"east":{"uv":[13,48,16,50],"texture":0},"south":{"uv":[38,50,39,52],"texture":0},"west":{"uv":[48,14,51,16],"texture":0},"up":{"uv":[12,63,11,60],"texture":0},"down":{"uv":[13,60,12,63],"texture":0}},"type":"cube","uuid":"c801537a-5f55-042c-3435-e557b199c416"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.41736,28.91,0.58625],"to":[1.95736,32.91,1.44625],"autouv":0,"color":1,"rotation":[0,45,0],"origin":[1.77736,29.71,-0.21375],"faces":{"north":{"uv":[4,55,5,60],"texture":0},"east":{"uv":[5,55,6,60],"texture":0},"south":{"uv":[55,9,56,14],"texture":0},"west":{"uv":[10,55,11,60],"texture":0},"up":{"uv":[34,66,33,65],"texture":0},"down":{"uv":[66,34,65,35],"texture":0}},"type":"cube","uuid":"cc3612bb-4f96-dc1e-46ad-d21b86d4df71"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.59736,31.79,-1.79375],"to":[4.37736,32.89,0.60625],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[2.89736,32.11,-0.39375],"faces":{"north":{"uv":[62,39,64,40],"texture":0},"east":{"uv":[39,32,42,33],"texture":0},"south":{"uv":[62,40,64,41],"texture":0},"west":{"uv":[40,3,43,4],"texture":0},"up":{"uv":[49,50,47,47],"texture":0},"down":{"uv":[2,48,0,51],"texture":0}},"type":"cube","uuid":"83e3beb0-d1dd-6e57-57ce-dee9c9bdd8b9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.06264,28.91,0.58625],"to":[-1.54264,32.91,1.44625],"autouv":0,"color":1,"rotation":[0,-45,0],"origin":[-1.90264,29.71,-0.21375],"faces":{"north":{"uv":[11,55,12,60],"texture":0},"east":{"uv":[12,55,13,60],"texture":0},"south":{"uv":[13,55,14,60],"texture":0},"west":{"uv":[14,55,15,60],"texture":0},"up":{"uv":[36,66,35,65],"texture":0},"down":{"uv":[66,35,65,36],"texture":0}},"type":"cube","uuid":"2d2daeae-d4e6-d6a9-d0d7-ac5a204040fe"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.62264,30.45,-1.59375],"to":[-2.74264,32.51,0.40625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[-2.22264,30.11,-0.39375],"faces":{"north":{"uv":[60,40,61,43],"texture":0},"east":{"uv":[31,40,34,43],"texture":0},"south":{"uv":[60,43,61,46],"texture":0},"west":{"uv":[34,40,37,43],"texture":0},"up":{"uv":[45,63,44,60],"texture":0},"down":{"uv":[52,60,51,63],"texture":0}},"type":"cube","uuid":"7f7169fb-d626-e57e-2176-8ade273cc9f4"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.42264,30.65,-1.79375],"to":[-2.72264,32.13,0.58625],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[-3.02264,32.11,-0.39375],"faces":{"north":{"uv":[0,57,2,59],"texture":0},"east":{"uv":[13,50,16,52],"texture":0},"south":{"uv":[57,2,59,4],"texture":0},"west":{"uv":[16,50,19,52],"texture":0},"up":{"uv":[21,53,19,50],"texture":0},"down":{"uv":[26,50,24,53],"texture":0}},"type":"cube","uuid":"d47bc743-9279-3d45-6344-f7035899232d"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.39736,32.11,-1.99375],"to":[3.55736,35.09,0.60625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[1.74736,32.91,-0.69375],"faces":{"north":{"uv":[16,23,19,27],"texture":0},"east":{"uv":[0,30,3,34],"texture":0},"south":{"uv":[3,30,6,34],"texture":0},"west":{"uv":[6,30,9,34],"texture":0},"up":{"uv":[28,43,25,40],"texture":0},"down":{"uv":[31,40,28,43],"texture":0}},"type":"cube","uuid":"e81b7843-20f5-c76e-a1a1-e0e6e87d6767"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.20264,31.71,-1.99375],"to":[1.15736,33.71,-1.19375],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[-0.05264,32.51,-2.49375],"faces":{"north":{"uv":[50,8,52,11],"texture":0},"east":{"uv":[36,60,37,63],"texture":0},"south":{"uv":[50,11,52,14],"texture":0},"west":{"uv":[37,60,38,63],"texture":0},"up":{"uv":[65,11,63,10],"texture":0},"down":{"uv":[65,11,63,12],"texture":0}},"type":"cube","uuid":"5729025f-74e3-e5dc-3d49-8d8ec85d1766"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.97736,33.31,0.60625],"to":[4.77736,35.11,2.40625],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[3.04736,32.71,1.40625],"faces":{"north":{"uv":[56,13,58,15],"texture":0},"east":{"uv":[56,15,58,17],"texture":0},"south":{"uv":[56,17,58,19],"texture":0},"west":{"uv":[19,56,21,58],"texture":0},"up":{"uv":[58,21,56,19],"texture":0},"down":{"uv":[58,21,56,23],"texture":0}},"type":"cube","uuid":"571974f4-bc0d-5b57-c405-e5653335ee06"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.96264,31.19,2.40625],"to":[-0.42264,32.39,3.82625],"autouv":0,"color":5,"origin":[2.29736,27.15,-0.39375],"faces":{"north":{"uv":[48,21,51,23],"texture":0},"east":{"uv":[56,0,58,2],"texture":0},"south":{"uv":[48,23,51,25],"texture":0},"west":{"uv":[6,56,8,58],"texture":0},"up":{"uv":[27,50,24,48],"texture":0},"down":{"uv":[51,25,48,27],"texture":0}},"type":"cube","uuid":"676e5395-2ca6-1959-efe3-e41270439957"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.40264,28.51,0.10625],"to":[2.29736,30.89,1.52625],"autouv":0,"color":1,"rotation":[45,0,0],"origin":[-0.10264,29.35,0.83625],"faces":{"north":{"uv":[13,20,19,23],"texture":0},"east":{"uv":[22,49,24,52],"texture":0},"south":{"uv":[21,0,27,3],"texture":0},"west":{"uv":[39,49,41,52],"texture":0},"up":{"uv":[25,31,19,29],"texture":0},"down":{"uv":[31,29,25,31],"texture":0}},"type":"cube","uuid":"8c0b6782-b9e2-cf2d-31b4-c96d97905fd0"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.71736,29.85,-0.79375],"to":[3.39736,31.91,0.00625],"autouv":0,"color":9,"origin":[2.19736,29.51,-0.19375],"faces":{"north":{"uv":[13,60,14,63],"texture":0},"east":{"uv":[14,60,15,63],"texture":0},"south":{"uv":[15,60,16,63],"texture":0},"west":{"uv":[16,60,17,63],"texture":0},"up":{"uv":[17,66,16,65],"texture":0},"down":{"uv":[66,16,65,17],"texture":0}},"type":"cube","uuid":"2d82ea57-745f-beb3-0c88-6c509f7a74ed"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.67736,29.29,-0.79375],"to":[3.03736,30.11,0.00625],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[2.59736,29.08,-0.19375],"faces":{"north":{"uv":[17,65,18,66],"texture":0},"east":{"uv":[65,17,66,18],"texture":0},"south":{"uv":[18,65,19,66],"texture":0},"west":{"uv":[19,65,20,66],"texture":0},"up":{"uv":[66,20,65,19],"texture":0},"down":{"uv":[66,20,65,21],"texture":0}},"type":"cube","uuid":"9a8f1ae5-db04-1afe-d3e5-624faaad48e9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.34264,35.75,-1.97375],"to":[-0.62264,36.59,2.40625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[0.11736,36.03,0.21625],"faces":{"north":{"uv":[62,48,64,49],"texture":0},"east":{"uv":[26,54,31,55],"texture":0},"south":{"uv":[62,54,64,55],"texture":0},"west":{"uv":[54,28,59,29],"texture":0},"up":{"uv":[27,40,25,35],"texture":0},"down":{"uv":[29,35,27,40],"texture":0}},"type":"cube","uuid":"448668fe-7911-1061-80f2-3d5c40d07984"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.56264,33.23,-1.97375],"to":[-1.62264,35.09,2.40625],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[-2.62264,34.49,0.60625],"faces":{"north":{"uv":[6,44,10,46],"texture":0},"east":{"uv":[34,11,39,13],"texture":0},"south":{"uv":[44,6,48,8],"texture":0},"west":{"uv":[34,13,39,15],"texture":0},"up":{"uv":[19,20,15,15],"texture":0},"down":{"uv":[21,0,17,5],"texture":0}},"type":"cube","uuid":"2ebc9aa9-73df-e4ed-0a7f-f22cc51c6feb"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.00264,33.35,-1.97375],"to":[0.17736,34.59,-1.19375],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[-0.94264,34.49,0.60625],"faces":{"north":{"uv":[35,0,40,2],"texture":0},"east":{"uv":[20,62,21,64],"texture":0},"south":{"uv":[35,2,40,4],"texture":0},"west":{"uv":[24,62,25,64],"texture":0},"up":{"uv":[58,37,53,36],"texture":0},"down":{"uv":[59,7,54,8],"texture":0}},"type":"cube","uuid":"8ec78ad0-d84a-0628-5443-fbd1c509e136"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.575,26.75,-0.4],"to":[0.875,30,1.025],"autouv":0,"color":1,"rotation":[0,45,0],"origin":[0.175,28.4,0.525],"faces":{"north":{"uv":[43,47,44,50],"texture":0},"east":{"uv":[5,60,6,63],"texture":0},"south":{"uv":[45,47,46,50],"texture":0},"west":{"uv":[10,60,11,63],"texture":0},"up":{"uv":[64,38,62,37],"texture":0},"down":{"uv":[64,38,62,39],"texture":0}},"type":"cube","uuid":"41e7b572-b3cb-756c-9a95-64b8f5f33769"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.54264,31.31,0.60625],"to":[-1.26264,32.99,2.40625],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[-3.17264,32.71,1.40625],"faces":{"north":{"uv":[51,19,54,21],"texture":0},"east":{"uv":[57,57,59,59],"texture":0},"south":{"uv":[51,21,54,23],"texture":0},"west":{"uv":[58,0,60,2],"texture":0},"up":{"uv":[54,25,51,23],"texture":0},"down":{"uv":[54,25,51,27],"texture":0}},"type":"cube","uuid":"9b4e3438-f7a3-6771-95bc-51b94b1899e8"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.34264,32.79,2.62625],"to":[2.57736,34.27,3.40625],"autouv":0,"color":9,"rotation":[-45,0,0],"origin":[0.11736,33.71,1.25625],"faces":{"north":{"uv":[28,6,34,8],"texture":0},"east":{"uv":[60,62,61,64],"texture":0},"south":{"uv":[28,8,34,10],"texture":0},"west":{"uv":[4,63,5,65],"texture":0},"up":{"uv":[34,11,28,10],"texture":0},"down":{"uv":[54,18,48,19],"texture":0}},"type":"cube","uuid":"0335203d-2335-ce64-0a28-16715076876f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.80264,29.95,0.94625],"to":[2.69736,32.75,2.40625],"autouv":0,"color":9,"origin":[2.89736,29.71,-0.39375],"faces":{"north":{"uv":[0,10,7,14],"texture":0},"east":{"uv":[4,23,6,27],"texture":0},"south":{"uv":[7,10,14,14],"texture":0},"west":{"uv":[16,40,18,44],"texture":0},"up":{"uv":[22,29,15,27],"texture":0},"down":{"uv":[29,27,22,29],"texture":0}},"type":"cube","uuid":"f0e1b661-bd04-bf34-5724-e4ce9acc5e75"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.36264,29.95,2.08625],"to":[2.57736,32.59,3.12625],"autouv":0,"color":9,"rotation":[22.5,0,0],"origin":[0.10736,31.73,2.77625],"faces":{"north":{"uv":[19,18,25,21],"texture":0},"east":{"uv":[34,60,35,63],"texture":0},"south":{"uv":[0,20,6,23],"texture":0},"west":{"uv":[35,60,36,63],"texture":0},"up":{"uv":[55,42,49,41],"texture":0},"down":{"uv":[55,42,49,43],"texture":0}},"type":"cube","uuid":"b3cf2f69-86ff-ed07-8934-90d0425f413e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.56264,31.37,-1.79375],"to":[-4.00264,32.95,0.58625],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[-3.02264,32.91,-0.39375],"faces":{"north":{"uv":[12,63,13,65],"texture":0},"east":{"uv":[26,50,29,52],"texture":0},"south":{"uv":[13,63,14,65],"texture":0},"west":{"uv":[29,50,32,52],"texture":0},"up":{"uv":[61,40,60,37],"texture":0},"down":{"uv":[39,60,38,63],"texture":0}},"type":"cube","uuid":"6b77988c-a45d-1c22-dc83-86073939178e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.78652,28.63002,-1.59375],"to":[3.68652,29.59002,-0.99375],"autouv":0,"color":9,"origin":[3.22652,29.23002,-1.29375],"faces":{"north":{"uv":[0,65,1,66],"texture":0},"east":{"uv":[65,0,66,1],"texture":0},"south":{"uv":[1,65,2,66],"texture":0},"west":{"uv":[65,1,66,2],"texture":0},"up":{"uv":[3,66,2,65],"texture":0},"down":{"uv":[66,2,65,3],"texture":0}},"type":"cube","uuid":"5573dea6-d06d-26ef-659c-5e9dfee7c967"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.6518,29.35002,-1.59375],"to":[-2.8318,30.23002,-0.99375],"autouv":0,"color":9,"origin":[-3.2118,29.41002,-1.29375],"faces":{"north":{"uv":[7,65,8,66],"texture":0},"east":{"uv":[8,65,9,66],"texture":0},"south":{"uv":[9,65,10,66],"texture":0},"west":{"uv":[65,9,66,10],"texture":0},"up":{"uv":[11,66,10,65],"texture":0},"down":{"uv":[66,10,65,11],"texture":0}},"type":"cube","uuid":"b3fd65a2-c4d8-8976-2bd9-eecda71c86b9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.25736,31.53,0.60625],"to":[3.67736,33.73,2.40625],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[1.94736,31.33,1.40625],"faces":{"north":{"uv":[49,47,51,50],"texture":0},"east":{"uv":[50,0,52,3],"texture":0},"south":{"uv":[50,3,52,6],"texture":0},"west":{"uv":[6,50,8,53],"texture":0},"up":{"uv":[38,58,36,56],"texture":0},"down":{"uv":[40,56,38,58],"texture":0}},"type":"cube","uuid":"8ba2c460-3e0d-40be-a9fc-cd7ae4a4574c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.68264,34.05,-0.15375],"to":[2.37736,35.51,1.28625],"autouv":0,"color":9,"rotation":[-45,0,0],"origin":[4.15736,31.57,-0.39375],"faces":{"north":{"uv":[35,27,40,29],"texture":0},"east":{"uv":[53,55,55,57],"texture":0},"south":{"uv":[36,6,41,8],"texture":0},"west":{"uv":[55,54,57,56],"texture":0},"up":{"uv":[41,10,36,8],"texture":0},"down":{"uv":[41,15,36,17],"texture":0}},"type":"cube","uuid":"84e0afe0-c3b1-e5ee-63bf-491983d93639"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.06264,34.13,0.46625],"to":[-0.68264,34.25,1.42625],"autouv":0,"color":9,"rotation":[-45,0,0],"origin":[0.87736,31.01,0.40625],"faces":{"north":{"uv":[60,26,63,27],"texture":0},"east":{"uv":[23,65,24,66],"texture":0},"south":{"uv":[60,27,63,28],"texture":0},"west":{"uv":[65,23,66,24],"texture":0},"up":{"uv":[31,61,28,60],"texture":0},"down":{"uv":[63,30,60,31],"texture":0}},"type":"cube","uuid":"a32e0821-ed45-ccd3-91c6-ac768ec3310c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.54264,33.73,-2.51375],"to":[0.91736,34.81,-1.99375],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[-0.81264,33.85,-2.33375],"faces":{"north":{"uv":[60,1,63,2],"texture":0},"east":{"uv":[21,65,22,66],"texture":0},"south":{"uv":[60,8,63,9],"texture":0},"west":{"uv":[65,21,66,22],"texture":0},"up":{"uv":[63,22,60,21],"texture":0},"down":{"uv":[63,22,60,23],"texture":0}},"type":"cube","uuid":"4458cb4c-42b1-5a2f-c0fa-8730c2a5e29b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.01736,32.53,-2.51375],"to":[2.09736,34.71,-1.99375],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[0.54736,33.85,-2.33375],"faces":{"north":{"uv":[60,23,61,26],"texture":0},"east":{"uv":[25,60,26,63],"texture":0},"south":{"uv":[26,60,27,63],"texture":0},"west":{"uv":[27,60,28,63],"texture":0},"up":{"uv":[23,66,22,65],"texture":0},"down":{"uv":[66,22,65,23],"texture":0}},"type":"cube","uuid":"058b2d35-b9d7-23da-a53a-98f0b8533465"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.15736,32.39,0.00625],"to":[2.59736,34.59,1.02625],"autouv":0,"color":9,"rotation":[0,45,0],"origin":[3.62736,33.49,0.51625],"faces":{"north":{"uv":[2,49,4,52],"texture":0},"east":{"uv":[32,60,33,63],"texture":0},"south":{"uv":[4,49,6,52],"texture":0},"west":{"uv":[33,60,34,63],"texture":0},"up":{"uv":[65,10,63,9],"texture":0},"down":{"uv":[12,63,10,64],"texture":0}},"type":"cube","uuid":"3d38f265-16fb-b26d-0851-3b04a28b6d64"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.42264,31.97,-1.97375],"to":[-3.10264,33.53,2.40625],"autouv":0,"color":9,"origin":[-2.48264,32.93,0.60625],"faces":{"north":{"uv":[49,55,51,57],"texture":0},"east":{"uv":[34,29,39,31],"texture":0},"south":{"uv":[55,52,57,54],"texture":0},"west":{"uv":[34,31,39,33],"texture":0},"up":{"uv":[16,40,14,35],"texture":0},"down":{"uv":[18,35,16,40],"texture":0}},"type":"cube","uuid":"84d754cf-915e-8061-2884-d5a7dc9f7bd1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.75736,29.31,-1.19375],"to":[3.49736,32.91,0.40625],"autouv":0,"color":1,"origin":[2.89736,29.71,-0.39375],"faces":{"north":{"uv":[23,54,24,59],"texture":0},"east":{"uv":[10,34,12,39],"texture":0},"south":{"uv":[54,23,55,28],"texture":0},"west":{"uv":[12,34,14,39],"texture":0},"up":{"uv":[7,44,6,42],"texture":0},"down":{"uv":[13,44,12,46],"texture":0}},"type":"cube","uuid":"ecc83fce-7df5-b985-53a8-a3303f95832f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.88264,29.87,-1.03375],"to":[-0.38264,31.91,-1.03375],"autouv":0,"color":8,"origin":[-4.20264,32.31,-0.39375],"faces":{"north":{"uv":[40,0,43,3],"texture":0},"east":{"uv":[0,0,0,3],"texture":0},"south":{"uv":[13,40,16,43],"texture":0},"west":{"uv":[0,0,0,3],"texture":0},"up":{"uv":[3,0,0,0],"texture":0},"down":{"uv":[3,0,0,0],"texture":0}},"type":"cube","uuid":"350e3a30-4658-77a6-4df2-1d7b331a098b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.38264,28.51,-1.19375],"to":[0.25736,32.91,0.40625],"autouv":0,"color":1,"origin":[4.07736,29.71,-0.39375],"faces":{"north":{"uv":[41,47,42,53],"texture":0},"east":{"uv":[15,29,17,35],"texture":0},"south":{"uv":[42,47,43,53],"texture":0},"west":{"uv":[17,29,19,35],"texture":0},"up":{"uv":[8,39,7,37],"texture":0},"down":{"uv":[41,27,40,29],"texture":0}},"type":"cube","uuid":"21b22b7a-bce2-d3cd-bafe-6bb8ca56111e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.25736,29.87,-1.03375],"to":[2.75736,31.91,-1.03375],"autouv":0,"color":8,"origin":[4.07736,32.31,-0.39375],"faces":{"north":{"uv":[39,29,42,32],"texture":0},"east":{"uv":[0,0,0,3],"texture":0},"south":{"uv":[39,39,42,42],"texture":0},"west":{"uv":[0,0,0,3],"texture":0},"up":{"uv":[3,0,0,0],"texture":0},"down":{"uv":[3,0,0,0],"texture":0}},"type":"cube","uuid":"5c446ca3-fa6a-9098-3658-6b2188e41ef4"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.25736,28.51,-1.19375],"to":[2.71736,29.87,0.40625],"autouv":0,"color":1,"origin":[4.07736,29.71,-0.39375],"faces":{"north":{"uv":[47,33,50,35],"texture":0},"east":{"uv":[55,41,57,43],"texture":0},"south":{"uv":[47,35,50,37],"texture":0},"west":{"uv":[45,55,47,57],"texture":0},"up":{"uv":[50,39,47,37],"texture":0},"down":{"uv":[50,39,47,41],"texture":0}},"type":"cube","uuid":"cd5d87f1-1f75-5842-de2a-3eeaf3af1f59"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.84264,28.51,-1.19375],"to":[-0.38264,29.87,0.40625],"autouv":0,"color":1,"origin":[-4.20264,29.71,-0.39375],"faces":{"north":{"uv":[31,46,34,48],"texture":0},"east":{"uv":[21,3,23,5],"texture":0},"south":{"uv":[34,46,37,48],"texture":0},"west":{"uv":[55,39,57,41],"texture":0},"up":{"uv":[50,31,47,29],"texture":0},"down":{"uv":[50,31,47,33],"texture":0}},"type":"cube","uuid":"c73b2cca-267a-3b8a-82dc-1eeafcd6927d"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.17736,29.51,-1.19375],"to":[3.21736,29.87,-1.03375],"autouv":0,"color":1,"rotation":[0,0,22.5],"origin":[1.96736,28.87,-1.15375],"faces":{"north":{"uv":[17,5,18,6],"texture":0},"east":{"uv":[27,6,28,7],"texture":0},"south":{"uv":[14,34,15,35],"texture":0},"west":{"uv":[13,39,14,40],"texture":0},"up":{"uv":[3,45,2,44],"texture":0},"down":{"uv":[45,28,44,29],"texture":0}},"type":"cube","uuid":"ec21fc72-01ba-0601-895f-94d76063b07b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.34264,29.51,-1.19375],"to":[-2.30264,29.87,-1.03375],"autouv":0,"color":1,"rotation":[0,0,-22.5],"origin":[-2.09264,28.87,-1.15375],"faces":{"north":{"uv":[55,64,56,65],"texture":0},"east":{"uv":[64,55,65,56],"texture":0},"south":{"uv":[56,64,57,65],"texture":0},"west":{"uv":[64,56,65,57],"texture":0},"up":{"uv":[58,65,57,64],"texture":0},"down":{"uv":[65,57,64,58],"texture":0}},"type":"cube","uuid":"f322ebee-1063-7c39-2350-8d74edf49a71"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.35201,29.36694,-1.19375],"to":[0.30799,30.02694,-1.03375],"autouv":0,"color":1,"rotation":[0,0,-45],"origin":[0.16799,29.84694,-1.15375],"faces":{"north":{"uv":[56,51,57,52],"texture":0},"east":{"uv":[46,64,47,65],"texture":0},"south":{"uv":[47,64,48,65],"texture":0},"west":{"uv":[64,47,65,48],"texture":0},"up":{"uv":[49,65,48,64],"texture":0},"down":{"uv":[65,48,64,49],"texture":0}},"type":"cube","uuid":"2c80f29a-68d9-2841-a9b3-7de033e342f5"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.51736,31.35,-1.19375],"to":[1.55736,31.71,-1.03375],"autouv":0,"color":1,"rotation":[0,0,22.5],"origin":[0.30736,30.71,-1.15375],"faces":{"north":{"uv":[49,64,50,65],"texture":0},"east":{"uv":[50,64,51,65],"texture":0},"south":{"uv":[51,64,52,65],"texture":0},"west":{"uv":[64,51,65,52],"texture":0},"up":{"uv":[53,65,52,64],"texture":0},"down":{"uv":[54,64,53,65],"texture":0}},"type":"cube","uuid":"85e31557-bf10-290c-931b-a1855a815d1c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.44264,31.35,-1.19375],"to":[-0.64264,31.71,-1.03375],"autouv":0,"color":1,"rotation":[0,0,-22.5],"origin":[-0.43264,30.71,-1.15375],"faces":{"north":{"uv":[6,20,8,21],"texture":0},"east":{"uv":[54,64,55,65],"texture":0},"south":{"uv":[39,14,41,15],"texture":0},"west":{"uv":[64,54,65,55],"texture":0},"up":{"uv":[5,42,3,41],"texture":0},"down":{"uv":[52,40,50,41],"texture":0}},"type":"cube","uuid":"bacad5c3-b6bc-3fc9-529e-b6f6ff181e82"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.82264,31.77,-1.19375],"to":[2.95736,33.23,-1.03375],"autouv":0,"color":1,"origin":[0.30736,31.11,-1.15375],"faces":{"north":{"uv":[27,11,34,13],"texture":0},"east":{"uv":[23,7,24,9],"texture":0},"south":{"uv":[27,13,34,15],"texture":0},"west":{"uv":[18,35,19,37],"texture":0},"up":{"uv":[31,47,24,46],"texture":0},"down":{"uv":[48,46,41,47],"texture":0}},"type":"cube","uuid":"374c990f-cca9-7642-54f6-8fcc2ae47386"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.37736,31.49,-0.39375],"to":[3.41736,31.85,-0.31375],"autouv":0,"color":1,"rotation":[0,0,-22.5],"origin":[2.16736,30.85,-0.35375],"faces":{"north":{"uv":[24,45,25,46],"texture":0},"east":{"uv":[41,45,42,46],"texture":0},"south":{"uv":[8,50,9,51],"texture":0},"west":{"uv":[55,43,56,44],"texture":0},"up":{"uv":[56,50,55,49],"texture":0},"down":{"uv":[57,2,56,3],"texture":0}},"type":"cube","uuid":"29d56430-7916-7008-9cc8-b6565185a324"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.04264,30.37,-1.19375],"to":[-2.86264,30.91,-1.03375],"autouv":0,"color":1,"rotation":[0,0,22.5],"origin":[-2.23264,30.91,-1.15375],"faces":{"north":{"uv":[58,64,59,65],"texture":0},"east":{"uv":[64,58,65,59],"texture":0},"south":{"uv":[64,59,65,60],"texture":0},"west":{"uv":[60,64,61,65],"texture":0},"up":{"uv":[65,61,64,60],"texture":0},"down":{"uv":[62,64,61,65],"texture":0}},"type":"cube","uuid":"17633f4b-13a7-8951-8eec-bf2f1952ec70"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.79736,30.37,-1.19375],"to":[2.97736,30.91,-1.03375],"autouv":0,"color":1,"rotation":[0,0,-22.5],"origin":[2.16736,30.91,-1.15375],"faces":{"north":{"uv":[64,61,65,62],"texture":0},"east":{"uv":[62,64,63,65],"texture":0},"south":{"uv":[64,62,65,63],"texture":0},"west":{"uv":[63,64,64,65],"texture":0},"up":{"uv":[65,64,64,63],"texture":0},"down":{"uv":[65,64,64,65],"texture":0}},"type":"cube","uuid":"42c42a1c-3468-5c32-6bfe-36250a72b9af"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.41736,32.71,-0.21375],"to":[3.97736,34.07,2.08625],"autouv":0,"color":9,"rotation":[-22.5,0,0],"origin":[8.15736,30.21,2.00625],"faces":{"north":{"uv":[5,63,6,65],"texture":0},"east":{"uv":[19,48,22,50],"texture":0},"south":{"uv":[63,7,64,9],"texture":0},"west":{"uv":[48,19,51,21],"texture":0},"up":{"uv":[18,63,17,60],"texture":0},"down":{"uv":[19,60,18,63],"texture":0}},"type":"cube","uuid":"a20d7470-bd5d-94a6-8bed-c93cf0d2d161"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.36264,32.39,2.40625],"to":[2.57736,34.59,3.42625],"autouv":0,"color":9,"origin":[2.89736,29.75,-0.39375],"faces":{"north":{"uv":[18,9,24,12],"texture":0},"east":{"uv":[31,60,32,63],"texture":0},"south":{"uv":[19,15,25,18],"texture":0},"west":{"uv":[60,31,61,34],"texture":0},"up":{"uv":[54,28,48,27],"texture":0},"down":{"uv":[54,46,48,47],"texture":0}},"type":"cube","uuid":"27defe3c-bdd3-18bb-85d3-6405e21b1e03"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.96264,28.99,2.60625],"to":[-1.50264,31.19,3.62625],"autouv":0,"color":9,"origin":[1.89736,27.15,-0.19375],"faces":{"north":{"uv":[51,47,53,50],"texture":0},"east":{"uv":[52,60,53,63],"texture":0},"south":{"uv":[51,50,53,53],"texture":0},"west":{"uv":[55,60,56,63],"texture":0},"up":{"uv":[65,20,63,19],"texture":0},"down":{"uv":[65,21,63,22],"texture":0}},"type":"cube","uuid":"c0d88dd0-a196-b17c-e123-954163d428e7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.70264,31.79,2.40625],"to":[-2.28264,32.61,3.42625],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[-2.10264,32.39,2.91625],"faces":{"north":{"uv":[30,65,31,66],"texture":0},"east":{"uv":[65,30,66,31],"texture":0},"south":{"uv":[31,65,32,66],"texture":0},"west":{"uv":[65,31,66,32],"texture":0},"up":{"uv":[33,66,32,65],"texture":0},"down":{"uv":[66,32,65,33],"texture":0}},"type":"cube","uuid":"2e41d6ce-8800-9b74-6f26-94e295cac2bf"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.29736,31.19,2.40625],"to":[2.83736,32.39,3.82625],"autouv":0,"color":5,"origin":[-2.42264,27.15,-0.39375],"faces":{"north":{"uv":[27,48,30,50],"texture":0},"east":{"uv":[56,9,58,11],"texture":0},"south":{"uv":[30,48,33,50],"texture":0},"west":{"uv":[56,11,58,13],"texture":0},"up":{"uv":[36,50,33,48],"texture":0},"down":{"uv":[39,48,36,50],"texture":0}},"type":"cube","uuid":"512ee88e-a107-34c4-b782-b3cf94747612"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.37736,29.59,2.60625],"to":[2.83736,31.19,3.62625],"autouv":0,"color":9,"origin":[-2.02264,27.15,-0.19375],"faces":{"north":{"uv":[58,21,60,23],"texture":0},"east":{"uv":[23,63,24,65],"texture":0},"south":{"uv":[25,58,27,60],"texture":0},"west":{"uv":[25,63,26,65],"texture":0},"up":{"uv":[65,23,63,22],"texture":0},"down":{"uv":[28,63,26,64],"texture":0}},"type":"cube","uuid":"58c102f9-86e6-7ce8-e0ca-7b8d31dbf167"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.15736,31.79,2.40625],"to":[2.57736,32.61,3.42625],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[1.97736,32.39,2.91625],"faces":{"north":{"uv":[65,24,66,25],"texture":0},"east":{"uv":[25,65,26,66],"texture":0},"south":{"uv":[65,26,66,27],"texture":0},"west":{"uv":[65,27,66,28],"texture":0},"up":{"uv":[29,66,28,65],"texture":0},"down":{"uv":[30,65,29,66],"texture":0}},"type":"cube","uuid":"876f030b-55de-7dd6-e835-00488a4996f9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.63736,30.63,2.60625],"to":[1.61736,31.59,3.62625],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[1.36736,30.86,3.11625],"faces":{"north":{"uv":[65,56,66,57],"texture":0},"east":{"uv":[57,65,58,66],"texture":0},"south":{"uv":[65,57,66,58],"texture":0},"west":{"uv":[58,65,59,66],"texture":0},"up":{"uv":[66,59,65,58],"texture":0},"down":{"uv":[60,65,59,66],"texture":0}},"type":"cube","uuid":"eab62980-956f-9ac3-e675-d862905ecc98"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.74264,30.63,2.60625],"to":[-0.76264,31.59,3.62625],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[-1.49264,30.86,3.11625],"faces":{"north":{"uv":[65,48,66,49],"texture":0},"east":{"uv":[49,65,50,66],"texture":0},"south":{"uv":[65,49,66,50],"texture":0},"west":{"uv":[50,65,51,66],"texture":0},"up":{"uv":[66,51,65,50],"texture":0},"down":{"uv":[52,65,51,66],"texture":0}},"type":"cube","uuid":"ae538734-efc4-2f34-17c4-995c75624993"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.58264,25.59,2.76625],"to":[-1.88264,28.99,3.46625],"autouv":0,"color":5,"rotation":[0,45,0],"origin":[-2.23264,28.89,3.11625],"faces":{"north":{"uv":[6,58,7,62],"texture":0},"east":{"uv":[7,58,8,62],"texture":0},"south":{"uv":[58,9,59,13],"texture":0},"west":{"uv":[58,13,59,17],"texture":0},"up":{"uv":[66,52,65,51],"texture":0},"down":{"uv":[53,65,52,66],"texture":0}},"type":"cube","uuid":"152d16d5-b078-564f-133e-d531ffcc2818"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.42264,25.39,2.92625],"to":[-2.04264,25.59,3.30625],"autouv":0,"color":9,"rotation":[0,45,0],"origin":[-2.23264,28.89,3.11625],"faces":{"north":{"uv":[65,52,66,53],"texture":0},"east":{"uv":[53,65,54,66],"texture":0},"south":{"uv":[65,53,66,54],"texture":0},"west":{"uv":[54,65,55,66],"texture":0},"up":{"uv":[66,55,65,54],"texture":0},"down":{"uv":[56,65,55,66],"texture":0}},"type":"cube","uuid":"25b70ee3-36dc-db96-bcdb-b06c141eff67"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.50264,21.99,2.84625],"to":[-1.96264,25.39,3.38625],"autouv":0,"color":5,"origin":[-2.23264,25.29,3.11625],"faces":{"north":{"uv":[58,17,59,21],"texture":0},"east":{"uv":[19,58,20,62],"texture":0},"south":{"uv":[20,58,21,62],"texture":0},"west":{"uv":[24,58,25,62],"texture":0},"up":{"uv":[66,56,65,55],"texture":0},"down":{"uv":[57,65,56,66],"texture":0}},"type":"cube","uuid":"15d2938a-7b52-b9b8-5e46-b1591e2f5234"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.75736,26.99,2.76625],"to":[2.45736,29.59,3.46625],"autouv":0,"color":5,"rotation":[0,-45,0],"origin":[2.10736,29.49,3.11625],"faces":{"north":{"uv":[56,60,57,63],"texture":0},"east":{"uv":[60,59,61,62],"texture":0},"south":{"uv":[0,61,1,64],"texture":0},"west":{"uv":[1,61,2,64],"texture":0},"up":{"uv":[66,60,65,59],"texture":0},"down":{"uv":[61,65,60,66],"texture":0}},"type":"cube","uuid":"9eaf4c53-3dbf-fcda-fd62-0a7b2682ac03"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.91736,26.79,2.92625],"to":[2.29736,26.99,3.30625],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[2.10736,30.29,3.11625],"faces":{"north":{"uv":[65,60,66,61],"texture":0},"east":{"uv":[61,65,62,66],"texture":0},"south":{"uv":[65,61,66,62],"texture":0},"west":{"uv":[62,65,63,66],"texture":0},"up":{"uv":[66,63,65,62],"texture":0},"down":{"uv":[64,65,63,66],"texture":0}},"type":"cube","uuid":"6a04a94f-b682-b4ec-6f46-89134ba4bb1a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.83736,24.19,2.84625],"to":[2.37736,26.79,3.38625],"autouv":0,"color":5,"origin":[2.10736,26.69,3.11625],"faces":{"north":{"uv":[61,2,62,5],"texture":0},"east":{"uv":[8,61,9,64],"texture":0},"south":{"uv":[9,61,10,64],"texture":0},"west":{"uv":[61,9,62,12],"texture":0},"up":{"uv":[66,64,65,63],"texture":0},"down":{"uv":[65,65,64,66],"texture":0}},"type":"cube","uuid":"0511aed9-00e4-dadf-a777-d5572b593baf"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.75,24.975,-1.25],"to":[1.75,26.75,2.25],"autouv":0,"color":1,"origin":[0,24.75,-1.5],"faces":{"north":{"uv":[44,8,48,10],"texture":0},"east":{"uv":[44,10,48,12],"texture":0},"south":{"uv":[44,12,48,14],"texture":0},"west":{"uv":[44,14,48,16],"texture":0},"up":{"uv":[4,27,0,23],"texture":0},"down":{"uv":[27,3,23,7],"texture":0}},"type":"cube","uuid":"eac3ad48-ae72-b3bc-8b95-018333dc3051"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.525,23.35,-2],"to":[0.275,26,2.5],"autouv":0,"color":5,"rotation":[0,0,-45],"origin":[1.475,23.625,0],"faces":{"north":{"uv":[62,9,63,12],"texture":0},"east":{"uv":[25,18,30,21],"texture":0},"south":{"uv":[62,14,63,17],"texture":0},"west":{"uv":[25,21,30,24],"texture":0},"up":{"uv":[16,60,15,55],"texture":0},"down":{"uv":[17,55,16,60],"texture":0}},"type":"cube","uuid":"b93cff5c-2a56-1aab-a8c7-ed4d7397794a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.625,23.2,-2.25],"to":[3.175,25.725,2.75],"autouv":0,"color":5,"rotation":[0,0,-45],"origin":[3.425,23.575,0],"faces":{"north":{"uv":[43,53,45,56],"texture":0},"east":{"uv":[5,27,10,30],"texture":0},"south":{"uv":[53,47,55,50],"texture":0},"west":{"uv":[10,27,15,30],"texture":0},"up":{"uv":[22,44,20,39],"texture":0},"down":{"uv":[39,39,37,44],"texture":0}},"type":"cube","uuid":"e9d83ef5-2045-ab67-4a98-bed4ba14d518"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.25,24.1,-2.8],"to":[2.25,26.3,-1.475],"autouv":0,"color":5,"rotation":[45,0,0],"origin":[0,24.225,-2.1],"faces":{"north":{"uv":[36,21,41,23],"texture":0},"east":{"uv":[64,16,65,18],"texture":0},"south":{"uv":[36,23,41,25],"texture":0},"west":{"uv":[20,64,21,66],"texture":0},"up":{"uv":[60,36,55,35],"texture":0},"down":{"uv":[41,55,36,56],"texture":0}},"type":"cube","uuid":"2a40ace8-91f0-0a80-b85c-f1bfea213a68"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.925,24,-2],"to":[2.75,26.25,2.5],"autouv":0,"color":5,"rotation":[0,0,-22.5],"origin":[3,24.375,0],"faces":{"north":{"uv":[59,18,61,20],"texture":0},"east":{"uv":[38,4,43,6],"texture":0},"south":{"uv":[21,59,23,61],"texture":0},"west":{"uv":[0,39,5,41],"texture":0},"up":{"uv":[7,42,5,37],"texture":0},"down":{"uv":[25,37,23,42],"texture":0}},"type":"cube","uuid":"aee73142-be32-3036-4fe2-ddeccf72a5b1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.75,24,-2],"to":[-0.925,26.25,2.5],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[-3,24.375,0],"faces":{"north":{"uv":[59,28,61,30],"texture":0},"east":{"uv":[39,10,44,12],"texture":0},"south":{"uv":[40,59,42,61],"texture":0},"west":{"uv":[39,12,44,14],"texture":0},"up":{"uv":[13,44,11,39],"texture":0},"down":{"uv":[20,39,18,44],"texture":0}},"type":"cube","uuid":"8bfbd4a0-a8fa-aa56-046f-d97347ba5903"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.275,23.35,-2],"to":[0.525,26,2.5],"autouv":0,"color":5,"rotation":[0,0,45],"origin":[-1.475,23.625,0],"faces":{"north":{"uv":[62,17,63,20],"texture":0},"east":{"uv":[27,0,32,3],"texture":0},"south":{"uv":[19,62,20,65],"texture":0},"west":{"uv":[27,3,32,6],"texture":0},"up":{"uv":[18,60,17,55],"texture":0},"down":{"uv":[19,55,18,60],"texture":0}},"type":"cube","uuid":"55a299fb-f460-6263-e740-75923a2d9d86"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.175,23.2,-2.25],"to":[-1.625,25.725,2.75],"autouv":0,"color":5,"rotation":[0,0,45],"origin":[-3.425,23.575,0],"faces":{"north":{"uv":[53,33,55,36],"texture":0},"east":{"uv":[25,24,30,27],"texture":0},"south":{"uv":[41,53,43,56],"texture":0},"west":{"uv":[0,27,5,30],"texture":0},"up":{"uv":[9,44,7,39],"texture":0},"down":{"uv":[11,39,9,44],"texture":0}},"type":"cube","uuid":"add2bd23-961a-d6ee-0953-9458433b0e7e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.025,21.775,-1.75],"to":[2.1,23.95,2],"autouv":0,"color":0,"rotation":[0,0,-22.5],"origin":[2.35,21.375,0],"faces":{"north":{"uv":[64,23,65,25],"texture":0},"east":{"uv":[46,4,50,6],"texture":0},"south":{"uv":[24,64,25,66],"texture":0},"west":{"uv":[6,46,10,48],"texture":0},"up":{"uv":[44,62,43,58],"texture":0},"down":{"uv":[24,59,23,63],"texture":0}},"type":"cube","uuid":"d9e7e5c3-da02-8477-fd18-15eb3ec1609c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.1,21.775,-1.75],"to":[-0.925,23.95,2],"autouv":0,"color":0,"rotation":[0,0,22.5],"origin":[-2.35,21.375,0],"faces":{"north":{"uv":[26,64,27,66],"texture":0},"east":{"uv":[12,46,16,48],"texture":0},"south":{"uv":[27,64,28,66],"texture":0},"west":{"uv":[16,46,20,48],"texture":0},"up":{"uv":[40,63,39,59],"texture":0},"down":{"uv":[60,39,59,43],"texture":0}},"type":"cube","uuid":"ac4a4b83-622d-18a3-f3a4-a2a64a04a81a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.25,22.975,-3.175],"to":[2.25,24.425,-1.55],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[0,23.35,-2.45],"faces":{"north":{"uv":[55,37,60,38],"texture":0},"east":{"uv":[64,18,66,19],"texture":0},"south":{"uv":[55,38,60,39],"texture":0},"west":{"uv":[21,64,23,65],"texture":0},"up":{"uv":[41,27,36,25],"texture":0},"down":{"uv":[5,37,0,39],"texture":0}},"type":"cube","uuid":"6e9d44d7-1f0b-5134-4382-e916bf84baba"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.25,21.425,-1.525],"to":[2.25,23.225,0.6],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[0,21.475,-0.8],"faces":{"north":{"uv":[18,37,23,39],"texture":0},"east":{"uv":[59,14,61,16],"texture":0},"south":{"uv":[37,33,42,35],"texture":0},"west":{"uv":[59,16,61,18],"texture":0},"up":{"uv":[42,37,37,35],"texture":0},"down":{"uv":[42,37,37,39],"texture":0}},"type":"cube","uuid":"033aa5bf-e96a-e214-6edd-35746a2e92b5"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.975,23.5,2],"to":[2.975,25,2.75],"autouv":0,"color":0,"origin":[0,24.75,-1],"faces":{"north":{"uv":[32,4,38,6],"texture":0},"east":{"uv":[39,63,40,65],"texture":0},"south":{"uv":[31,33,37,35],"texture":0},"west":{"uv":[42,63,43,65],"texture":0},"up":{"uv":[55,44,49,43],"texture":0},"down":{"uv":[59,29,53,30],"texture":0}},"type":"cube","uuid":"8fe284b0-23bf-a450-600b-44047d4f2b61"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.225,21.7,1.75],"to":[2.225,23.7,2.5],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[0,22.95,2.125],"faces":{"north":{"uv":[46,0,50,2],"texture":0},"east":{"uv":[44,63,45,65],"texture":0},"south":{"uv":[46,2,50,4],"texture":0},"west":{"uv":[59,63,60,65],"texture":0},"up":{"uv":[63,8,59,7],"texture":0},"down":{"uv":[63,13,59,14],"texture":0}},"type":"cube","uuid":"7ca2ade1-2f88-677d-8126-849c195f075c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.275,21.15,-1.45],"to":[2.275,22.05,2],"autouv":0,"color":0,"origin":[0,24.75,-1.5],"faces":{"north":{"uv":[55,30,60,31],"texture":0},"east":{"uv":[62,2,65,3],"texture":0},"south":{"uv":[31,55,36,56],"texture":0},"west":{"uv":[62,3,65,4],"texture":0},"up":{"uv":[27,15,22,12],"texture":0},"down":{"uv":[30,15,25,18],"texture":0}},"type":"cube","uuid":"1416e039-1c93-1f55-854c-2ca9f6651c99"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.275,18.55,-1.45],"to":[-0.225,19.3,2],"autouv":0,"color":1,"origin":[0,22,-1.5],"faces":{"north":{"uv":[63,30,65,31],"texture":0},"east":{"uv":[60,49,63,50],"texture":0},"south":{"uv":[31,63,33,64],"texture":0},"west":{"uv":[60,50,63,51],"texture":0},"up":{"uv":[54,6,52,3],"texture":0},"down":{"uv":[6,52,4,55],"texture":0}},"type":"cube","uuid":"15fe5bf8-7f4f-e9db-baa8-6822a6077ce7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.225,18.55,-1.45],"to":[2.275,19.3,2],"autouv":0,"color":1,"origin":[0,22,-1.5],"faces":{"north":{"uv":[63,26,65,27],"texture":0},"east":{"uv":[60,34,63,35],"texture":0},"south":{"uv":[63,27,65,28],"texture":0},"west":{"uv":[60,35,63,36],"texture":0},"up":{"uv":[54,3,52,0],"texture":0},"down":{"uv":[4,52,2,55],"texture":0}},"type":"cube","uuid":"255c4dc9-0053-0fed-0d04-3030dc27ee40"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.025,19.3,-1.45],"to":[-0.225,21.15,2],"autouv":0,"color":1,"origin":[0,24,-1.5],"faces":{"north":{"uv":[59,9,61,11],"texture":0},"east":{"uv":[52,39,55,41],"texture":0},"south":{"uv":[59,11,61,13],"texture":0},"west":{"uv":[10,53,13,55],"texture":0},"up":{"uv":[8,56,6,53],"texture":0},"down":{"uv":[21,53,19,56],"texture":0}},"type":"cube","uuid":"b4336081-325e-dbfb-9cad-39c227e78c67"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.225,18.55,-1.35],"to":[0.225,21.15,1.9],"autouv":0,"color":1,"origin":[0,24,-1.5],"faces":{"north":{"uv":[61,37,62,40],"texture":0},"east":{"uv":[41,20,44,23],"texture":0},"south":{"uv":[40,61,41,64],"texture":0},"west":{"uv":[41,23,44,26],"texture":0},"up":{"uv":[62,43,61,40],"texture":0},"down":{"uv":[42,61,41,64],"texture":0}},"type":"cube","uuid":"f81abf97-3003-da0c-7fa2-018be5cd9b8d"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.225,19.3,-1.45],"to":[2.025,21.15,2],"autouv":0,"color":1,"origin":[0,24,-1.5],"faces":{"north":{"uv":[27,58,29,60],"texture":0},"east":{"uv":[52,8,55,10],"texture":0},"south":{"uv":[29,58,31,60],"texture":0},"west":{"uv":[52,10,55,12],"texture":0},"up":{"uv":[15,55,13,52],"texture":0},"down":{"uv":[17,52,15,55],"texture":0}},"type":"cube","uuid":"6997413b-4ced-3c9b-d695-66afa5722357"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.65,21.75,-1.45],"to":[1.825,22.075,2],"autouv":0,"color":1,"rotation":[0,0,-45],"origin":[0.925,21.075,0.025],"faces":{"north":{"uv":[65,64,66,65],"texture":0},"east":{"uv":[61,5,64,6],"texture":0},"south":{"uv":[65,65,66,66],"texture":0},"west":{"uv":[61,6,64,7],"texture":0},"up":{"uv":[62,17,61,14],"texture":0},"down":{"uv":[62,17,61,20],"texture":0}},"type":"cube","uuid":"6d49082a-c952-04f5-6a33-5cf53b88d970"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.825,21.75,-1.45],"to":[-1.65,22.075,2],"autouv":0,"color":1,"rotation":[0,0,45],"origin":[-0.925,21.075,0.025],"faces":{"north":{"uv":[2,66,3,67],"texture":0},"east":{"uv":[61,28,64,29],"texture":0},"south":{"uv":[66,2,67,3],"texture":0},"west":{"uv":[61,29,64,30],"texture":0},"up":{"uv":[31,64,30,61],"texture":0},"down":{"uv":[62,31,61,34],"texture":0}},"type":"cube","uuid":"531af403-a2fa-98db-bb4e-16441ceab62e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.65,18.375,-1.45],"to":[1.825,18.7,2],"autouv":0,"color":1,"rotation":[0,0,45],"origin":[0.925,19.375,0.025],"faces":{"north":{"uv":[1,66,2,67],"texture":0},"east":{"uv":[61,24,64,25],"texture":0},"south":{"uv":[66,1,67,2],"texture":0},"west":{"uv":[61,25,64,26],"texture":0},"up":{"uv":[29,64,28,61],"texture":0},"down":{"uv":[30,61,29,64],"texture":0}},"type":"cube","uuid":"4225da25-6240-50c1-7d61-035b7cd5d4ef"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.825,18.375,-1.45],"to":[-1.65,18.7,2],"autouv":0,"color":1,"rotation":[0,0,-45],"origin":[-0.925,19.375,0.025],"faces":{"north":{"uv":[0,66,1,67],"texture":0},"east":{"uv":[61,12,64,13],"texture":0},"south":{"uv":[66,0,67,1],"texture":0},"west":{"uv":[61,23,64,24],"texture":0},"up":{"uv":[22,64,21,61],"texture":0},"down":{"uv":[23,61,22,64],"texture":0}},"type":"cube","uuid":"87ccc294-b6b9-e5b8-72dc-6cbcefb36497"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.025,22.575,-1.175],"to":[5.425,25.075,1.675],"autouv":0,"color":4,"rotation":[0,0,-45],"origin":[3.925,24.075,0.25],"faces":{"north":{"uv":[43,3,46,6],"texture":0},"east":{"uv":[13,43,16,46],"texture":0},"south":{"uv":[25,43,28,46],"texture":0},"west":{"uv":[28,43,31,46],"texture":0},"up":{"uv":[34,46,31,43],"texture":0},"down":{"uv":[37,43,34,46],"texture":0}},"type":"cube","uuid":"49960836-36d8-fde1-6010-119a680e1792"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.925,21.97145,-1.15],"to":[4.925,22.97145,1.65],"autouv":0,"color":4,"origin":[3.925,21.72145,0.25],"faces":{"north":{"uv":[64,41,66,42],"texture":0},"east":{"uv":[62,31,65,32],"texture":0},"south":{"uv":[64,42,66,43],"texture":0},"west":{"uv":[62,32,65,33],"texture":0},"up":{"uv":[56,20,54,17],"texture":0},"down":{"uv":[56,20,54,23],"texture":0}},"type":"cube","uuid":"70921fe4-f2c5-95e6-5dab-9ba9df357552"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.675,22.34645,-1.15],"to":[4.775,24.24645,1.65],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[3.925,21.72145,0.25],"faces":{"north":{"uv":[57,59,59,61],"texture":0},"east":{"uv":[54,3,57,5],"texture":0},"south":{"uv":[59,57,61,59],"texture":0},"west":{"uv":[54,5,57,7],"texture":0},"up":{"uv":[10,57,8,54],"texture":0},"down":{"uv":[56,14,54,17],"texture":0}},"type":"cube","uuid":"be833fd0-a635-7db7-63ec-8b3555252f2c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.525,18.47145,-0.15],"to":[4.575,22.47145,0.9],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[3.925,20.72145,0.25],"faces":{"north":{"uv":[50,59,51,63],"texture":0},"east":{"uv":[59,59,60,63],"texture":0},"south":{"uv":[2,60,3,64],"texture":0},"west":{"uv":[3,60,4,64],"texture":0},"up":{"uv":[68,27,67,26],"texture":0},"down":{"uv":[28,67,27,68],"texture":0}},"type":"cube","uuid":"b098efed-8cd2-8935-e65b-ec57de1527a6"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.075,21.09645,-0.2],"to":[4.525,22.84645,0.725],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[3.925,21.72145,0.25],"faces":{"north":{"uv":[43,64,44,66],"texture":0},"east":{"uv":[64,43,65,45],"texture":0},"south":{"uv":[45,64,46,66],"texture":0},"west":{"uv":[64,45,65,47],"texture":0},"up":{"uv":[68,31,67,30],"texture":0},"down":{"uv":[32,67,31,68],"texture":0}},"type":"cube","uuid":"cf7539d4-1cb6-0d62-d9d7-3a5537399b0e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.76264,35.55,-1.97375],"to":[1.07736,36.27,2.40625],"autouv":0,"color":9,"origin":[1.69736,35.71,0.21625],"faces":{"north":{"uv":[62,61,64,62],"texture":0},"east":{"uv":[31,54,36,55],"texture":0},"south":{"uv":[62,62,64,63],"texture":0},"west":{"uv":[36,54,41,55],"texture":0},"up":{"uv":[31,40,29,35],"texture":0},"down":{"uv":[33,35,31,40],"texture":0}},"type":"cube","uuid":"da9a971e-d1c8-574f-1c0c-e63d98c1e9f6"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.71736,34.83,-1.97375],"to":[2.33736,35.55,2.40625],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[2.67736,34.99,0.21625],"faces":{"north":{"uv":[63,0,65,1],"texture":0},"east":{"uv":[54,46,59,47],"texture":0},"south":{"uv":[63,1,65,2],"texture":0},"west":{"uv":[55,8,60,9],"texture":0},"up":{"uv":[35,40,33,35],"texture":0},"down":{"uv":[37,35,35,40],"texture":0}},"type":"cube","uuid":"3f8a8ca3-5437-215f-de7e-08e69187015c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.41736,32.17,0.60625],"to":[4.21736,33.73,2.40625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[2.48736,33.13,1.40625],"faces":{"north":{"uv":[24,56,26,58],"texture":0},"east":{"uv":[26,56,28,58],"texture":0},"south":{"uv":[28,56,30,58],"texture":0},"west":{"uv":[30,56,32,58],"texture":0},"up":{"uv":[34,58,32,56],"texture":0},"down":{"uv":[36,56,34,58],"texture":0}},"type":"cube","uuid":"c4478cd8-3191-c3b4-69cf-3377cc78af98"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.59736,31.07,-1.79375],"to":[4.39736,32.39,0.86625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[2.89736,32.11,-0.39375],"faces":{"north":{"uv":[47,55,49,57],"texture":0},"east":{"uv":[6,48,9,50],"texture":0},"south":{"uv":[55,47,57,49],"texture":0},"west":{"uv":[48,6,51,8],"texture":0},"up":{"uv":[50,11,48,8],"texture":0},"down":{"uv":[11,48,9,51],"texture":0}},"type":"cube","uuid":"942cc6a2-71a8-88e6-b9dc-70b575a485e7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.01736,30.81,-1.79375],"to":[4.67736,31.71,0.86625],"autouv":0,"color":9,"origin":[2.89736,32.11,-0.39375],"faces":{"north":{"uv":[62,41,64,42],"texture":0},"east":{"uv":[41,9,44,10],"texture":0},"south":{"uv":[62,42,64,43],"texture":0},"west":{"uv":[59,20,62,21],"texture":0},"up":{"uv":[13,51,11,48],"texture":0},"down":{"uv":[50,11,48,14],"texture":0}},"type":"cube","uuid":"2db4c218-3307-c625-7799-b63ef622617a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.25736,30.89,0.60625],"to":[3.45736,32.19,2.40625],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[1.94736,31.33,1.40625],"faces":{"north":{"uv":[40,56,42,58],"texture":0},"east":{"uv":[42,56,44,58],"texture":0},"south":{"uv":[56,43,58,45],"texture":0},"west":{"uv":[56,49,58,51],"texture":0},"up":{"uv":[53,58,51,56],"texture":0},"down":{"uv":[57,56,55,58],"texture":0}},"type":"cube","uuid":"667ec079-4b84-0569-1687-845a993a638f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.26264,30.29,-0.11375],"to":[-3.26264,31.49,1.28625],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[-3.46264,30.69,0.58625],"faces":{"north":{"uv":[63,13,64,15],"texture":0},"east":{"uv":[45,57,47,59],"texture":0},"south":{"uv":[14,63,15,65],"texture":0},"west":{"uv":[47,57,49,59],"texture":0},"up":{"uv":[16,65,15,63],"texture":0},"down":{"uv":[64,15,63,17],"texture":0}},"type":"cube","uuid":"fabe1f10-892d-dcfe-78dc-3bb27af4eb24"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.42264,28.93,-0.27375],"to":[-3.18264,30.61,1.44625],"autouv":0,"color":5,"origin":[-2.46264,29.93,0.88625],"faces":{"north":{"uv":[57,47,59,49],"texture":0},"east":{"uv":[49,57,51,59],"texture":0},"south":{"uv":[57,51,59,53],"texture":0},"west":{"uv":[53,57,55,59],"texture":0},"up":{"uv":[59,55,57,53],"texture":0},"down":{"uv":[59,55,57,57],"texture":0}},"type":"cube","uuid":"831b1360-4fcf-59d3-82d5-df3af6f87c87"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.02264,28.73,0.12625],"to":[-3.22264,29.13,1.04625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[-3.62142,28.43,0.58503],"faces":{"north":{"uv":[38,65,39,66],"texture":0},"east":{"uv":[39,65,40,66],"texture":0},"south":{"uv":[65,39,66,40],"texture":0},"west":{"uv":[65,40,66,41],"texture":0},"up":{"uv":[43,66,42,65],"texture":0},"down":{"uv":[66,43,65,44],"texture":0}},"type":"cube","uuid":"107ef96f-b460-6a0c-a9e2-31d74b77b79a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.06142,27.13,0.14332],"to":[-3.18142,28.73,1.02332],"autouv":0,"color":5,"rotation":[0,-45,22.5],"origin":[-3.62142,28.43,0.58503],"faces":{"north":{"uv":[16,63,17,65],"texture":0},"east":{"uv":[17,63,18,65],"texture":0},"south":{"uv":[63,17,64,19],"texture":0},"west":{"uv":[18,63,19,65],"texture":0},"up":{"uv":[66,48,65,47],"texture":0},"down":{"uv":[49,65,48,66],"texture":0}},"type":"cube","uuid":"ece00d0a-b988-98b1-0ce7-c89cef99cdba"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.925,18.04645,-0.4],"to":[4.725,18.54645,0.9],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.075,18.54645,0.25],"faces":{"north":{"uv":[67,31,68,32],"texture":0},"east":{"uv":[32,67,33,68],"texture":0},"south":{"uv":[67,32,68,33],"texture":0},"west":{"uv":[33,67,34,68],"texture":0},"up":{"uv":[68,34,67,33],"texture":0},"down":{"uv":[35,67,34,68],"texture":0}},"type":"cube","uuid":"71dd0d2e-1f35-9c20-f0e4-e7243ef62c59"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.75,17.25,-1.7],"to":[1.75,19.05,2.25],"autouv":0,"color":0,"origin":[0,16.5,-0.5],"faces":{"north":{"uv":[16,44,20,46],"texture":0},"east":{"uv":[44,16,48,18],"texture":0},"south":{"uv":[44,18,48,20],"texture":0},"west":{"uv":[44,20,48,22],"texture":0},"up":{"uv":[16,27,12,23],"texture":0},"down":{"uv":[28,7,24,11],"texture":0}},"type":"cube","uuid":"9eacb6d9-6979-c722-5a5a-7f3f755d162d"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.65,12.65,-1.95],"to":[3.65,19.05,2.5],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[2.15,17,-0.5],"faces":{"north":{"uv":[6,21,9,27],"texture":0},"east":{"uv":[13,0,17,6],"texture":0},"south":{"uv":[9,21,12,27],"texture":0},"west":{"uv":[0,14,4,20],"texture":0},"up":{"uv":[28,35,25,31],"texture":0},"down":{"uv":[31,31,28,35],"texture":0}},"type":"cube","uuid":"ee8190eb-8d0c-759e-fb79-bcfac04aaa59"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,16.25,-2.45],"to":[1.75,17.8,3],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[0,18.4,0.275],"faces":{"north":{"uv":[29,27,35,29],"texture":0},"east":{"uv":[36,17,41,19],"texture":0},"south":{"uv":[30,23,36,25],"texture":0},"west":{"uv":[36,19,41,21],"texture":0},"up":{"uv":[13,5,7,0],"texture":0},"down":{"uv":[13,5,7,10],"texture":0}},"type":"cube","uuid":"ecea20f4-a55d-fe20-ac80-243b297f225d"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.675,13,-1.45],"to":[1.725,16.8,2],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[0,15.9,0.275],"faces":{"north":{"uv":[9,30,12,34],"texture":0},"east":{"uv":[12,30,15,34],"texture":0},"south":{"uv":[30,15,33,19],"texture":0},"west":{"uv":[30,19,33,23],"texture":0},"up":{"uv":[44,29,41,26],"texture":0},"down":{"uv":[6,42,3,45],"texture":0}},"type":"cube","uuid":"467613ba-eae9-d8c5-c500-764d19b63bdb"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.575,12.5,-1.95],"to":[3.65,14.3,2.5],"autouv":0,"color":5,"rotation":[0,0,-22.5],"origin":[2.15,12.25,-0.5],"faces":{"north":{"uv":[52,37,55,39],"texture":0},"east":{"uv":[44,26,48,28],"texture":0},"south":{"uv":[38,52,41,54],"texture":0},"west":{"uv":[42,44,46,46],"texture":0},"up":{"uv":[36,23,33,19],"texture":0},"down":{"uv":[25,33,22,37],"texture":0}},"type":"cube","uuid":"c6a80580-5139-f0a6-9f30-9ade3c37c5dc"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.96815,13.21336,-1.95],"to":[7.21815,13.91336,2.5],"autouv":0,"color":5,"rotation":[0,0,45],"origin":[5.86815,10.96336,0.275],"faces":{"north":{"uv":[63,35,65,36],"texture":0},"east":{"uv":[58,36,62,37],"texture":0},"south":{"uv":[47,63,49,64],"texture":0},"west":{"uv":[39,58,43,59],"texture":0},"up":{"uv":[22,48,20,44],"texture":0},"down":{"uv":[39,44,37,48],"texture":0}},"type":"cube","uuid":"4b3e6c3f-287d-2368-a998-c33d81b96ee4"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.65,12.65,-1.95],"to":[-0.65,19.05,2.5],"autouv":0,"color":5,"rotation":[0,0,-22.5],"origin":[-2.15,17,-0.5],"faces":{"north":{"uv":[19,21,22,27],"texture":0},"east":{"uv":[4,14,8,20],"texture":0},"south":{"uv":[22,21,25,27],"texture":0},"west":{"uv":[14,6,18,12],"texture":0},"up":{"uv":[36,19,33,15],"texture":0},"down":{"uv":[22,33,19,37],"texture":0}},"type":"cube","uuid":"74f23901-ff08-52ac-dc12-6f1ee74b946a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.21815,13.21336,-1.95],"to":[-4.96815,13.91336,2.5],"autouv":0,"color":5,"rotation":[0,0,-45],"origin":[-5.86815,10.96336,0.275],"faces":{"north":{"uv":[33,63,35,64],"texture":0},"east":{"uv":[56,45,60,46],"texture":0},"south":{"uv":[63,34,65,35],"texture":0},"west":{"uv":[57,6,61,7],"texture":0},"up":{"uv":[2,48,0,44],"texture":0},"down":{"uv":[12,44,10,48],"texture":0}},"type":"cube","uuid":"a66a23f5-34f4-0e99-bfd4-9943ea997a8f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.65,12.5,-1.95],"to":[-0.575,14.3,2.5],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[-2.15,12.25,-0.5],"faces":{"north":{"uv":[52,12,55,14],"texture":0},"east":{"uv":[44,22,48,24],"texture":0},"south":{"uv":[21,52,24,54],"texture":0},"west":{"uv":[44,24,48,26],"texture":0},"up":{"uv":[34,33,31,29],"texture":0},"down":{"uv":[35,0,32,4],"texture":0}},"type":"cube","uuid":"59a35e30-c471-c0d5-ff54-b604450c2cce"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.925,10,-0.55],"to":[2.625,14,1.15],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[1.65,11.25,0.425],"faces":{"north":{"uv":[45,32,47,36],"texture":0},"east":{"uv":[45,36,47,40],"texture":0},"south":{"uv":[39,45,41,49],"texture":0},"west":{"uv":[45,40,47,44],"texture":0},"up":{"uv":[2,61,0,59],"texture":0},"down":{"uv":[61,2,59,4],"texture":0}},"type":"cube","uuid":"29e89ffb-c9f6-6591-85c4-4dc2d8d97531"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.675,9.075,-1.525],"to":[2.625,10,1.4],"autouv":0,"color":4,"origin":[-0.25,13.4,0.675],"faces":{"north":{"uv":[64,5,66,6],"texture":0},"east":{"uv":[61,55,64,56],"texture":0},"south":{"uv":[64,6,66,7],"texture":0},"west":{"uv":[61,56,64,57],"texture":0},"up":{"uv":[49,55,47,52],"texture":0},"down":{"uv":[51,52,49,55],"texture":0}},"type":"cube","uuid":"448df43a-1136-018d-c5ee-785cea750b64"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.675,10.2,-1.5],"to":[2.625,10.725,0.25],"autouv":0,"color":4,"rotation":[-22.5,0,0],"origin":[1.65,10.225,0.35],"faces":{"north":{"uv":[64,12,66,13],"texture":0},"east":{"uv":[64,13,66,14],"texture":0},"south":{"uv":[64,14,66,15],"texture":0},"west":{"uv":[64,15,66,16],"texture":0},"up":{"uv":[61,6,59,4],"texture":0},"down":{"uv":[10,59,8,61],"texture":0}},"type":"cube","uuid":"f7aa46f3-7631-0bc0-28bb-f612ea592aee"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.475,9.425,-1.55],"to":[2.9,10.425,1.4],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[1.925,9.925,0.3],"faces":{"north":{"uv":[37,66,38,67],"texture":0},"east":{"uv":[61,57,64,58],"texture":0},"south":{"uv":[66,37,67,38],"texture":0},"west":{"uv":[61,58,64,59],"texture":0},"up":{"uv":[55,64,54,61],"texture":0},"down":{"uv":[58,61,57,64],"texture":0}},"type":"cube","uuid":"3d9603af-d62c-a46e-fbfb-abf81268a2d9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.4,9.425,-1.55],"to":[0.825,10.425,1.4],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[1.375,9.925,0.3],"faces":{"north":{"uv":[38,66,39,67],"texture":0},"east":{"uv":[61,59,64,60],"texture":0},"south":{"uv":[66,38,67,39],"texture":0},"west":{"uv":[61,60,64,61],"texture":0},"up":{"uv":[59,64,58,61],"texture":0},"down":{"uv":[62,61,61,64],"texture":0}},"type":"cube","uuid":"32e01dc5-5dae-1c4c-b1be-35eb3a079515"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[1.275,9.075,-2.6],"to":[2.55,10,-1.325],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[2.275,9.5375,-1.45],"faces":{"north":{"uv":[34,66,35,67],"texture":0},"east":{"uv":[66,34,67,35],"texture":0},"south":{"uv":[35,66,36,67],"texture":0},"west":{"uv":[66,35,67,36],"texture":0},"up":{"uv":[37,67,36,66],"texture":0},"down":{"uv":[67,36,66,37],"texture":0}},"type":"cube","uuid":"4db14d3d-0f93-84c0-5625-5298d979eb41"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.675,9.7,1.125],"to":[2.625,10.55,1.45],"autouv":0,"color":4,"rotation":[22.5,0,0],"origin":[1.65,9.5375,-0.075],"faces":{"north":{"uv":[64,7,66,8],"texture":0},"east":{"uv":[33,66,34,67],"texture":0},"south":{"uv":[8,64,10,65],"texture":0},"west":{"uv":[66,33,67,34],"texture":0},"up":{"uv":[66,9,64,8],"texture":0},"down":{"uv":[12,64,10,65],"texture":0}},"type":"cube","uuid":"50f40905-4d6d-7098-57ec-25bb1db92c9a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.625,10,-0.55],"to":[-0.925,14,1.15],"autouv":0,"color":4,"rotation":[0,-45,0],"origin":[-1.65,11.25,0.425],"faces":{"north":{"uv":[2,45,4,49],"texture":0},"east":{"uv":[4,45,6,49],"texture":0},"south":{"uv":[22,45,24,49],"texture":0},"west":{"uv":[45,28,47,32],"texture":0},"up":{"uv":[60,45,58,43],"texture":0},"down":{"uv":[60,49,58,51],"texture":0}},"type":"cube","uuid":"8c485616-0002-04d6-6cf3-9d32de2c3d01"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.625,9.075,-1.525],"to":[-0.675,10,1.4],"autouv":0,"color":4,"origin":[0.25,13.4,0.675],"faces":{"north":{"uv":[49,63,51,64],"texture":0},"east":{"uv":[61,43,64,44],"texture":0},"south":{"uv":[63,49,65,50],"texture":0},"west":{"uv":[61,44,64,45],"texture":0},"up":{"uv":[19,55,17,52],"texture":0},"down":{"uv":[47,52,45,55],"texture":0}},"type":"cube","uuid":"7b67db12-b5ce-3ac3-bdda-5016a85904b8"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.625,9.7,1.125],"to":[-0.675,10.55,1.45],"autouv":0,"color":4,"rotation":[22.5,0,0],"origin":[-1.65,9.5375,-0.075],"faces":{"north":{"uv":[63,50,65,51],"texture":0},"east":{"uv":[27,66,28,67],"texture":0},"south":{"uv":[51,63,53,64],"texture":0},"west":{"uv":[66,27,67,28],"texture":0},"up":{"uv":[65,53,63,52],"texture":0},"down":{"uv":[65,53,63,54],"texture":0}},"type":"cube","uuid":"91d99b1a-9f51-fadc-5f4a-8ec6ea9fb9ae"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.55,9.075,-2.6],"to":[-1.275,10,-1.325],"autouv":0,"color":4,"rotation":[0,-45,0],"origin":[-2.275,9.5375,-1.45],"faces":{"north":{"uv":[28,66,29,67],"texture":0},"east":{"uv":[66,28,67,29],"texture":0},"south":{"uv":[29,66,30,67],"texture":0},"west":{"uv":[66,29,67,30],"texture":0},"up":{"uv":[31,67,30,66],"texture":0},"down":{"uv":[67,30,66,31],"texture":0}},"type":"cube","uuid":"2280ee81-b151-2526-efae-1722409b0821"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.9,9.425,-1.55],"to":[-2.475,10.425,1.4],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-1.925,9.925,0.3],"faces":{"north":{"uv":[31,66,32,67],"texture":0},"east":{"uv":[61,45,64,46],"texture":0},"south":{"uv":[66,31,67,32],"texture":0},"west":{"uv":[61,46,64,47],"texture":0},"up":{"uv":[46,64,45,61],"texture":0},"down":{"uv":[47,61,46,64],"texture":0}},"type":"cube","uuid":"ea1adedd-fb2e-60ce-3ccd-145bac9176cb"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.825,9.425,-1.55],"to":[-0.4,10.425,1.4],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[-1.375,9.925,0.3],"faces":{"north":{"uv":[32,66,33,67],"texture":0},"east":{"uv":[61,47,64,48],"texture":0},"south":{"uv":[66,32,67,33],"texture":0},"west":{"uv":[61,51,64,52],"texture":0},"up":{"uv":[62,55,61,52],"texture":0},"down":{"uv":[54,61,53,64],"texture":0}},"type":"cube","uuid":"706e638e-8e86-fce7-c6e6-e2789cf10cc5"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.625,10.2,-1.5],"to":[-0.675,10.725,0.25],"autouv":0,"color":4,"rotation":[-22.5,0,0],"origin":[-1.65,10.225,0.35],"faces":{"north":{"uv":[55,63,57,64],"texture":0},"east":{"uv":[62,63,64,64],"texture":0},"south":{"uv":[0,64,2,65],"texture":0},"west":{"uv":[2,64,4,65],"texture":0},"up":{"uv":[53,60,51,58],"texture":0},"down":{"uv":[57,58,55,60],"texture":0}},"type":"cube","uuid":"bdc513a2-6aeb-d4c6-093c-9eebed04c782"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.725,17.69645,-0.4],"to":[5.075,18.59645,0.65],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[3.95,18.54645,0.25],"faces":{"north":{"uv":[67,34,68,35],"texture":0},"east":{"uv":[35,67,36,68],"texture":0},"south":{"uv":[67,35,68,36],"texture":0},"west":{"uv":[36,67,37,68],"texture":0},"up":{"uv":[68,37,67,36],"texture":0},"down":{"uv":[38,67,37,68],"texture":0}},"type":"cube","uuid":"0ea452bb-ba2b-016e-f030-ec3360d55b63"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.81773,17.24898,0.35],"to":[4.56773,17.49898,0.65],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,43,68,44],"texture":0},"east":{"uv":[44,67,45,68],"texture":0},"south":{"uv":[67,44,68,45],"texture":0},"west":{"uv":[45,67,46,68],"texture":0},"up":{"uv":[68,46,67,45],"texture":0},"down":{"uv":[47,67,46,68],"texture":0}},"type":"cube","uuid":"62b9158c-cca3-ecbd-e46c-3407f8d0306e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.81773,17.24898,-0.4],"to":[4.56773,17.49898,-0.1],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,55,68,56],"texture":0},"east":{"uv":[56,67,57,68],"texture":0},"south":{"uv":[67,56,68,57],"texture":0},"west":{"uv":[57,67,58,68],"texture":0},"up":{"uv":[68,58,67,57],"texture":0},"down":{"uv":[59,67,58,68],"texture":0}},"type":"cube","uuid":"5b39d357-c3a2-39d1-1c69-88c062c4ba3a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.81773,17.24898,-0.025],"to":[4.56773,17.49898,0.275],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,58,68,59],"texture":0},"east":{"uv":[59,67,60,68],"texture":0},"south":{"uv":[67,59,68,60],"texture":0},"west":{"uv":[60,67,61,68],"texture":0},"up":{"uv":[68,61,67,60],"texture":0},"down":{"uv":[62,67,61,68],"texture":0}},"type":"cube","uuid":"376ba5a3-f0b4-f973-ed18-3feb313a350f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.81773,17.49898,0.35],"to":[4.09273,17.74898,0.65],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,52,68,53],"texture":0},"east":{"uv":[53,67,54,68],"texture":0},"south":{"uv":[67,53,68,54],"texture":0},"west":{"uv":[54,67,55,68],"texture":0},"up":{"uv":[68,55,67,54],"texture":0},"down":{"uv":[56,67,55,68],"texture":0}},"type":"cube","uuid":"08b8a4dd-9817-f43b-7486-512f7efd2396"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.81773,17.49898,-0.4],"to":[4.09273,17.74898,-0.1],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,49,68,50],"texture":0},"east":{"uv":[50,67,51,68],"texture":0},"south":{"uv":[67,50,68,51],"texture":0},"west":{"uv":[51,67,52,68],"texture":0},"up":{"uv":[68,52,67,51],"texture":0},"down":{"uv":[53,67,52,68],"texture":0}},"type":"cube","uuid":"ba2d481e-f120-fe09-5b95-35e6946f3ef5"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.81773,17.49898,-0.025],"to":[4.09273,17.74898,0.275],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,46,68,47],"texture":0},"east":{"uv":[47,67,48,68],"texture":0},"south":{"uv":[67,47,68,48],"texture":0},"west":{"uv":[48,67,49,68],"texture":0},"up":{"uv":[68,49,67,48],"texture":0},"down":{"uv":[50,67,49,68],"texture":0}},"type":"cube","uuid":"f2c157b6-a525-ba83-2b2b-bcfa39f73d78"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.725,17.69645,0.65],"to":[5.025,18.34645,1.05],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[3.95,18.54645,0.25],"faces":{"north":{"uv":[67,37,68,38],"texture":0},"east":{"uv":[38,67,39,68],"texture":0},"south":{"uv":[67,38,68,39],"texture":0},"west":{"uv":[39,67,40,68],"texture":0},"up":{"uv":[68,40,67,39],"texture":0},"down":{"uv":[41,67,40,68],"texture":0}},"type":"cube","uuid":"56e24b25-c478-848b-1b04-d7ce1e6f032c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.325,17.69645,0.65],"to":[4.725,17.99645,1.05],"autouv":0,"color":4,"rotation":[0,0,-22.5],"origin":[3.95,18.54645,0.25],"faces":{"north":{"uv":[67,40,68,41],"texture":0},"east":{"uv":[41,67,42,68],"texture":0},"south":{"uv":[67,41,68,42],"texture":0},"west":{"uv":[42,67,43,68],"texture":0},"up":{"uv":[68,43,67,42],"texture":0},"down":{"uv":[44,67,43,68],"texture":0}},"type":"cube","uuid":"b36424db-8870-a556-61f4-ec7c1f63a964"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.775,18.22145,-0.15],"to":[4.575,18.47145,0.65],"autouv":0,"color":4,"origin":[3.925,20.72145,0.25],"faces":{"north":{"uv":[67,27,68,28],"texture":0},"east":{"uv":[28,67,29,68],"texture":0},"south":{"uv":[67,28,68,29],"texture":0},"west":{"uv":[29,67,30,68],"texture":0},"up":{"uv":[68,30,67,29],"texture":0},"down":{"uv":[31,67,30,68],"texture":0}},"type":"cube","uuid":"0056ac3d-d6be-eb04-edd0-15cdce2e96a9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.425,22.575,-1.175],"to":[-2.025,25.075,1.675],"autouv":0,"color":4,"rotation":[0,0,45],"origin":[-3.925,24.075,0.25],"faces":{"north":{"uv":[42,32,45,35],"texture":0},"east":{"uv":[42,35,45,38],"texture":0},"south":{"uv":[42,38,45,41],"texture":0},"west":{"uv":[39,42,42,45],"texture":0},"up":{"uv":[45,44,42,41],"texture":0},"down":{"uv":[46,0,43,3],"texture":0}},"type":"cube","uuid":"c2834cb3-01dd-0214-bc6c-5f5265af0476"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.775,22.34645,-1.15],"to":[-2.675,24.24645,1.65],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-3.925,21.72145,0.25],"faces":{"north":{"uv":[45,59,47,61],"texture":0},"east":{"uv":[53,44,56,46],"texture":0},"south":{"uv":[59,46,61,48],"texture":0},"west":{"uv":[53,50,56,52],"texture":0},"up":{"uv":[53,56,51,53],"texture":0},"down":{"uv":[55,52,53,55],"texture":0}},"type":"cube","uuid":"a3e39edd-509f-a6e6-2351-ff79b7a61f6c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.925,21.97145,-1.15],"to":[-2.925,22.97145,1.65],"autouv":0,"color":4,"origin":[-3.925,21.72145,0.25],"faces":{"north":{"uv":[64,25,66,26],"texture":0},"east":{"uv":[62,4,65,5],"texture":0},"south":{"uv":[28,64,30,65],"texture":0},"west":{"uv":[62,20,65,21],"texture":0},"up":{"uv":[2,57,0,54],"texture":0},"down":{"uv":[56,0,54,3],"texture":0}},"type":"cube","uuid":"5dec9d98-097d-f3e9-9827-c2dcd146712f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.575,18.47145,-0.15],"to":[-3.525,22.47145,0.9],"autouv":0,"color":4,"rotation":[0,-45,0],"origin":[-3.925,20.72145,0.25],"faces":{"north":{"uv":[42,59,43,63],"texture":0},"east":{"uv":[47,59,48,63],"texture":0},"south":{"uv":[48,59,49,63],"texture":0},"west":{"uv":[49,59,50,63],"texture":0},"up":{"uv":[59,67,58,66],"texture":0},"down":{"uv":[67,58,66,59],"texture":0}},"type":"cube","uuid":"20c07f3a-08c5-782d-37e8-d229fcfa893a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.575,18.22145,-0.15],"to":[-3.775,18.47145,0.65],"autouv":0,"color":4,"origin":[-3.925,20.72145,0.25],"faces":{"north":{"uv":[59,66,60,67],"texture":0},"east":{"uv":[66,59,67,60],"texture":0},"south":{"uv":[60,66,61,67],"texture":0},"west":{"uv":[66,60,67,61],"texture":0},"up":{"uv":[62,67,61,66],"texture":0},"down":{"uv":[67,61,66,62],"texture":0}},"type":"cube","uuid":"11bb47a0-0738-66fb-7887-f3499e766f41"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.525,21.09645,-0.2],"to":[-4.075,22.84645,0.725],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-3.925,21.72145,0.25],"faces":{"north":{"uv":[34,64,35,66],"texture":0},"east":{"uv":[64,39,65,41],"texture":0},"south":{"uv":[40,64,41,66],"texture":0},"west":{"uv":[41,64,42,66],"texture":0},"up":{"uv":[63,67,62,66],"texture":0},"down":{"uv":[67,62,66,63],"texture":0}},"type":"cube","uuid":"faa9c325-4ca4-5292-9be5-eb74a175eadd"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.725,18.04645,-0.4],"to":[-3.925,18.54645,0.9],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.075,18.54645,0.25],"faces":{"north":{"uv":[63,66,64,67],"texture":0},"east":{"uv":[66,63,67,64],"texture":0},"south":{"uv":[64,66,65,67],"texture":0},"west":{"uv":[66,64,67,65],"texture":0},"up":{"uv":[66,67,65,66],"texture":0},"down":{"uv":[67,65,66,66],"texture":0}},"type":"cube","uuid":"7779802d-844c-1e58-67ba-9d26d4d7aacb"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.075,17.69645,-0.4],"to":[-4.725,18.59645,0.65],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-3.95,18.54645,0.25],"faces":{"north":{"uv":[66,66,67,67],"texture":0},"east":{"uv":[0,67,1,68],"texture":0},"south":{"uv":[67,0,68,1],"texture":0},"west":{"uv":[1,67,2,68],"texture":0},"up":{"uv":[68,2,67,1],"texture":0},"down":{"uv":[3,67,2,68],"texture":0}},"type":"cube","uuid":"e0c86f4e-ff80-ead8-9b92-5246c36f8b34"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.025,17.69645,0.65],"to":[-4.725,18.34645,1.05],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-3.95,18.54645,0.25],"faces":{"north":{"uv":[67,2,68,3],"texture":0},"east":{"uv":[3,67,4,68],"texture":0},"south":{"uv":[67,3,68,4],"texture":0},"west":{"uv":[4,67,5,68],"texture":0},"up":{"uv":[68,5,67,4],"texture":0},"down":{"uv":[6,67,5,68],"texture":0}},"type":"cube","uuid":"9e677dad-9f30-868d-f8a6-b4f1ca772402"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.725,17.69645,0.65],"to":[-4.325,17.99645,1.05],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-3.95,18.54645,0.25],"faces":{"north":{"uv":[67,5,68,6],"texture":0},"east":{"uv":[6,67,7,68],"texture":0},"south":{"uv":[67,6,68,7],"texture":0},"west":{"uv":[7,67,8,68],"texture":0},"up":{"uv":[68,8,67,7],"texture":0},"down":{"uv":[9,67,8,68],"texture":0}},"type":"cube","uuid":"ee55ecef-75f5-6169-4093-ca093fe26167"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.56773,17.24898,0.35],"to":[-3.81773,17.49898,0.65],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,8,68,9],"texture":0},"east":{"uv":[9,67,10,68],"texture":0},"south":{"uv":[67,9,68,10],"texture":0},"west":{"uv":[10,67,11,68],"texture":0},"up":{"uv":[68,11,67,10],"texture":0},"down":{"uv":[12,67,11,68],"texture":0}},"type":"cube","uuid":"b462094a-f846-3cb1-75a7-4432dde1c9a3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.09273,17.49898,-0.025],"to":[-3.81773,17.74898,0.275],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,11,68,12],"texture":0},"east":{"uv":[12,67,13,68],"texture":0},"south":{"uv":[67,12,68,13],"texture":0},"west":{"uv":[13,67,14,68],"texture":0},"up":{"uv":[68,14,67,13],"texture":0},"down":{"uv":[15,67,14,68],"texture":0}},"type":"cube","uuid":"5a8b5a47-d25a-9f4f-c151-22ceffc086f7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.09273,17.49898,-0.4],"to":[-3.81773,17.74898,-0.1],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,14,68,15],"texture":0},"east":{"uv":[15,67,16,68],"texture":0},"south":{"uv":[67,15,68,16],"texture":0},"west":{"uv":[16,67,17,68],"texture":0},"up":{"uv":[68,17,67,16],"texture":0},"down":{"uv":[18,67,17,68],"texture":0}},"type":"cube","uuid":"2873cfd9-8e30-f048-b028-8a08b6605032"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.09273,17.49898,0.35],"to":[-3.81773,17.74898,0.65],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,17,68,18],"texture":0},"east":{"uv":[18,67,19,68],"texture":0},"south":{"uv":[67,18,68,19],"texture":0},"west":{"uv":[19,67,20,68],"texture":0},"up":{"uv":[68,20,67,19],"texture":0},"down":{"uv":[21,67,20,68],"texture":0}},"type":"cube","uuid":"150732da-710a-804b-7ad6-0937f1647292"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.56773,17.24898,-0.4],"to":[-3.81773,17.49898,-0.1],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,20,68,21],"texture":0},"east":{"uv":[21,67,22,68],"texture":0},"south":{"uv":[67,21,68,22],"texture":0},"west":{"uv":[22,67,23,68],"texture":0},"up":{"uv":[68,23,67,22],"texture":0},"down":{"uv":[24,67,23,68],"texture":0}},"type":"cube","uuid":"fb92591b-ca3a-8a99-8628-b52142ad0610"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-4.56773,17.24898,-0.025],"to":[-3.81773,17.49898,0.275],"autouv":0,"color":4,"rotation":[0,0,22.5],"origin":[-4.24273,17.42398,0.125],"faces":{"north":{"uv":[67,23,68,24],"texture":0},"east":{"uv":[24,67,25,68],"texture":0},"south":{"uv":[67,24,68,25],"texture":0},"west":{"uv":[25,67,26,68],"texture":0},"up":{"uv":[68,26,67,25],"texture":0},"down":{"uv":[27,67,26,68],"texture":0}},"type":"cube","uuid":"dd1b6bb9-1371-7468-c811-690929986466"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.80652,28.23002,-1.59375],"to":[3.42652,28.87002,-0.99375],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[3.26652,28.45002,-1.29375],"faces":{"north":{"uv":[3,65,4,66],"texture":0},"east":{"uv":[65,3,66,4],"texture":0},"south":{"uv":[4,65,5,66],"texture":0},"west":{"uv":[65,4,66,5],"texture":0},"up":{"uv":[6,66,5,65],"texture":0},"down":{"uv":[7,65,6,66],"texture":0}},"type":"cube","uuid":"17f99990-ab5b-6c49-5c4e-3498f582fd93"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.6518,29.01002,-1.59375],"to":[-3.0718,29.59002,-0.99375],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[-3.2418,29.18002,-1.29375],"faces":{"north":{"uv":[11,65,12,66],"texture":0},"east":{"uv":[65,11,66,12],"texture":0},"south":{"uv":[12,65,13,66],"texture":0},"west":{"uv":[13,65,14,66],"texture":0},"up":{"uv":[15,66,14,65],"texture":0},"down":{"uv":[16,65,15,66],"texture":0}},"type":"cube","uuid":"9f7d1c1a-c9fd-ebd0-5737-915b7bb6942c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.94264,26.73,0.20625],"to":[-3.30264,27.13,0.96625],"autouv":0,"color":9,"rotation":[0,0,22.5],"origin":[-3.62142,28.43,0.58503],"faces":{"north":{"uv":[44,65,45,66],"texture":0},"east":{"uv":[65,44,66,45],"texture":0},"south":{"uv":[65,45,66,46],"texture":0},"west":{"uv":[46,65,47,66],"texture":0},"up":{"uv":[66,47,65,46],"texture":0},"down":{"uv":[48,65,47,66],"texture":0}},"type":"cube","uuid":"55a38668-5660-2226-d6e6-6e3e82332eaf"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.075,19.72145,-0.65],"to":[-3.175,20.22145,1.15],"autouv":0,"color":2,"rotation":[0,0,22.5],"origin":[-4.125,19.97145,0.25],"faces":{"north":{"uv":[64,28,66,29],"texture":0},"east":{"uv":[64,29,66,30],"texture":0},"south":{"uv":[30,64,32,65],"texture":0},"west":{"uv":[32,64,34,65],"texture":0},"up":{"uv":[61,53,59,51],"texture":0},"down":{"uv":[55,59,53,61],"texture":0}},"type":"cube","uuid":"12f8aa93-1559-25fb-8b0c-6458b326b202"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.075,20.97145,-0.65],"to":[-3.175,21.47145,1.15],"autouv":0,"color":2,"rotation":[0,0,22.5],"origin":[-4.125,21.22145,0.25],"faces":{"north":{"uv":[64,33,66,34],"texture":0},"east":{"uv":[64,36,66,37],"texture":0},"south":{"uv":[64,37,66,38],"texture":0},"west":{"uv":[64,38,66,39],"texture":0},"up":{"uv":[61,55,59,53],"texture":0},"down":{"uv":[61,55,59,57],"texture":0}},"type":"cube","uuid":"a65ddbfa-a090-fa94-36f9-a83e90783a60"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.975,21.6,3.175],"to":[1.975,24.6,3.175],"autouv":0,"color":5,"origin":[0,24.35,-0.575],"faces":{"north":{"uv":[4,34,8,37],"texture":0},"east":{"uv":[4,34,8,37],"texture":0},"south":{"uv":[4,34,8,37],"texture":0},"west":{"uv":[4,34,8,37],"texture":0},"up":{"uv":[4,34,8,37],"texture":0},"down":{"uv":[4,34,8,37],"texture":0}},"type":"cube","uuid":"c7fc4bf2-2bdb-6f57-e1d2-1b55fbadc572"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.475,24.1,2],"to":[2.475,24.7,2.75],"autouv":0,"color":5,"rotation":[-45,0,0],"origin":[0,24.85,2.375],"faces":{"north":{"uv":[55,31,60,32],"texture":0},"east":{"uv":[39,66,40,67],"texture":0},"south":{"uv":[55,32,60,33],"texture":0},"west":{"uv":[66,39,67,40],"texture":0},"up":{"uv":[60,34,55,33],"texture":0},"down":{"uv":[60,34,55,35],"texture":0}},"type":"cube","uuid":"064202fa-1ea8-2e3d-08f6-15645a4d4f5c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.475,18.6,3.175],"to":[1.475,21.6,3.175],"autouv":0,"color":5,"origin":[0,21.35,-0.575],"faces":{"north":{"uv":[42,29,45,32],"texture":0},"east":{"uv":[42,29,45,32],"texture":0},"south":{"uv":[42,29,45,32],"texture":0},"west":{"uv":[42,29,45,32],"texture":0},"up":{"uv":[42,29,45,32],"texture":0},"down":{"uv":[42,29,45,32],"texture":0}},"type":"cube","uuid":"d084c28f-189e-764b-538f-cf0537a5c344"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1.225,21.025,3.175],"to":[-0.775,22.3,3.175],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[0.125,20.1,3.175],"faces":{"north":{"uv":[66,40,67,41],"texture":0},"east":{"uv":[66,40,67,41],"texture":0},"south":{"uv":[66,40,67,41],"texture":0},"west":{"uv":[66,40,67,41],"texture":0},"up":{"uv":[66,40,67,41],"texture":0},"down":{"uv":[66,40,67,41],"texture":0}},"type":"cube","uuid":"41d62fcf-9109-d04a-1897-b6d0024546f1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.775,21.025,3.175],"to":[1.225,22.3,3.175],"autouv":0,"color":5,"rotation":[0,0,-22.5],"origin":[-0.125,20.1,3.175],"faces":{"north":{"uv":[66,41,67,42],"texture":0},"east":{"uv":[66,41,67,42],"texture":0},"south":{"uv":[66,41,67,42],"texture":0},"west":{"uv":[66,41,67,42],"texture":0},"up":{"uv":[66,41,67,42],"texture":0},"down":{"uv":[66,41,67,42],"texture":0}},"type":"cube","uuid":"5f6f0613-a341-4bc1-caa8-71c6a4d9f006"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.975,15.6,3.175],"to":[0.975,18.6,3.175],"autouv":0,"color":5,"origin":[0,18.35,-0.575],"faces":{"north":{"uv":[53,30,55,33],"texture":0},"east":{"uv":[53,30,55,33],"texture":0},"south":{"uv":[53,30,55,33],"texture":0},"west":{"uv":[53,30,55,33],"texture":0},"up":{"uv":[53,30,55,33],"texture":0},"down":{"uv":[53,30,55,33],"texture":0}},"type":"cube","uuid":"96cda888-fe8e-9317-9335-171c7a631c52"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.725,18.025,3.175],"to":[-0.275,19.3,3.175],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[0.625,17.1,3.175],"faces":{"north":{"uv":[66,42,67,43],"texture":0},"east":{"uv":[66,42,67,43],"texture":0},"south":{"uv":[66,42,67,43],"texture":0},"west":{"uv":[66,42,67,43],"texture":0},"up":{"uv":[66,42,67,43],"texture":0},"down":{"uv":[66,42,67,43],"texture":0}},"type":"cube","uuid":"b5702594-4dba-0c01-6343-02d2faac24e1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.275,18.025,3.175],"to":[0.725,19.3,3.175],"autouv":0,"color":5,"rotation":[0,0,-22.5],"origin":[-0.625,17.1,3.175],"faces":{"north":{"uv":[66,43,67,44],"texture":0},"east":{"uv":[66,43,67,44],"texture":0},"south":{"uv":[66,43,67,44],"texture":0},"west":{"uv":[66,43,67,44],"texture":0},"up":{"uv":[66,43,67,44],"texture":0},"down":{"uv":[66,43,67,44],"texture":0}},"type":"cube","uuid":"013aa42b-83c2-5f5f-327d-8a74970cdb49"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.475,12.6,3.175],"to":[0.475,15.6,3.175],"autouv":0,"color":5,"origin":[0,15.35,-0.575],"faces":{"north":{"uv":[7,62,8,65],"texture":0},"east":{"uv":[7,62,8,65],"texture":0},"south":{"uv":[7,62,8,65],"texture":0},"west":{"uv":[7,62,8,65],"texture":0},"up":{"uv":[7,62,8,65],"texture":0},"down":{"uv":[7,62,8,65],"texture":0}},"type":"cube","uuid":"8fbc89dc-96ae-1dfb-471a-f121b77bdba1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.225,15.025,3.175],"to":[0.225,16.3,3.175],"autouv":0,"color":5,"rotation":[0,0,22.5],"origin":[1.125,14.1,3.175],"faces":{"north":{"uv":[66,44,67,45],"texture":0},"east":{"uv":[66,44,67,45],"texture":0},"south":{"uv":[66,44,67,45],"texture":0},"west":{"uv":[66,44,67,45],"texture":0},"up":{"uv":[66,44,67,45],"texture":0},"down":{"uv":[66,44,67,45],"texture":0}},"type":"cube","uuid":"d1867ca7-e2c9-2a22-d67d-6a3117fef579"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-0.225,15.025,3.175],"to":[0.225,16.3,3.175],"autouv":0,"color":5,"rotation":[0,0,-22.5],"origin":[-1.125,14.1,3.175],"faces":{"north":{"uv":[66,45,67,46],"texture":0},"east":{"uv":[66,45,67,46],"texture":0},"south":{"uv":[66,45,67,46],"texture":0},"west":{"uv":[66,45,67,46],"texture":0},"up":{"uv":[66,45,67,46],"texture":0},"down":{"uv":[66,45,67,46],"texture":0}},"type":"cube","uuid":"f3b7b5ac-f8dd-ee5a-2837-8b886bc89be3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.82264,31.77,-1.11375],"to":[2.95736,33.79,-0.95375],"autouv":0,"color":1,"origin":[0.30736,33.11,-1.15375],"faces":{"north":{"uv":[8,14,15,17],"texture":0},"east":{"uv":[22,39,23,42],"texture":0},"south":{"uv":[15,12,22,15],"texture":0},"west":{"uv":[4,60,5,63],"texture":0},"up":{"uv":[53,45,46,44],"texture":0},"down":{"uv":[53,45,46,46],"texture":0}},"type":"cube","uuid":"2c82fef6-2f1c-e5d9-a1b0-10313da9a8cd"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,11.49545,-1.45],"to":[-5.2322,13.49545,2],"autouv":0,"color":5,"origin":[-5.2322,10.99545,0.275],"faces":{"north":{"uv":[35,52,38,54],"texture":null},"east":{"uv":[35,52,38,54],"texture":0},"south":{"uv":[35,52,38,54],"texture":null},"west":{"uv":[35,52,38,54],"texture":0},"up":{"uv":[35,52,38,54],"texture":null},"down":{"uv":[35,52,38,54],"texture":null}},"type":"cube","uuid":"9aaec269-21fa-fc94-f584-04cf13144345"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,12.92045,-0.8],"to":[-5.2322,14.17045,-0.35],"autouv":0,"color":5,"rotation":[-22.5,0,0],"origin":[-5.2322,10.99545,0.275],"faces":{"north":{"uv":[66,21,67,22],"texture":0},"east":{"uv":[66,21,67,22],"texture":0},"south":{"uv":[66,21,67,22],"texture":0},"west":{"uv":[66,21,67,22],"texture":0},"up":{"uv":[66,21,67,22],"texture":0},"down":{"uv":[66,21,67,22],"texture":0}},"type":"cube","uuid":"5500625f-17c2-64b1-406a-697250abf2ef"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,12.92045,0.875],"to":[-5.2322,14.17045,1.325],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[-5.2322,10.99545,0.25],"faces":{"north":{"uv":[0,0,0,1],"texture":0},"east":{"uv":[22,66,23,67],"texture":0},"south":{"uv":[0,0,0,1],"texture":0},"west":{"uv":[66,22,67,23],"texture":0},"up":{"uv":[0,1,0,0],"texture":0},"down":{"uv":[0,0,0,1],"texture":0}},"type":"cube","uuid":"31163dd6-e25e-d315-3db0-b755cb62a140"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,9.49545,-0.95],"to":[-5.2322,11.49545,1.5],"autouv":0,"color":5,"origin":[-5.2322,8.99545,0.275],"faces":{"north":{"uv":[37,58,39,60],"texture":0},"east":{"uv":[37,58,39,60],"texture":0},"south":{"uv":[37,58,39,60],"texture":0},"west":{"uv":[37,58,39,60],"texture":0},"up":{"uv":[37,58,39,60],"texture":0},"down":{"uv":[37,58,39,60],"texture":0}},"type":"cube","uuid":"1e8fb086-03d6-eabd-c193-db916605a3c7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,10.92045,-0.3],"to":[-5.2322,12.17045,0.15],"autouv":0,"color":5,"rotation":[-22.5,0,0],"origin":[-5.2322,8.99545,0.775],"faces":{"north":{"uv":[66,23,67,24],"texture":0},"east":{"uv":[66,23,67,24],"texture":0},"south":{"uv":[66,23,67,24],"texture":0},"west":{"uv":[66,23,67,24],"texture":0},"up":{"uv":[66,23,67,24],"texture":0},"down":{"uv":[66,23,67,24],"texture":0}},"type":"cube","uuid":"a9e565b5-91ac-43ec-b6e8-fb95376fe40f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,10.92045,0.375],"to":[-5.2322,12.17045,0.825],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[-5.2322,8.99545,-0.25],"faces":{"north":{"uv":[66,24,67,25],"texture":0},"east":{"uv":[66,24,67,25],"texture":0},"south":{"uv":[66,24,67,25],"texture":0},"west":{"uv":[66,24,67,25],"texture":0},"up":{"uv":[66,24,67,25],"texture":0},"down":{"uv":[66,24,67,25],"texture":0}},"type":"cube","uuid":"e7ef1199-ed3e-93ee-adad-e9230cad7d6c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,7.49545,-0.45],"to":[-5.2322,9.49545,1],"autouv":0,"color":5,"origin":[-5.2322,6.99545,0.275],"faces":{"north":{"uv":[38,63,39,65],"texture":0},"east":{"uv":[38,63,39,65],"texture":0},"south":{"uv":[38,63,39,65],"texture":0},"west":{"uv":[38,63,39,65],"texture":0},"up":{"uv":[38,63,39,65],"texture":0},"down":{"uv":[38,63,39,65],"texture":0}},"type":"cube","uuid":"b2b3fdbe-0de5-84ae-df96-288f90b79d94"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,8.92045,0.2],"to":[-5.2322,10.17045,0.65],"autouv":0,"color":5,"rotation":[-22.5,0,0],"origin":[-5.2322,6.99545,1.275],"faces":{"north":{"uv":[66,25,67,26],"texture":0},"east":{"uv":[66,25,67,26],"texture":0},"south":{"uv":[66,25,67,26],"texture":0},"west":{"uv":[66,25,67,26],"texture":0},"up":{"uv":[66,25,67,26],"texture":0},"down":{"uv":[66,25,67,26],"texture":0}},"type":"cube","uuid":"91fa364d-41e6-5c82-2bfa-1cc1570fd514"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5.2322,8.92045,-0.125],"to":[-5.2322,10.17045,0.325],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[-5.2322,6.99545,-0.75],"faces":{"north":{"uv":[66,26,67,27],"texture":0},"east":{"uv":[66,26,67,27],"texture":0},"south":{"uv":[66,26,67,27],"texture":0},"west":{"uv":[66,26,67,27],"texture":0},"up":{"uv":[66,26,67,27],"texture":0},"down":{"uv":[66,26,67,27],"texture":0}},"type":"cube","uuid":"f6378b2a-8e1f-a751-beda-9e1f6feda543"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,11.49545,-1.45],"to":[5.2322,13.49545,2],"autouv":0,"color":5,"origin":[5.2322,10.99545,0.275],"faces":{"north":{"uv":[26,52,29,54],"texture":null},"east":{"uv":[26,52,29,54],"texture":0},"south":{"uv":[26,52,29,54],"texture":null},"west":{"uv":[26,52,29,54],"texture":0},"up":{"uv":[26,52,29,54],"texture":null},"down":{"uv":[26,52,29,54],"texture":null}},"type":"cube","uuid":"6afb3580-9d2a-a800-44e0-7ad32af91987"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,12.92045,-0.8],"to":[5.2322,14.17045,-0.35],"autouv":0,"color":5,"rotation":[-22.5,0,0],"origin":[5.2322,10.99545,0.275],"faces":{"north":{"uv":[15,66,16,67],"texture":0},"east":{"uv":[15,66,16,67],"texture":0},"south":{"uv":[15,66,16,67],"texture":0},"west":{"uv":[15,66,16,67],"texture":0},"up":{"uv":[15,66,16,67],"texture":0},"down":{"uv":[15,66,16,67],"texture":0}},"type":"cube","uuid":"e885a9ed-6555-231f-5a14-7d4f6dfc8839"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,12.92045,0.875],"to":[5.2322,14.17045,1.325],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[5.2322,10.99545,0.25],"faces":{"north":{"uv":[16,66,17,67],"texture":0},"east":{"uv":[16,66,17,67],"texture":0},"south":{"uv":[16,66,17,67],"texture":0},"west":{"uv":[16,66,17,67],"texture":0},"up":{"uv":[16,66,17,67],"texture":0},"down":{"uv":[16,66,17,67],"texture":0}},"type":"cube","uuid":"5e4c7faf-af00-d46f-ef74-f7616ec8b21d"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,9.49545,-0.95],"to":[5.2322,11.49545,1.5],"autouv":0,"color":5,"origin":[5.2322,8.99545,0.275],"faces":{"north":{"uv":[31,58,33,60],"texture":0},"east":{"uv":[31,58,33,60],"texture":0},"south":{"uv":[31,58,33,60],"texture":0},"west":{"uv":[31,58,33,60],"texture":0},"up":{"uv":[31,58,33,60],"texture":0},"down":{"uv":[31,58,33,60],"texture":0}},"type":"cube","uuid":"179fd506-fced-8fc5-f228-b39dfd836eab"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,10.92045,-0.3],"to":[5.2322,12.17045,0.15],"autouv":0,"color":5,"rotation":[-22.5,0,0],"origin":[5.2322,8.99545,0.775],"faces":{"north":{"uv":[17,66,18,67],"texture":0},"east":{"uv":[17,66,18,67],"texture":0},"south":{"uv":[17,66,18,67],"texture":0},"west":{"uv":[17,66,18,67],"texture":0},"up":{"uv":[17,66,18,67],"texture":0},"down":{"uv":[17,66,18,67],"texture":0}},"type":"cube","uuid":"dcc13d2a-e8b4-9cf2-fb6a-1222d2c42980"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,10.92045,0.375],"to":[5.2322,12.17045,0.825],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[5.2322,8.99545,-0.25],"faces":{"north":{"uv":[18,66,19,67],"texture":0},"east":{"uv":[18,66,19,67],"texture":0},"south":{"uv":[18,66,19,67],"texture":0},"west":{"uv":[18,66,19,67],"texture":0},"up":{"uv":[18,66,19,67],"texture":0},"down":{"uv":[18,66,19,67],"texture":0}},"type":"cube","uuid":"9ff9fa0a-03b9-b05d-2344-8fc7bef11e46"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,7.49545,-0.45],"to":[5.2322,9.49545,1],"autouv":0,"color":5,"origin":[5.2322,6.99545,0.275],"faces":{"north":{"uv":[35,63,36,65],"texture":0},"east":{"uv":[35,63,36,65],"texture":0},"south":{"uv":[35,63,36,65],"texture":0},"west":{"uv":[35,63,36,65],"texture":0},"up":{"uv":[35,63,36,65],"texture":0},"down":{"uv":[35,63,36,65],"texture":0}},"type":"cube","uuid":"60ac24f4-f0d5-2709-8498-d5929693e212"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,8.92045,0.2],"to":[5.2322,10.17045,0.65],"autouv":0,"color":5,"rotation":[-22.5,0,0],"origin":[5.2322,6.99545,1.275],"faces":{"north":{"uv":[19,66,20,67],"texture":0},"east":{"uv":[19,66,20,67],"texture":0},"south":{"uv":[19,66,20,67],"texture":0},"west":{"uv":[19,66,20,67],"texture":0},"up":{"uv":[19,66,20,67],"texture":0},"down":{"uv":[19,66,20,67],"texture":0}},"type":"cube","uuid":"120acc01-548e-637c-0f2f-6fc565133c9a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2322,8.92045,-0.125],"to":[5.2322,10.17045,0.325],"autouv":0,"color":5,"rotation":[22.5,0,0],"origin":[5.2322,6.99545,-0.75],"faces":{"north":{"uv":[20,66,21,67],"texture":0},"east":{"uv":[20,66,21,67],"texture":0},"south":{"uv":[20,66,21,67],"texture":0},"west":{"uv":[20,66,21,67],"texture":0},"up":{"uv":[20,66,21,67],"texture":0},"down":{"uv":[20,66,21,67],"texture":0}},"type":"cube","uuid":"3698deba-f32c-bf34-b848-ba738866e0fc"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-2.75,11.5,2.25],"to":[2.75,13.1,2.25],"autouv":0,"color":5,"origin":[1,13.65,4.025],"faces":{"north":{"uv":[19,31,25,33],"texture":0},"east":{"uv":[19,31,25,33],"texture":null},"south":{"uv":[19,31,25,33],"texture":0},"west":{"uv":[19,31,25,33],"texture":null},"up":{"uv":[19,31,25,33],"texture":null},"down":{"uv":[19,31,25,33],"texture":null}},"type":"cube","uuid":"34dfbab7-01ac-2496-8629-e92b0a014b99"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,13.06318,-2.225],"to":[-2.75,14.06318,-1.975],"autouv":0,"color":3,"rotation":[0,0,-45],"origin":[-3.25,13.56318,-2.1],"faces":{"north":{"uv":[3,66,4,67],"texture":0},"east":{"uv":[66,3,67,4],"texture":0},"south":{"uv":[4,66,5,67],"texture":0},"west":{"uv":[66,4,67,5],"texture":0},"up":{"uv":[6,67,5,66],"texture":0},"down":{"uv":[67,5,66,6],"texture":0}},"type":"cube","uuid":"75f4c75b-74ad-f2a8-ff6b-e18c7f4fdedf"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.75,13.06318,-2.225],"to":[3.75,14.06318,-1.975],"autouv":0,"color":3,"rotation":[0,0,45],"origin":[3.25,13.56318,-2.1],"faces":{"north":{"uv":[6,66,7,67],"texture":0},"east":{"uv":[66,6,67,7],"texture":0},"south":{"uv":[7,66,8,67],"texture":0},"west":{"uv":[66,7,67,8],"texture":0},"up":{"uv":[9,67,8,66],"texture":0},"down":{"uv":[67,8,66,9],"texture":0}},"type":"cube","uuid":"d4f3afd2-536b-fd5d-03b6-b453a765ab11"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.75,13.06318,2.525],"to":[3.75,14.06318,2.775],"autouv":0,"color":3,"rotation":[0,0,45],"origin":[3.25,13.56318,2.65],"faces":{"north":{"uv":[12,66,13,67],"texture":0},"east":{"uv":[66,12,67,13],"texture":0},"south":{"uv":[13,66,14,67],"texture":0},"west":{"uv":[66,13,67,14],"texture":0},"up":{"uv":[15,67,14,66],"texture":0},"down":{"uv":[67,14,66,15],"texture":0}},"type":"cube","uuid":"ca75edbd-a5d8-27c4-cc93-71bbbb4a9711"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,13.06318,2.525],"to":[-2.75,14.06318,2.775],"autouv":0,"color":3,"rotation":[0,0,-45],"origin":[-3.25,13.56318,2.65],"faces":{"north":{"uv":[9,66,10,67],"texture":0},"east":{"uv":[66,9,67,10],"texture":0},"south":{"uv":[10,66,11,67],"texture":0},"west":{"uv":[66,10,67,11],"texture":0},"up":{"uv":[12,67,11,66],"texture":0},"down":{"uv":[67,11,66,12],"texture":0}},"type":"cube","uuid":"57155ef8-9d6a-186e-0e88-41fe79b2d637"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,24.31318,-2.475],"to":[-2.75,25.31318,-2.225],"autouv":0,"color":3,"rotation":[0,0,-45],"origin":[-3.25,24.81318,-2.35],"faces":{"north":{"uv":[46,66,47,67],"texture":0},"east":{"uv":[66,46,67,47],"texture":0},"south":{"uv":[47,66,48,67],"texture":0},"west":{"uv":[66,47,67,48],"texture":0},"up":{"uv":[49,67,48,66],"texture":0},"down":{"uv":[67,48,66,49],"texture":0}},"type":"cube","uuid":"29745a85-fe96-2a85-74b5-c4c68d141744"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.75,24.31318,-2.475],"to":[3.75,25.31318,-2.225],"autouv":0,"color":3,"rotation":[0,0,45],"origin":[3.25,24.81318,-2.35],"faces":{"north":{"uv":[49,66,50,67],"texture":0},"east":{"uv":[66,49,67,50],"texture":0},"south":{"uv":[50,66,51,67],"texture":0},"west":{"uv":[66,50,67,51],"texture":0},"up":{"uv":[52,67,51,66],"texture":0},"down":{"uv":[67,51,66,52],"texture":0}},"type":"cube","uuid":"5b094f76-a7f5-f24e-e320-c58b2ac2b435"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[2.75,24.31318,2.775],"to":[3.75,25.31318,3.025],"autouv":0,"color":3,"rotation":[0,0,45],"origin":[3.25,24.81318,2.9],"faces":{"north":{"uv":[55,66,56,67],"texture":0},"east":{"uv":[66,55,67,56],"texture":0},"south":{"uv":[56,66,57,67],"texture":0},"west":{"uv":[66,56,67,57],"texture":0},"up":{"uv":[58,67,57,66],"texture":0},"down":{"uv":[67,57,66,58],"texture":0}},"type":"cube","uuid":"f4ec532a-b1be-af15-da29-2dbbe5c35892"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,24.31318,2.775],"to":[-2.75,25.31318,3.025],"autouv":0,"color":3,"rotation":[0,0,-45],"origin":[-3.25,24.81318,2.9],"faces":{"north":{"uv":[52,66,53,67],"texture":0},"east":{"uv":[66,52,67,53],"texture":0},"south":{"uv":[53,66,54,67],"texture":0},"west":{"uv":[66,53,67,54],"texture":0},"up":{"uv":[55,67,54,66],"texture":0},"down":{"uv":[67,54,66,55],"texture":0}},"type":"cube","uuid":"26d54259-68ec-56a5-7d1b-b9e1022233fb"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.48264,31.93,-1.57375],"to":[-1.26264,33.15,-1.19375],"autouv":0,"color":9,"rotation":[0,0,45],"origin":[-2.34264,33.07,0.60625],"faces":{"north":{"uv":[16,48,19,50],"texture":0},"east":{"uv":[43,62,44,64],"texture":0},"south":{"uv":[48,16,51,18],"texture":0},"west":{"uv":[62,52,63,54],"texture":0},"up":{"uv":[62,49,59,48],"texture":0},"down":{"uv":[63,0,60,1],"texture":0}},"type":"cube","uuid":"4584d514-f657-2cb1-f174-9ddae5cd00e3"},{"name":"left_hair_locator","position":[-2.26264,21.99,3.08625],"rotation":[0,0,0],"ignore_inherited_scale":false,"visibility":true,"locked":false,"uuid":"2a453afd-be32-1082-9486-3cc8b6d0f61f","type":"locator"},{"name":"right_hair_locator","position":[2.13736,24.19,3.08625],"rotation":[0,0,0],"ignore_inherited_scale":false,"visibility":true,"locked":false,"uuid":"a1cb5820-0374-7914-881a-c780d8340eb4","type":"locator"},{"name":"left_hair_ik","position":[-2.26264,21.99,3.08625],"ik_target":"2a453afd-be32-1082-9486-3cc8b6d0f61f","ik_source":"","lock_ik_target_rotation":false,"visibility":true,"locked":false,"uuid":"185b2cad-6cc7-a818-34f9-59c70db1d853","type":"null_object"},{"name":"right_hair_ik","position":[2.13736,24.19,3.08625],"ik_target":"a1cb5820-0374-7914-881a-c780d8340eb4","ik_source":"","lock_ik_target_rotation":false,"visibility":true,"locked":false,"uuid":"ac61223e-e43c-ced3-6869-82c8e685056c","type":"null_object"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-5,0,-5],"to":[5,0,5],"autouv":1,"color":2,"visibility":false,"origin":[0,0,0],"faces":{"north":{"uv":[0,2,10,2]},"east":{"uv":[0,2,10,2]},"south":{"uv":[0,2,10,2]},"west":{"uv":[0,2,10,2]},"up":{"uv":[0,0,10,10]},"down":{"uv":[0,0,10,10]}},"type":"cube","uuid":"a0e67201-0de5-f611-feff-0dbad6b8e941"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-6,8,-4.5],"to":[6,39,5],"autouv":1,"color":1,"visibility":false,"origin":[0,8,-1.5],"faces":{"north":{"uv":[0,1,12,32]},"east":{"uv":[0,1,9.5,32]},"south":{"uv":[0,1,12,32]},"west":{"uv":[0,1,9.5,32]},"up":{"uv":[0,0,12,9.5]},"down":{"uv":[0,0,12,9.5]}},"type":"cube","uuid":"9afe6ef9-752d-2a94-3213-08adb08aa0df"},{"name":"glow_cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-14,23,8.4],"to":[14,51,8.4],"autouv":0,"color":8,"rotation":[-22.5,0,0],"origin":[0,36,9.4],"faces":{"north":{"uv":[1,1,32,32],"texture":1},"east":{"uv":[2,3,2,21],"texture":null},"south":{"uv":[1,1,32,32],"texture":1},"west":{"uv":[0,3,0,21],"texture":null},"up":{"uv":[2,0,20,0],"texture":null},"down":{"uv":[2,2,20,2],"texture":null}},"type":"cube","uuid":"4fa795c4-3f80-a25f-5dbf-b99637163aca"}],"groups":[{"uuid":"10ee45d0-a3e0-f40b-990e-a0dac19721d1","export":true,"locked":false,"origin":[0,27,0.4],"rotation":[0,0,0],"color":0,"name":"hi_head","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"c0820792-9e91-8bcb-812c-a6d07670799a","export":true,"locked":false,"origin":[-2.18628,31.1902,3.13333],"rotation":[0,0,0],"color":0,"name":"left_hair_1","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"a7c9dbe8-f0b2-caf9-e2ae-fa40543a508a","export":true,"locked":false,"origin":[2.12317,31.1853,3.09375],"rotation":[0,0,0],"color":0,"name":"right_hair_1","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"8ee82b29-c9f1-ae08-9c3b-dcd5f8787db5","export":true,"locked":false,"origin":[-2.2625,28.975,3.1125],"rotation":[0,0,0],"color":0,"name":"left_hair_2","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"5a05f380-61c5-f164-885f-d3b2cc27acbe","export":true,"locked":false,"origin":[-2.225,25.56667,3.1],"rotation":[0,0,0],"color":0,"name":"left_hair_3","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"aecdc16a-4f38-07a6-d0ed-48834f8a5836","export":true,"locked":false,"origin":[2.0625,29.6,3.1375],"rotation":[0,0,0],"color":0,"name":"right_hair_2","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"6492768f-2a1a-0139-2a5a-af6c200210f8","export":true,"locked":false,"origin":[2.1,26.99167,3.1],"rotation":[0,0,0],"color":0,"name":"right_hair_3","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"469494ea-42f7-b718-90ba-46d9298a3b38","export":true,"locked":false,"origin":[0.00072,22.89038,0.61914],"rotation":[0,0,0],"color":0,"name":"body","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"0266d8da-f639-7c87-40f6-834b24e671e2","export":true,"locked":false,"origin":[3.925,24.075,0.25],"rotation":[0,0,0],"color":0,"name":"right_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"a44819f0-9e13-12f5-b270-88d133e31f46","export":true,"locked":false,"origin":[3.925,21.97145,0.25],"rotation":[0,0,0],"color":0,"name":"right_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"98b261b3-0a69-4a62-2229-0d1aee17c78e","export":true,"locked":false,"origin":[0,24.11363,-1.76293],"rotation":[0,0,0],"color":0,"name":"chest","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"b6fc560a-2b3f-cb7b-2280-dfdae36d9d53","export":true,"locked":false,"origin":[4.05799,18.46909,0.2725],"rotation":[0,0,0],"color":0,"name":"right_hand","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"5cdc1be0-a703-9397-813d-b0c2fe914a14","export":true,"locked":false,"origin":[-0.02111,19.10476,0.38027],"rotation":[0,0,0],"color":0,"name":"under_body","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"20832476-f83d-a3bf-a371-dfb6ddc221f1","export":true,"locked":false,"origin":[1.65052,13.0185,0.25211],"rotation":[0,0,0],"color":0,"name":"right_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"14f70d2a-4f4d-e8e6-fa68-cb95be5d9ff7","export":true,"locked":false,"origin":[1.65105,10.6869,0.256],"rotation":[-30,0,0],"color":0,"name":"right_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"2e3cc482-b167-33b8-1714-5c7b341d65b4","export":true,"locked":false,"origin":[-1.65052,13.01847,0.2521],"rotation":[0,0,0],"color":0,"name":"left_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"7113ca1c-2ae3-a77e-c3ab-63794d1ba17f","export":true,"locked":false,"origin":[-1.65105,10.68693,0.25598],"rotation":[-30,0,0],"color":0,"name":"left_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"6f3eac45-0aee-fcf4-e283-e1fb93e876d6","export":true,"locked":false,"origin":[-3.925,24.075,0.25],"rotation":[0,0,0],"color":0,"name":"left_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"7c168fd7-cdf9-c904-82b2-464caba505cc","export":true,"locked":false,"origin":[-3.925,21.97145,0.25],"rotation":[0,0,0],"color":0,"name":"left_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"842ad7bd-7015-fa76-ebca-314ed49a789f","export":true,"locked":false,"origin":[-4.05799,18.46909,0.2725],"rotation":[0,0,0],"color":0,"name":"left_hand","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"2404264b-66f1-5dc1-5fe5-db0c33526a27","export":true,"locked":false,"origin":[0,24.6,3.175],"rotation":[0,0,0],"color":0,"name":"middle_cape_1","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"1a9595e0-ba95-9397-76c5-68d018b0975a","export":true,"locked":false,"origin":[0,21.60036,3.175],"rotation":[0,0,0],"color":0,"name":"middle_cape_2","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"f719d2bb-6c28-0ab6-a585-41f54792160c","export":true,"locked":false,"origin":[0,18.60036,3.175],"rotation":[0,0,0],"color":0,"name":"middle_cape_3","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"b2fd5761-a84d-7020-38c7-2efcd6c39298","export":true,"locked":false,"origin":[0,15.60036,3.175],"rotation":[0,0,0],"color":0,"name":"middle_cape_4","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"9b4610f1-5d4d-9c62-d7db-b098f5a0a8e8","export":true,"locked":false,"origin":[0.1625,34.2375,-6.4],"rotation":[0,0,0],"color":0,"name":"eye","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"62421ffb-7685-a28e-c939-59b0d8123776","export":true,"locked":false,"origin":[-5.2322,13.52419,0.26667],"rotation":[0,0,0],"color":0,"name":"left_cape_1","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"167d2bb7-4626-6746-c72b-ba3c8dab3e2f","export":true,"locked":false,"origin":[-5.2322,11.52419,0.26667],"rotation":[0,0,0],"color":0,"name":"left_cape_2","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"57123914-f464-2f02-3eb3-e1ac924f8ef2","export":true,"locked":false,"origin":[-5.2322,9.52419,0.26667],"rotation":[0,0,0],"color":0,"name":"left_cape_3","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"06102678-1ae6-d632-30b0-6964f314e6bf","export":true,"locked":false,"origin":[5.2322,13.52419,0.2667],"rotation":[0,0,0],"color":0,"name":"right_cape_1","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"977a05f5-98e7-7f45-0d7b-f345af018dd3","export":true,"locked":false,"origin":[5.2322,11.52419,0.26667],"rotation":[0,0,0],"color":0,"name":"right_cape_2","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"18d36cc2-46b6-f929-3624-07aef443f80d","export":true,"locked":false,"origin":[5.2322,9.52419,0.26667],"rotation":[0,0,0],"color":0,"name":"right_cape_3","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"0d53fe40-29a8-f10f-e40f-59194c70c775","export":true,"locked":false,"origin":[0,0,0],"rotation":[0,0,0],"color":0,"name":"shadow","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":false,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"d8e887a2-a16d-3dfc-b2e0-ba8aca6bb84f","export":true,"locked":false,"origin":[0,4,-1.5],"rotation":[0,0,0],"color":0,"name":"hitbox","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false},{"uuid":"aa6e0a4a-d036-a286-5c6f-ada2c220032b","export":true,"locked":false,"origin":[0,36.5412,8.09344],"rotation":[0,0,0],"color":0,"name":"circle","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true,"primary_selected":false}],"outliner":[{"uuid":"469494ea-42f7-b718-90ba-46d9298a3b38","isOpen":true,"children":[{"uuid":"10ee45d0-a3e0-f40b-990e-a0dac19721d1","isOpen":true,"children":["fdcdff03-57c1-e071-5769-588fbc314386","5c446ca3-fa6a-9098-3658-6b2188e41ef4","c73b2cca-267a-3b8a-82dc-1eeafcd6927d","cd5d87f1-1f75-5842-de2a-3eeaf3af1f59","ec21fc72-01ba-0601-895f-94d76063b07b","29d56430-7916-7008-9cc8-b6565185a324","2c80f29a-68d9-2841-a9b3-7de033e342f5","85e31557-bf10-290c-931b-a1855a815d1c","374c990f-cca9-7642-54f6-8fcc2ae47386",{"uuid":"9b4610f1-5d4d-9c62-d7db-b098f5a0a8e8","isOpen":true,"children":["2c82fef6-2f1c-e5d9-a1b0-10313da9a8cd"]},"bacad5c3-b6bc-3fc9-529e-b6f6ff181e82","f322ebee-1063-7c39-2350-8d74edf49a71","17633f4b-13a7-8951-8eec-bf2f1952ec70","42c42a1c-3468-5c32-6bfe-36250a72b9af","21b22b7a-bce2-d3cd-bafe-6bb8ca56111e","350e3a30-4658-77a6-4df2-1d7b331a098b","ecc83fce-7df5-b985-53a8-a3303f95832f","3ba71253-feb2-8e7b-4548-01cd3e1df6ab","41e7b572-b3cb-756c-9a95-64b8f5f33769","83e3beb0-d1dd-6e57-57ce-dee9c9bdd8b9","942cc6a2-71a8-88e6-b9dc-70b575a485e7","2db4c218-3307-c625-7799-b63ef622617a","c801537a-5f55-042c-3435-e557b199c416","5573dea6-d06d-26ef-659c-5e9dfee7c967","17f99990-ab5b-6c49-5c4e-3498f582fd93","b3fd65a2-c4d8-8976-2bd9-eecda71c86b9","9f7d1c1a-c9fd-ebd0-5737-915b7bb6942c","2d82ea57-745f-beb3-0c88-6c509f7a74ed","9a8f1ae5-db04-1afe-d3e5-624faaad48e9","2ebc9aa9-73df-e4ed-0a7f-f22cc51c6feb","84d754cf-915e-8061-2884-d5a7dc9f7bd1","8ec78ad0-d84a-0628-5443-fbd1c509e136","4584d514-f657-2cb1-f174-9ddae5cd00e3","448668fe-7911-1061-80f2-3d5c40d07984","da9a971e-d1c8-574f-1c0c-e63d98c1e9f6","3f8a8ca3-5437-215f-de7e-08e69187015c","0335203d-2335-ce64-0a28-16715076876f","a20d7470-bd5d-94a6-8bed-c93cf0d2d161","84e0afe0-c3b1-e5ee-63bf-491983d93639","4458cb4c-42b1-5a2f-c0fa-8730c2a5e29b","058b2d35-b9d7-23da-a53a-98f0b8533465","a32e0821-ed45-ccd3-91c6-ac768ec3310c","676e5395-2ca6-1959-efe3-e41270439957","876f030b-55de-7dd6-e835-00488a4996f9","512ee88e-a107-34c4-b782-b3cf94747612","2e41d6ce-8800-9b74-6f26-94e295cac2bf","27defe3c-bdd3-18bb-85d3-6405e21b1e03","3d38f265-16fb-b26d-0851-3b04a28b6d64","b3cf2f69-86ff-ed07-8934-90d0425f413e","f0e1b661-bd04-bf34-5724-e4ce9acc5e75","8c0b6782-b9e2-cf2d-31b4-c96d97905fd0","571974f4-bc0d-5b57-c405-e5653335ee06","c4478cd8-3191-c3b4-69cf-3377cc78af98","8ba2c460-3e0d-40be-a9fc-cd7ae4a4574c","667ec079-4b84-0569-1687-845a993a638f","e81b7843-20f5-c76e-a1a1-e0e6e87d6767","5729025f-74e3-e5dc-3d49-8d8ec85d1766","3072f681-6e4b-aef4-27b4-6990650a7f1b","cc3612bb-4f96-dc1e-46ad-d21b86d4df71","d47bc743-9279-3d45-6344-f7035899232d","6b77988c-a45d-1c22-dc83-86073939178e","7f7169fb-d626-e57e-2176-8ade273cc9f4","2d2daeae-d4e6-d6a9-d0d7-ac5a204040fe","1812dc1c-20ad-522f-4394-729108bec75e","9a276e96-6c77-ddd3-368c-f181c5dfb929","d4d3e984-46f1-f6da-5c1f-cae44bfd11b3","de0f931c-847a-9947-b487-f6a601951d69","72a8e5fb-7b1b-994b-fc27-349d355d138b","0f36a065-bbfb-34aa-9cd2-94c66b683a68","fabe1f10-892d-dcfe-78dc-3bb27af4eb24","831b1360-4fcf-59d3-82d5-df3af6f87c87","107ef96f-b460-6a0c-a9e2-31d74b77b79a","55a38668-5660-2226-d6e6-6e3e82332eaf","ece00d0a-b988-98b1-0ce7-c89cef99cdba","9b4e3438-f7a3-6771-95bc-51b94b1899e8","185b2cad-6cc7-a818-34f9-59c70db1d853","ac61223e-e43c-ced3-6869-82c8e685056c",{"uuid":"c0820792-9e91-8bcb-812c-a6d07670799a","isOpen":true,"children":["ae538734-efc4-2f34-17c4-995c75624993","c0d88dd0-a196-b17c-e123-954163d428e7",{"uuid":"8ee82b29-c9f1-ae08-9c3b-dcd5f8787db5","isOpen":true,"children":["152d16d5-b078-564f-133e-d531ffcc2818",{"uuid":"5a05f380-61c5-f164-885f-d3b2cc27acbe","isOpen":true,"children":["25b70ee3-36dc-db96-bcdb-b06c141eff67","15d2938a-7b52-b9b8-5e46-b1591e2f5234","2a453afd-be32-1082-9486-3cc8b6d0f61f"]}]}]},{"uuid":"a7c9dbe8-f0b2-caf9-e2ae-fa40543a508a","isOpen":true,"children":["eab62980-956f-9ac3-e675-d862905ecc98",{"uuid":"aecdc16a-4f38-07a6-d0ed-48834f8a5836","isOpen":true,"children":["58c102f9-86e6-7ce8-e0ca-7b8d31dbf167","9eaf4c53-3dbf-fcda-fd62-0a7b2682ac03",{"uuid":"6492768f-2a1a-0139-2a5a-af6c200210f8","isOpen":true,"children":["6a04a94f-b682-b4ec-6f46-89134ba4bb1a","0511aed9-00e4-dadf-a777-d5572b593baf","a1cb5820-0374-7914-881a-c780d8340eb4"]}]}]}]},"eac3ad48-ae72-b3bc-8b95-018333dc3051","255c4dc9-0053-0fed-0d04-3030dc27ee40","15fe5bf8-7f4f-e9db-baa8-6822a6077ce7","6997413b-4ced-3c9b-d695-66afa5722357","6d49082a-c952-04f5-6a33-5cf53b88d970","87ccc294-b6b9-e5b8-72dc-6cbcefb36497","4225da25-6240-50c1-7d61-035b7cd5d4ef","531af403-a2fa-98db-bb4e-16441ceab62e","f81abf97-3003-da0c-7fa2-018be5cd9b8d",{"uuid":"5cdc1be0-a703-9397-813d-b0c2fe914a14","isOpen":true,"children":["75f4c75b-74ad-f2a8-ff6b-e18c7f4fdedf","d4f3afd2-536b-fd5d-03b6-b453a765ab11","57155ef8-9d6a-186e-0e88-41fe79b2d637","ca75edbd-a5d8-27c4-cc93-71bbbb4a9711","9eacb6d9-6979-c722-5a5a-7f3f755d162d","467613ba-eae9-d8c5-c500-764d19b63bdb","ecea20f4-a55d-fe20-ac80-243b297f225d","34dfbab7-01ac-2496-8629-e92b0a014b99","ee8190eb-8d0c-759e-fb79-bcfac04aaa59","59a35e30-c471-c0d5-ff54-b604450c2cce","a66a23f5-34f4-0e99-bfd4-9943ea997a8f","74f23901-ff08-52ac-dc12-6f1ee74b946a",{"uuid":"06102678-1ae6-d632-30b0-6964f314e6bf","isOpen":true,"children":["6afb3580-9d2a-a800-44e0-7ad32af91987","e885a9ed-6555-231f-5a14-7d4f6dfc8839","5e4c7faf-af00-d46f-ef74-f7616ec8b21d",{"uuid":"977a05f5-98e7-7f45-0d7b-f345af018dd3","isOpen":true,"children":["179fd506-fced-8fc5-f228-b39dfd836eab","dcc13d2a-e8b4-9cf2-fb6a-1222d2c42980","9ff9fa0a-03b9-b05d-2344-8fc7bef11e46",{"uuid":"18d36cc2-46b6-f929-3624-07aef443f80d","isOpen":true,"children":["60ac24f4-f0d5-2709-8498-d5929693e212","120acc01-548e-637c-0f2f-6fc565133c9a","3698deba-f32c-bf34-b848-ba738866e0fc"]}]}]},{"uuid":"62421ffb-7685-a28e-c939-59b0d8123776","isOpen":true,"children":["9aaec269-21fa-fc94-f584-04cf13144345","5500625f-17c2-64b1-406a-697250abf2ef","31163dd6-e25e-d315-3db0-b755cb62a140",{"uuid":"167d2bb7-4626-6746-c72b-ba3c8dab3e2f","isOpen":true,"children":["1e8fb086-03d6-eabd-c193-db916605a3c7","a9e565b5-91ac-43ec-b6e8-fb95376fe40f","e7ef1199-ed3e-93ee-adad-e9230cad7d6c",{"uuid":"57123914-f464-2f02-3eb3-e1ac924f8ef2","isOpen":true,"children":["b2b3fdbe-0de5-84ae-df96-288f90b79d94","91fa364d-41e6-5c82-2bfa-1cc1570fd514","f6378b2a-8e1f-a751-beda-9e1f6feda543"]}]}]},"4b3e6c3f-287d-2368-a998-c33d81b96ee4","c6a80580-5139-f0a6-9f30-9ade3c37c5dc",{"uuid":"2e3cc482-b167-33b8-1714-5c7b341d65b4","isOpen":true,"children":["8c485616-0002-04d6-6cf3-9d32de2c3d01",{"uuid":"7113ca1c-2ae3-a77e-c3ab-63794d1ba17f","isOpen":true,"children":["7b67db12-b5ce-3ac3-bdda-5016a85904b8","91d99b1a-9f51-fadc-5f4a-8ec6ea9fb9ae","2280ee81-b151-2526-efae-1722409b0821","ea1adedd-fb2e-60ce-3ccd-145bac9176cb","706e638e-8e86-fce7-c6e6-e2789cf10cc5","bdc513a2-6aeb-d4c6-093c-9eebed04c782"]}]},{"uuid":"20832476-f83d-a3bf-a371-dfb6ddc221f1","isOpen":true,"children":["29e89ffb-c9f6-6591-85c4-4dc2d8d97531",{"uuid":"14f70d2a-4f4d-e8e6-fa68-cb95be5d9ff7","isOpen":true,"children":["448df43a-1136-018d-c5ee-785cea750b64","50f40905-4d6d-7098-57ec-25bb1db92c9a","4db14d3d-0f93-84c0-5625-5298d979eb41","3d9603af-d62c-a46e-fbfb-abf81268a2d9","32e01dc5-5dae-1c4c-b1be-35eb3a079515","f7aa46f3-7631-0bc0-28bb-f612ea592aee"]}]}]},"b4336081-325e-dbfb-9cad-39c227e78c67","1416e039-1c93-1f55-854c-2ca9f6651c99","8fe284b0-23bf-a450-600b-44047d4f2b61","064202fa-1ea8-2e3d-08f6-15645a4d4f5c",{"uuid":"2404264b-66f1-5dc1-5fe5-db0c33526a27","isOpen":true,"children":["c7fc4bf2-2bdb-6f57-e1d2-1b55fbadc572",{"uuid":"1a9595e0-ba95-9397-76c5-68d018b0975a","isOpen":true,"children":["d084c28f-189e-764b-538f-cf0537a5c344","41d62fcf-9109-d04a-1897-b6d0024546f1","5f6f0613-a341-4bc1-caa8-71c6a4d9f006",{"uuid":"f719d2bb-6c28-0ab6-a585-41f54792160c","isOpen":true,"children":["96cda888-fe8e-9317-9335-171c7a631c52","b5702594-4dba-0c01-6343-02d2faac24e1","013aa42b-83c2-5f5f-327d-8a74970cdb49",{"uuid":"b2fd5761-a84d-7020-38c7-2efcd6c39298","isOpen":true,"children":["8fbc89dc-96ae-1dfb-471a-f121b77bdba1","d1867ca7-e2c9-2a22-d67d-6a3117fef579","f3b7b5ac-f8dd-ee5a-2837-8b886bc89be3"]}]}]}]},"7ca2ade1-2f88-677d-8126-849c195f075c",{"uuid":"98b261b3-0a69-4a62-2229-0d1aee17c78e","isOpen":true,"children":["2a40ace8-91f0-0a80-b85c-f1bfea213a68","6e9d44d7-1f0b-5134-4382-e916bf84baba"]},"033aa5bf-e96a-e214-6edd-35746a2e92b5","b93cff5c-2a56-1aab-a8c7-ed4d7397794a","aee73142-be32-3036-4fe2-ddeccf72a5b1","d9e7e5c3-da02-8477-fd18-15eb3ec1609c","ac4a4b83-622d-18a3-f3a4-a2a64a04a81a","add2bd23-961a-d6ee-0953-9458433b0e7e","29745a85-fe96-2a85-74b5-c4c68d141744","5b094f76-a7f5-f24e-e320-c58b2ac2b435","26d54259-68ec-56a5-7d1b-b9e1022233fb","f4ec532a-b1be-af15-da29-2dbbe5c35892","55a299fb-f460-6263-e740-75923a2d9d86","8bfbd4a0-a8fa-aa56-046f-d97347ba5903","e9d83ef5-2045-ab67-4a98-bed4ba14d518",{"uuid":"6f3eac45-0aee-fcf4-e283-e1fb93e876d6","isOpen":true,"children":["c2834cb3-01dd-0214-bc6c-5f5265af0476","a3e39edd-509f-a6e6-2351-ff79b7a61f6c","5dec9d98-097d-f3e9-9827-c2dcd146712f",{"uuid":"7c168fd7-cdf9-c904-82b2-464caba505cc","isOpen":true,"children":["20c07f3a-08c5-782d-37e8-d229fcfa893a","12f8aa93-1559-25fb-8b0c-6458b326b202","a65ddbfa-a090-fa94-36f9-a83e90783a60","11bb47a0-0738-66fb-7887-f3499e766f41","faa9c325-4ca4-5292-9be5-eb74a175eadd",{"uuid":"842ad7bd-7015-fa76-ebca-314ed49a789f","isOpen":true,"children":["7779802d-844c-1e58-67ba-9d26d4d7aacb","e0c86f4e-ff80-ead8-9b92-5246c36f8b34","9e677dad-9f30-868d-f8a6-b4f1ca772402","ee55ecef-75f5-6169-4093-ca093fe26167","b462094a-f846-3cb1-75a7-4432dde1c9a3","5a8b5a47-d25a-9f4f-c151-22ceffc086f7","2873cfd9-8e30-f048-b028-8a08b6605032","150732da-710a-804b-7ad6-0937f1647292","fb92591b-ca3a-8a99-8628-b52142ad0610","dd1b6bb9-1371-7468-c811-690929986466"]}]}]},{"uuid":"0266d8da-f639-7c87-40f6-834b24e671e2","isOpen":true,"children":["49960836-36d8-fde1-6010-119a680e1792","be833fd0-a635-7db7-63ec-8b3555252f2c","70921fe4-f2c5-95e6-5dab-9ba9df357552",{"uuid":"a44819f0-9e13-12f5-b270-88d133e31f46","isOpen":true,"children":["b098efed-8cd2-8935-e65b-ec57de1527a6","0056ac3d-d6be-eb04-edd0-15cdce2e96a9","cf7539d4-1cb6-0d62-d9d7-3a5537399b0e",{"uuid":"b6fc560a-2b3f-cb7b-2280-dfdae36d9d53","isOpen":true,"children":["71dd0d2e-1f35-9c20-f0e4-e7243ef62c59","0ea452bb-ba2b-016e-f030-ec3360d55b63","56e24b25-c478-848b-1b04-d7ce1e6f032c","b36424db-8870-a556-61f4-ec7c1f63a964","62b9158c-cca3-ecbd-e46c-3407f8d0306e","f2c157b6-a525-ba83-2b2b-bcfa39f73d78","ba2d481e-f120-fe09-5b95-35e6946f3ef5","08b8a4dd-9817-f43b-7486-512f7efd2396","5b39d357-c3a2-39d1-1c69-88c062c4ba3a","376ba5a3-f0b4-f973-ed18-3feb313a350f"]}]}]},{"uuid":"d8e887a2-a16d-3dfc-b2e0-ba8aca6bb84f","isOpen":true,"children":["9afe6ef9-752d-2a94-3213-08adb08aa0df"]},{"uuid":"aa6e0a4a-d036-a286-5c6f-ada2c220032b","isOpen":true,"children":["4fa795c4-3f80-a25f-5dbf-b99637163aca"]}]},{"uuid":"0d53fe40-29a8-f10f-e40f-59194c70c775","isOpen":true,"children":["a0e67201-0de5-f611-feff-0dbad6b8e941"]}],"textures":[{"name":"blue_wizard.png","path":"","folder":"","namespace":"","id":"1","group":"","width":128,"height":128,"uv_width":128,"uv_height":128,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"9ee971d3-c578-3c74-5c3e-3f10b011a254","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAQAElEQVR4AexdB2BUxdb+Zkt6QhJ6NUgVUKoKAoI8paooFhTrExUQGwKiIpanok9EVESFh4og2HgKKAh28NEhiEDoJISSkGST3exutu/858zmLpuQIPoDJuwu+905c+bM3MmZMzNnZu5edDjJZ9gXO2RFmL2tSD75S47815pj8qlVuTLzSE4ZrP10vFzH+KQ0JJp5w2d/LwdPW6xw41tfywc++kneOHtlEOnbMyRjy46dMp2wYdc+uWLXQbkyY688cuSI7DzguiDKV3vlzgNy/YHDctW8RxS6XXeLZAyevkwyNPltu/fKbbv2lNY3VxbYHYr+fdETst99j0hNLlzCkxpAZUoodvngl35YHF7EG8QJYkUWO8wmO3xOPzwlPtgKnSgwWWGyOYKyHp8fOWZbMK4RBoMRvuRaMCYmQ+f3oV6jRjjq1SE1NVUTqTD0S2o7Uf7PKVu3vXv3RjsMsYDOAJvNjqKiQmRnZsKat7/CMsOBWV5jp/Q3+0jK7WeFS+h1gmJlvy6PF26fDyUuN+xON0zFVtjsTkhupBBRP5cREu/Uro24qHUL0aVhbXFhk/qiS5tWYtavW/D977uxZfPmEEmg7yuz5MBpcxWYlkJACFFGRieNWPzQQDG2SypWrV0nR9w42vnEkHvw7C/ZGPvlj3ji618VLurYQ2Vc8Z83VVimkHM88pcMQA+BIrsXDpcfTv+JOrv+wVni2odniqtGThf9H3hbDH1ijrj18Tli8ZjBJ+CP9DuiaRImXdER9Zs2Q51arVG7TkvUanQefC4XvE6Hgt/jQbzPDWfOQXS8oIlCTHwCEgwN8I9bHpDl7+Fx2IMsPxkqT2HfzamFHjfdeYJsUPAcJf6SAVD7o9gjUUBTgbNcL/6zGlx4by+hIVTHh6yOXhy/qHVL0bRhfQW32wGnxwxXiR3RSTVgiImFnhCVkAi/1wt/jdqcRcFpt6HYl4USZyFaO9fjAttmykv5S5w0LdlgWvWtQtHq75R8atzJpxgldA5edDw0VobirAxUBG7kGnEGRBsNKHYL1Lb8BK2RzqfGOh16apwYu7J8OSWuY9T4xXBR42q931c6CngcNugs+cEsaxd9KjT4dQZ4/W7EGGNh8EXh5faHMWt0d8wY0Q2bv13EdUdhSSHik1OC+cOF+EsjwNjONUX3JomoHR+NeL0Pn/oG/L/1Zd00VTKoILavMsjb/KHMWzdbLn7yKrz24nN487UpePHmq/HKNT0x885rFRo3OQ8t83+BI08Ppymaijn+fR7DMEHchrTXF6Dth1+g48CxovPV40XX6yaITs27gMEjgN1cdDxTmFC6v/p3xgg/DbsecvK8KHb6/moxp5TPTXO2w2yCs7gIeloh6KUPBodF5bVYAiFHZJSA12eDx2UBz+uMrRm75Mjax/BcGy8++Ect8W6vlDJOS/q+TWCE2wjA+mL8ZQPwqblfwk8OAXvgXNip4Pedu+WWzMPyQPYRuf/QUbl2/xG5JWO31PIWuTw4VuKCyeHGMVpOOt0eeNx26ONiab6PQjvLCrSyrkJz+zq0kJvRoU1rNYQXFZmR1/QGFHmNsDj1KFz7EfNF+zatFc5v1KBMw2v369O+Lxgcj4wArIVTRLFHwOYTiDNIJEaduh0ZyW8wGo3w0xrf6ZOIkx5aluuDd42i/QWQV+/0+qCLMsLh9aOo/nU43ORmHK1/LQQtOwXdT0QbFK1l9Bmj4NIboePBiJraT1Vi32bM1xly6Px0+fDSPfLQzzPlgW9fkkNm/SInLU5XRvfT1u/Em0s/1ooJu1B3ebeuojJMH9YDFYGHVlDPjzfqEaPXQUeNglP8XNC8mWjXqK5ontZEtE1rKNo3TxPtWzanJgsUEB8TLerWSBANE2PFI9/sxeil+/Dadi/e2mjCtC12xF8wTMS3u10kdrhTJLS9XdBHZdR7XDA47cg2ZyPbcgiFhYcU/1B+ES0ZHWRTHoCMwm9IVHy/x6lCvlzYqI5gH6DEenw6YX44gFTy1/7MuCjA5ZNqH8DNve4UirHvXSzNO7+S5owvpfn3hbLo9y+keceX0k78xC5jBeMUiikjsvb3DND2MTrRJhKjTCJFPHYLGAU0ReQcy0LWoX3EBTLzzFi9YZNcsyldskEX0ipAJYTZ5S8bwPC2KeK9q+qL1/vUFw+0L+tYVaZDDw3nQmeAdHvhK7ZD2tyQtBHD8rRLWIOg1v4cZyy4qa0oj/dfuE1+Ovmf8jMC01FCjeQsrvC2tR/etgWgGKUXH00rxTYbiui+X97fWyx4YKDofkkXcVmXToKXsKnhug9Qqp+zEiRfcL2o0eoakdLhZlGzx10i9bLbRMpFN4n4FoNpJBcWIcQJa//yFeM9fxPt45vsJWCam9/v52tA0k093m2zgMGcN4Z0BKNlWiNcec90cdOj7wvmM7jnkwMoGYXhOgJcPeULWRkeWvA/ROd8j6ij3ynElNLGw8txbMv841pnbZ4l3PfMAjF68ufigZc+E0xHx8aiZs1Uteyb+vgQ2bf4KzBWPH2H8m0SafUA+sTpgWveWygHTf9E1Xvsoi0qJCcQDBIJy+8fTgF6nR4Gg0FBp9fT4Y8ORvK4abhGnrlYHjZbZWaRVe4ptMkNu/bL9O075fpdB+SafYflhp375KaMPXL+lLtPQPtb75WtBt2ooB05T16VqRoFf+ZTYQ5/sISDXj3q1a4JK40YzNQZjRwg11KiQur9ahk4btE4oRjn+KX8n6dzO0vgoe1UDe4Qmnlu8q49bjfcLie8tN/u9Xnh9rjBy7jY2BjEUQ+0CQP8ZCjcFnpSsEEvEO+xQ+h05e8XjGsHg1Ijgil/jmjXvKmaw3keH/vqlyKARcHGrAUv7F6J/bmFZQq2FRaoeOLdo3kECMorZhhddLyn7lFGwIZQAm8IzfzGl94jGl36T9G463DR8OK7RKNLiKZ4gy53CSF0cNK5fgypr5ZRwOD3klPnBVkLfOTc6WmppSNeRfoUlIf5QpQSFBFC4OaPN8qhIbh53kZ5bN0smbtqpjy2/n22MZI89a/pWA6yD+w/IcNFafXx6FdbT+CHG6PyLnoKmsihLWCbxwcvGYGZ1oKd6fy+A+28dW7bWvCSTMNt4+eI8qio4/uJ6aNtX28IfLS2txUdgcOaBycd+PR9eaYMxSlUE15DtBJrmNQAneo1UbSfDNPnciian1JSRBhe/l8GkGIAYkmRifAhjvBn9OeymhGTlIykhudBW+pNvLypMMTGozziUupD0MaDz1Nc0S14VNBwQjob4SWtmwkPGZXJVIjth/KUzME8C74Ze33w+QTFDMOL7rmrO4DxzC29Vcg045N+Ap8M0uNkn1px0aJxSqJolJokGqQkHR/LT5YpJI06fEgsQHqpl3upsTR4KF6/60iRdsUEcf5Vk8R3T44og0CuE6/dSp8J1FI8JTa8MKgdXhvSWa0Y5j8wiJLUJgIbD9FAXGINFYbTRechx47hIyePduXBNOPTqEF4t6Q3Xlzxu7x5zho5/Msdcmb6UVYWAxV9pq7cJd9cmyVnbj4i39pwWI5bvlcO+2i1egiUh1kNI+f8qMqgKf+EYuZ0TMecizZjdtt9+KDDLnxE8ROEyjLY8BTW7DsiV61bL/kMoKwI8O6dlyM6OlodELHDKKVfZB45ysagREevtmUqIswuOg/tkHGD+6CDj5w6Dx3CMC8hxoBitx/bj1oghEB8tBE16ADmZPrZkElD7LESCFoReOg0xkeOoI/8g/J5copsilXRCPCRGIZpeT0hhRXP7G2Dt9zDlKx2ufyx12QoND6HdrsVutoNmawUmSFPMLMhMJjHGUrC8Swg2boZ8ab1iM9ejqRjv6D/Fb1E/z5XCBtt1xZ7dTAaDHBDD5ML4AdB+748E4SgI5ZrKpSsQAZ3a+6KPtqZizMKOMhB9PmPr8kR8ulw6wS0u3Ykul17L576Yb98fMU+eednW+SDneuLZ665RKR2GyPeHHa5mHhZfS4ymNNLU0Ionv9hr3z91yz53Pd75VXtW4sezRqpDaC1pU8EaRm5oRkWuxNFm+bBum4uNn75iuw1cYpkPsudy1MA/30VQeemhvZTI3mosbzU+zUhm8uHJCNovS/BZz01iHbTXr6WroVFbol42g/Q4m46IGJDgfTD4XJDUjev3bQl4hs1LwMvzfMuuw3smOUVFCK/oAAOm1UrptIwrc9NCMXBvCLsPmpCdr650jyhCV4anYwQ8BkIfIpJ9QtNDzdaxw0khKCdPl2Zv12SURRYbHCTjxAjvbA5XSgqcZeR4YibhvgSSmOa4eJHwtW04oOTjEoIoZ7k8ViLwHAXF6rwv/f1Foz5d3UXs2+5WHx4W1fxxfDLubfzQFIeXHSFKHZ6YKJeXex0Y/OBQ3L1vkNyA/kCFQoTk29w4eDxouPgx9UjYe6MdLDDSEkIyykgmuZ2vV5HPRVkBHrWg4KLGtZFjc8Gwgw3jQ7FTi+TZRAt/PBwc5VyWd7l8cNNIwEQSLCa8uG2mBQ8ZABMl4qf1iA12oDk2CiUSB6zKi66c/MmbAMq8cCv78gF43phwYxpKp6+fHEwTTHC4KLbVH8oZvgG4V15NWb5B+HpH/dL3pPfb3JCHx2HqBq1cEmLhmjXoAb2FzpRq3kPhWbteoOXZK0b1hUtGtZT27Gf332Z+PTWDuLpXk1Fnxp2TOgQj6e61sazV7Uog6T6jTHu2z1ywnf75YNLMuSwBZvU7h/7EQTlmXO4/0gO6CMIlX6n9DsfGlgoDn6cF2dk8g+xeVM6MrZvUyuDtXf1TgvLn4aV0LyreiX1TFuRCUZPCVzkTfNQrcHoc2DbgSPQ4hzm5wU2VP5QyxUI8NxvNltgo/N5R4kdHgL7BOVFdeUZFOdNo9wXx4AxspEVh7IPVggSPeH7wpIN8sn/rg8MS5R605jZIvXAOqx9vJNM37fppIZG4ufkV5efbwJKLGAjcNEw7SMv20nLIY4HQQ3kNJuUTJBHBmMtfZRbC0M11Kn0CZ2Kws9prp9968VixpD24n2a/7+4t5d4bWCr0OyKjqLevHpPlvw244B8dMmOzDv+uyNz1OIdwfV6R+NqaGhlXokWpuVoUbwML/6ytcxKRVu17DlaiAM5+diy9DW5dfnrcsf3b8hihw8MOhUMGoa6eZhcdE8O7ITZ9/XDO4TFYwaLf13fTfz75p4idNieO+dd7PphXhCZ67/G0Z2rzqiK/OSdp6am0lZxE1qCekFbFORSCOionzo9DtgKbQj9+GQJQHsXMioKtVv0zGrVtkcWT1GhmDeyv/jswatFx0HjRPv+j4noKCOa3XA/+k3fKjKyt2HFf96k0kNLPfdpnSRH7Rh59wY69q3szxXl1FI+ruW7b8lu+cwvh+TEn7PlhB8OylHf7JW3Ldwh71u8K/gzc022fBgfHw8NebTY8MYmYsPatXj3p3X4cftelHhAB7uA3Qs4zU54pQeJpc8Rcthg4POi/pVPVGZ/twAAEABJREFUiwZXPC2SjBJRFc0fdNPNu/YFe3rzXqNF896jaUcwRzao2RDnog9Af/JJvzo3efv8XL8QZVuZ9wa0nNQZNZLMhUwmqMIgWxEuWvbl25wocXnhoWWknzOSbGL08dWFEvyDi5+8eKvbg7ptO+G+rhdh/D8uRtayWWmFy2el5S75MC25dgqSa6Yg9EO3EnuXPaSO/RIMALf/sArebyBLn1HoPOD4uwa0jaDQ8sKF1nHTpAjvCX+vh9byGjPUNgTon9BSyoZzb2gj3r26hZjW/3yRnZ0NPot3WQrKCoXErnlvodQw/NPvoKHb+Y1El/qponXdZNGxdoJo06COcJMf4ib/xO06PvRb1rwvTT/NlJb0BWzDssXA6S4uvnZCdFpSrCGNHcby0Lmd4M83D16KpY/0xNsXZisnMDk+TH8cWic5SWhgxdz/1Y7MR8jhmncoKmvqLpmVkpKCRa+9gN1LFwrGpue74X9jL8DGWVMrMQMupSxOWbBstjIxJ+0aMhwOM20wOWAtsIJ/IKLTSzgLA496axnWZ2Ri656DGDxtsSwP25aZWDXvEbm3MBePWftj+sXzEH1nGP8wRFOaFgoiomhYYKcrmYZSilb65bm3oP4wMJiuTHDi5U3w2lXnqXN/lsm3WGUB4euRNwrGtJv64b2hfcE0g2XKY23p3j6HzmInfDRqJXX5p0jpNVLUvfIZrnYwi1c91eQAO7Xl4aDpSYOWwWYtDtvHwniq1PSgQtrEg4vm7TynYKda8Sq7SCmTK0sL5fv8PsTHRAVZeoMRhf6QW9OcH0MeeVDgDwie/xmViWmNHpo+amWRZCxq8SIY2/ccReeNk3DXqmFwLBiBein1Zah8uNC6gZM/lgNe+lgOnDxfDnhxrszd/Vtay0Y10ga1SUmbenXbpn+gleAY8Y9/vS0HvjEvCEveTmgvfjDQ0kyoZy8CauUHR1NijxtEnfgYMroynTgg+AdX66ap6iflHGqi176xRF77xtcK07YUylfW58rnftov6xtKEC3deOfy5Oh7imdj7KtfqQdIk2L1YPAqICyXgbTXAkHjvYG2fQEBr8uJzYfdyDYHHEMrefYeOt5FBR8hRBkPz0PztNdRAobfF8jP2QSdKeQ6mQrAyw4mrT4CMagfmbIXp8UrCvvfPV72/+c49SYv3gdgVCTnslngspkVxnRMFU9cWk8816eZeKZ7Q/FG77qCVjf3Wu3KV1TZa8frweBIWL4iJrlVRyS3aI+EJi2R0qoTWrbvgii9HnpaH8z5LUeuOOLHc+uKwEuq6esPyfkYqtbfrDCGNkL8+MyD4vuJo0T/3gNwdd+r8eOkB4Nd2kcGUFPQQp4zEPin317iEam+kvYgomiaUBG68L0YRAa/fpuAy20Grwac/KoYAvsdGjTBx+qtgwaNZ1r5vizcNEdVVa/XvxNDh0a/ZeySjObPbhRFQz5C49GTNPFzIjzVP0JXXrDQZ0CNKAG/14P9Fomi450Fh0sEfs93l8kSbOVS7j7Kk20u+xBIzaQEMEhEpm/PkLbcw8g5dJAPfdTDJNyoBTZ7ME5yJ3yt3myoVYDdCl6yMUKFeBpghPI02i2L4HZbVJTvf9n1T4gObVorMNMTFYf4xs2z4sPxFTHl18kcj9V5EaOXyKd98hL2CllLBBvRBY6yjUvsMl8pJYrdJ5fhDKH7DIUmE0weib22ssbFchqSatcFIzk58Fg38/PS50gG09pI0O/eGUID8xlLf92Cb37cwCTMMII/32zfL9fvz1Yvkxw74QlmwR7ur4hpdHEPyXj/8TsxvnsadW4/HZQ4lXL4oiNvvU68gclK4fa4kRp9wsByojwZisY0RkUhnp0RfdmyeWuWMfDWp9XwbXQlQe+NVfsATjoP8NI0oJVxqmFCjSQlmmTNhyvvqKJ1MmAUKhJml5O2VOtkgabJx731WrTRKnwetWfODdP/zgmqYUJ1lpZkRLQ47gByGsmi3/AxGHDnhODv+C/t1EGMmvQaRj01DeOnzsC9I0biyYdG48Z7R3GWMvAlWFXcE11MZuKB5gM4rXlw2QuVf8I+Q3lwplVr18mWVz6EeyfNJ4MGLmlcV3Rq3kU+eseDYIAc3zdefBLh+gkaQPn50+Z0y9EXN8SQFol49rKaSKxZF4UiEdMHtlCKZIXJaCdGzF2i0PflwC92JvVuKp6/omlQhuX4vX4up42cODtHg9A7E2DQkVUFOQGC78UIxAJXR2JjMOx1aiLh2lv4xU7i/KueFY17PnbCOwR4GmMEcp78yr3fbDafXOgcTg0awODhK9DSHReEpbgY2bn51D/o8IeOVvgYlpbMp6yKPu37qt/dcwYXLQ/5IROHs8yqEX463/OCjnFZKAReOkjiqGfDTjB4fe6lc4BQcPqpQHv9TagsO5AaVi+eq4yVLyXWgKMYKnuu00EDKP+HNqxTSzRrWE+0bVhHtCb4abDno1iW4wbRwG/rDAWnlwdv32oITZs3Mg2f3tMaX9zfgV/YKDZ/u0jBoCtbLR6dVgxPRCiYxwgtL5TOPJIjGQeP5qqQ6d633SsZhwsOQgPnuarnZWLA4i1pTIcbgppmpyoUFSmiIufe63QgFFo+rSwtXlHod5ggdRLCGKxGGbFgGdF11N6DQ1cTBWY7Zn2+Gtv35TKP5YMNzI3M+PfPu+TcPQ71qNjBg1mY8NNhzNhSSP6CTYEzheL6Wb/I1ITUUFa1pf9sxYOa1xwrh6tEOVnbj+bLjByTTM8tklzozD61BIPp1dt2yjWZR+WmnXskv63zleuuCL61c+eRfLn3aJ7Uygt1zDivhkeX7JDv6m/AqINdMGJvF2hyN36wWpqydoPBZaj6mAPLQz85oJxfJ3jAZqpibD9kwrYDR4KJHocdJnMx1pYeKO05slv8/Pt67MjaoWS+ur+3KLQVKjrcLkED4IciGVv2bxYcemk552NFM8ppJUZIxDit0NF2r6BtZG9CspLwCj149DbQcvH1ST0w48UrFL+ii1564XK51ehRUTrzOP87L/WBwaDjKKyWgLNmhF7FK7sUmwrU84va/M8N/OHt3ZTVrJg9WjK2fzURl17QFbwi4M2hsB8Byiuzw3kNxYX1UkWnurQWLJeoNOnzocRuR8e2rWlpVYeXd+CGj/J7scvqwsGcgMM3vksSHu9SI3gUzEXtoNFlRI/zcU+Xeui2a6o6leOTOfbcF97TXTAeviDQyEUWO3TGAF0jMQVxcQ3w4L8/F91umcLVUOAnehiFZjOO0T5/8cZlsK/9GVfe9JgavfieGrRNIg7Z0Bmd2rVRI0B6OP4uQFNMZWHu/2bJvE0fnqBIlq+RUsm8qQs0GGdKoMaz6owsHkSJx0vTjAs5NgfxWIpR8e6h3eGE22MiOcDn9cNRpH4roOLlL/x2Ua+zBG67A26ayrwuByy//Ue+9ONeefuCzXLrxM4K2gqFw9AyOvUfzBUJZZ3zdGBsPcmf6dX7ablWVoB7DOPCls1UDyRFgqFJvbNsCxbG3IyXc3vCrzdSY7gw5n+FmQMXHsi8+7uCzIvPqy8uql9L9GzWiI5k+ViWsUiMX1ucOXRJduaon02ZU9bnYTLlnyuvg8EYpYrW0yGOISFBnd3z+T3dUy01mWa4bFa4aSmXUCcFsXViMfGFR7Df0B27j+TDTmkI+by/7CMV4zK++naFVJEwvPyhAXhop02QCfz21aty+5f/llsWvlypsngYZvA7+jS0ql9b/KNlE1EnRofmtKtYIy7QmBXpmt2NenE6xNGqQMvvtlswx301Xv91v5xR3BtTzX0qyqp4fbp3EwwVoUuUoAt9vbE1EJtSm6iy35+2ficYzI34AKyFCtD0yqdE3S7DBQ/QPmohr65UqxXIaqz/3NwK/J6+/w45bl9PdklOe+vKRmlv9kji9TYb0QnLt1GN7WlvkszUHilpv7xwL9ZNeZDLwbrdh5B+IBcZWUdxxFSMU/00qVkD/ExjRfJFRUX8ilmuB64f0E/5ABXJneu84y1Ef2kmbZ5QUOG30/WPo/31j4tal96l/gOmUCHuRQyNl+rdq5HB8N1nh2Le87fji8n3YP4Ld+CTl+7C/XMWqW1kbTuZQ8rAFiZcLpewWCyKdppNsBYWqB+XuiyFyC3KEYxXP3sH0z5/B6t2/IY129Mpa+A78/knwQjEjl93HXOCwZxxtz0KBtP89nAeAeKq8Sti+O/4KyhjAKEFDJ2/WTJCeX+WdvujwEbFGPLoLFz+yHtodu04tLhjCm6d+JF6gOTjUbeCof3PHyzLCL3X4jGDxdh+bfH0dZ3Vg55aWj4dT5t8Ak5JCHnCSNJJI4OnI4Ymr4VsrBqYx+8ODtt9gI179ssN2zPUUMjK0OBzOcGY/cJtkqHx/0yoUxNHIAc3iE0G7M3wFzZdYmks4DICpQWuLVMS1atrnl2agYnLj486/9lmxnu/mQNCdOXlJWO2pR9mlvREy4atpAZKDk4FYXkWYPIAvM/PighFVGIyGJ6QR7dC0yuieShlFBpaqGQDHQuPXbobOTk52HfoKJK2fYQ6OctRuOEDrPpojJy6qSBz1i53Vp3kJPXzcu6tjDf36rImbyoK/giUC7v4gubikvMbkxlwLAB+SWW8IbDkZM5lbXrKy5r1kiazFe7AD0Ak8RkUAHZPDnx2P3gnUAOfJ/CKZl7MRoTlFNC/bTPR9cI2ZRTL2vJ7yTKIGPX8Z3yWTtSpf1/a5MOwOavloI8D27FOlwtuOjPg9wV5eQfQUkI7gLQXUPaxgZAbCPA/Zgz/eI3815INsseNdwQbst99j8iBt06SI8Y+gZEE67IlyF32HmKMsYgyRqHPD/9E16W3c3ZBF3EFHQIxdAYDBox9ipesavnIS8DELmNZJix/GEq6oXNevhJ6XT9C3vHIeKKAvPQ50mO3UiMdP6p976mhKo29Z95iVZHSizZne0psYNgtZjjJWeNkN/GapqWhaaMGkC2GIK3v42LQmHdEnxHTxQ1pSHu6a62mLMdlMJi+s1ZBGq0aFN9UkIfMY2YYoqI4KQhfQrHiMd8bZwE/as7z+oc/zg3KcHkvr8ySTjqOZrAs7SYFTwIP06kgC/M+ABtVWE4BDy/do365G6NLgTE6hvWhIP0+eOkQhYZ0MBKoZ6mECi5F+jjF5UMXxsejBoiPuu9RPC5DEeUu17y3kFw11fnKpQCOkuOGdzu+wYf39RXG6NigHD8jkLlsOaKIx3xGdHyCSjebLZj0rRczNzaE2WLBntwidQgUm1mo5DfvP6KmgFW70rEyIx22jPlqZOHj7bDcCjb4Paqn+5Os1KOilRJz8y1gz/jCJrWzmMG92qgzMFkhavgdit+2dUssf3JomVYtH1eCpRc+OyglKw3cvkCSLmSuD3AAQTzmM7TdwqKQBzt5tKKlJLQPy786oKWK+jxemPKPIaHNbYL3ARQzDC+6rJx81dN1ND8yMnPz5EX9HlGNeCzQruBePfTZD156Lg0AAAVQSURBVBUvJSUFPLSu2BD4X7cGv71cPvblb+DwEJ3Vh+rwjduvVVHuiYoodzm84iV8Pm24GoEmrinEh4fjlMF1v6SLeGtLoXIC/XS4xNm4h3LYb8r7qscyXbPv/WBwGqNT8y5y3F0PcZIC7wqyIasIXaI694RDH0UUcMcVN+HhG0Yqmka4YJmKUY0u/9+q6swzJoPBwyqD5oFgmcXU+7JeHYe896cEeRoRTSMH01527ujQxUdw0YaN9mvc239tDu6BvZ6aIjm8e8bHYHS77hb1KPZTByYjJuNXdCr+Hf/KuBuZ81+AxXfcoz/sDIw4M1Y7MXBY4KlgzquPioGjaSoP4+qEcWbT5SjYukA1IJ/sMTYvvl/92FMb3q2bpsoln9zF1UWbejUFx9lfYDDzlRbb1IOuTIcbdE/MnwE+GGFljJ07HaDTNFYC9/Lxl6Sm/Xfl1+oBTOal034BL9MYvbteqt6ssXTc9YLfus3gDRuW45DBPRCQYCeQQ05bW/pQRrdX0wX/LJt/mcPgtDcvS2i69tPxknFTwuG0jdsC+xO++MBTwSwzppPyDfH9r2sY8taV56NW+2GC07jO/GDJD/ntwI4dD+/9X/5MsqfP9FvXdFcPnmjpnIfTnZYjOLJ7O0fDDrq0/OXw7FmIPT9Pl8f+vRrcuB8M+0D+PP5bdGxSX3DIWmHeb5PXMQmmGa9lJWaNXm3LDEWjS/6hhnElSJeVkx8XXCaH1s8/U0swYuOaS4dI3orlpRgP3eykMX/ytP2Y+PJWTLj9Ubz4w17lwNHwzkmK5uf32Ig8C8xg+oF90Vg2apkaAXa+uEWNCtFLfGBwpoezE4PpI597BabvZqm9f5732fOXB9Zg/FIHGrZqx+JhB92QZxfhoQX7FF4/PB/8A8nP4reCwdrgkHkfeH4Bg8/M3y5cjHsW3CPWvnBbGmNGd+q5LxynWb4i1L6iO4yXXKCG26/Xfyl41OE48/Nq+BQ/M9qiZJi//6u3VH2oHqJ8eZMLPlXyT2fOxMB3BwpuTKZJFm96V0CjWY7B+e3kIDKYZjkyLBEK5oUbdNtX/RhUAtP/+2JuMM7KYAUxTwMvlRicxiGjPK3Jlg+5LA2ch6HF+d5Ma6FGcxksx2F5sIx2/8ro8nm0OJcZARDYnI9oImw1EDGAsG36wB8eMYCAHqrd9XRVOGIAp0uT1bSciAFU04Y7XdWOGMDp0mQ1LSdiANW04U5XtSMGcLo0WU3LiRhANW2401XtiAGcLk1W03IiBlDNGu50VzdiAKdbo9WsvIgBVLMGO93VjRjA6dZoNSsvYgDVrMFOd3UjBnC6NVrNyosYQDVrsNNd3YgBnG6NVrPyIgZQTRrsTFUzYgBnSrPVpNyIAVSThjpT1YwYwJnSbDUpN2IA1aShzlQ1IwZwpjRbTcqNGEA1aagzVc2IAZwpzVaTciMGUMUb6kxXL2IAZ1rDVbz8iAFU8QY609WLGMCZ1nAVLz9iAFW8gc509SIGcKY1XMXLjxhAFW+gM129iAGcaQ1X8fIjBlBFG+hsVStiAGdL01X0PhEDqKINc7aqFTGAs6XpKnqfiAFU0YY5W9WKGMDZ0nQVvU/EAKpow5ytakUM4GxpuoreJ2IAVaxhznZ1IgZwtjVexe4XMYAq1iBnuzoRAzjbGq9i94sYQBVrkLNdnYgBnG2NV7H7RQygijXI2a5OxADOtsar2P0iBlBFGuTvqkbEAP4uzVeR+0YMoIo0xN9VjYgB/F2aryL3jRhAFWmIv6saEQP4uzRfRe4bMYAq0hB/VzUiBvB3ab6K3DdiAH9zQ/zdt/8/AAAA//8FxbxzAAAABklEQVQDAELM3R7RUxLNAAAAAElFTkSuQmCC"},{"name":"blue_wizard_circle.png","path":"","folder":"","namespace":"","id":"2","group":"","width":32,"height":32,"uv_width":32,"uv_height":32,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"081651c7-604c-d7b8-f059-ab2f3d57f3c3","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAE+0lEQVR4AcRXS4hcVRA9dwYhYiQKIXa7G9CFMRFdOLiRCKLS6kCQsQPuDAQU7UGcEFwM5AV6NflApiMGl3GhToKM+GlQBIMbGVDHD3EjcdmdEMhoIgZC+uac6luvX2ZePtNZpHn16lbdU6fq3lfv0yO4w7/bLmD+/xhvZw3DF9CNY57c9Nn40DCFDF8AMK6E9btDkEYPT5pe42n4Airh0/oyDmr11IdQCR+vMbfB11ZAjCM4E59DJ36Abvxl5l5M/3YFoH5Xtvk1L5zR81Qc01x5XLcArUxC4g8hkk58EV38PnMPvmHCN2bW43Hw11zfvwSyzc954Rj3ssWdwVHxSAhfdVy3AEfOb8AuEi6R/CvKZiWU/HAZkIhYWiK/RDgW9AXjfrV4JyvR5QXEOFpfxjHh0xZv1Vjk0tvO92+9k5fQlu3a/Y5jIVsUL4zxkVfjopQXEMIVVLCTQacEFuFjoxpROrHHhJHSoLWYEizKpkT2QY9+CK84jcnzp/ggXjkKUl6AAF28wBVs1lDbXNC67aayBfwhn0uyp2iHFXg16SMgH+dWHeUFRHY7sF9orSK/1+WIOJySybpGzM95dypO8cmetaZMhqvyAs7iWa0+D+a2cxtRP8/kn2MBg994GrpGxnnhhPfLIR7yPQryJnyuygvo4RVHeGPRboic2o5sO7bM34earrW0bJvgKeHUIyjEg0/LnJcwOwYFdOMY790d5g14SroQPJUVrnnG5FxRSw3IlZ6Qli2/4iRZH6+eGBSReLkzk8w1JlxeAO/X05RPOHmA97A9ZARgZ7epN2bb8UySPVxxiz40L6CBanjVNB3yE7OHYli6NqZ4DgHj7cT9xB1nrtNyWgHetWoaTk5rQtdNmnaNspcrdKnJz6QnmPyIxtJm0yCuRnHsXsbW3loHFPh2Kw+hUF4roOjglh7S5MzF/sOGdpuyjwlctCNgkknu1tvCSptNg7g2xbH7GNt+/xJXP+CzFxihUF4rwIwLeLj+D17jaqabF7GE9Nu2DlrxuWwB3yeZrS9z6znPpC0mP26atvzEzFIMS9e5FM8hd0G81bCbuB115pMzLwCbwl/wV2rEj5o8eX//RcPxXMbGo7YjY4NxlY10B0xKy5bfADxlffwceyDmPIkX1TBv+YgbFEAjP0bwmY9TsJ5+q4rgStq6A6QzFuUxWUpOOzDZIEeBl3N2DCbNTKdN+I4rOuV9QJLAZpK0EnkCYjENXEPzxLYoSq7CIR7xgbwJn6vyAkLoIeA9oRQs0TjJNTuRfKaUnIM5ih2Kk5ghPvGaMTiVF9B/F0zYtU0fHOpYSQrNixBGvmJy4STy6/ZLmIlbexcoub5iNvBDhAy6V6lsG6V5OUbY2YGih9G4+fiBKpuibbdF+co9ng+eXSDvyiIMnEj6ittk4L4FNthHzX9xWKaTpsYEE9Z0j0tr3v2O43U/onjNSYyX/Bq7rC7AZ6gZfAwVvI4Hwzske4lijakET98FSJRUWiK/RDg+SyZQDQ3FGw/5yo7SAnT9JAzeCf+KqYavaW9t/ofnmeAoEyyJUAmlZZuf88KhEr6U3+L1dcX/D8ZpzsGptIB82pO7Q9v3QPiWK3uTCZ5gwgNqMOqDss2veeE8Rnolj3xJblxAApWqTpzkvW4vFuppPpLrGOI3fAEBPymfdzlG8bPstcrwBVTC335NTetdUpL9Zq6rAAAA///1H1lOAAAABklEQVQDAOnDcf7i3dppAAAAAElFTkSuQmCC"}],"animations":[{"uuid":"544eb61c-2138-d7f8-b67b-a49ae5e1b313","name":"idle","loop":"loop","override":false,"length":3,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"10ee45d0-a3e0-f40b-990e-a0dac19721d1":{"name":"hi_head","type":"bone","rotation_global":false,"quaternion_interpolation":false},"c0820792-9e91-8bcb-812c-a6d07670799a":{"name":"left_hair_1","type":"bone","rotation_global":false,"quaternion_interpolation":false},"a7c9dbe8-f0b2-caf9-e2ae-fa40543a508a":{"name":"right_hair_1","type":"bone","rotation_global":false,"quaternion_interpolation":false},"8ee82b29-c9f1-ae08-9c3b-dcd5f8787db5":{"name":"left_hair_2","type":"bone","rotation_global":false,"quaternion_interpolation":false},"5a05f380-61c5-f164-885f-d3b2cc27acbe":{"name":"left_hair_3","type":"bone","rotation_global":false,"quaternion_interpolation":false},"aecdc16a-4f38-07a6-d0ed-48834f8a5836":{"name":"right_hair_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"47b7bce3-22c2-07fe-4b8a-06c75634a25e","time":0,"color":-1,"interpolation":"catmullrom"}]},"6492768f-2a1a-0139-2a5a-af6c200210f8":{"name":"right_hair_3","type":"bone","rotation_global":false,"quaternion_interpolation":false},"469494ea-42f7-b718-90ba-46d9298a3b38":{"name":"body","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"math.sin(q.anim_time * 360 / 3)","y":"0","z":"0"}],"uuid":"34a05409-ee8d-3e9f-f50b-8b55c57fec2c","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"math.cos(q.anim_time * 360 / 3) * 3","z":"0"}],"uuid":"46e3668b-2c2b-c8b6-1d43-c71e6721980f","time":0,"color":-1,"interpolation":"catmullrom"}]},"0266d8da-f639-7c87-40f6-834b24e671e2":{"name":"right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"5"}],"uuid":"7eebee87-dcb3-0b51-d498-1713cfa99aae","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.477660105","y":"1.1540833125","z":"7.2178270917"}],"uuid":"57299846-a34c-0742-7559-a702c3f050d6","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.4106286282","y":"2.306436698","z":"9.4374532452"}],"uuid":"908b3b7d-339f-4d99-1ab9-663ccad1f5e6","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.477660105","y":"1.1540833125","z":"7.2178270917"}],"uuid":"68d54e19-c145-dc76-f64f-1462f039e2aa","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"5"}],"uuid":"1f19ac3b-ee96-d885-7fd7-0296243ee7ac","time":3,"color":-1,"interpolation":"catmullrom"}]},"a44819f0-9e13-12f5-b270-88d133e31f46":{"name":"right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-32.5"}],"uuid":"42132fd5-19d9-b65e-4d7f-7c92bf3f4a33","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-35"}],"uuid":"dee6c331-8b3d-5c9a-316c-bd1347743b9a","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-37.5"}],"uuid":"94e557ba-6111-5014-9cdf-7af9bb92f61b","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-35"}],"uuid":"6ff51896-550f-9292-421e-3b24889b5d00","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-32.5"}],"uuid":"25c6f1f3-e6bb-53e4-e302-38e47cebbf95","time":3,"color":-1,"interpolation":"catmullrom"}]},"98b261b3-0a69-4a62-2229-0d1aee17c78e":{"name":"chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"math.sin(q.anim_time * 360 / 3) * 5","y":"0","z":"0"}],"uuid":"c112a855-cdbf-d5ed-66b3-5484fbcb17a1","time":0,"color":-1,"interpolation":"linear"}]},"b6fc560a-2b3f-cb7b-2280-dfdae36d9d53":{"name":"right_hand","type":"bone","rotation_global":false,"quaternion_interpolation":false},"5cdc1be0-a703-9397-813d-b0c2fe914a14":{"name":"under_body","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"2 * math.sin(q.anim_time * 360 / 3)","y":"0","z":"0"}],"uuid":"e953d30a-0a38-99c3-2c25-907a1411d11b","time":0,"color":-1,"interpolation":"linear"}]},"20832476-f83d-a3bf-a371-dfb6ddc221f1":{"name":"right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-math.sin(q.anim_time * 180 / 3) * 30","y":"0","z":"0"}],"uuid":"daaabeb3-26f4-e58c-89a7-84003868aa82","time":0,"color":-1,"interpolation":"catmullrom"}]},"14f70d2a-4f4d-e8e6-fa68-cb95be5d9ff7":{"name":"right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false},"2e3cc482-b167-33b8-1714-5c7b341d65b4":{"name":"left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-math.sin(q.anim_time * 180 / 3) * 5","y":"0","z":"0"}],"uuid":"4834ce42-16d6-b7c2-0d2d-e8bb7cbb41f4","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0.5","z":"0"}],"uuid":"c5eeabec-6a71-526f-0ee1-6de37fa7e4f7","time":0,"color":-1,"interpolation":"catmullrom"}]},"7113ca1c-2ae3-a77e-c3ab-63794d1ba17f":{"name":"left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false},"6f3eac45-0aee-fcf4-e283-e1fb93e876d6":{"name":"left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.4549832862","y":"0","z":"-14.7309816032"}],"uuid":"5dd3f31e-13ae-2442-f497-e1609dfe68b0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4479674937","y":"-0.3242667753","z":"-17.2098758475"}],"uuid":"82d37488-d6bc-4996-08a3-7423a30c6904","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4269321258","y":"-0.647926641","z":"-19.6889287011"}],"uuid":"0a0c9559-71b5-5d9c-4b63-433a8120ea5c","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4479674937","y":"-0.3242667753","z":"-17.2098758475"}],"uuid":"2f5d4523-dc0d-b2e4-35e2-86ab1c4ae846","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4549832862","y":"0","z":"-14.7309816032"}],"uuid":"359528d7-459b-1ca4-589b-c1db6c895a81","time":3,"color":-1,"interpolation":"catmullrom"}]},"7c168fd7-cdf9-c904-82b2-464caba505cc":{"name":"left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"32.5"}],"uuid":"96b97287-9155-d23e-878e-e5e7006d6db5","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"37.5"}],"uuid":"49d4221f-7853-b238-18c7-5f7b1de37c9d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"42.5"}],"uuid":"006a5cf4-48cf-6abc-1360-1871f70fc03a","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"37.5"}],"uuid":"515964d2-4bf0-cb3d-6a63-23cefc265ad3","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"32.5"}],"uuid":"83a3dadd-ceff-4caa-6900-785cacf03576","time":3,"color":-1,"interpolation":"catmullrom"}]},"842ad7bd-7015-fa76-ebca-314ed49a789f":{"name":"left_hand","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-25"}],"uuid":"d88421b2-d101-85eb-db68-b5c6e4dfd4b4","time":0,"color":-1,"interpolation":"catmullrom"}]},"2404264b-66f1-5dc1-5fe5-db0c33526a27":{"name":"middle_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-math.sin(q.anim_time * 360 / 3) * 20 - 20","y":"0","z":"0"}],"uuid":"fd867778-ff7c-b045-d87a-db2b6ab78c90","time":0,"color":-1,"interpolation":"catmullrom"}]},"1a9595e0-ba95-9397-76c5-68d018b0975a":{"name":"middle_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-math.sin(q.anim_time * 360 / 3) * 20 - 20","y":"0","z":"0"}],"uuid":"9993f148-16e8-e729-7f55-fa31876a5a86","time":0,"color":-1,"interpolation":"catmullrom"}]},"f719d2bb-6c28-0ab6-a585-41f54792160c":{"name":"middle_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-math.sin(q.anim_time * 360 / 3) * 20 - 20","y":"0","z":"0"}],"uuid":"002d8cd7-15d4-a2e6-7f24-bc9cd6940c60","time":0,"color":-1,"interpolation":"catmullrom"}]},"b2fd5761-a84d-7020-38c7-2efcd6c39298":{"name":"middle_cape_4","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-math.sin(q.anim_time * 360 / 3) * 20 - 20","y":"0","z":"0"}],"uuid":"ca5c5ff7-879f-a8d1-707e-9dab331e36ce","time":0,"color":-1,"interpolation":"catmullrom"}]},"9b4610f1-5d4d-9c62-d7db-b098f5a0a8e8":{"name":"eye","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0e9fa4e5-5b9d-e6be-951d-4bf3d72035d7","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"ff2953e8-2e48-4911-59fe-1c28121b9186","time":0.25,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fe28f4ef-e08c-75fe-4fd4-62e7fcd8795a","time":0.5,"color":-1,"interpolation":"linear"}]},"62421ffb-7685-a28e-c939-59b0d8123776":{"name":"left_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-math.sin(q.anim_time * 360 / 3) * 30 - 30"}],"uuid":"0b8b70a0-f19a-9921-feee-a8c334e1a75f","time":0,"color":-1,"interpolation":"catmullrom"}]},"167d2bb7-4626-6746-c72b-ba3c8dab3e2f":{"name":"left_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-math.sin(q.anim_time * 360 / 3) * 30 - 30"}],"uuid":"5f8e1906-344a-24c3-a15b-306f5ac34fd8","time":0,"color":-1,"interpolation":"catmullrom"}]},"57123914-f464-2f02-3eb3-e1ac924f8ef2":{"name":"left_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-math.sin(q.anim_time * 360 / 3) * 30 - 30"}],"uuid":"88df0856-42d2-8850-67ac-46e0dd92ba0e","time":0,"color":-1,"interpolation":"catmullrom"}]},"06102678-1ae6-d632-30b0-6964f314e6bf":{"name":"right_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"math.sin(q.anim_time * 360 / 3) * 30 + 30"}],"uuid":"aa01fc61-85cc-9124-2306-071af11bd9ee","time":0,"color":-1,"interpolation":"catmullrom"}]},"977a05f5-98e7-7f45-0d7b-f345af018dd3":{"name":"right_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"math.sin(q.anim_time * 360 / 3) * 30 + 30"}],"uuid":"869d48de-7430-ebc2-8037-cd89a936a5bf","time":0,"color":-1,"interpolation":"catmullrom"}]},"18d36cc2-46b6-f929-3624-07aef443f80d":{"name":"right_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"math.sin(q.anim_time * 360 / 3) * 30 + 30"}],"uuid":"b1eeb626-97ff-6c85-56aa-8189d2ab5c44","time":0,"color":-1,"interpolation":"catmullrom"}]},"185b2cad-6cc7-a818-34f9-59c70db1d853":{"name":"left_hair_ik","type":"null_object","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"-5","y":"8","z":"6"}],"uuid":"542933d9-6504-5d5f-cb51-00959fd860c2","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-5","y":"10","z":"6"}],"uuid":"379c4ef7-5e54-792b-4b7e-fcb6103b578d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-3","y":"1","z":"2"}],"uuid":"b9b27118-1bd3-682d-5022-0bcd8ab5a9b4","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-1","y":"-2","z":"1"}],"uuid":"2dbeeda9-aea8-f8f0-3449-b06960b17478","time":2.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-5","y":"8","z":"6"}],"uuid":"26cd5f0f-cb71-0fa0-3bb6-ae77559c611d","time":3,"color":-1,"interpolation":"catmullrom"}]},"ac61223e-e43c-ced3-6869-82c8e685056c":{"name":"right_hair_ik","type":"null_object","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"1","y":"1","z":"5"}],"uuid":"9de7d69f-76cd-27d0-1c86-77128759988b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"3","y":"2","z":"3"}],"uuid":"9e690c75-0313-2524-872c-cea00543b8b3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"1","y":"1","z":"5"}],"uuid":"ffc799a8-5277-90d3-91a7-d2beac14db51","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"1","y":"-1","z":"0"}],"uuid":"428276f0-6248-802c-8358-fa847b89ab42","time":2.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"1","y":"1","z":"5"}],"uuid":"277df8ed-0145-f839-0357-2491ef8cf7b5","time":3,"color":-1,"interpolation":"catmullrom"}]},"0d53fe40-29a8-f10f-e40f-59194c70c775":{"name":"shadow","type":"bone","rotation_global":false,"quaternion_interpolation":false},"d8e887a2-a16d-3dfc-b2e0-ba8aca6bb84f":{"name":"hitbox","type":"bone","rotation_global":false,"quaternion_interpolation":false},"aa6e0a4a-d036-a286-5c6f-ada2c220032b":{"name":"circle","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"math.cos(q.anim_time * 360 / 3) * 1","z":"0"}],"uuid":"4dec9ec2-3b21-2000-feff-e9c53de07d24","time":0,"color":-1,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0.875 + 0.125 * math.sin(q.anim_time * 360 / 3)","y":"0.875 + 0.125 * math.sin(q.anim_time * 360 / 3)","z":"0.875 + 0.125 * math.sin(q.anim_time * 360 / 3)"}],"uuid":"c1d68df8-c996-6634-e09c-76480b65e836","time":0,"color":-1,"uniform":true,"interpolation":"linear"}]}}},{"uuid":"e88eba94-3ea0-aba8-4245-5d5bed60f1c8","name":"walk","loop":"loop","override":false,"length":2,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"10ee45d0-a3e0-f40b-990e-a0dac19721d1":{"name":"hi_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"15 - 5 * math.sin(q.anim_time * 180 / 2)","y":"0","z":"0"}],"uuid":"7c0324ce-6bd5-dd91-3612-591a1b214ae5","time":0,"color":-1,"interpolation":"linear"}]},"c0820792-9e91-8bcb-812c-a6d07670799a":{"name":"left_hair_1","type":"bone","rotation_global":false,"quaternion_interpolation":false},"a7c9dbe8-f0b2-caf9-e2ae-fa40543a508a":{"name":"right_hair_1","type":"bone","rotation_global":false,"quaternion_interpolation":false},"8ee82b29-c9f1-ae08-9c3b-dcd5f8787db5":{"name":"left_hair_2","type":"bone","rotation_global":false,"quaternion_interpolation":false},"5a05f380-61c5-f164-885f-d3b2cc27acbe":{"name":"left_hair_3","type":"bone","rotation_global":false,"quaternion_interpolation":false},"aecdc16a-4f38-07a6-d0ed-48834f8a5836":{"name":"right_hair_2","type":"bone","rotation_global":false,"quaternion_interpolation":false},"6492768f-2a1a-0139-2a5a-af6c200210f8":{"name":"right_hair_3","type":"bone","rotation_global":false,"quaternion_interpolation":false},"469494ea-42f7-b718-90ba-46d9298a3b38":{"name":"body","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-15 - 5 * math.sin(q.anim_time * 180 / 2)","y":"0","z":"0"}],"uuid":"9ac12e1c-0e37-a0c9-473a-b227c47c78be","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"-1 * math.sin(q.anim_time * 360 / 2)","z":"0"}],"uuid":"ed0fdf5c-780d-67ed-ea0d-291c8d6b1123","time":0,"color":-1,"interpolation":"linear"}]},"0266d8da-f639-7c87-40f6-834b24e671e2":{"name":"right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"15"}],"uuid":"51adc5fd-7a5b-3f97-c3c2-a983769ac904","time":0,"color":-1,"interpolation":"linear"}]},"a44819f0-9e13-12f5-b270-88d133e31f46":{"name":"right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false},"98b261b3-0a69-4a62-2229-0d1aee17c78e":{"name":"chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"3 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"0"}],"uuid":"97c430b4-43d2-7ffd-6e1d-d4772a776170","time":0,"color":-1,"interpolation":"linear"}]},"b6fc560a-2b3f-cb7b-2280-dfdae36d9d53":{"name":"right_hand","type":"bone","rotation_global":false,"quaternion_interpolation":false},"5cdc1be0-a703-9397-813d-b0c2fe914a14":{"name":"under_body","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"- 2.5 * math.sin(q.anim_time * 180 / 2)","y":"0","z":"0"}],"uuid":"af272626-f3d3-41bc-4512-9d594e18bdff","time":0,"color":-1,"interpolation":"linear"}]},"20832476-f83d-a3bf-a371-dfb6ddc221f1":{"name":"right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-30 * math.sin(q.anim_time * 180 / 2)","y":"0","z":"0"}],"uuid":"deaaadec-5af8-9fe9-7e2b-74362b59a72b","time":0,"color":-1,"interpolation":"linear"}]},"14f70d2a-4f4d-e8e6-fa68-cb95be5d9ff7":{"name":"right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false},"2e3cc482-b167-33b8-1714-5c7b341d65b4":{"name":"left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 * math.sin(q.anim_time * 180 / 2)","y":"0","z":"0"}],"uuid":"43c7d13c-5d5a-fd15-897a-efdb2721bcb0","time":0,"color":-1,"interpolation":"linear"}]},"7113ca1c-2ae3-a77e-c3ab-63794d1ba17f":{"name":"left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false},"6f3eac45-0aee-fcf4-e283-e1fb93e876d6":{"name":"left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"-15"}],"uuid":"a5b267c1-1db0-75c9-a2e2-6d6a9a4f0b7f","time":0,"color":-1,"interpolation":"linear"}]},"7c168fd7-cdf9-c904-82b2-464caba505cc":{"name":"left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false},"842ad7bd-7015-fa76-ebca-314ed49a789f":{"name":"left_hand","type":"bone","rotation_global":false,"quaternion_interpolation":false},"2404264b-66f1-5dc1-5fe5-db0c33526a27":{"name":"middle_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 - 10 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"0"}],"uuid":"94fcf1c0-9bb4-7f78-c9c6-bf6d0dda91f1","time":0,"color":-1,"interpolation":"linear"}]},"1a9595e0-ba95-9397-76c5-68d018b0975a":{"name":"middle_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 - 10 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"0"}],"uuid":"75d4eda2-619b-293d-4c18-2d143db300ee","time":0,"color":-1,"interpolation":"linear"}]},"f719d2bb-6c28-0ab6-a585-41f54792160c":{"name":"middle_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 - 10 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"0"}],"uuid":"780943fe-961e-bc48-6047-2497a48a4fe9","time":0,"color":-1,"interpolation":"linear"}]},"b2fd5761-a84d-7020-38c7-2efcd6c39298":{"name":"middle_cape_4","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10 - 10 * math.sin(q.anim_time * 360 / 2)","y":"0","z":"0"}],"uuid":"eb78dade-923f-0da4-34f1-28ff5054f823","time":0,"color":-1,"interpolation":"catmullrom"}]},"9b4610f1-5d4d-9c62-d7db-b098f5a0a8e8":{"name":"eye","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b655a4a9-5def-c25f-2e66-7717e20ef344","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"2717040f-2a45-c08d-5a50-62cd85fea8be","time":0.25,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0a1a24a5-8a0f-f8b8-0e3f-c9633e130c67","time":0.5,"color":-1,"interpolation":"linear"}]},"62421ffb-7685-a28e-c939-59b0d8123776":{"name":"left_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"4b431b84-fe75-d364-d695-8027690aa95e","time":0,"color":-1,"interpolation":"linear"}]},"167d2bb7-4626-6746-c72b-ba3c8dab3e2f":{"name":"left_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"7ca65143-6b80-f5db-aec6-b5a468f90a36","time":0,"color":-1,"interpolation":"linear"}]},"57123914-f464-2f02-3eb3-e1ac924f8ef2":{"name":"left_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"b1dfdaa8-4bb8-c1a2-c775-278d5f27d440","time":0,"color":-1,"interpolation":"linear"}]},"06102678-1ae6-d632-30b0-6964f314e6bf":{"name":"right_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-5 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"414a2d73-47b1-628c-8d42-55278090393e","time":0,"color":-1,"interpolation":"linear"}]},"977a05f5-98e7-7f45-0d7b-f345af018dd3":{"name":"right_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-5 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"79642c40-f278-f686-8f4c-32e8680c1cd1","time":0,"color":-1,"interpolation":"linear"}]},"18d36cc2-46b6-f929-3624-07aef443f80d":{"name":"right_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-5 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"b450bfee-2948-10c1-7a1f-b0ef0c5164b6","time":0,"color":-1,"interpolation":"linear"}]},"0d53fe40-29a8-f10f-e40f-59194c70c775":{"name":"shadow","type":"bone","rotation_global":false,"quaternion_interpolation":false},"185b2cad-6cc7-a818-34f9-59c70db1d853":{"name":"left_hair_ik","type":"null_object","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"5","z":"8 + 3 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"18e2972b-b171-d1a6-3678-d6f458aa2b4c","time":0,"color":-1,"interpolation":"linear"}]},"ac61223e-e43c-ced3-6869-82c8e685056c":{"name":"right_hair_ik","type":"null_object","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"3","z":"5 + 3 * math.sin(q.anim_time * 360 / 2)"}],"uuid":"80187124-3b99-ac77-f79b-5b2fbb23f4c5","time":0,"color":-1,"interpolation":"linear"}]},"d8e887a2-a16d-3dfc-b2e0-ba8aca6bb84f":{"name":"hitbox","type":"bone","rotation_global":false,"quaternion_interpolation":false},"aa6e0a4a-d036-a286-5c6f-ada2c220032b":{"name":"circle","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"math.cos(q.anim_time * 360 / 2) * 1","z":"0"}],"uuid":"62fd1ca1-47f1-3644-d09d-34e935ee21bd","time":0,"color":-1,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0.875 + 0.125 * math.sin(q.anim_time * 360 / 2) ","y":"0.875 + 0.125 * math.sin(q.anim_time * 360 / 2) ","z":"0.875 + 0.125 * math.sin(q.anim_time * 360 / 2) "}],"uuid":"f696e54d-a5f8-8309-261e-06286f89a356","time":0,"color":-1,"uniform":true,"interpolation":"linear"}]}}},{"uuid":"bd0a18b7-d804-edb7-7d30-590e1f252148","name":"death","loop":"once","override":false,"length":1.25,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"10ee45d0-a3e0-f40b-990e-a0dac19721d1":{"name":"hi_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"a16f4d44-6756-5a48-4c23-5262260725ef","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f83ad7a7-55fd-79b7-f1ac-339cbdf6060c","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"25","y":"0","z":"0"}],"uuid":"695c990b-d178-58e8-379e-40e86db015b1","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"37.5","y":"0","z":"0"}],"uuid":"b7ab6b9c-2fdf-efd1-35e3-d01d47d83aa7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"52.5","y":"0","z":"0"}],"uuid":"ffc95691-fca5-2e1f-c4cd-bc6f1cd2aea9","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"a7ffee60-152c-2236-a83c-bbdafee5bb78","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"a727b42c-3777-e465-5ff3-30cd4a3bca04","time":1,"color":-1,"interpolation":"catmullrom"}]},"c0820792-9e91-8bcb-812c-a6d07670799a":{"name":"left_hair_1","type":"bone","rotation_global":false,"quaternion_interpolation":false},"a7c9dbe8-f0b2-caf9-e2ae-fa40543a508a":{"name":"right_hair_1","type":"bone","rotation_global":false,"quaternion_interpolation":false},"8ee82b29-c9f1-ae08-9c3b-dcd5f8787db5":{"name":"left_hair_2","type":"bone","rotation_global":false,"quaternion_interpolation":false},"5a05f380-61c5-f164-885f-d3b2cc27acbe":{"name":"left_hair_3","type":"bone","rotation_global":false,"quaternion_interpolation":false},"aecdc16a-4f38-07a6-d0ed-48834f8a5836":{"name":"right_hair_2","type":"bone","rotation_global":false,"quaternion_interpolation":false},"6492768f-2a1a-0139-2a5a-af6c200210f8":{"name":"right_hair_3","type":"bone","rotation_global":false,"quaternion_interpolation":false},"469494ea-42f7-b718-90ba-46d9298a3b38":{"name":"body","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"5.1754392272","y":"14.9415876115","z":"1.3378029844"}],"uuid":"3db9ca7d-a189-2dee-10ee-87384d5fa038","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1d8754eb-447e-2377-79d7-6b302a1fefa7","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9764bf92-4968-3692-7d8e-7d4c8ceb3d46","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-65","y":"0","z":"0"}],"uuid":"405f6707-0c30-df63-06df-432413b007ec","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-90","y":"0","z":"0"}],"uuid":"ae93e6f5-d158-e85c-4e43-66b2a36a8a68","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-9","z":"0"}],"uuid":"3062ce3e-061c-b4f8-759d-e4d9854cf4e9","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b7ab2662-8f70-6498-1321-916b85e33d14","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-8.25","z":"0"}],"uuid":"0d0e3e73-33d8-0ce9-3a20-175a7a93862d","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-15","z":"-15"}],"uuid":"1b5ee9ff-17de-454b-85f2-cb0570050b90","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-19","z":"-18"}],"uuid":"6ea49149-0200-7d3a-600a-24402efb557b","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-21","z":"-18"}],"uuid":"db23f3c5-a08e-7bf7-683e-cf126d1232b6","time":1,"color":-1,"interpolation":"catmullrom"}]},"0266d8da-f639-7c87-40f6-834b24e671e2":{"name":"right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4abae333-f2ae-9e7f-2ce9-b578b2ff278b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"64.2307378684","y":"-13.566260371","z":"6.4606648089"}],"uuid":"86d6faeb-6de5-ed09-799d-20633666516d","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"114.1486747733","y":"-20.7048110546","z":"-9.0071669588"}],"uuid":"4bc9b965-ae34-7011-dd25-b5d34eb2b0d2","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"136.7808211063","y":"-13.9954453589","z":"-14.4327550432"}],"uuid":"bd929d72-4a46-4250-2938-e870ce86437b","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"151.9244457898","y":"-11.0310955788","z":"-19.7338984647"}],"uuid":"22ddf6da-2a28-1c1a-29bf-bae2b4748338","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.0831222389","y":"-20.4611761434","z":"6.4190509652"}],"uuid":"44b182f2-3ccd-7794-fcf7-3f93dcd9d92c","time":0.33333,"color":-1,"interpolation":"catmullrom"}]},"a44819f0-9e13-12f5-b270-88d133e31f46":{"name":"right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b215c7ca-397b-ad1c-e4d2-7604f2acd935","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"50","y":"0","z":"0"}],"uuid":"c427d401-c525-3687-1f8e-ed18c9306141","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"1d0c9879-06a1-9dbe-c324-ae7481efae95","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"98b261b3-0a69-4a62-2229-0d1aee17c78e":{"name":"chest","type":"bone","rotation_global":false,"quaternion_interpolation":false},"b6fc560a-2b3f-cb7b-2280-dfdae36d9d53":{"name":"right_hand","type":"bone","rotation_global":false,"quaternion_interpolation":false},"5cdc1be0-a703-9397-813d-b0c2fe914a14":{"name":"under_body","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"77a167f4-b81e-d422-37ac-b834f0f3b6d2","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"a92329ea-c77f-d8c7-ed0b-e01d23b40210","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"806eb3e5-3807-f9a4-98a4-2f043200b554","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"72fa22e2-294c-6fec-7355-1c49ed019a72","time":1,"color":-1,"interpolation":"catmullrom"}]},"20832476-f83d-a3bf-a371-dfb6ddc221f1":{"name":"right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"05ff9b91-ec75-d719-db63-3293c0b4b599","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"a958082f-6e42-e767-a165-a83c1afc65cf","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"c8f33edc-34a6-75ce-0ef5-f877961e16a2","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"0922a78c-999f-4564-c405-3f9d420e1eee","time":1,"color":-1,"interpolation":"catmullrom"}]},"14f70d2a-4f4d-e8e6-fa68-cb95be5d9ff7":{"name":"right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"628f7969-9587-c8e5-0d31-62e5c5863564","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"55","y":"0","z":"0"}],"uuid":"22c3ebe0-6d73-0b53-7181-91f24c94d51d","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"33.75","y":"0","z":"0"}],"uuid":"f6beca42-d9a3-8df1-00de-db5253cc33ff","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"37.5","y":"0","z":"0"}],"uuid":"503e1745-76d8-cbd7-c233-c19d42d2abb1","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"2e3cc482-b167-33b8-1714-5c7b341d65b4":{"name":"left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"e4f11bce-2710-3eb4-e355-555ee508b0e8","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"d8988bec-15d2-f639-8898-cec491aa4545","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"e45704cb-a1a1-b8a4-2c06-e8d54252c215","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"97f5e8cb-75da-c0fc-77c4-646785658f24","time":1,"color":-1,"interpolation":"catmullrom"}]},"7113ca1c-2ae3-a77e-c3ab-63794d1ba17f":{"name":"left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"13c4106c-330a-a553-e151-1d486f2c12d9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"e608ccd2-cdce-c6be-a069-e7124ef76611","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1d644f29-8475-7b84-e57d-f0b3fac1814a","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"6f3eac45-0aee-fcf4-e283-e1fb93e876d6":{"name":"left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"804fa243-e81b-4d15-e396-73477487588e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"a3031388-d83b-d005-b810-023106bdf976","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"138.849121742","y":"11.7214510344","z":"13.0867025974"}],"uuid":"aae1ccdb-040e-1a64-4654-fcc0176820ea","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"145.927602266","y":"8.5372576322","z":"12.3796053964"}],"uuid":"b213405f-1a8b-11be-9ac8-cf4243be8c9f","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"156.6930376522","y":"9.3072682329","z":"20.576385285"}],"uuid":"291bebf0-5608-26de-5f15-5060aaaf68a0","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"76.8171724368","y":"17.0659528795","z":"-3.9323555472"}],"uuid":"2b1fd463-cc83-a524-c358-5094a714e5e9","time":0.33333,"color":-1,"interpolation":"catmullrom"}]},"7c168fd7-cdf9-c904-82b2-464caba505cc":{"name":"left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"42.5","y":"0","z":"0"}],"uuid":"8503d8e7-3d18-c261-d32b-5441c8cdd820","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"a8fe171c-1644-dc02-1426-1417e1ee6b76","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"d5170b4c-a695-0814-961b-996bed022005","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"842ad7bd-7015-fa76-ebca-314ed49a789f":{"name":"left_hand","type":"bone","rotation_global":false,"quaternion_interpolation":false},"2404264b-66f1-5dc1-5fe5-db0c33526a27":{"name":"middle_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2d8a8767-3c51-0746-4c41-8b01d495ab62","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-35","y":"0","z":"0"}],"uuid":"91ba3a67-624e-4c4a-fd7f-7872ec688bbe","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"d93201b7-b457-450a-aef9-44f6ea7c6775","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"695acdab-d134-2b12-4062-1b5c0fd60b18","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"2ae406dd-633c-cc46-6e99-89e47500a45a","time":1,"color":-1,"interpolation":"catmullrom"}]},"1a9595e0-ba95-9397-76c5-68d018b0975a":{"name":"middle_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fc68f8b7-5a03-b3dc-16b4-d9f2a56bb98b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"3d852dde-dd3a-56a6-3bb9-5ca4d8fbe77e","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"6c427534-c258-99d1-095a-f4b415860f83","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"a0d3135b-3746-7353-99e1-7cd08d16f1d0","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"f719d2bb-6c28-0ab6-a585-41f54792160c":{"name":"middle_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9829f20c-f4f2-33c0-97ed-61f91d6da71d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"9638942e-15bd-a3d0-8054-d420dfd4a061","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"bdcc3fdd-3f05-b3ae-a1b3-951c3accfd04","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"b86307a6-9e36-1c49-d7b3-a5c07b55f61a","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"b2fd5761-a84d-7020-38c7-2efcd6c39298":{"name":"middle_cape_4","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ed564ce8-2580-adc3-df1a-66f3dd90ef50","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.5","y":"0","z":"0"}],"uuid":"f1f9962f-75b9-3d94-8aaf-ac302dda24ca","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"4f1d202a-13c6-a371-60ee-d81501131b0f","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5","y":"0","z":"0"}],"uuid":"8e18f727-3b17-f8a6-133f-07e5ba157926","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"9b4610f1-5d4d-9c62-d7db-b098f5a0a8e8":{"name":"eye","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"18313221-3fa4-a587-cc55-890a3eff17ac","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2bda4425-6bf7-2d59-6847-7ab4d2e51612","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"3897b300-98c5-809f-88c0-f74a6688feee","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"62421ffb-7685-a28e-c939-59b0d8123776":{"name":"left_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7379dd2a-f6f7-5e8f-0102-b7f1b31f9e92","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-50"}],"uuid":"71b45432-232c-99c1-328d-e34ef8949cb5","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-30"}],"uuid":"60e20a55-d34a-d34f-99e1-b2c26278de4a","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-17.5"}],"uuid":"6b0ff250-0887-ad33-48cc-6eaec769b82f","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"167d2bb7-4626-6746-c72b-ba3c8dab3e2f":{"name":"left_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"dae60c74-bb07-e254-9c30-770a54621a78","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-42.5"}],"uuid":"4eabc75f-4981-874f-9ce4-476a91235f6d","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-25"}],"uuid":"6884ac67-dd1f-1724-5b89-fe0fb6a8fde7","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"2.5"}],"uuid":"1714a02c-64ac-4daf-5e28-6c9854fffc28","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"57123914-f464-2f02-3eb3-e1ac924f8ef2":{"name":"left_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f6101bb7-3bdb-a222-ba5e-3d32f02a8070","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-27.5"}],"uuid":"8082874f-2842-f8e3-0feb-88bb7050a7fe","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-20"}],"uuid":"8874cc5b-1e99-5217-547b-724ea9dbbce1","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-7.5"}],"uuid":"d524e0c7-ff1b-3322-ee1b-ccd272d52756","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"06102678-1ae6-d632-30b0-6964f314e6bf":{"name":"right_cape_1","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"077830f5-80a0-9f13-0a4a-036fa6f1e73f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"42.5"}],"uuid":"373c5e53-95f9-f7b6-8ba5-9ca3aefd5e82","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"27.5"}],"uuid":"787c4895-9b5e-3397-16cc-70bb01bb2d82","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"10"}],"uuid":"9759a6e5-b8de-8d59-3973-bddc97ba3ad8","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"977a05f5-98e7-7f45-0d7b-f345af018dd3":{"name":"right_cape_2","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"62671d38-f946-cedd-ea91-d7d2beb67149","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"72.5"}],"uuid":"0c3ad74d-3852-a53a-5313-394b42a291d4","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"52.5"}],"uuid":"81ced27f-4fa9-edd4-c1c7-0260f7289ee9","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"22.5"}],"uuid":"4f4d433f-8685-692b-2e07-9b0c1d8f63a3","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"18d36cc2-46b6-f929-3624-07aef443f80d":{"name":"right_cape_3","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3cefc86c-cfb9-f1b2-9546-608ca9783f11","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"40"}],"uuid":"5a4095ba-df21-9670-d087-e0c396308e76","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"25"}],"uuid":"30d13ac6-c5a7-d523-d57e-caca63d2d277","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-5"}],"uuid":"a16ff8f7-573d-fd01-b08c-b34a496709a5","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"0d53fe40-29a8-f10f-e40f-59194c70c775":{"name":"shadow","type":"bone","rotation_global":false,"quaternion_interpolation":false},"d8e887a2-a16d-3dfc-b2e0-ba8aca6bb84f":{"name":"hitbox","type":"bone","rotation_global":false,"quaternion_interpolation":false},"aa6e0a4a-d036-a286-5c6f-ada2c220032b":{"name":"circle","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"scale","data_points":[{"x":"1","y":"1","z":"1"}],"uuid":"2e5fa573-aa72-0a92-7dd2-0563204132db","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"},{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5729eb61-1e9a-ec6d-8b7f-d400d4910bbf","time":0.25,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"185b2cad-6cc7-a818-34f9-59c70db1d853":{"name":"left_hair_ik","type":"null_object","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"128f1ec0-f81f-d7fe-3a59-edc6c522fb10","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-3","y":"10","z":"9"}],"uuid":"82fb9f83-7715-3c34-7d10-64159bf9d223","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-3","y":"14","z":"4"}],"uuid":"f89754fe-dab1-5766-6d1f-614f9d2eec8c","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"-4","y":"12","z":"-7"}],"uuid":"5cbbcc40-75af-2ac2-868c-b116419a9eff","time":1,"color":-1,"interpolation":"catmullrom"}]},"ac61223e-e43c-ced3-6869-82c8e685056c":{"name":"right_hair_ik","type":"null_object","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"92a45fc3-4f29-8496-bb52-7cc3e3ad1c18","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"1","y":"3","z":"4"}],"uuid":"c0cef96a-9e56-5e34-dbeb-f67c3f2f23dc","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"1","y":"9","z":"4"}],"uuid":"88be0498-d67d-f58f-14c5-8e89cc829669","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"4","y":"3","z":"-6"}],"uuid":"7f369f69-8a9c-6a3d-b72d-d9b1948e857c","time":1,"color":-1,"interpolation":"catmullrom"}]}}}]} ================================================ FILE: core/src/main/resources/config.yml ================================================ # BetterModel Configuration # # This file contains all the configuration options for the BetterModel plugin. # For detailed information on each option, please refer to the official documentation. # Debugging options for development and troubleshooting. # Enable these only if you are diagnosing issues. debug: # Toggles debug messages for hitbox creation and interaction. hitbox: false # Toggles detailed stack traces for exceptions handled by the plugin. exception: false # Toggles debug messages related to resource pack generation. pack: false # Toggles debug messages for model tracker lifecycle and updates. tracker: false # Visual indicator settings. indicator: # Shows a progress bar during resource pack generation. progress_bar: true # Core feature modules. # Disable modules you don't need to save resources. module: # Enables general entity models. model: true # Enables player-specific animations and models (e.g., custom limbs). player-animation: true # Resource pack generation settings. pack: # Generates models compatible with modern Minecraft versions (>=1.21.4). generate-modern-model: true # Generates models compatible with legacy Minecraft versions (<=1.21.3). generate-legacy-model: true # Obfuscates model and texture names in the resource pack to prevent easy extraction. use-obfuscation: true # Toggles metrics collection via bStats (https://bstats.org/plugin/bukkit/BetterModel/24237). # Disabling this helps us less to improve the plugin. metrics: true # Enables sight-trace culling: models are only rendered if there is a clear line of sight. sight-trace: true # Merges the generated resource pack with packs from other plugins. merge-with-external-resources: true # The base item used for custom model data. # It's recommended to use an item that is not commonly used for other purposes. item: leather_horse_armor # The namespace used for custom model items. item-namespace: bm_models # Maximum distance (in blocks) for sending animation packets. -1 for default server view distance. max-sight: -1 # Minimum distance (in blocks) before animation packets are sent. min-sight: 5 # The namespace used for resource pack assets (e.g., assets/bettermodel/...). namespace: "bettermodel" # The format of the generated resource pack. # 'zip': A standard .zip file. # 'folder': A raw folder structure (useful for merging or debugging). pack-type: zip # The location where the resource pack file or folder will be generated, relative to the server root. build-folder-location: BetterModel/build # If enabled, models attached to invisible mobs will also be invisible. follow-mob-invisibility: true # If enabled, utilizes Purpur's AFK API to pause animations for AFK players. use-purpur-afk: true # Checks for new plugin versions on startup and notifies admins. version-check: true # The default movement controller for mountable models. # 'walk': Standard ground-based movement. # 'fly': Flying movement. default-mount-controller: walk # The time in ticks between inserted keyframes for smooth interpolation (lerp). # Lower values result in smoother but potentially more resource-intensive animations. lerp-frame-time: 3 # Prevents players from swapping hotbar slots if they have a custom player model or animation active. cancel-player-model-inventory: false # The delay in ticks before a player's original entity is hidden after a model is applied. # This can help prevent visual glitches. player-hide-delay: 3 # The number of packets to bundle together before sending. # Higher values can reduce network overhead but may increase perceived latency. 0 to disable. packet-bundling-size: 16 # Enables strict loading mode. If true, the plugin will fail to load models with unsupported features. # If false, it will attempt to load them by ignoring unsupported parts, which may cause visual issues. enable-strict-loading: false ================================================ FILE: core/src/main/resources/demon_knight.bbmodel ================================================ {"meta":{"format_version":"4.10","model_format":"free","box_uv":false},"name":"demon_knight","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":128,"height":128},"elements":[{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.7625,44.2,-2.55],"to":[2.7625,45.05,0.85],"autouv":0,"color":9,"origin":[0,39.1,0],"faces":{"north":{"uv":[103,34,109,35],"texture":0},"east":{"uv":[91,31,94,32],"texture":0},"south":{"uv":[103,63,109,64],"texture":0},"west":{"uv":[6,93,9,94],"texture":0},"up":{"uv":[83,21,77,18],"texture":0},"down":{"uv":[83,21,77,24],"texture":0}},"type":"cube","uuid":"aeb4b18d-0dfa-fbba-3213-36a2b8301fbe"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.125,39.95,-2.55],"to":[4.25,43.35,3.4],"autouv":0,"color":9,"rotation":[0,25,0],"origin":[3.4,41.225,1.7],"faces":{"north":{"uv":[19,70,21,73],"texture":0},"east":{"uv":[76,59,82,62],"texture":0},"south":{"uv":[80,56,82,59],"texture":0},"west":{"uv":[70,76,76,79],"texture":0},"up":{"uv":[26,95,24,89],"texture":0},"down":{"uv":[28,89,26,95],"texture":0}},"type":"cube","uuid":"fe3c2995-33c1-f5d9-bd1b-d6bef0b142c1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.4,37.825,-0.6375],"to":[2.55,39.1,5.3125],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[0,38.25,2.7625],"faces":{"north":{"uv":[103,15,109,16],"texture":0},"east":{"uv":[103,16,109,17],"texture":0},"south":{"uv":[18,103,24,104],"texture":0},"west":{"uv":[24,103,30,104],"texture":0},"up":{"uv":[57,14,51,8],"texture":0},"down":{"uv":[47,53,41,59],"texture":0}},"type":"cube","uuid":"b75f3760-7df4-f694-3187-05e86def041a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.85,39.1,-0.85],"to":[0.85,43.35,0.85],"autouv":0,"color":9,"origin":[0,39.1,0],"faces":{"north":{"uv":[39,60,41,64],"texture":0},"east":{"uv":[88,99,90,103],"texture":0},"south":{"uv":[12,100,14,104],"texture":0},"west":{"uv":[47,100,49,104],"texture":0},"up":{"uv":[83,49,81,47],"texture":0},"down":{"uv":[86,72,84,74],"texture":0}},"type":"cube","uuid":"a7aba758-5ffe-e499-3d63-2584906072f5"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.55,43.35,-0.85],"to":[2.55,44.2,0.85],"autouv":0,"color":9,"origin":[0,39.1,0],"faces":{"north":{"uv":[33,24,38,25],"texture":0},"east":{"uv":[31,51,33,52],"texture":0},"south":{"uv":[109,23,114,24],"texture":0},"west":{"uv":[41,52,43,53],"texture":0},"up":{"uv":[43,35,38,33],"texture":0},"down":{"uv":[82,74,77,76],"texture":0}},"type":"cube","uuid":"820916df-84dd-05b4-f88d-bbe80c9c07bf"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.55,43.35,-1.7],"to":[4.25,45.05,3.4],"autouv":0,"color":9,"rotation":[0,25,0],"origin":[3.4,41.225,1.7],"faces":{"north":{"uv":[68,111,70,113],"texture":0},"east":{"uv":[25,95,30,97],"texture":0},"south":{"uv":[111,69,113,71],"texture":0},"west":{"uv":[95,27,100,29],"texture":0},"up":{"uv":[36,100,34,95],"texture":0},"down":{"uv":[38,95,36,100],"texture":0}},"type":"cube","uuid":"dc68f85b-829a-93b5-feba-70599a0a5929"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.25,39.95,-2.55],"to":[-2.125,43.35,3.4],"autouv":0,"color":9,"rotation":[0,-25,0],"origin":[-3.4,41.225,1.7],"faces":{"north":{"uv":[44,103,46,106],"texture":0},"east":{"uv":[76,76,82,79],"texture":0},"south":{"uv":[54,103,56,106],"texture":0},"west":{"uv":[0,77,6,80],"texture":0},"up":{"uv":[30,95,28,89],"texture":0},"down":{"uv":[37,89,35,95],"texture":0}},"type":"cube","uuid":"cdca1aaf-a948-f853-f008-51da7819ece6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.25,43.35,-1.7],"to":[-2.55,45.05,3.4],"autouv":0,"color":9,"rotation":[0,-25,0],"origin":[-3.4,41.225,1.7],"faces":{"north":{"uv":[111,65,113,67],"texture":0},"east":{"uv":[94,57,99,59],"texture":0},"south":{"uv":[111,67,113,69],"texture":0},"west":{"uv":[93,94,98,96],"texture":0},"up":{"uv":[97,5,95,0],"texture":0},"down":{"uv":[25,95,23,100],"texture":0}},"type":"cube","uuid":"f68c9e09-063b-d45d-67ba-5784f7f12563"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.4,45.05,-0.6375],"to":[2.55,45.9,5.3125],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[0,45.05,2.7625],"faces":{"north":{"uv":[103,26,109,27],"texture":0},"east":{"uv":[103,27,109,28],"texture":0},"south":{"uv":[103,28,109,29],"texture":0},"west":{"uv":[103,29,109,30],"texture":0},"up":{"uv":[61,20,55,14],"texture":0},"down":{"uv":[61,20,55,26],"texture":0}},"type":"cube","uuid":"d2d8ae28-2190-b3e0-8982-4c98ab732c72"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1.7,39.1,5.1],"to":[1.7,45.05,6.8],"autouv":0,"color":9,"origin":[0,39.1,5.95],"faces":{"north":{"uv":[16,76,19,82],"texture":0},"east":{"uv":[82,87,84,93],"texture":0},"south":{"uv":[56,76,59,82],"texture":0},"west":{"uv":[22,88,24,94],"texture":0},"up":{"uv":[21,58,18,56],"texture":0},"down":{"uv":[80,52,77,54],"texture":0}},"type":"cube","uuid":"58cc8b17-a41a-147f-cca7-e3435f2929d4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.4,39.1,2.55],"to":[3.4,45.05,5.1],"autouv":0,"color":9,"origin":[0,39.1,5.95],"faces":{"north":{"uv":[48,38,55,44],"texture":0},"east":{"uv":[59,76,62,82],"texture":0},"south":{"uv":[48,44,55,50],"texture":0},"west":{"uv":[62,76,65,82],"texture":0},"up":{"uv":[79,12,72,9],"texture":0},"down":{"uv":[79,12,72,15],"texture":0}},"type":"cube","uuid":"07318316-9d72-9ddb-064d-bbf4bf2ae85b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.25,40.8,-0.85],"to":[5.1,43.35,2.55],"autouv":0,"color":9,"rotation":[0,25,0],"origin":[3.4,42.075,1.7],"faces":{"north":{"uv":[47,53,48,56],"texture":0},"east":{"uv":[12,73,15,76],"texture":0},"south":{"uv":[52,73,53,76],"texture":0},"west":{"uv":[76,95,79,98],"texture":0},"up":{"uv":[40,78,39,75],"texture":0},"down":{"uv":[66,76,65,79],"texture":0}},"type":"cube","uuid":"2a05fb72-2e28-bf6e-8f2a-9450b726fefd"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.99625,43.9875,0.27625],"to":[5.3125,46.5375,2.82625],"autouv":0,"color":9,"rotation":[-10.11783,22.9824,-24.56202],"origin":[2.7625,44.4125,1.7],"faces":{"north":{"uv":[85,9,87,12],"texture":0},"east":{"uv":[49,96,52,99],"texture":0},"south":{"uv":[82,96,84,99],"texture":0},"west":{"uv":[52,96,55,99],"texture":0},"up":{"uv":[12,106,10,103],"texture":0},"down":{"uv":[35,103,33,106],"texture":0}},"type":"cube","uuid":"6e105804-4673-4479-eaed-0423f5918b41"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.91,45.2625,0.44625],"to":[5.95,46.9625,2.65625],"autouv":0,"color":9,"rotation":[1.16524,24.97457,2.75806],"origin":[2.7625,44.4125,1.7],"faces":{"north":{"uv":[3,98,5,100],"texture":0},"east":{"uv":[17,98,19,100],"texture":0},"south":{"uv":[35,110,37,112],"texture":0},"west":{"uv":[47,110,49,112],"texture":0},"up":{"uv":[63,112,61,110],"texture":0},"down":{"uv":[7,111,5,113],"texture":0}},"type":"cube","uuid":"2f5d276f-2374-36e5-0d78-d927dd2f992b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.17509,46.61728,-0.02179],"to":[5.10884,47.91353,1.67821],"autouv":0,"color":9,"rotation":[9.06159,23.39896,21.88023],"origin":[4.28009,48.31728,0.82821],"faces":{"north":{"uv":[58,106,60,107],"texture":0},"east":{"uv":[88,106,90,107],"texture":0},"south":{"uv":[116,86,118,87],"texture":0},"west":{"uv":[89,116,91,117],"texture":0},"up":{"uv":[14,113,12,111],"texture":0},"down":{"uv":[113,26,111,28],"texture":0}},"type":"cube","uuid":"db0e95b6-4b6c-acca-df2c-3ac9b66cb68e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.17509,47.67978,0.23321],"to":[4.89634,48.93353,1.42321],"autouv":0,"color":9,"rotation":[17.48614,18.15489,45.31509],"origin":[4.28009,48.31728,0.82821],"faces":{"north":{"uv":[91,116,93,117],"texture":0},"east":{"uv":[89,76,90,77],"texture":0},"south":{"uv":[116,93,118,94],"texture":0},"west":{"uv":[91,30,92,31],"texture":0},"up":{"uv":[118,95,116,94],"texture":0},"down":{"uv":[97,116,95,117],"texture":0}},"type":"cube","uuid":"c1a05c0c-3ab3-9553-a6c4-6e0d418cd875"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.06884,48.31728,0.40321],"to":[4.68384,49.88978,1.25321],"autouv":0,"color":9,"rotation":[9.48702,23.2378,22.95495],"origin":[4.28009,48.31728,0.82821],"faces":{"north":{"uv":[28,111,30,113],"texture":0},"east":{"uv":[31,73,32,75],"texture":0},"south":{"uv":[111,31,113,33],"texture":0},"west":{"uv":[73,44,74,46],"texture":0},"up":{"uv":[118,96,116,95],"texture":0},"down":{"uv":[118,96,116,97],"texture":0}},"type":"cube","uuid":"d11eb44e-4f69-fbf5-844a-7c4cecd2193f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.64384,49.25228,0.48821],"to":[4.00384,50.35728,1.16821],"autouv":0,"color":9,"rotation":[-0.69935,24.99084,-1.65498],"origin":[4.28009,48.31728,0.82821],"faces":{"north":{"uv":[23,94,24,95],"texture":0},"east":{"uv":[34,94,35,95],"texture":0},"south":{"uv":[79,95,80,96],"texture":0},"west":{"uv":[5,96,6,97],"texture":0},"up":{"uv":[101,7,100,6],"texture":0},"down":{"uv":[101,45,100,46],"texture":0}},"type":"cube","uuid":"ca91afbe-2f07-a0dd-e8fe-da988c1e6e68"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.84122,50.17004,0.96671],"to":[4.09497,51.23254,1.47671],"autouv":0,"color":9,"rotation":[0.46627,24.99593,1.10335],"origin":[3.43622,50.91379,1.22171],"faces":{"north":{"uv":[75,100,76,101],"texture":0},"east":{"uv":[100,75,101,76],"texture":0},"south":{"uv":[101,21,102,22],"texture":0},"west":{"uv":[101,47,102,48],"texture":0},"up":{"uv":[55,103,54,102],"texture":0},"down":{"uv":[79,103,78,104],"texture":0}},"type":"cube","uuid":"26039549-7222-b70e-e174-6a9ef3e572c4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.01122,51.23254,1.05171],"to":[3.92497,51.95504,1.39171],"autouv":0,"color":9,"rotation":[0.46627,24.99593,1.10335],"origin":[3.43622,50.91379,1.22171],"faces":{"north":{"uv":[105,20,106,21],"texture":0},"east":{"uv":[105,46,106,47],"texture":0},"south":{"uv":[40,106,41,107],"texture":0},"west":{"uv":[91,107,92,108],"texture":0},"up":{"uv":[106,108,105,107],"texture":0},"down":{"uv":[6,108,5,109],"texture":0}},"type":"cube","uuid":"8291fbf4-6863-c022-2659-40bc06c7fe75"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.23758,51.91237,1.02009],"to":[3.81133,52.61362,1.36009],"autouv":0,"color":9,"rotation":[-1.86306,24.93493,-4.41196],"origin":[3.49789,52.6508,1.19009],"faces":{"north":{"uv":[87,108,88,109],"texture":0},"east":{"uv":[108,93,109,94],"texture":0},"south":{"uv":[101,108,102,109],"texture":0},"west":{"uv":[109,8,110,9],"texture":0},"up":{"uv":[18,110,17,109],"texture":0},"down":{"uv":[50,110,49,111],"texture":0}},"type":"cube","uuid":"b954a18e-ac4f-0766-3526-c851081a0c17"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.35682,52.61238,1.02009],"to":[3.65432,53.46238,1.36009],"autouv":0,"color":9,"rotation":[-3.0217,24.82838,-7.16529],"origin":[3.49789,52.6508,1.19009],"faces":{"north":{"uv":[34,111,35,112],"texture":0},"east":{"uv":[44,111,45,112],"texture":0},"south":{"uv":[53,111,54,112],"texture":0},"west":{"uv":[63,111,64,112],"texture":0},"up":{"uv":[56,113,55,112],"texture":0},"down":{"uv":[68,112,67,113],"texture":0}},"type":"cube","uuid":"1348e076-4cc5-4f8c-f4e2-a261ff98ab5b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.3125,43.9875,0.27625],"to":[-2.99625,46.5375,2.82625],"autouv":0,"color":9,"rotation":[-10.11783,-22.9824,24.56202],"origin":[-2.7625,44.4125,1.7],"faces":{"north":{"uv":[35,103,37,106],"texture":0},"east":{"uv":[96,59,99,62],"texture":0},"south":{"uv":[37,103,39,106],"texture":0},"west":{"uv":[96,62,99,65],"texture":0},"up":{"uv":[41,106,39,103],"texture":0},"down":{"uv":[105,42,103,45],"texture":0}},"type":"cube","uuid":"5365895f-d7bd-c3f7-e1a3-73eac3124234"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.89634,47.67978,0.23321],"to":[-3.17509,48.93353,1.42321],"autouv":0,"color":9,"rotation":[17.48614,-18.15489,-45.31509],"origin":[-4.28009,48.31728,0.82821],"faces":{"north":{"uv":[116,104,118,105],"texture":0},"east":{"uv":[118,23,119,24],"texture":0},"south":{"uv":[116,105,118,106],"texture":0},"west":{"uv":[28,118,29,119],"texture":0},"up":{"uv":[108,117,106,116],"texture":0},"down":{"uv":[118,106,116,107],"texture":0}},"type":"cube","uuid":"2ca2424c-e508-d492-3fe5-e9222fc5921f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.10884,46.61728,-0.02179],"to":[-3.17509,47.91353,1.67821],"autouv":0,"color":9,"rotation":[9.06159,-23.39896,-21.88023],"origin":[-4.28009,48.31728,0.82821],"faces":{"north":{"uv":[116,100,118,101],"texture":0},"east":{"uv":[116,101,118,102],"texture":0},"south":{"uv":[116,102,118,103],"texture":0},"west":{"uv":[104,116,106,117],"texture":0},"up":{"uv":[58,113,56,111],"texture":0},"down":{"uv":[113,57,111,59],"texture":0}},"type":"cube","uuid":"46051a90-1e40-4677-7aa0-1c2f182902fc"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.95,45.2625,0.44625],"to":[-3.91,46.9625,2.65625],"autouv":0,"color":9,"rotation":[1.16524,-24.97457,-2.75806],"origin":[-2.7625,44.4125,1.7],"faces":{"north":{"uv":[42,111,44,113],"texture":0},"east":{"uv":[111,43,113,45],"texture":0},"south":{"uv":[49,111,51,113],"texture":0},"west":{"uv":[51,111,53,113],"texture":0},"up":{"uv":[113,55,111,53],"texture":0},"down":{"uv":[113,55,111,57],"texture":0}},"type":"cube","uuid":"f36f47e6-b78d-00d0-582d-beb80e4b41c4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.68384,48.31728,0.40321],"to":[-3.06884,49.88978,1.25321],"autouv":0,"color":9,"rotation":[9.48702,-23.2378,-22.95495],"origin":[-4.28009,48.31728,0.82821],"faces":{"north":{"uv":[32,111,34,113],"texture":0},"east":{"uv":[76,62,77,64],"texture":0},"south":{"uv":[111,35,113,37],"texture":0},"west":{"uv":[47,84,48,86],"texture":0},"up":{"uv":[118,98,116,97],"texture":0},"down":{"uv":[118,98,116,99],"texture":0}},"type":"cube","uuid":"f8fa9e70-c280-fbed-5a29-37340fa87ce8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.09497,50.17004,0.96671],"to":[-2.84122,51.23254,1.47671],"autouv":0,"color":9,"rotation":[0.46627,-24.99593,-1.10335],"origin":[-3.43622,50.91379,1.22171],"faces":{"north":{"uv":[118,11,119,12],"texture":0},"east":{"uv":[14,118,15,119],"texture":0},"south":{"uv":[118,17,119,18],"texture":0},"west":{"uv":[118,18,119,19],"texture":0},"up":{"uv":[21,119,20,118],"texture":0},"down":{"uv":[22,118,21,119],"texture":0}},"type":"cube","uuid":"cf4d790b-d4b4-7a65-6206-b6c09a260faa"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.00384,49.25228,0.48821],"to":[-2.64384,50.35728,1.16821],"autouv":0,"color":9,"rotation":[-0.69935,-24.99084,1.65498],"origin":[-4.28009,48.31728,0.82821],"faces":{"north":{"uv":[7,118,8,119],"texture":0},"east":{"uv":[8,118,9,119],"texture":0},"south":{"uv":[118,8,119,9],"texture":0},"west":{"uv":[9,118,10,119],"texture":0},"up":{"uv":[119,10,118,9],"texture":0},"down":{"uv":[119,10,118,11],"texture":0}},"type":"cube","uuid":"01a2d1ed-ed02-b637-51a2-5cf940965937"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.81133,51.91237,1.02009],"to":[-3.23758,52.61362,1.36009],"autouv":0,"color":9,"rotation":[-1.86306,-24.93493,4.41196],"origin":[-3.49789,52.6508,1.19009],"faces":{"north":{"uv":[117,115,118,116],"texture":0},"east":{"uv":[117,116,118,117],"texture":0},"south":{"uv":[117,117,118,118],"texture":0},"west":{"uv":[118,3,119,4],"texture":0},"up":{"uv":[119,5,118,4],"texture":0},"down":{"uv":[119,5,118,6],"texture":0}},"type":"cube","uuid":"9f6c769f-5d82-d995-6b33-028a7b5730d4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.92497,51.23254,1.05171],"to":[-3.01122,51.95504,1.39171],"autouv":0,"color":9,"rotation":[0.46627,-24.99593,-1.10335],"origin":[-3.43622,50.91379,1.22171],"faces":{"north":{"uv":[117,109,118,110],"texture":0},"east":{"uv":[117,110,118,111],"texture":0},"south":{"uv":[111,117,112,118],"texture":0},"west":{"uv":[117,111,118,112],"texture":0},"up":{"uv":[113,118,112,117],"texture":0},"down":{"uv":[118,112,117,113],"texture":0}},"type":"cube","uuid":"74341beb-c746-c557-18e6-3bdcc8579260"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.65432,52.61238,1.02009],"to":[-3.35682,53.46238,1.36009],"autouv":0,"color":9,"rotation":[-3.0217,-24.82838,7.16529],"origin":[-3.49789,52.6508,1.19009],"faces":{"north":{"uv":[73,113,74,114],"texture":0},"east":{"uv":[100,117,101,118],"texture":0},"south":{"uv":[106,117,107,118],"texture":0},"west":{"uv":[107,117,108,118],"texture":0},"up":{"uv":[109,118,108,117],"texture":0},"down":{"uv":[118,108,117,109],"texture":0}},"type":"cube","uuid":"331420c7-b85d-798a-9191-a0d65b746238"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.58007,39.9925,-1.2242],"to":[4.43007,41.225,2.1758],"autouv":0,"color":9,"rotation":[-18.2489,17.38772,-47.81374],"origin":[4.00507,40.8,0.3908],"faces":{"north":{"uv":[77,43,78,44],"texture":0},"east":{"uv":[23,58,26,59],"texture":0},"south":{"uv":[64,82,65,83],"texture":0},"west":{"uv":[28,63,31,64],"texture":0},"up":{"uv":[10,118,9,115],"texture":0},"down":{"uv":[15,115,14,118],"texture":0}},"type":"cube","uuid":"b15ccc49-db06-5d54-8e86-936d7d405485"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.43007,39.9925,-1.2242],"to":[-3.58007,41.225,2.1758],"autouv":0,"color":9,"rotation":[-18.2489,-17.38772,47.81374],"origin":[-4.00507,40.8,0.3908],"faces":{"north":{"uv":[83,51,84,52],"texture":0},"east":{"uv":[74,49,77,50],"texture":0},"south":{"uv":[55,85,56,86],"texture":0},"west":{"uv":[82,78,85,79],"texture":0},"up":{"uv":[116,59,115,56],"texture":0},"down":{"uv":[59,115,58,118],"texture":0}},"type":"cube","uuid":"ed613350-2d17-57fa-a4d2-b97cb9cdd58a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.1,40.8,-0.85],"to":[-4.25,43.35,2.55],"autouv":0,"color":9,"rotation":[0,-25,0],"origin":[-3.4,42.075,1.7],"faces":{"north":{"uv":[75,114,76,117],"texture":0},"east":{"uv":[90,95,93,98],"texture":0},"south":{"uv":[86,114,87,117],"texture":0},"west":{"uv":[96,48,99,51],"texture":0},"up":{"uv":[101,117,100,114],"texture":0},"down":{"uv":[109,114,108,117],"texture":0}},"type":"cube","uuid":"41431638-6882-7e35-c079-f79f0a85c310"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.85,45.05,0.425],"to":[0.85,46.75,3.825],"autouv":0,"color":9,"rotation":[0,0,-45],"origin":[0,45.9,2.125],"faces":{"north":{"uv":[96,65,98,67],"texture":0},"east":{"uv":[16,82,19,84],"texture":0},"south":{"uv":[96,70,98,72],"texture":0},"west":{"uv":[30,92,33,94],"texture":0},"up":{"uv":[18,65,16,62],"texture":0},"down":{"uv":[23,65,21,68],"texture":0}},"type":"cube","uuid":"1242df13-8c4c-5466-4de7-c98d18085b08"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.36,39.1,4.73875],"to":[3.74,45.05,5.95],"autouv":0,"color":9,"rotation":[0,45,0],"origin":[2.55,42.075,5.95],"faces":{"north":{"uv":[88,77,90,83],"texture":0},"east":{"uv":[41,101,42,107],"texture":0},"south":{"uv":[84,88,86,94],"texture":0},"west":{"uv":[42,101,43,107],"texture":0},"up":{"uv":[80,4,78,3],"texture":0},"down":{"uv":[85,42,83,43],"texture":0}},"type":"cube","uuid":"ece57724-7e60-507a-ab93-1f99a2f0eb03"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.74,39.1,4.73875],"to":[-1.36,45.05,5.95],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[-2.55,42.075,5.95],"faces":{"north":{"uv":[86,88,88,94],"texture":0},"east":{"uv":[43,101,44,107],"texture":0},"south":{"uv":[88,88,90,94],"texture":0},"west":{"uv":[53,102,54,108],"texture":0},"up":{"uv":[12,85,10,84],"texture":0},"down":{"uv":[88,77,86,78],"texture":0}},"type":"cube","uuid":"623ac13d-7644-fa69-45e2-fdefbd5ef451"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.125,39.1,-1.7],"to":[4.25,39.95,3.4],"autouv":0,"color":9,"rotation":[0,25,0],"origin":[3.4,41.225,1.7],"faces":{"north":{"uv":[92,54,94,55],"texture":0},"east":{"uv":[109,90,114,91],"texture":0},"south":{"uv":[80,92,82,93],"texture":0},"west":{"uv":[109,99,114,100],"texture":0},"up":{"uv":[88,99,86,94],"texture":0},"down":{"uv":[90,94,88,99],"texture":0}},"type":"cube","uuid":"997d2204-820b-a00f-3097-9963e1100c6a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.25,39.1,-1.7],"to":[-2.125,39.95,3.4],"autouv":0,"color":9,"rotation":[0,-25,0],"origin":[-3.4,41.225,1.7],"faces":{"north":{"uv":[88,46,90,47],"texture":0},"east":{"uv":[109,34,114,35],"texture":0},"south":{"uv":[92,21,94,22],"texture":0},"west":{"uv":[109,64,114,65],"texture":0},"up":{"uv":[45,98,43,93],"texture":0},"down":{"uv":[86,94,84,99],"texture":0}},"type":"cube","uuid":"15067340-dab6-b719-c5db-ccddd8c13f0d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.73839,39.44,-2.05932],"to":[-0.61339,40.05625,-0.84807],"autouv":0,"color":9,"rotation":[45,-25,0],"origin":[-1.59089,39.7375,-1.46432],"faces":{"north":{"uv":[50,93,52,94],"texture":0},"east":{"uv":[26,50,27,51],"texture":0},"south":{"uv":[94,24,96,25],"texture":0},"west":{"uv":[61,55,62,56],"texture":0},"up":{"uv":[97,6,95,5],"texture":0},"down":{"uv":[98,51,96,52],"texture":0}},"type":"cube","uuid":"0e146668-2a18-534a-dd8b-05002a4f7f5c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.61339,39.44,-2.05932],"to":[2.73839,40.05625,-0.84807],"autouv":0,"color":9,"rotation":[45,25,0],"origin":[1.59089,39.7375,-1.46432],"faces":{"north":{"uv":[41,97,43,98],"texture":0},"east":{"uv":[15,69,16,70],"texture":0},"south":{"uv":[98,71,100,72],"texture":0},"west":{"uv":[71,54,72,55],"texture":0},"up":{"uv":[46,107,44,106],"texture":0},"down":{"uv":[108,50,106,51],"texture":0}},"type":"cube","uuid":"1a7991e4-745f-7efc-444b-299209fd9f79"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.39293,41.48,-1.66843],"to":[5.39168,43.07375,1.73157],"autouv":0,"color":9,"rotation":[-14.06573,20.88127,-35.10451],"origin":[4.96668,42.925,0.03157],"faces":{"north":{"uv":[67,20,68,22],"texture":0},"east":{"uv":[14,101,17,103],"texture":0},"south":{"uv":[67,36,68,38],"texture":0},"west":{"uv":[30,103,33,105],"texture":0},"up":{"uv":[16,93,15,90],"texture":0},"down":{"uv":[50,91,49,94],"texture":0}},"type":"cube","uuid":"da8540f3-6a51-c071-0867-38b58f566207"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.39168,41.48,-1.66843],"to":[-4.39293,43.07375,1.73157],"autouv":0,"color":9,"rotation":[-14.06573,-20.88127,35.10451],"origin":[-4.96668,42.925,0.03157],"faces":{"north":{"uv":[72,18,73,20],"texture":0},"east":{"uv":[103,30,106,32],"texture":0},"south":{"uv":[72,34,73,36],"texture":0},"west":{"uv":[103,32,106,34],"texture":0},"up":{"uv":[23,100,22,97],"texture":0},"down":{"uv":[58,113,57,116],"texture":0}},"type":"cube","uuid":"f0b034ea-a228-d9e8-8bc5-94e6d27857a7"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.4,44.2,-2.3375],"to":[-1.7,45.05,-0.6375],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[0,44.2,1.0625],"faces":{"north":{"uv":[26,68,28,69],"texture":0},"east":{"uv":[70,9,72,10],"texture":0},"south":{"uv":[6,77,8,78],"texture":0},"west":{"uv":[77,24,79,25],"texture":0},"up":{"uv":[67,12,65,10],"texture":0},"down":{"uv":[72,74,70,76],"texture":0}},"type":"cube","uuid":"60360fc3-03e2-1007-9b85-358674639cf6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1.9125,36.55,0.85],"to":[1.0625,37.825,3.825],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[0,36.975,2.7625],"faces":{"north":{"uv":[19,93,22,94],"texture":0},"east":{"uv":[90,101,93,102],"texture":0},"south":{"uv":[114,99,117,100],"texture":0},"west":{"uv":[115,5,118,6],"texture":0},"up":{"uv":[82,99,79,96],"texture":0},"down":{"uv":[99,85,96,88],"texture":0}},"type":"cube","uuid":"89820f44-93e4-0e7c-484b-8fa0e83e56ec"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3,35,-0.25],"to":[3,36.5,5.25],"autouv":0,"color":1,"origin":[0.5,35.5,3.25],"faces":{"north":{"uv":[89,4,95,6],"texture":0},"east":{"uv":[37,89,43,91],"texture":0},"south":{"uv":[89,72,95,74],"texture":0},"west":{"uv":[89,74,95,76],"texture":0},"up":{"uv":[61,44,55,38],"texture":0},"down":{"uv":[61,44,55,50],"texture":0}},"type":"cube","uuid":"56c157e4-61fb-c7ec-de55-bc79f81ffd66"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.32507,33,-2.88425],"to":[7.32507,37.5,1.61575],"autouv":0,"color":1,"rotation":[5,-15,10],"origin":[3.32507,35.25,-0.13425],"faces":{"north":{"uv":[87,32,90,37],"texture":0},"east":{"uv":[33,64,38,69],"texture":0},"south":{"uv":[87,37,90,42],"texture":0},"west":{"uv":[38,64,43,69],"texture":0},"up":{"uv":[90,52,87,47],"texture":0},"down":{"uv":[90,62,87,67],"texture":0}},"type":"cube","uuid":"976142e5-835f-0783-2b66-047893b31e8d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.32507,33,3.38425],"to":[7.32507,37.5,7.88425],"autouv":0,"color":1,"rotation":[-5,15,10],"origin":[3.32507,35.25,5.13425],"faces":{"north":{"uv":[73,87,76,92],"texture":0},"east":{"uv":[62,65,67,70],"texture":0},"south":{"uv":[76,87,79,92],"texture":0},"west":{"uv":[66,50,71,55],"texture":0},"up":{"uv":[82,92,79,87],"texture":0},"down":{"uv":[90,83,87,88],"texture":0}},"type":"cube","uuid":"30860483-4b6d-b211-99f0-8b5163830762"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1,28,-1.25],"to":[4,32,6.25],"autouv":0,"color":1,"origin":[0.5,35.5,3.25],"faces":{"north":{"uv":[40,69,43,73],"texture":0},"east":{"uv":[55,26,63,30],"texture":0},"south":{"uv":[43,89,46,93],"texture":0},"west":{"uv":[57,8,65,12],"texture":0},"up":{"uv":[56,76,53,68],"texture":0},"down":{"uv":[59,68,56,76],"texture":0}},"type":"cube","uuid":"9a75352d-e331-7910-3b95-f3aa4d6fda3e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.82507,32.75,3.38425],"to":[4.82507,37.25,7.88425],"autouv":0,"color":1,"rotation":[-1.55797,15.71814,22.94361],"origin":[3.32507,35.25,5.13425],"faces":{"north":{"uv":[0,88,3,93],"texture":0},"east":{"uv":[67,0,72,5],"texture":0},"south":{"uv":[6,88,9,93],"texture":0},"west":{"uv":[67,10,72,15],"texture":0},"up":{"uv":[19,93,16,88],"texture":0},"down":{"uv":[22,88,19,93],"texture":0}},"type":"cube","uuid":"fefb8a0c-4dc9-eb37-14ea-19ed11636ce9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.82507,32.75,-2.88425],"to":[4.82507,37.25,1.61575],"autouv":0,"color":1,"rotation":[1.55797,-15.71814,22.94361],"origin":[3.32507,35.25,-0.13425],"faces":{"north":{"uv":[64,87,67,92],"texture":0},"east":{"uv":[65,5,70,10],"texture":0},"south":{"uv":[67,87,70,92],"texture":0},"west":{"uv":[16,65,21,70],"texture":0},"up":{"uv":[90,72,87,67],"texture":0},"down":{"uv":[73,87,70,92],"texture":0}},"type":"cube","uuid":"c5de4175-68a1-cce8-dda1-6f7519bc1b2c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.75,35.025,-2.875],"to":[2.75,36.275,-0.125],"autouv":0,"color":1,"rotation":[-12.5,0,0],"origin":[0,36,-1.25],"faces":{"north":{"uv":[64,103,70,104],"texture":0},"east":{"uv":[115,28,118,29],"texture":0},"south":{"uv":[103,64,109,65],"texture":0},"west":{"uv":[115,33,118,34],"texture":0},"up":{"uv":[83,37,77,34],"texture":0},"down":{"uv":[83,37,77,40],"texture":0}},"type":"cube","uuid":"02fddf8c-7397-7a63-4a7c-c467b91d5448"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.75,35.025,5.125],"to":[2.75,36.275,7.875],"autouv":0,"color":1,"rotation":[12.5,0,0],"origin":[0,36,6.25],"faces":{"north":{"uv":[18,104,24,105],"texture":0},"east":{"uv":[115,59,118,60],"texture":0},"south":{"uv":[24,104,30,105],"texture":0},"west":{"uv":[115,65,118,66],"texture":0},"up":{"uv":[83,43,77,40],"texture":0},"down":{"uv":[83,49,77,52],"texture":0}},"type":"cube","uuid":"b4c05bb6-f96e-2f4d-93ec-7e5dd4a9252d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[7.41742,35.25,-0.15232],"to":[12.91742,37.45,5.94768],"autouv":0,"color":6,"origin":[8.51742,34.15,2.34768],"faces":{"north":{"uv":[92,19,98,21],"texture":0},"east":{"uv":[92,52,98,54],"texture":0},"south":{"uv":[64,92,70,94],"texture":0},"west":{"uv":[70,92,76,94],"texture":0},"up":{"uv":[61,56,55,50],"texture":0},"down":{"uv":[6,56,0,62],"texture":0}},"type":"cube","uuid":"9681954d-8e83-f105-06c4-e9fa51ed312b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.43951,31.95,3.81133],"to":[15.63951,34.425,6.61133],"autouv":0,"color":6,"rotation":[0,-22.5,0],"origin":[13.89367,33.7375,2.8875],"faces":{"north":{"uv":[93,113,95,115],"texture":0},"east":{"uv":[108,43,111,45],"texture":0},"south":{"uv":[95,113,97,115],"texture":0},"west":{"uv":[47,108,50,110],"texture":0},"up":{"uv":[52,111,50,108],"texture":0},"down":{"uv":[110,50,108,53],"texture":0}},"type":"cube","uuid":"953becdc-ed25-d2ba-a418-ea805624ad64"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[12.91742,31.95,1.24768],"to":[15.39242,34.425,4.54768],"autouv":0,"color":6,"origin":[13.89367,33.7375,2.8875],"faces":{"north":{"uv":[113,87,115,89],"texture":0},"east":{"uv":[35,108,38,110],"texture":0},"south":{"uv":[91,113,93,115],"texture":0},"west":{"uv":[108,35,111,37],"texture":0},"up":{"uv":[110,40,108,37],"texture":0},"down":{"uv":[110,40,108,43],"texture":0}},"type":"cube","uuid":"d97eb0a1-681c-6e0f-ae41-2dff2b52b9e4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.43951,31.95,-0.83633],"to":[15.63951,34.425,1.96367],"autouv":0,"color":6,"rotation":[0,22.5,0],"origin":[13.89367,33.7375,2.8875],"faces":{"north":{"uv":[28,113,30,115],"texture":0},"east":{"uv":[102,107,105,109],"texture":0},"south":{"uv":[32,113,34,115],"texture":0},"west":{"uv":[2,108,5,110],"texture":0},"up":{"uv":[56,109,54,106],"texture":0},"down":{"uv":[20,107,18,110],"texture":0}},"type":"cube","uuid":"3a2af0ee-d802-79a2-cb5a-99891015755e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[15.39242,32.775,2.45768],"to":[16.49242,33.6,3.33768],"autouv":0,"color":6,"origin":[13.89367,33.7375,2.8875],"faces":{"north":{"uv":[35,118,36,119],"texture":0},"east":{"uv":[118,35,119,36],"texture":0},"south":{"uv":[36,118,37,119],"texture":0},"west":{"uv":[118,36,119,37],"texture":0},"up":{"uv":[39,119,38,118],"texture":0},"down":{"uv":[40,118,39,119],"texture":0}},"type":"cube","uuid":"7acdfb4e-2563-277e-8412-a2a4d2fab162"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[12.88951,34.0675,-0.28633],"to":[15.08951,36.5425,1.96367],"autouv":0,"color":6,"rotation":[0,22.5,12.5],"origin":[13.34367,35.855,2.8875],"faces":{"north":{"uv":[69,113,71,115],"texture":0},"east":{"uv":[113,69,115,71],"texture":0},"south":{"uv":[71,113,73,115],"texture":0},"west":{"uv":[76,113,78,115],"texture":0},"up":{"uv":[80,115,78,113],"texture":0},"down":{"uv":[115,78,113,80],"texture":0}},"type":"cube","uuid":"f193499c-22db-5035-ce51-f61078f27294"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[12.36742,34.0675,1.24768],"to":[14.84242,36.5425,4.54768],"autouv":0,"color":6,"rotation":[0,0,12.5],"origin":[13.34367,35.855,2.8875],"faces":{"north":{"uv":[67,113,69,115],"texture":0},"east":{"uv":[108,6,111,8],"texture":0},"south":{"uv":[113,67,115,69],"texture":0},"west":{"uv":[20,108,23,110],"texture":0},"up":{"uv":[63,110,61,107],"texture":0},"down":{"uv":[78,107,76,110],"texture":0}},"type":"cube","uuid":"8d01d4a6-df49-be4c-8e8a-7b488fc41a79"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[12.88951,34.0675,3.81133],"to":[15.08951,36.5425,6.06133],"autouv":0,"color":6,"rotation":[0,-22.5,12.5],"origin":[13.34367,35.855,2.8875],"faces":{"north":{"uv":[42,113,44,115],"texture":0},"east":{"uv":[113,53,115,55],"texture":0},"south":{"uv":[55,113,57,115],"texture":0},"west":{"uv":[113,55,115,57],"texture":0},"up":{"uv":[115,59,113,57],"texture":0},"down":{"uv":[115,65,113,67],"texture":0}},"type":"cube","uuid":"29a5e95c-df63-f7b3-7495-1a02c37f40b8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.95451,36.02,0.51367],"to":[14.15451,38.495,1.96367],"autouv":0,"color":6,"rotation":[0,22.5,25],"origin":[12.40867,37.8075,2.8875],"faces":{"north":{"uv":[84,113,86,115],"texture":0},"east":{"uv":[117,35,118,37],"texture":0},"south":{"uv":[87,113,89,115],"texture":0},"west":{"uv":[37,117,38,119],"texture":0},"up":{"uv":[119,38,117,37],"texture":0},"down":{"uv":[40,117,38,118],"texture":0}},"type":"cube","uuid":"34e10c22-6551-673a-cd19-e2d348ba1e23"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.43242,36.02,1.24768],"to":[13.90742,38.495,4.54768],"autouv":0,"color":6,"rotation":[0,0,25],"origin":[12.40867,37.8075,2.8875],"faces":{"north":{"uv":[82,113,84,115],"texture":0},"east":{"uv":[23,108,26,110],"texture":0},"south":{"uv":[113,82,115,84],"texture":0},"west":{"uv":[108,24,111,26],"texture":0},"up":{"uv":[86,110,84,107],"texture":0},"down":{"uv":[35,108,33,111],"texture":0}},"type":"cube","uuid":"6952340a-c4cc-6f9f-7cae-86bf966078b4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.95451,36.02,3.81133],"to":[14.15451,38.495,5.26133],"autouv":0,"color":6,"rotation":[0,-22.5,25],"origin":[12.40867,37.8075,2.8875],"faces":{"north":{"uv":[80,113,82,115],"texture":0},"east":{"uv":[117,31,118,33],"texture":0},"south":{"uv":[113,80,115,82],"texture":0},"west":{"uv":[32,117,33,119],"texture":0},"up":{"uv":[35,118,33,117],"texture":0},"down":{"uv":[37,117,35,118],"texture":0}},"type":"cube","uuid":"137bc3e4-445b-981d-20fd-f1039c3c2cbf"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.90742,36.845,2.45768],"to":[15.00742,37.67,3.33768],"autouv":0,"color":6,"rotation":[0,0,25],"origin":[12.40867,37.8075,2.8875],"faces":{"north":{"uv":[118,31,119,32],"texture":0},"east":{"uv":[118,32,119,33],"texture":0},"south":{"uv":[33,118,34,119],"texture":0},"west":{"uv":[118,33,119,34],"texture":0},"up":{"uv":[35,119,34,118],"texture":0},"down":{"uv":[119,34,118,35],"texture":0}},"type":"cube","uuid":"84f643b0-2711-5778-7f8c-81ccbc28a2b1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.2088,37.2,0.6688],"to":[12.6088,38.575,5.0688],"autouv":0,"color":6,"rotation":[0,-45,7.5],"origin":[10.5338,38.80625,2.8063],"faces":{"north":{"uv":[113,27,117,28],"texture":0},"east":{"uv":[113,31,117,32],"texture":0},"south":{"uv":[113,32,117,33],"texture":0},"west":{"uv":[113,35,117,36],"texture":0},"up":{"uv":[87,22,83,18],"texture":0},"down":{"uv":[87,22,83,26],"texture":0}},"type":"cube","uuid":"0721e534-324b-d2ee-e49b-171b151cd7b6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[7.69242,33.05,-0.70232],"to":[12.91742,35.25,6.49768],"autouv":0,"color":6,"origin":[8.51742,31.95,2.34768],"faces":{"north":{"uv":[96,7,101,9],"texture":0},"east":{"uv":[88,0,95,2],"texture":0},"south":{"uv":[14,96,19,98],"texture":0},"west":{"uv":[88,2,95,4],"texture":0},"up":{"uv":[52,63,47,56],"texture":0},"down":{"uv":[57,56,52,63],"texture":0}},"type":"cube","uuid":"4dc33995-1196-37e1-bf4c-f3fe418da321"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[7.96742,30.85,-1.25232],"to":[12.91742,33.05,7.04768],"autouv":0,"color":6,"origin":[8.51742,29.75,2.34768],"faces":{"north":{"uv":[96,17,101,19],"texture":0},"east":{"uv":[79,16,87,18],"texture":0},"south":{"uv":[96,24,101,26],"texture":0},"west":{"uv":[79,32,87,34],"texture":0},"up":{"uv":[26,58,21,50],"texture":0},"down":{"uv":[31,51,26,59],"texture":0}},"type":"cube","uuid":"bdf0da6b-28a5-f3e1-7130-ad8588783047"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.96451,29.75,3.81133],"to":[15.36451,32.225,7.16133],"autouv":0,"color":6,"rotation":[0,-22.5,-15],"origin":[13.61867,31.5375,2.8875],"faces":{"north":{"uv":[101,81,105,83],"texture":0},"east":{"uv":[108,69,111,71],"texture":0},"south":{"uv":[101,83,105,85],"texture":0},"west":{"uv":[78,108,81,110],"texture":0},"up":{"uv":[97,12,93,9],"texture":0},"down":{"uv":[97,12,93,15],"texture":0}},"type":"cube","uuid":"47499439-5a44-785c-6e79-f143d2ce5073"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[12.64242,29.75,1.24768],"to":[15.11742,32.225,4.54768],"autouv":0,"color":6,"rotation":[0,0,-15],"origin":[13.61867,31.5375,2.8875],"faces":{"north":{"uv":[97,113,99,115],"texture":0},"east":{"uv":[108,65,111,67],"texture":0},"south":{"uv":[113,100,115,102],"texture":0},"west":{"uv":[108,67,111,69],"texture":0},"up":{"uv":[54,111,52,108],"texture":0},"down":{"uv":[110,75,108,78],"texture":0}},"type":"cube","uuid":"fa971384-8820-d365-fcb5-a99f37f4f75f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.96451,29.75,-1.38633],"to":[15.36451,32.225,1.96367],"autouv":0,"color":6,"rotation":[0,22.5,-15],"origin":[13.61867,31.5375,2.8875],"faces":{"north":{"uv":[101,77,105,79],"texture":0},"east":{"uv":[108,53,111,55],"texture":0},"south":{"uv":[101,79,105,81],"texture":0},"west":{"uv":[108,55,111,57],"texture":0},"up":{"uv":[80,95,76,92],"texture":0},"down":{"uv":[4,93,0,96],"texture":0}},"type":"cube","uuid":"560d51d1-acc3-bfae-e3cd-a81b839151ae"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.0338,38.575,1.4938],"to":[11.7838,38.875,4.2438],"autouv":0,"color":6,"rotation":[0,-45,7.5],"origin":[10.5338,38.80625,2.8063],"faces":{"north":{"uv":[116,13,119,14],"texture":0},"east":{"uv":[116,14,119,15],"texture":0},"south":{"uv":[116,15,119,16],"texture":0},"west":{"uv":[116,16,119,17],"texture":0},"up":{"uv":[23,103,20,100],"texture":0},"down":{"uv":[26,100,23,103],"texture":0}},"type":"cube","uuid":"b2a87c88-5035-521e-27bc-4aeac5271565"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.81742,29.45,0.97268],"to":[14.21742,30.825,5.37268],"autouv":0,"color":6,"rotation":[0,-45,0],"origin":[12.01742,31.1,2.07268],"faces":{"north":{"uv":[113,36,117,37],"texture":0},"east":{"uv":[113,43,117,44],"texture":0},"south":{"uv":[113,44,117,45],"texture":0},"west":{"uv":[49,113,53,114],"texture":0},"up":{"uv":[87,30,83,26],"texture":0},"down":{"uv":[87,34,83,38],"texture":0}},"type":"cube","uuid":"dfc17fef-2439-97eb-2f60-f03d53a9b1c0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.31742,21.95,1.72268],"to":[13.21742,22.95,4.62268],"autouv":0,"color":6,"rotation":[0,-45,0],"origin":[11.76742,25.35,2.07268],"faces":{"north":{"uv":[19,116,22,117],"texture":0},"east":{"uv":[116,20,119,21],"texture":0},"south":{"uv":[116,21,119,22],"texture":0},"west":{"uv":[116,22,119,23],"texture":0},"up":{"uv":[29,103,26,100],"texture":0},"down":{"uv":[103,26,100,29],"texture":0}},"type":"cube","uuid":"67a5e263-dd1e-b00e-9b95-a310c307f747"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.81742,24.95,1.22268],"to":[13.71742,29.825,5.12268],"autouv":0,"color":6,"rotation":[0,-45,0],"origin":[11.76742,28.1,2.07268],"faces":{"north":{"uv":[74,44,78,49],"texture":0},"east":{"uv":[66,74,70,79],"texture":0},"south":{"uv":[31,75,35,80],"texture":0},"west":{"uv":[35,75,39,80],"texture":0},"up":{"uv":[68,87,64,83],"texture":0},"down":{"uv":[72,83,68,87],"texture":0}},"type":"cube","uuid":"639ce11a-9c6a-6c42-3379-700283fa19d8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4,31,-0.25],"to":[8,35.5,5.25],"autouv":0,"color":1,"origin":[0.5,35.5,3.25],"faces":{"north":{"uv":[27,73,31,78],"texture":0},"east":{"uv":[61,12,67,17],"texture":0},"south":{"uv":[73,34,77,39],"texture":0},"west":{"uv":[61,17,67,22],"texture":0},"up":{"uv":[19,76,15,70],"texture":0},"down":{"uv":[66,70,62,76],"texture":0}},"type":"cube","uuid":"d6db19a7-5456-61a3-c464-720c60d0ad72"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4,28,0],"to":[7,31,5],"autouv":0,"color":1,"origin":[0.5,35.5,3.25],"faces":{"north":{"uv":[41,98,44,101],"texture":0},"east":{"uv":[84,4,89,7],"texture":0},"south":{"uv":[98,51,101,54],"texture":0},"west":{"uv":[30,84,35,87],"texture":0},"up":{"uv":[38,89,35,84],"texture":0},"down":{"uv":[41,84,38,89],"texture":0}},"type":"cube","uuid":"c1a5837c-09d2-f191-8927-3147276487e0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-0.75],"to":[4,28,5.75],"autouv":0,"color":1,"origin":[0.5,31.5,3.25],"faces":{"north":{"uv":[59,34,67,38],"texture":0},"east":{"uv":[61,22,68,26],"texture":0},"south":{"uv":[31,60,39,64],"texture":0},"west":{"uv":[61,38,68,42],"texture":0},"up":{"uv":[35,42,27,35],"texture":0},"down":{"uv":[43,35,35,42],"texture":0}},"type":"cube","uuid":"a6fd2059-bc63-6b1e-1adb-b110a04138fe"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.1,24.675,0.5],"to":[5.925,29.35,4.5],"autouv":0,"color":1,"rotation":[0,0,-40],"origin":[4.6,27.2,2.5],"faces":{"north":{"uv":[24,84,27,89],"texture":0},"east":{"uv":[72,54,76,59],"texture":0},"south":{"uv":[27,84,30,89],"texture":0},"west":{"uv":[72,59,76,64],"texture":0},"up":{"uv":[15,94,12,90],"texture":0},"down":{"uv":[93,13,90,17],"texture":0}},"type":"cube","uuid":"e3e2d9f0-1441-b200-dd73-7e161d346cb0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1,28.35,-1.75],"to":[4.5,32.2,-0.75],"autouv":0,"color":1,"rotation":[-15,0,0],"origin":[2.5,30.2,-1.25],"faces":{"north":{"uv":[48,78,52,82],"texture":0},"east":{"uv":[100,110,101,114],"texture":0},"south":{"uv":[79,12,83,16],"texture":0},"west":{"uv":[7,111,8,115],"texture":0},"up":{"uv":[90,60,86,59],"texture":0},"down":{"uv":[91,55,87,56],"texture":0}},"type":"cube","uuid":"bcdd576f-aa6b-f54b-c58b-1143876ab139"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1,28.35,5.725],"to":[4.5,32.2,6.725],"autouv":0,"color":1,"rotation":[15,0,0],"origin":[2.5,30.2,6.225],"faces":{"north":{"uv":[79,24,83,28],"texture":0},"east":{"uv":[9,111,10,115],"texture":0},"south":{"uv":[79,28,83,32],"texture":0},"west":{"uv":[14,111,15,115],"texture":0},"up":{"uv":[100,27,96,26],"texture":0},"down":{"uv":[115,28,111,29],"texture":0}},"type":"cube","uuid":"5990e492-34e4-0cf6-bf08-baf0fb599311"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1,28,-0.75],"to":[1,32,5.75],"autouv":0,"color":1,"origin":[-4.5,35.5,3.25],"faces":{"north":{"uv":[73,100,75,104],"texture":0},"east":{"uv":[61,42,68,46],"texture":0},"south":{"uv":[99,100,101,104],"texture":0},"west":{"uv":[61,46,68,50],"texture":0},"up":{"uv":[52,93,50,86],"texture":0},"down":{"uv":[35,87,33,94],"texture":0}},"type":"cube","uuid":"550e740c-3b6c-a212-ebfd-d12fbacfee1c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.5,32,-2.25],"to":[5.5,35.5,7.25],"autouv":0,"color":1,"origin":[0.5,35.5,3.25],"faces":{"north":{"uv":[44,14,55,18],"texture":0},"east":{"uv":[51,0,61,4],"texture":0},"south":{"uv":[44,18,55,22],"texture":0},"west":{"uv":[51,4,61,8],"texture":0},"up":{"uv":[38,35,27,25],"texture":0},"down":{"uv":[44,0,33,10],"texture":0}},"type":"cube","uuid":"26ce26ab-08fe-cbe9-470c-982100b16eeb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-7,28,0],"to":[-4,31,5],"autouv":0,"color":1,"origin":[-0.5,35.5,3.25],"faces":{"north":{"uv":[98,54,101,57],"texture":0},"east":{"uv":[86,56,91,59],"texture":0},"south":{"uv":[98,65,101,68],"texture":0},"west":{"uv":[87,19,92,22],"texture":0},"up":{"uv":[90,27,87,22],"texture":0},"down":{"uv":[33,87,30,92],"texture":0}},"type":"cube","uuid":"80406cd6-c894-2499-2966-49eeeb4d05b9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.925,24.675,0.5],"to":[-3.1,29.35,4.5],"autouv":0,"color":1,"rotation":[0,0,40],"origin":[-4.6,27.2,2.5],"faces":{"north":{"uv":[87,9,90,14],"texture":0},"east":{"uv":[19,73,23,78],"texture":0},"south":{"uv":[87,14,90,19],"texture":0},"west":{"uv":[23,73,27,78],"texture":0},"up":{"uv":[55,94,52,90],"texture":0},"down":{"uv":[40,91,37,95],"texture":0}},"type":"cube","uuid":"20981f6a-ae7b-25df-06bc-490e3d7ff703"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-8,31,-0.25],"to":[-4,35.5,5.25],"autouv":0,"color":1,"origin":[-0.5,35.5,3.25],"faces":{"north":{"uv":[8,73,12,78],"texture":0},"east":{"uv":[41,59,47,64],"texture":0},"south":{"uv":[73,18,77,23],"texture":0},"west":{"uv":[61,0,67,5],"texture":0},"up":{"uv":[36,75,32,69],"texture":0},"down":{"uv":[40,69,36,75],"texture":0}},"type":"cube","uuid":"b413c645-64a0-6eeb-9cfc-b24522d8f63a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.82507,32.75,-2.88425],"to":[-1.82507,37.25,1.61575],"autouv":0,"color":1,"rotation":[1.55797,15.71814,-22.94361],"origin":[-3.32507,35.25,-0.13425],"faces":{"north":{"uv":[55,86,58,91],"texture":0},"east":{"uv":[57,63,62,68],"texture":0},"south":{"uv":[58,86,61,91],"texture":0},"west":{"uv":[28,64,33,69],"texture":0},"up":{"uv":[64,91,61,86],"texture":0},"down":{"uv":[89,72,86,77],"texture":0}},"type":"cube","uuid":"c668b80a-6004-f47e-6897-f60fe8fae744"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-7.32507,33,-2.88425],"to":[-4.32507,37.5,1.61575],"autouv":0,"color":1,"rotation":[5,15,-10],"origin":[-3.32507,35.25,-0.13425],"faces":{"north":{"uv":[85,42,88,47],"texture":0},"east":{"uv":[47,63,52,68],"texture":0},"south":{"uv":[52,85,55,90],"texture":0},"west":{"uv":[52,63,57,68],"texture":0},"up":{"uv":[88,83,85,78],"texture":0},"down":{"uv":[50,86,47,91],"texture":0}},"type":"cube","uuid":"1652a13e-4cf6-7d97-cf8f-1de6a42a62f9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-7.32507,33,3.38425],"to":[-4.32507,37.5,7.88425],"autouv":0,"color":1,"rotation":[-5,-15,-10],"origin":[-3.32507,35.25,5.13425],"faces":{"north":{"uv":[84,67,87,72],"texture":0},"east":{"uv":[62,60,67,65],"texture":0},"south":{"uv":[84,83,87,88],"texture":0},"west":{"uv":[23,63,28,68],"texture":0},"up":{"uv":[13,90,10,85],"texture":0},"down":{"uv":[16,85,13,90],"texture":0}},"type":"cube","uuid":"bb38f1ab-01f0-5aff-e1a3-8322fb354dda"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.82507,32.75,3.38425],"to":[-1.82507,37.25,7.88425],"autouv":0,"color":1,"rotation":[-1.55797,-15.71814,-22.94361],"origin":[-3.32507,35.25,5.13425],"faces":{"north":{"uv":[41,84,44,89],"texture":0},"east":{"uv":[61,50,66,55],"texture":0},"south":{"uv":[44,84,47,89],"texture":0},"west":{"uv":[62,55,67,60],"texture":0},"up":{"uv":[87,56,84,51],"texture":0},"down":{"uv":[87,62,84,67],"texture":0}},"type":"cube","uuid":"ca345538-c5b6-cc10-73dd-3f9acc846f95"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,28,-1.25],"to":[-1,32,6.25],"autouv":0,"color":1,"origin":[-0.5,35.5,3.25],"faces":{"north":{"uv":[9,90,12,94],"texture":0},"east":{"uv":[23,59,31,63],"texture":0},"south":{"uv":[90,9,93,13],"texture":0},"west":{"uv":[59,30,67,34],"texture":0},"up":{"uv":[62,76,59,68],"texture":0},"down":{"uv":[3,69,0,77],"texture":0}},"type":"cube","uuid":"004b5756-3745-4694-ac32-533f6ec57441"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.5,28.35,5.725],"to":[-1,32.2,6.725],"autouv":0,"color":1,"rotation":[15,0,0],"origin":[-2.5,30.2,6.225],"faces":{"north":{"uv":[63,26,67,30],"texture":0},"east":{"uv":[37,110,38,114],"texture":0},"south":{"uv":[43,64,47,68],"texture":0},"west":{"uv":[86,110,87,114],"texture":0},"up":{"uv":[72,26,68,25],"texture":0},"down":{"uv":[76,4,72,5],"texture":0}},"type":"cube","uuid":"17175f5b-35b1-3d5b-3d47-33e3b89cfcf7"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.3,21.95,0.75],"to":[3.55,24.45,4.25],"autouv":0,"color":1,"rotation":[0,0,10],"origin":[2.925,23.075,2.5],"faces":{"north":{"uv":[44,30,48,33],"texture":0},"east":{"uv":[61,5,65,8],"texture":0},"south":{"uv":[90,22,94,25],"texture":0},"west":{"uv":[90,32,94,35],"texture":0},"up":{"uv":[69,83,65,79],"texture":0},"down":{"uv":[73,79,69,83],"texture":0}},"type":"cube","uuid":"04c0418d-5912-cd30-2b8b-0ad684dbc87f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.21451,28,3.81133],"to":[14.61451,30.475,6.66133],"autouv":0,"color":6,"rotation":[0,-22.5,-7.5],"origin":[12.86867,29.7875,2.8875],"faces":{"north":{"uv":[101,100,105,102],"texture":0},"east":{"uv":[108,78,111,80],"texture":0},"south":{"uv":[102,21,106,23],"texture":0},"west":{"uv":[108,80,111,82],"texture":0},"up":{"uv":[19,96,15,93],"texture":0},"down":{"uv":[84,93,80,96],"texture":0}},"type":"cube","uuid":"16fc3b7f-37f9-d76e-bafd-e1397a513ffd"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.89242,28,1.24768],"to":[14.36742,30.475,4.54768],"autouv":0,"color":6,"rotation":[0,0,-7.5],"origin":[12.86867,29.7875,2.8875],"faces":{"north":{"uv":[101,113,103,115],"texture":0},"east":{"uv":[81,108,84,110],"texture":0},"south":{"uv":[113,102,115,104],"texture":0},"west":{"uv":[108,82,111,84],"texture":0},"up":{"uv":[93,111,91,108],"texture":0},"down":{"uv":[95,108,93,111],"texture":0}},"type":"cube","uuid":"419267b6-458e-9b4f-83d2-43b0e6c69f8d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.21451,28,-0.88633],"to":[14.61451,30.475,1.96367],"autouv":0,"color":6,"rotation":[0,22.5,-7.5],"origin":[12.86867,29.7875,2.8875],"faces":{"north":{"uv":[102,47,106,49],"texture":0},"east":{"uv":[95,108,98,110],"texture":0},"south":{"uv":[49,102,53,104],"texture":0},"west":{"uv":[98,108,101,110],"texture":0},"up":{"uv":[97,94,93,91],"texture":0},"down":{"uv":[10,94,6,97],"texture":0}},"type":"cube","uuid":"b58b8a38-dee1-2ef6-f951-1f08a07bdbf4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.71451,26.25,3.81133],"to":[14.11451,28.725,5.66133],"autouv":0,"color":6,"rotation":[0,-22.5,0],"origin":[12.36867,28.0375,2.8875],"faces":{"north":{"uv":[102,49,106,51],"texture":0},"east":{"uv":[109,113,111,115],"texture":0},"south":{"uv":[102,57,106,59],"texture":0},"west":{"uv":[113,111,115,113],"texture":0},"up":{"uv":[106,61,102,59],"texture":0},"down":{"uv":[106,61,102,63],"texture":0}},"type":"cube","uuid":"ccf114a8-f053-924b-d8ca-24b0a70368da"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.86742,27.075,2.45768],"to":[14.96742,27.9,3.33768],"autouv":0,"color":6,"origin":[12.36867,28.0375,2.8875],"faces":{"north":{"uv":[43,118,44,119],"texture":0},"east":{"uv":[118,43,119,44],"texture":0},"south":{"uv":[44,118,45,119],"texture":0},"west":{"uv":[118,44,119,45],"texture":0},"up":{"uv":[119,46,118,45],"texture":0},"down":{"uv":[47,118,46,119],"texture":0}},"type":"cube","uuid":"fdc93b97-72a5-e783-9db3-5a032ac3608a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.39242,26.25,1.24768],"to":[13.86742,28.725,4.54768],"autouv":0,"color":6,"origin":[12.36867,28.0375,2.8875],"faces":{"north":{"uv":[113,113,115,115],"texture":0},"east":{"uv":[108,100,111,102],"texture":0},"south":{"uv":[0,114,2,116],"texture":0},"west":{"uv":[108,102,111,104],"texture":0},"up":{"uv":[107,111,105,108],"texture":0},"down":{"uv":[109,108,107,111],"texture":0}},"type":"cube","uuid":"3251f438-779f-574a-a206-d180ea007ccb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.71451,26.25,0.11367],"to":[14.11451,28.725,1.96367],"autouv":0,"color":6,"rotation":[0,22.5,0],"origin":[12.36867,28.0375,2.8875],"faces":{"north":{"uv":[79,102,83,104],"texture":0},"east":{"uv":[114,0,116,2],"texture":0},"south":{"uv":[83,102,87,104],"texture":0},"west":{"uv":[2,114,4,116],"texture":0},"up":{"uv":[106,87,102,85],"texture":0},"down":{"uv":[106,87,102,89],"texture":0}},"type":"cube","uuid":"fa153ca0-6723-e9f0-8cfb-7a3617eedafe"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.50662,21.95,-0.99545],"to":[3.50662,24.2,2.25455],"autouv":0,"color":1,"rotation":[0,-22.5,10],"origin":[2.925,23.075,2.5],"faces":{"north":{"uv":[106,20,109,22],"texture":0},"east":{"uv":[106,30,109,32],"texture":0},"south":{"uv":[106,32,109,34],"texture":0},"west":{"uv":[33,106,36,108],"texture":0},"up":{"uv":[31,100,28,97],"texture":0},"down":{"uv":[34,97,31,100],"texture":0}},"type":"cube","uuid":"722552e7-6fd6-e7ea-9dab-4d9bc3971265"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.50662,21.95,2.74545],"to":[3.50662,24.2,5.99545],"autouv":0,"color":1,"rotation":[0,22.5,10],"origin":[2.925,23.075,2.5],"faces":{"north":{"uv":[106,96,109,98],"texture":0},"east":{"uv":[106,98,109,100],"texture":0},"south":{"uv":[99,106,102,108],"texture":0},"west":{"uv":[106,104,109,106],"texture":0},"up":{"uv":[17,101,14,98],"texture":0},"down":{"uv":[101,19,98,22],"texture":0}},"type":"cube","uuid":"6d219fa0-914b-4c91-2184-7e3e52b423bd"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.55,21.95,-0.5],"to":[4.8,24.2,5.5],"autouv":0,"color":1,"rotation":[0,0,10],"origin":[2.925,23.075,2.5],"faces":{"north":{"uv":[45,93,46,95],"texture":0},"east":{"uv":[90,17,96,19],"texture":0},"south":{"uv":[14,94,15,96],"texture":0},"west":{"uv":[90,25,96,27],"texture":0},"up":{"uv":[88,108,87,102],"texture":0},"down":{"uv":[99,102,98,108],"texture":0}},"type":"cube","uuid":"fc34e1fb-b43d-b65a-94a7-9137ff36bdb2"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.6,19.875,-0.5],"to":[5.85,22.125,5.5],"autouv":0,"color":1,"rotation":[0,0,10],"origin":[3.975,21,2.5],"faces":{"north":{"uv":[116,111,117,113],"texture":0},"east":{"uv":[90,63,96,65],"texture":0},"south":{"uv":[114,116,115,118],"texture":0},"west":{"uv":[90,65,96,67],"texture":0},"up":{"uv":[10,111,9,105],"texture":0},"down":{"uv":[33,105,32,111],"texture":0}},"type":"cube","uuid":"7069c39d-7c58-42b2-26d5-f9812eb9eece"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.55662,19.875,-0.99545],"to":[4.55662,22.125,2.25455],"autouv":0,"color":1,"rotation":[0,-22.5,10],"origin":[3.975,21,2.5],"faces":{"north":{"uv":[105,81,108,83],"texture":0},"east":{"uv":[105,83,108,85],"texture":0},"south":{"uv":[105,100,108,102],"texture":0},"west":{"uv":[105,102,108,104],"texture":0},"up":{"uv":[22,100,19,97],"texture":0},"down":{"uv":[28,97,25,100],"texture":0}},"type":"cube","uuid":"3a929634-fa05-3210-ff3e-5605ad95f727"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.55662,19.875,2.74545],"to":[4.55662,22.125,5.99545],"autouv":0,"color":1,"rotation":[0,22.5,10],"origin":[3.975,21,2.5],"faces":{"north":{"uv":[75,105,78,107],"texture":0},"east":{"uv":[105,75,108,77],"texture":0},"south":{"uv":[105,77,108,79],"texture":0},"west":{"uv":[105,79,108,81],"texture":0},"up":{"uv":[14,100,11,97],"texture":0},"down":{"uv":[100,12,97,15],"texture":0}},"type":"cube","uuid":"ed0088d8-cbda-85d1-1d53-989379b69dbe"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.25,19.875,0.75],"to":[4.6,22.125,4.25],"autouv":0,"color":1,"rotation":[0,0,10],"origin":[3.975,21,2.5],"faces":{"north":{"uv":[95,29,100,31],"texture":0},"east":{"uv":[57,12,61,14],"texture":0},"south":{"uv":[38,95,43,97],"texture":0},"west":{"uv":[73,23,77,25],"texture":0},"up":{"uv":[77,68,72,64],"texture":0},"down":{"uv":[77,68,72,72],"texture":0}},"type":"cube","uuid":"4fd10ae8-f304-3b56-048e-f387a9b4c279"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.50662,21.95,-0.99545],"to":[-0.50662,24.2,2.25455],"autouv":0,"color":1,"rotation":[0,22.5,-10],"origin":[-2.925,23.075,2.5],"faces":{"north":{"uv":[106,89,109,91],"texture":0},"east":{"uv":[92,106,95,108],"texture":0},"south":{"uv":[106,94,109,96],"texture":0},"west":{"uv":[95,106,98,108],"texture":0},"up":{"uv":[100,94,97,91],"texture":0},"down":{"uv":[3,98,0,101],"texture":0}},"type":"cube","uuid":"f78e3b4c-9ec3-6e3f-34b0-f82d64ce42f3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.55662,19.875,-0.99545],"to":[-1.55662,22.125,2.25455],"autouv":0,"color":1,"rotation":[0,22.5,-10],"origin":[-3.975,21,2.5],"faces":{"north":{"uv":[78,106,81,108],"texture":0},"east":{"uv":[81,106,84,108],"texture":0},"south":{"uv":[106,85,109,87],"texture":0},"west":{"uv":[106,87,109,89],"texture":0},"up":{"uv":[73,100,70,97],"texture":0},"down":{"uv":[76,97,73,100],"texture":0}},"type":"cube","uuid":"108f8152-a871-a04e-3694-ee8429454f4d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.85,19.875,-0.5],"to":[-4.6,22.125,5.5],"autouv":0,"color":1,"rotation":[0,0,-10],"origin":[-3.975,21,2.5],"faces":{"north":{"uv":[0,117,1,119],"texture":0},"east":{"uv":[90,87,96,89],"texture":0},"south":{"uv":[1,117,2,119],"texture":0},"west":{"uv":[90,89,96,91],"texture":0},"up":{"uv":[12,112,11,106],"texture":0},"down":{"uv":[40,106,39,112],"texture":0}},"type":"cube","uuid":"9f7af667-56fc-1051-3344-31768975a667"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.8,21.95,-0.5],"to":[-3.55,24.2,5.5],"autouv":0,"color":1,"rotation":[0,0,-10],"origin":[-2.925,23.075,2.5],"faces":{"north":{"uv":[115,116,116,118],"texture":0},"east":{"uv":[90,70,96,72],"texture":0},"south":{"uv":[116,116,117,118],"texture":0},"west":{"uv":[90,85,96,87],"texture":0},"up":{"uv":[64,111,63,105],"texture":0},"down":{"uv":[11,106,10,112],"texture":0}},"type":"cube","uuid":"efd0ecbf-1a4e-dc52-e0be-fe4c9b79e954"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.55662,19.875,2.74545],"to":[-1.55662,22.125,5.99545],"autouv":0,"color":1,"rotation":[0,-22.5,-10],"origin":[-3.975,21,2.5],"faces":{"north":{"uv":[50,106,53,108],"texture":0},"east":{"uv":[106,57,109,59],"texture":0},"south":{"uv":[106,59,109,61],"texture":0},"west":{"uv":[106,61,109,63],"texture":0},"up":{"uv":[67,100,64,97],"texture":0},"down":{"uv":[70,97,67,100],"texture":0}},"type":"cube","uuid":"a9a3fdae-afce-dfd5-2b6d-f2eab998494f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.50662,21.95,2.74545],"to":[-0.50662,24.2,5.99545],"autouv":0,"color":1,"rotation":[0,-22.5,-10],"origin":[-2.925,23.075,2.5],"faces":{"north":{"uv":[36,106,39,108],"texture":0},"east":{"uv":[106,46,109,48],"texture":0},"south":{"uv":[47,106,50,108],"texture":0},"west":{"uv":[106,48,109,50],"texture":0},"up":{"uv":[41,100,38,97],"texture":0},"down":{"uv":[58,97,55,100],"texture":0}},"type":"cube","uuid":"db3d2176-8b10-9f62-b5ba-2582cd79869e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.6,19.875,0.75],"to":[0.25,22.125,4.25],"autouv":0,"color":1,"rotation":[0,0,-10],"origin":[-3.975,21,2.5],"faces":{"north":{"uv":[95,42,100,44],"texture":0},"east":{"uv":[100,73,104,75],"texture":0},"south":{"uv":[95,44,100,46],"texture":0},"west":{"uv":[100,91,104,93],"texture":0},"up":{"uv":[77,76,72,72],"texture":0},"down":{"uv":[8,73,3,77],"texture":0}},"type":"cube","uuid":"233e6b71-b925-8a60-e09d-c2c1d3dcc613"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.55,21.95,0.75],"to":[0.3,24.45,4.25],"autouv":0,"color":1,"rotation":[0,0,-10],"origin":[-2.925,23.075,2.5],"faces":{"north":{"uv":[90,67,94,70],"texture":0},"east":{"uv":[90,76,94,79],"texture":0},"south":{"uv":[90,79,94,82],"texture":0},"west":{"uv":[90,82,94,85],"texture":0},"up":{"uv":[77,83,73,79],"texture":0},"down":{"uv":[81,79,77,83],"texture":0}},"type":"cube","uuid":"2e873e0e-30e9-2e8c-0ec5-76ae8cbbfc1c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.35,17.725,0.75],"to":[5.425,19.975,4.25],"autouv":0,"color":1,"rotation":[0,0,10],"origin":[4.8,18.85,2.5],"faces":{"north":{"uv":[90,39,96,41],"texture":0},"east":{"uv":[98,22,102,24],"texture":0},"south":{"uv":[90,46,96,48],"texture":0},"west":{"uv":[99,15,103,17],"texture":0},"up":{"uv":[74,50,68,46],"texture":0},"down":{"uv":[9,69,3,73],"texture":0}},"type":"cube","uuid":"f4bd8628-fad1-fde9-fbb6-68b207058fad"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.38162,17.725,2.74545],"to":[5.38162,19.975,5.99545],"autouv":0,"color":1,"rotation":[0,22.5,10],"origin":[4.8,18.85,2.5],"faces":{"north":{"uv":[105,37,108,39],"texture":0},"east":{"uv":[105,39,108,41],"texture":0},"south":{"uv":[105,42,108,44],"texture":0},"west":{"uv":[105,44,108,46],"texture":0},"up":{"uv":[99,99,96,96],"texture":0},"down":{"uv":[100,0,97,3],"texture":0}},"type":"cube","uuid":"4f38d578-03f6-6f4a-c0cf-9ade07a86ed9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.38162,17.725,-0.99545],"to":[5.38162,19.975,2.25455],"autouv":0,"color":1,"rotation":[0,-22.5,10],"origin":[4.8,18.85,2.5],"faces":{"north":{"uv":[70,103,73,105],"texture":0},"east":{"uv":[75,103,78,105],"texture":0},"south":{"uv":[47,104,50,106],"texture":0},"west":{"uv":[50,104,53,106],"texture":0},"up":{"uv":[99,91,96,88],"texture":0},"down":{"uv":[96,96,93,99],"texture":0}},"type":"cube","uuid":"00ac818e-4fca-67c4-cb16-1bf156ea1ec8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[5.425,17.725,-0.5],"to":[6.675,19.975,5.5],"autouv":0,"color":1,"rotation":[0,0,10],"origin":[4.8,18.85,2.5],"faces":{"north":{"uv":[44,98,45,100],"texture":0},"east":{"uv":[90,35,96,37],"texture":0},"south":{"uv":[98,94,99,96],"texture":0},"west":{"uv":[90,37,96,39],"texture":0},"up":{"uv":[47,109,46,103],"texture":0},"down":{"uv":[57,103,56,109],"texture":0}},"type":"cube","uuid":"f9e2b3b4-83be-9820-5fd2-6cc5671c0d06"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.425,17.725,0.75],"to":[0.35,19.975,4.25],"autouv":0,"color":1,"rotation":[0,0,-10],"origin":[-4.8,18.85,2.5],"faces":{"north":{"uv":[90,59,96,61],"texture":0},"east":{"uv":[99,63,103,65],"texture":0},"south":{"uv":[90,61,96,63],"texture":0},"west":{"uv":[100,71,104,73],"texture":0},"up":{"uv":[15,73,9,69],"texture":0},"down":{"uv":[32,69,26,73],"texture":0}},"type":"cube","uuid":"91c6c002-aa48-c40c-eb94-c2d2c6ce9387"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.38162,17.725,-0.99545],"to":[-2.38162,19.975,2.25455],"autouv":0,"color":1,"rotation":[0,22.5,-10],"origin":[-4.8,18.85,2.5],"faces":{"north":{"uv":[105,65,108,67],"texture":0},"east":{"uv":[105,67,108,69],"texture":0},"south":{"uv":[105,69,108,71],"texture":0},"west":{"uv":[70,105,73,107],"texture":0},"up":{"uv":[11,100,8,97],"texture":0},"down":{"uv":[100,9,97,12],"texture":0}},"type":"cube","uuid":"a8cf07da-f774-cb70-728f-fddd7dd86939"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.675,17.725,-0.5],"to":[-5.425,19.975,5.5],"autouv":0,"color":1,"rotation":[0,0,-10],"origin":[-4.8,18.85,2.5],"faces":{"north":{"uv":[2,101,3,103],"texture":0},"east":{"uv":[90,48,96,50],"texture":0},"south":{"uv":[97,116,98,118],"texture":0},"west":{"uv":[90,50,96,52],"texture":0},"up":{"uv":[58,109,57,103],"texture":0},"down":{"uv":[87,104,86,110],"texture":0}},"type":"cube","uuid":"174a8673-5e4f-54fb-8845-de9053585a39"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.38162,17.725,2.74545],"to":[-2.38162,19.975,5.99545],"autouv":0,"color":1,"rotation":[0,-22.5,-10],"origin":[-4.8,18.85,2.5],"faces":{"north":{"uv":[105,51,108,53],"texture":0},"east":{"uv":[105,53,108,55],"texture":0},"south":{"uv":[105,55,108,57],"texture":0},"west":{"uv":[60,105,63,107],"texture":0},"up":{"uv":[100,6,97,3],"texture":0},"down":{"uv":[8,97,5,100],"texture":0}},"type":"cube","uuid":"c7ff5c49-b255-2025-50e2-7dbc631d35b1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.84137,17.08997,-1.41551],"to":[3.34137,19.33997,0.83449],"autouv":0,"color":1,"rotation":[2.59945,-7.70999,9.31682],"origin":[1.0023,20.45817,-0.72757],"faces":{"north":{"uv":[105,24,108,26],"texture":0},"east":{"uv":[112,48,114,50],"texture":0},"south":{"uv":[26,105,29,107],"texture":0},"west":{"uv":[53,112,55,114],"texture":0},"up":{"uv":[32,107,29,105],"texture":0},"down":{"uv":[108,35,105,37],"texture":0}},"type":"cube","uuid":"43e12532-2dd1-ac2b-45a8-200ffb0c32eb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.43567,19.35056,-1.24746],"to":[2.93567,21.60056,1.00254],"autouv":0,"color":1,"rotation":[2.59945,-7.70999,9.31682],"origin":[1.0023,20.45817,-0.72757],"faces":{"north":{"uv":[112,29,114,31],"texture":0},"east":{"uv":[34,112,36,114],"texture":0},"south":{"uv":[39,112,41,114],"texture":0},"west":{"uv":[44,112,46,114],"texture":0},"up":{"uv":[48,114,46,112],"texture":0},"down":{"uv":[114,46,112,48],"texture":0}},"type":"cube","uuid":"b6c79338-054b-389c-c5fb-b4847f74bdf7"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.31323,21.57637,-0.98964],"to":[2.31323,24.07637,1.26036],"autouv":0,"color":1,"rotation":[2.59945,-7.70999,9.31682],"origin":[1.0023,20.45817,-0.72757],"faces":{"north":{"uv":[88,103,90,106],"texture":0},"east":{"uv":[12,104,14,107],"texture":0},"south":{"uv":[64,104,66,107],"texture":0},"west":{"uv":[66,104,68,107],"texture":0},"up":{"uv":[78,113,76,111],"texture":0},"down":{"uv":[80,111,78,113],"texture":0}},"type":"cube","uuid":"92fea33d-fa66-6a60-cefd-7ddb861f07de"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.18677,21.57637,3.78964],"to":[1.81323,24.07637,6.03964],"autouv":0,"color":1,"rotation":[-2.59945,7.70999,9.31682],"origin":[1.0023,20.45817,0.72757],"faces":{"north":{"uv":[105,17,107,20],"texture":0},"east":{"uv":[20,105,22,108],"texture":0},"south":{"uv":[22,105,24,108],"texture":0},"west":{"uv":[24,105,26,108],"texture":0},"up":{"uv":[114,22,112,20],"texture":0},"down":{"uv":[28,112,26,114],"texture":0}},"type":"cube","uuid":"17e9cc5d-478c-6abb-ce1d-6510a8f229e5"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.06433,19.35056,4.04746],"to":[2.43567,21.60056,6.29746],"autouv":0,"color":1,"rotation":[-2.59945,7.70999,9.31682],"origin":[1.0023,20.45817,0.72757],"faces":{"north":{"uv":[2,112,4,114],"texture":0},"east":{"uv":[10,112,12,114],"texture":0},"south":{"uv":[112,12,114,14],"texture":0},"west":{"uv":[112,14,114,16],"texture":0},"up":{"uv":[17,114,15,112],"texture":0},"down":{"uv":[21,112,19,114],"texture":0}},"type":"cube","uuid":"3904e256-e178-2e6c-d063-3e84a6ce82b6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.34137,17.08997,4.21551],"to":[2.84137,19.33997,6.46551],"autouv":0,"color":1,"rotation":[-2.59945,7.70999,9.31682],"origin":[1.0023,20.45817,0.72757],"faces":{"north":{"uv":[104,71,107,73],"texture":0},"east":{"uv":[111,78,113,80],"texture":0},"south":{"uv":[104,73,107,75],"texture":0},"west":{"uv":[80,111,82,113],"texture":0},"up":{"uv":[81,106,78,104],"texture":0},"down":{"uv":[84,104,81,106],"texture":0}},"type":"cube","uuid":"d6f505c5-44ae-c646-2aff-fbf44961b32d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.84137,17.08997,4.21551],"to":[-0.34137,19.33997,6.46551],"autouv":0,"color":1,"rotation":[-2.59945,-7.70999,-9.31682],"origin":[-1.0023,20.45817,0.72757],"faces":{"north":{"uv":[6,105,9,107],"texture":0},"east":{"uv":[0,112,2,114],"texture":0},"south":{"uv":[105,6,108,8],"texture":0},"west":{"uv":[112,0,114,2],"texture":0},"up":{"uv":[17,107,14,105],"texture":0},"down":{"uv":[20,105,17,107],"texture":0}},"type":"cube","uuid":"82e0d2c1-d8ff-f573-2089-cb24e3f5066a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.43567,19.35056,4.04746],"to":[0.06433,21.60056,6.29746],"autouv":0,"color":1,"rotation":[-2.59945,-7.70999,-9.31682],"origin":[-1.0023,20.45817,0.72757],"faces":{"north":{"uv":[111,102,113,104],"texture":0},"east":{"uv":[105,111,107,113],"texture":0},"south":{"uv":[107,111,109,113],"texture":0},"west":{"uv":[111,108,113,110],"texture":0},"up":{"uv":[111,113,109,111],"texture":0},"down":{"uv":[113,110,111,112],"texture":0}},"type":"cube","uuid":"ee2d6ac7-e504-ec74-8603-62e046365557"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1.81323,21.57637,3.78964],"to":[0.18677,24.07637,6.03964],"autouv":0,"color":1,"rotation":[-2.59945,-7.70999,-9.31682],"origin":[-1.0023,20.45817,0.72757],"faces":{"north":{"uv":[102,104,104,107],"texture":0},"east":{"uv":[104,104,106,107],"texture":0},"south":{"uv":[2,105,4,108],"texture":0},"west":{"uv":[4,105,6,108],"texture":0},"up":{"uv":[113,102,111,100],"texture":0},"down":{"uv":[103,111,101,113],"texture":0}},"type":"cube","uuid":"58509bf9-3d6a-868c-d637-ff183a6cdc0b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.34137,17.08997,-1.41551],"to":[-0.84137,19.33997,0.83449],"autouv":0,"color":1,"rotation":[2.59945,7.70999,-9.31682],"origin":[-1.0023,20.45817,-0.72757],"faces":{"north":{"uv":[104,91,107,93],"texture":0},"east":{"uv":[95,111,97,113],"texture":0},"south":{"uv":[92,104,95,106],"texture":0},"west":{"uv":[97,111,99,113],"texture":0},"up":{"uv":[98,106,95,104],"texture":0},"down":{"uv":[102,104,99,106],"texture":0}},"type":"cube","uuid":"caaf69a3-d39a-aa71-72dc-fd4da017e228"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.93567,19.35056,-1.24746],"to":[-0.43567,21.60056,1.00254],"autouv":0,"color":1,"rotation":[2.59945,7.70999,-9.31682],"origin":[-1.0023,20.45817,-0.72757],"faces":{"north":{"uv":[111,82,113,84],"texture":0},"east":{"uv":[84,111,86,113],"texture":0},"south":{"uv":[87,111,89,113],"texture":0},"west":{"uv":[111,87,113,89],"texture":0},"up":{"uv":[93,113,91,111],"texture":0},"down":{"uv":[95,111,93,113],"texture":0}},"type":"cube","uuid":"08acc518-8e41-a360-e3fb-99702198e77e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.31323,21.57637,-0.98964],"to":[-0.31323,24.07637,1.26036],"autouv":0,"color":1,"rotation":[2.59945,7.70999,-9.31682],"origin":[-1.0023,20.45817,-0.72757],"faces":{"north":{"uv":[68,104,70,107],"texture":0},"east":{"uv":[73,104,75,107],"texture":0},"south":{"uv":[84,104,86,107],"texture":0},"west":{"uv":[90,104,92,107],"texture":0},"up":{"uv":[113,82,111,80],"texture":0},"down":{"uv":[84,111,82,113],"texture":0}},"type":"cube","uuid":"61a1fc5e-ca0c-2440-50ad-c9ed748263f8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0.9,15.35,-0.45],"to":[6.3,16.7,4.95],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[3.6,15.575,2.25],"faces":{"north":{"uv":[110,5,115,6],"texture":0},"east":{"uv":[110,37,115,38],"texture":0},"south":{"uv":[110,38,115,39],"texture":0},"west":{"uv":[110,39,115,40],"texture":0},"up":{"uv":[72,20,67,15],"texture":0},"down":{"uv":[72,26,67,31],"texture":0}},"type":"cube","uuid":"fa05b37a-8d36-473a-80b6-04517fb820f1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.575,8.6,0.225],"to":[5.625,12.65,4.275],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[3.6,15.575,2.25],"faces":{"north":{"uv":[80,52,84,56],"texture":0},"east":{"uv":[80,62,84,66],"texture":0},"south":{"uv":[80,66,84,70],"texture":0},"west":{"uv":[80,70,84,74],"texture":0},"up":{"uv":[16,85,12,81],"texture":0},"down":{"uv":[85,43,81,47],"texture":0}},"type":"cube","uuid":"1f0c2fcb-f77e-bb94-21d8-46a5aa7b0dd5"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.125,7.25,-0.225],"to":[6.075,8.6,4.725],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[3.6,15.575,2.25],"faces":{"north":{"uv":[110,51,115,52],"texture":0},"east":{"uv":[110,52,115,53],"texture":0},"south":{"uv":[110,71,115,72],"texture":0},"west":{"uv":[110,72,115,73],"texture":0},"up":{"uv":[72,65,67,60],"texture":0},"down":{"uv":[72,65,67,70],"texture":0}},"type":"cube","uuid":"d9a8de97-fd96-8be7-1d32-234b444005dc"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.7838,38.575,1.9938],"to":[11.2838,39.375,3.4938],"autouv":0,"color":6,"rotation":[0,-45,-2.5],"origin":[10.5338,38.80625,2.8063],"faces":{"north":{"uv":[117,26,119,27],"texture":0},"east":{"uv":[117,27,119,28],"texture":0},"south":{"uv":[28,117,30,118],"texture":0},"west":{"uv":[30,117,32,118],"texture":0},"up":{"uv":[14,115,12,113],"texture":0},"down":{"uv":[19,113,17,115],"texture":0}},"type":"cube","uuid":"18eb35ac-628a-8ef8-3c6e-8dbac690df22"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.31742,18.95,1.27268],"to":[12.46742,19.95,4.47268],"autouv":0,"color":6,"origin":[11.76742,23.35,2.07268],"faces":{"north":{"uv":[118,46,119,47],"texture":0},"east":{"uv":[48,116,51,117],"texture":0},"south":{"uv":[47,118,48,119],"texture":0},"west":{"uv":[116,48,119,49],"texture":0},"up":{"uv":[4,119,3,116],"texture":0},"down":{"uv":[5,116,4,119],"texture":0}},"type":"cube","uuid":"757cfe40-f697-b3d4-3ab5-931a0f300a16"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.56742,18.65,3.77268],"to":[12.21742,19.45,4.47268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[48,118,49,119],"texture":0},"east":{"uv":[49,118,50,119],"texture":0},"south":{"uv":[118,49,119,50],"texture":0},"west":{"uv":[50,118,51,119],"texture":0},"up":{"uv":[52,119,51,118],"texture":0},"down":{"uv":[119,51,118,52],"texture":0}},"type":"cube","uuid":"70d08c0b-6a6f-3aec-4064-a6b8dc9beb84"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.56742,18.55,2.17268],"to":[12.21742,19.45,2.87268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,79,119,80],"texture":0},"east":{"uv":[81,118,82,119],"texture":0},"south":{"uv":[82,118,83,119],"texture":0},"west":{"uv":[118,82,119,83],"texture":0},"up":{"uv":[119,84,118,83],"texture":0},"down":{"uv":[119,84,118,85],"texture":0}},"type":"cube","uuid":"a9292015-e3c2-6dc9-675c-6234fe1a6667"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.56742,18.65,1.37268],"to":[12.21742,19.45,2.07268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[103,118,104,119],"texture":0},"east":{"uv":[118,103,119,104],"texture":0},"south":{"uv":[104,118,105,119],"texture":0},"west":{"uv":[118,104,119,105],"texture":0},"up":{"uv":[106,119,105,118],"texture":0},"down":{"uv":[119,105,118,106],"texture":0}},"type":"cube","uuid":"c5f19499-a1b1-8a9b-1503-6f6e12280777"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.56742,18.55,2.97268],"to":[12.21742,19.45,3.67268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[100,118,101,119],"texture":0},"east":{"uv":[118,100,119,101],"texture":0},"south":{"uv":[101,118,102,119],"texture":0},"west":{"uv":[118,101,119,102],"texture":0},"up":{"uv":[103,119,102,118],"texture":0},"down":{"uv":[119,102,118,103],"texture":0}},"type":"cube","uuid":"69aa58aa-ca4d-d8ff-032f-fab509531f5a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.57992,18.7125,3.77268],"to":[12.22992,19.8125,4.47268],"autouv":0,"color":6,"rotation":[0,0,-135],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,75,119,76],"texture":0},"east":{"uv":[76,118,77,119],"texture":0},"south":{"uv":[77,118,78,119],"texture":0},"west":{"uv":[78,118,79,119],"texture":0},"up":{"uv":[119,79,118,78],"texture":0},"down":{"uv":[80,118,79,119],"texture":0}},"type":"cube","uuid":"e369a8bd-344b-ef80-43d3-765cbfcf672c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.57992,18.5125,2.97268],"to":[12.22992,19.7125,3.67268],"autouv":0,"color":6,"rotation":[0,0,-135],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[68,118,69,119],"texture":0},"east":{"uv":[69,118,70,119],"texture":0},"south":{"uv":[118,69,119,70],"texture":0},"west":{"uv":[71,118,72,119],"texture":0},"up":{"uv":[73,119,72,118],"texture":0},"down":{"uv":[119,74,118,75],"texture":0}},"type":"cube","uuid":"b4965020-4279-25d4-0eb4-1f7cccba52fe"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.57992,18.5125,2.17268],"to":[12.22992,19.7125,2.87268],"autouv":0,"color":6,"rotation":[0,0,-135],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[62,118,63,119],"texture":0},"east":{"uv":[63,118,64,119],"texture":0},"south":{"uv":[64,118,65,119],"texture":0},"west":{"uv":[118,64,119,65],"texture":0},"up":{"uv":[119,66,118,65],"texture":0},"down":{"uv":[68,118,67,119],"texture":0}},"type":"cube","uuid":"1a73524e-26f4-7fb3-97e2-8624cd92cede"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.57992,18.7125,1.37268],"to":[12.22992,19.8125,2.07268],"autouv":0,"color":6,"rotation":[0,0,-135],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,52,119,53],"texture":0},"east":{"uv":[118,53,119,54],"texture":0},"south":{"uv":[118,54,119,55],"texture":0},"west":{"uv":[58,118,59,119],"texture":0},"up":{"uv":[119,60,118,59],"texture":0},"down":{"uv":[62,118,61,119],"texture":0}},"type":"cube","uuid":"fb17833e-914e-9268-4340-f5603d6a03e4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.36742,18.05,2.17268],"to":[11.01742,18.95,2.87268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,96,119,97],"texture":0},"east":{"uv":[97,118,98,119],"texture":0},"south":{"uv":[118,97,119,98],"texture":0},"west":{"uv":[98,118,99,119],"texture":0},"up":{"uv":[119,99,118,98],"texture":0},"down":{"uv":[100,118,99,119],"texture":0}},"type":"cube","uuid":"b9e03e5e-eb23-1c48-294a-0003ec580bed"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.36742,18.05,2.97268],"to":[11.01742,18.95,3.67268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,93,119,94],"texture":0},"east":{"uv":[94,118,95,119],"texture":0},"south":{"uv":[118,94,119,95],"texture":0},"west":{"uv":[95,118,96,119],"texture":0},"up":{"uv":[119,96,118,95],"texture":0},"down":{"uv":[97,118,96,119],"texture":0}},"type":"cube","uuid":"564eb56f-b9b3-74a8-a7bf-fec1f711bca9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.36742,18.15,1.37268],"to":[11.01742,18.95,2.07268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,90,119,91],"texture":0},"east":{"uv":[91,118,92,119],"texture":0},"south":{"uv":[118,91,119,92],"texture":0},"west":{"uv":[92,118,93,119],"texture":0},"up":{"uv":[119,93,118,92],"texture":0},"down":{"uv":[94,118,93,119],"texture":0}},"type":"cube","uuid":"a6fa060e-d211-974f-c460-2142b9213f67"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.36742,18.15,3.77268],"to":[11.01742,18.95,4.47268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[11.29242,18.9375,3.32268],"faces":{"north":{"uv":[118,86,119,87],"texture":0},"east":{"uv":[118,87,119,88],"texture":0},"south":{"uv":[118,88,119,89],"texture":0},"west":{"uv":[89,118,90,119],"texture":0},"up":{"uv":[119,90,118,89],"texture":0},"down":{"uv":[91,118,90,119],"texture":0}},"type":"cube","uuid":"bdd6d7a9-0e80-b7c7-aab3-b5f075817d54"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.51742,19.95,1.27268],"to":[11.66742,21.95,4.47268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[11.89242,20.95,2.87268],"faces":{"north":{"uv":[40,117,41,119],"texture":0},"east":{"uv":[5,109,8,111],"texture":0},"south":{"uv":[42,117,43,119],"texture":0},"west":{"uv":[12,109,15,111],"texture":0},"up":{"uv":[11,119,10,116],"texture":0},"down":{"uv":[12,116,11,119],"texture":0}},"type":"cube","uuid":"f8c3aa34-a09e-a0a1-10df-c672519263e3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.81742,20.95,2.22268],"to":[12.71742,21.95,4.12268],"autouv":0,"color":6,"rotation":[0,-45,0],"origin":[11.76742,25.35,2.07268],"faces":{"north":{"uv":[117,38,119,39],"texture":0},"east":{"uv":[117,39,119,40],"texture":0},"south":{"uv":[117,40,119,41],"texture":0},"west":{"uv":[117,41,119,42],"texture":0},"up":{"uv":[12,116,10,114],"texture":0},"down":{"uv":[116,12,114,14],"texture":0}},"type":"cube","uuid":"73685916-cd25-a75a-fa4e-c1371475dee6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.31742,23.95,1.72268],"to":[13.21742,24.95,4.62268],"autouv":0,"color":6,"rotation":[0,-45,0],"origin":[11.76742,25.35,2.07268],"faces":{"north":{"uv":[116,24,119,25],"texture":0},"east":{"uv":[116,25,119,26],"texture":0},"south":{"uv":[34,116,37,117],"texture":0},"west":{"uv":[116,47,119,48],"texture":0},"up":{"uv":[32,103,29,100],"texture":0},"down":{"uv":[103,29,100,32],"texture":0}},"type":"cube","uuid":"0da7fcdb-b43d-8b27-971b-6df95829641c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.81742,22.95,1.22268],"to":[13.71742,23.95,5.12268],"autouv":0,"color":6,"rotation":[0,-45,0],"origin":[11.76742,25.35,2.07268],"faces":{"north":{"uv":[105,113,109,114],"texture":0},"east":{"uv":[113,108,117,109],"texture":0},"south":{"uv":[113,109,117,110],"texture":0},"west":{"uv":[113,110,117,111],"texture":0},"up":{"uv":[87,42,83,38],"texture":0},"down":{"uv":[87,47,83,51],"texture":0}},"type":"cube","uuid":"52345605-2265-faad-f6ad-eca1be3dec6c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.7838,39.175,2.1188],"to":[11.1588,39.975,3.4938],"autouv":0,"color":6,"rotation":[0,-45,-12.5],"origin":[10.5338,38.80625,2.8063],"faces":{"north":{"uv":[118,28,119,29],"texture":0},"east":{"uv":[29,118,30,119],"texture":0},"south":{"uv":[118,29,119,30],"texture":0},"west":{"uv":[30,118,31,119],"texture":0},"up":{"uv":[119,31,118,30],"texture":0},"down":{"uv":[32,118,31,119],"texture":0}},"type":"cube","uuid":"22bbe239-0026-6040-7756-3fa9d0c984b8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.91742,35.25,-0.15232],"to":[-7.41742,37.45,5.94768],"autouv":0,"color":6,"origin":[-8.51742,34.15,2.34768],"faces":{"north":{"uv":[93,15,99,17],"texture":0},"east":{"uv":[94,31,100,33],"texture":0},"south":{"uv":[94,33,100,35],"texture":0},"west":{"uv":[49,94,55,96],"texture":0},"up":{"uv":[12,62,6,56],"texture":0},"down":{"uv":[18,56,12,62],"texture":0}},"type":"cube","uuid":"d603d704-1925-f44c-266d-72cc47243394"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.91742,33.05,-0.70232],"to":[-7.69242,35.25,6.49768],"autouv":0,"color":6,"origin":[-8.51742,31.95,2.34768],"faces":{"north":{"uv":[96,35,101,37],"texture":0},"east":{"uv":[88,42,95,44],"texture":0},"south":{"uv":[96,37,101,39],"texture":0},"west":{"uv":[88,44,95,46],"texture":0},"up":{"uv":[62,63,57,56],"texture":0},"down":{"uv":[23,58,18,65],"texture":0}},"type":"cube","uuid":"508cda1a-e3ba-807d-6286-d5d1cc9fb6e4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.91742,30.85,-1.25232],"to":[-7.96742,33.05,7.04768],"autouv":0,"color":6,"origin":[-8.51742,29.75,2.34768],"faces":{"north":{"uv":[96,39,101,41],"texture":0},"east":{"uv":[82,60,90,62],"texture":0},"south":{"uv":[96,46,101,48],"texture":0},"west":{"uv":[83,30,91,32],"texture":0},"up":{"uv":[36,60,31,52],"texture":0},"down":{"uv":[41,52,36,60],"texture":0}},"type":"cube","uuid":"54c37abf-dbde-9f65-0956-990938e33023"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.6088,37.2,0.6688],"to":[-8.2088,38.575,5.0688],"autouv":0,"color":6,"rotation":[0,45,-7.5],"origin":[-10.5338,38.80625,2.8063],"faces":{"north":{"uv":[114,23,118,24],"texture":0},"east":{"uv":[114,29,118,30],"texture":0},"south":{"uv":[114,30,118,31],"texture":0},"west":{"uv":[114,34,118,35],"texture":0},"up":{"uv":[76,87,72,83],"texture":0},"down":{"uv":[80,83,76,87],"texture":0}},"type":"cube","uuid":"722703c7-5013-9ab7-215d-48e7c06ff787"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.21742,29.45,0.97268],"to":[-9.81742,30.825,5.37268],"autouv":0,"color":6,"rotation":[0,45,0],"origin":[-12.01742,31.1,2.07268],"faces":{"north":{"uv":[37,114,41,115],"texture":0},"east":{"uv":[44,114,48,115],"texture":0},"south":{"uv":[114,45,118,46],"texture":0},"west":{"uv":[114,46,118,47],"texture":0},"up":{"uv":[84,87,80,83],"texture":0},"down":{"uv":[4,84,0,88],"texture":0}},"type":"cube","uuid":"ef1ded35-14f3-8696-879a-601eae597e63"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.7838,38.575,1.4938],"to":[-9.0338,38.875,4.2438],"autouv":0,"color":6,"rotation":[0,45,-7.5],"origin":[-10.5338,38.80625,2.8063],"faces":{"north":{"uv":[59,116,62,117],"texture":0},"east":{"uv":[116,60,119,61],"texture":0},"south":{"uv":[116,61,119,62],"texture":0},"west":{"uv":[62,116,65,117],"texture":0},"up":{"uv":[35,103,32,100],"texture":0},"down":{"uv":[103,32,100,35],"texture":0}},"type":"cube","uuid":"ef81dede-8278-e312-7aad-6a074c2d8d48"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.2838,38.575,1.9938],"to":[-9.7838,39.375,3.4938],"autouv":0,"color":6,"rotation":[0,45,2.5],"origin":[-10.5338,38.80625,2.8063],"faces":{"north":{"uv":[117,55,119,56],"texture":0},"east":{"uv":[61,117,63,118],"texture":0},"south":{"uv":[63,117,65,118],"texture":0},"west":{"uv":[68,117,70,118],"texture":0},"up":{"uv":[28,116,26,114],"texture":0},"down":{"uv":[36,114,34,116],"texture":0}},"type":"cube","uuid":"de921633-70f2-da2a-1f66-363321a64837"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.1588,39.175,2.1188],"to":[-9.7838,39.975,3.4938],"autouv":0,"color":6,"rotation":[0,45,12.5],"origin":[-10.5338,38.80625,2.8063],"faces":{"north":{"uv":[119,93,120,94],"texture":0},"east":{"uv":[94,119,95,120],"texture":0},"south":{"uv":[119,94,120,95],"texture":0},"west":{"uv":[95,119,96,120],"texture":0},"up":{"uv":[120,96,119,95],"texture":0},"down":{"uv":[97,119,96,120],"texture":0}},"type":"cube","uuid":"845173dc-0d83-f68f-e654-bf3ca32bf352"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.63951,31.95,-0.83633],"to":[-13.43951,34.425,1.96367],"autouv":0,"color":6,"rotation":[0,-22.5,0],"origin":[-13.89367,33.7375,2.8875],"faces":{"north":{"uv":[114,47,116,49],"texture":0},"east":{"uv":[109,15,112,17],"texture":0},"south":{"uv":[49,114,51,116],"texture":0},"west":{"uv":[109,20,112,22],"texture":0},"up":{"uv":[28,112,26,109],"texture":0},"down":{"uv":[111,26,109,29],"texture":0}},"type":"cube","uuid":"e006de73-92a6-ca4a-8d6d-4690f7579d0c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.08951,34.0675,3.81133],"to":[-12.88951,36.5425,6.06133],"autouv":0,"color":6,"rotation":[0,22.5,-12.5],"origin":[-13.34367,35.855,2.8875],"faces":{"north":{"uv":[51,114,53,116],"texture":0},"east":{"uv":[53,114,55,116],"texture":0},"south":{"uv":[59,114,61,116],"texture":0},"west":{"uv":[114,60,116,62],"texture":0},"up":{"uv":[63,116,61,114],"texture":0},"down":{"uv":[116,62,114,64],"texture":0}},"type":"cube","uuid":"fdb96169-284e-3bbd-53d1-03b4d5fb6897"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.84242,34.0675,1.24768],"to":[-12.36742,36.5425,4.54768],"autouv":0,"color":6,"rotation":[0,0,-12.5],"origin":[-13.34367,35.855,2.8875],"faces":{"north":{"uv":[63,114,65,116],"texture":0},"east":{"uv":[28,109,31,111],"texture":0},"south":{"uv":[65,114,67,116],"texture":0},"west":{"uv":[109,29,112,31],"texture":0},"up":{"uv":[111,34,109,31],"texture":0},"down":{"uv":[42,109,40,112],"texture":0}},"type":"cube","uuid":"7fd3fcca-9ba4-2adc-2762-35fde297b11c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.08951,34.0675,-0.28633],"to":[-12.88951,36.5425,1.96367],"autouv":0,"color":6,"rotation":[0,-22.5,-12.5],"origin":[-13.34367,35.855,2.8875],"faces":{"north":{"uv":[73,114,75,116],"texture":0},"east":{"uv":[114,97,116,99],"texture":0},"south":{"uv":[104,114,106,116],"texture":0},"west":{"uv":[106,114,108,116],"texture":0},"up":{"uv":[113,116,111,114],"texture":0},"down":{"uv":[117,3,115,5],"texture":0}},"type":"cube","uuid":"3a0e61a6-2d75-adaa-7062-4259da88a4e0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.00742,36.845,2.45768],"to":[-13.90742,37.67,3.33768],"autouv":0,"color":6,"rotation":[0,0,-25],"origin":[-12.40867,37.8075,2.8875],"faces":{"north":{"uv":[119,96,120,97],"texture":0},"east":{"uv":[97,119,98,120],"texture":0},"south":{"uv":[119,97,120,98],"texture":0},"west":{"uv":[98,119,99,120],"texture":0},"up":{"uv":[120,99,119,98],"texture":0},"down":{"uv":[100,119,99,120],"texture":0}},"type":"cube","uuid":"91149954-6324-2c6c-09e2-731c212abb72"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.15451,36.02,3.81133],"to":[-11.95451,38.495,5.26133],"autouv":0,"color":6,"rotation":[0,22.5,-25],"origin":[-12.40867,37.8075,2.8875],"faces":{"north":{"uv":[5,115,7,117],"texture":0},"east":{"uv":[70,117,71,119],"texture":0},"south":{"uv":[7,115,9,117],"texture":0},"west":{"uv":[117,74,118,76],"texture":0},"up":{"uv":[119,77,117,76],"texture":0},"down":{"uv":[119,77,117,78],"texture":0}},"type":"cube","uuid":"b8894893-a530-25de-6c35-cbd6b147abe3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.90742,36.02,1.24768],"to":[-11.43242,38.495,4.54768],"autouv":0,"color":6,"rotation":[0,0,-25],"origin":[-12.40867,37.8075,2.8875],"faces":{"north":{"uv":[115,8,117,10],"texture":0},"east":{"uv":[42,109,45,111],"texture":0},"south":{"uv":[12,115,14,117],"texture":0},"west":{"uv":[109,46,112,48],"texture":0},"up":{"uv":[47,112,45,109],"texture":0},"down":{"uv":[56,109,54,112],"texture":0}},"type":"cube","uuid":"58b6c4b7-3e97-b3c7-1227-a0ed6ed79a99"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.15451,36.02,0.51367],"to":[-11.95451,38.495,1.96367],"autouv":0,"color":6,"rotation":[0,-22.5,-25],"origin":[-12.40867,37.8075,2.8875],"faces":{"north":{"uv":[17,115,19,117],"texture":0},"east":{"uv":[75,117,76,119],"texture":0},"south":{"uv":[22,115,24,117],"texture":0},"west":{"uv":[117,78,118,80],"texture":0},"up":{"uv":[83,118,81,117],"texture":0},"down":{"uv":[91,117,89,118],"texture":0}},"type":"cube","uuid":"df480e42-eb3d-2448-bd27-bfe5d6b68cc1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.39242,31.95,1.24768],"to":[-12.91742,34.425,4.54768],"autouv":0,"color":6,"origin":[-13.89367,33.7375,2.8875],"faces":{"north":{"uv":[24,115,26,117],"texture":0},"east":{"uv":[109,48,112,50],"texture":0},"south":{"uv":[28,115,30,117],"texture":0},"west":{"uv":[56,109,59,111],"texture":0},"up":{"uv":[111,60,109,57],"texture":0},"down":{"uv":[61,109,59,112],"texture":0}},"type":"cube","uuid":"dad8a33f-b4fd-6f3e-6c94-b04e0b7fb511"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-16.49242,32.775,2.45768],"to":[-15.39242,33.6,3.33768],"autouv":0,"color":6,"origin":[-13.89367,33.7375,2.8875],"faces":{"north":{"uv":[119,99,120,100],"texture":0},"east":{"uv":[100,119,101,120],"texture":0},"south":{"uv":[119,100,120,101],"texture":0},"west":{"uv":[101,119,102,120],"texture":0},"up":{"uv":[120,102,119,101],"texture":0},"down":{"uv":[103,119,102,120],"texture":0}},"type":"cube","uuid":"7187327e-ad38-ae9f-8f99-2ad9529ac9a3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.63951,31.95,3.81133],"to":[-13.43951,34.425,6.61133],"autouv":0,"color":6,"rotation":[0,22.5,0],"origin":[-13.89367,33.7375,2.8875],"faces":{"north":{"uv":[30,115,32,117],"texture":0},"east":{"uv":[109,60,112,62],"texture":0},"south":{"uv":[32,115,34,117],"texture":0},"west":{"uv":[109,62,112,64],"texture":0},"up":{"uv":[66,112,64,109],"texture":0},"down":{"uv":[68,109,66,112],"texture":0}},"type":"cube","uuid":"f877ff54-6c50-4e2a-11c0-e426a27ef36c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.36451,29.75,-1.38633],"to":[-10.96451,32.225,1.96367],"autouv":0,"color":6,"rotation":[0,-22.5,15],"origin":[-13.61867,31.5375,2.8875],"faces":{"north":{"uv":[102,89,106,91],"texture":0},"east":{"uv":[68,109,71,111],"texture":0},"south":{"uv":[90,102,94,104],"texture":0},"west":{"uv":[71,109,74,111],"texture":0},"up":{"uv":[98,57,94,54],"texture":0},"down":{"uv":[68,94,64,97],"texture":0}},"type":"cube","uuid":"59c92544-ec84-5abb-6d63-a1d70a6d7b06"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.11742,29.75,1.24768],"to":[-12.64242,32.225,4.54768],"autouv":0,"color":6,"rotation":[0,0,15],"origin":[-13.61867,31.5375,2.8875],"faces":{"north":{"uv":[37,115,39,117],"texture":0},"east":{"uv":[109,85,112,87],"texture":0},"south":{"uv":[115,37,117,39],"texture":0},"west":{"uv":[87,109,90,111],"texture":0},"up":{"uv":[76,112,74,109],"texture":0},"down":{"uv":[111,87,109,90],"texture":0}},"type":"cube","uuid":"9ec2de2a-8f71-a7e2-674e-8d9e602defde"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.36451,29.75,3.81133],"to":[-10.96451,32.225,7.16133],"autouv":0,"color":6,"rotation":[0,22.5,15],"origin":[-13.61867,31.5375,2.8875],"faces":{"north":{"uv":[94,102,98,104],"texture":0},"east":{"uv":[109,93,112,95],"texture":0},"south":{"uv":[102,94,106,96],"texture":0},"west":{"uv":[109,95,112,97],"texture":0},"up":{"uv":[98,70,94,67],"texture":0},"down":{"uv":[72,94,68,97],"texture":0}},"type":"cube","uuid":"ec32ecb8-e42d-396e-9431-867be23ad41f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.61451,28,3.81133],"to":[-10.21451,30.475,6.66133],"autouv":0,"color":6,"rotation":[0,22.5,7.5],"origin":[-12.86867,29.7875,2.8875],"faces":{"north":{"uv":[102,96,106,98],"texture":0},"east":{"uv":[109,97,112,99],"texture":0},"south":{"uv":[102,98,106,100],"texture":0},"west":{"uv":[101,109,104,111],"texture":0},"up":{"uv":[76,97,72,94],"texture":0},"down":{"uv":[98,76,94,79],"texture":0}},"type":"cube","uuid":"e25b8ff0-483c-0dbb-7459-b417b0e56469"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.36742,28,1.24768],"to":[-11.89242,30.475,4.54768],"autouv":0,"color":6,"rotation":[0,0,7.5],"origin":[-12.86867,29.7875,2.8875],"faces":{"north":{"uv":[39,115,41,117],"texture":0},"east":{"uv":[109,104,112,106],"texture":0},"south":{"uv":[115,39,117,41],"texture":0},"west":{"uv":[109,106,112,108],"texture":0},"up":{"uv":[111,111,109,108],"texture":0},"down":{"uv":[112,0,110,3],"texture":0}},"type":"cube","uuid":"45704531-8633-32b8-8a22-59a1514f7b9b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.61451,28,-0.88633],"to":[-10.21451,30.475,1.96367],"autouv":0,"color":6,"rotation":[0,-22.5,7.5],"origin":[-12.86867,29.7875,2.8875],"faces":{"north":{"uv":[101,102,105,104],"texture":0},"east":{"uv":[2,110,5,112],"texture":0},"south":{"uv":[103,0,107,2],"texture":0},"west":{"uv":[110,3,113,5],"texture":0},"up":{"uv":[98,82,94,79],"texture":0},"down":{"uv":[98,82,94,85],"texture":0}},"type":"cube","uuid":"50a5b6d1-8c29-200c-2a12-ba217ea71245"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.21742,21.95,1.72268],"to":[-10.31742,22.95,4.62268],"autouv":0,"color":6,"rotation":[0,45,0],"origin":[-11.76742,25.35,2.07268],"faces":{"north":{"uv":[116,62,119,63],"texture":0},"east":{"uv":[116,63,119,64],"texture":0},"south":{"uv":[116,66,119,67],"texture":0},"west":{"uv":[116,67,119,68],"texture":0},"up":{"uv":[38,103,35,100],"texture":0},"down":{"uv":[41,100,38,103],"texture":0}},"type":"cube","uuid":"fdddcb40-2248-3a33-3221-dfca397c82fc"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.71742,22.95,1.22268],"to":[-9.81742,23.95,5.12268],"autouv":0,"color":6,"rotation":[0,45,0],"origin":[-11.76742,25.35,2.07268],"faces":{"north":{"uv":[114,49,118,50],"texture":0},"east":{"uv":[114,64,118,65],"texture":0},"south":{"uv":[114,84,118,85],"texture":0},"west":{"uv":[114,90,118,91],"texture":0},"up":{"uv":[88,4,84,0],"texture":0},"down":{"uv":[10,84,6,88],"texture":0}},"type":"cube","uuid":"c44306cd-d399-8e82-5bd3-e47320ea1792"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.21742,23.95,1.72268],"to":[-10.31742,24.95,4.62268],"autouv":0,"color":6,"rotation":[0,45,0],"origin":[-11.76742,25.35,2.07268],"faces":{"north":{"uv":[68,116,71,117],"texture":0},"east":{"uv":[116,68,119,69],"texture":0},"south":{"uv":[116,70,119,71],"texture":0},"west":{"uv":[116,71,119,72],"texture":0},"up":{"uv":[103,45,100,42],"texture":0},"down":{"uv":[47,100,44,103],"texture":0}},"type":"cube","uuid":"718c9191-dce2-cf36-c73b-b759257f64f4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.71742,24.95,1.22268],"to":[-9.81742,29.825,5.12268],"autouv":0,"color":6,"rotation":[0,45,0],"origin":[-11.76742,28.1,2.07268],"faces":{"north":{"uv":[76,4,80,9],"texture":0},"east":{"uv":[12,76,16,81],"texture":0},"south":{"uv":[52,76,56,81],"texture":0},"west":{"uv":[76,54,80,59],"texture":0},"up":{"uv":[20,88,16,84],"texture":0},"down":{"uv":[24,84,20,88],"texture":0}},"type":"cube","uuid":"2d700834-8646-abd2-38c7-074d7eff5448"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.11451,26.25,3.81133],"to":[-9.71451,28.725,5.66133],"autouv":0,"color":6,"rotation":[0,22.5,0],"origin":[-12.36867,28.0375,2.8875],"faces":{"north":{"uv":[2,103,6,105],"texture":0},"east":{"uv":[115,41,117,43],"texture":0},"south":{"uv":[103,2,107,4],"texture":0},"west":{"uv":[42,115,44,117],"texture":0},"up":{"uv":[107,6,103,4],"texture":0},"down":{"uv":[10,103,6,105],"texture":0}},"type":"cube","uuid":"9c60e29d-b8ef-986c-fb03-327be81416a6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.96742,27.075,2.45768],"to":[-13.86742,27.9,3.33768],"autouv":0,"color":6,"origin":[-12.36867,28.0375,2.8875],"faces":{"north":{"uv":[119,102,120,103],"texture":0},"east":{"uv":[103,119,104,120],"texture":0},"south":{"uv":[119,103,120,104],"texture":0},"west":{"uv":[104,119,105,120],"texture":0},"up":{"uv":[120,105,119,104],"texture":0},"down":{"uv":[106,119,105,120],"texture":0}},"type":"cube","uuid":"62206ed9-99dd-90ed-2d52-cd55447fb334"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.86742,26.25,1.24768],"to":[-11.39242,28.725,4.54768],"autouv":0,"color":6,"origin":[-12.36867,28.0375,2.8875],"faces":{"north":{"uv":[44,115,46,117],"texture":0},"east":{"uv":[110,8,113,10],"texture":0},"south":{"uv":[46,115,48,117],"texture":0},"west":{"uv":[110,10,113,12],"texture":0},"up":{"uv":[112,15,110,12],"texture":0},"down":{"uv":[19,110,17,113],"texture":0}},"type":"cube","uuid":"f004b9df-bebc-3520-d00f-ae5b0be298c0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-14.11451,26.25,0.11367],"to":[-9.71451,28.725,1.96367],"autouv":0,"color":6,"rotation":[0,-22.5,0],"origin":[-12.36867,28.0375,2.8875],"faces":{"north":{"uv":[103,9,107,11],"texture":0},"east":{"uv":[115,50,117,52],"texture":0},"south":{"uv":[103,11,107,13],"texture":0},"west":{"uv":[115,52,117,54],"texture":0},"up":{"uv":[107,15,103,13],"texture":0},"down":{"uv":[18,103,14,105],"texture":0}},"type":"cube","uuid":"60f5e47b-26aa-497f-5a67-2da702d98cb6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.71742,20.95,2.22268],"to":[-10.81742,21.95,4.12268],"autouv":0,"color":6,"rotation":[0,45,0],"origin":[-11.76742,25.35,2.07268],"faces":{"north":{"uv":[91,117,93,118],"texture":0},"east":{"uv":[95,117,97,118],"texture":0},"south":{"uv":[117,99,119,100],"texture":0},"west":{"uv":[104,117,106,118],"texture":0},"up":{"uv":[117,56,115,54],"texture":0},"down":{"uv":[57,115,55,117],"texture":0}},"type":"cube","uuid":"a4707f71-4a31-7c53-b88e-7483a893a6e3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.46742,18.95,1.27268],"to":[-11.31742,19.95,4.47268],"autouv":0,"color":6,"origin":[-11.76742,23.35,2.07268],"faces":{"north":{"uv":[119,105,120,106],"texture":0},"east":{"uv":[116,72,119,73],"texture":0},"south":{"uv":[106,119,107,120],"texture":0},"west":{"uv":[116,73,119,74],"texture":0},"up":{"uv":[16,119,15,116],"texture":0},"down":{"uv":[17,116,16,119],"texture":0}},"type":"cube","uuid":"47b56504-f1d3-9686-415e-a4f0709dfafd"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.66742,19.95,1.27268],"to":[-10.51742,21.95,4.47268],"autouv":0,"color":6,"rotation":[0,0,-45],"origin":[-11.89242,20.95,2.87268],"faces":{"north":{"uv":[83,117,84,119],"texture":0},"east":{"uv":[110,17,113,19],"texture":0},"south":{"uv":[86,117,87,119],"texture":0},"west":{"uv":[19,110,22,112],"texture":0},"up":{"uv":[27,119,26,116],"texture":0},"down":{"uv":[28,116,27,119],"texture":0}},"type":"cube","uuid":"5ceffcd8-205b-97a8-bfeb-be8bbcb0cfca"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.21742,18.65,3.77268],"to":[-11.56742,19.45,4.47268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[119,106,120,107],"texture":0},"east":{"uv":[107,119,108,120],"texture":0},"south":{"uv":[119,107,120,108],"texture":0},"west":{"uv":[108,119,109,120],"texture":0},"up":{"uv":[120,109,119,108],"texture":0},"down":{"uv":[110,119,109,120],"texture":0}},"type":"cube","uuid":"63ad2ccc-25ba-c115-dbf5-20ab848089b9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.22992,18.7125,1.37268],"to":[-11.57992,19.8125,2.07268],"autouv":0,"color":6,"rotation":[0,0,135],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[119,109,120,110],"texture":0},"east":{"uv":[110,119,111,120],"texture":0},"south":{"uv":[119,110,120,111],"texture":0},"west":{"uv":[111,119,112,120],"texture":0},"up":{"uv":[120,112,119,111],"texture":0},"down":{"uv":[113,119,112,120],"texture":0}},"type":"cube","uuid":"8acf7fca-4399-fc24-c932-72bf5ca8da68"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.22992,18.5125,2.17268],"to":[-11.57992,19.7125,2.87268],"autouv":0,"color":6,"rotation":[0,0,135],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[119,112,120,113],"texture":0},"east":{"uv":[113,119,114,120],"texture":0},"south":{"uv":[119,113,120,114],"texture":0},"west":{"uv":[114,119,115,120],"texture":0},"up":{"uv":[120,115,119,114],"texture":0},"down":{"uv":[116,119,115,120],"texture":0}},"type":"cube","uuid":"8d0f979c-d198-2689-ad2d-5351f49c68a7"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.22992,18.5125,2.97268],"to":[-11.57992,19.7125,3.67268],"autouv":0,"color":6,"rotation":[0,0,135],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[119,115,120,116],"texture":0},"east":{"uv":[116,119,117,120],"texture":0},"south":{"uv":[119,116,120,117],"texture":0},"west":{"uv":[117,119,118,120],"texture":0},"up":{"uv":[120,118,119,117],"texture":0},"down":{"uv":[119,119,118,120],"texture":0}},"type":"cube","uuid":"1779dea4-7ee2-5b9f-985c-8616cbc88867"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.22992,18.7125,3.77268],"to":[-11.57992,19.8125,4.47268],"autouv":0,"color":6,"rotation":[0,0,135],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[119,118,120,119],"texture":0},"east":{"uv":[119,119,120,120],"texture":0},"south":{"uv":[0,120,1,121],"texture":0},"west":{"uv":[120,0,121,1],"texture":0},"up":{"uv":[2,121,1,120],"texture":0},"down":{"uv":[121,1,120,2],"texture":0}},"type":"cube","uuid":"939e2423-ada1-5d47-2c16-e0f6e92b53dd"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.21742,18.55,2.17268],"to":[-11.56742,19.45,2.87268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[2,120,3,121],"texture":0},"east":{"uv":[120,2,121,3],"texture":0},"south":{"uv":[3,120,4,121],"texture":0},"west":{"uv":[120,3,121,4],"texture":0},"up":{"uv":[5,121,4,120],"texture":0},"down":{"uv":[121,4,120,5],"texture":0}},"type":"cube","uuid":"4ca72344-81c5-e89d-19f4-dda145e7d3ae"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.01742,18.15,3.77268],"to":[-10.36742,18.95,4.47268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[5,120,6,121],"texture":0},"east":{"uv":[120,5,121,6],"texture":0},"south":{"uv":[6,120,7,121],"texture":0},"west":{"uv":[120,6,121,7],"texture":0},"up":{"uv":[8,121,7,120],"texture":0},"down":{"uv":[121,7,120,8],"texture":0}},"type":"cube","uuid":"f0e925fc-4f13-150a-775c-1c5d29164e3c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.01742,18.15,1.37268],"to":[-10.36742,18.95,2.07268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[8,120,9,121],"texture":0},"east":{"uv":[120,8,121,9],"texture":0},"south":{"uv":[9,120,10,121],"texture":0},"west":{"uv":[120,9,121,10],"texture":0},"up":{"uv":[11,121,10,120],"texture":0},"down":{"uv":[121,10,120,11],"texture":0}},"type":"cube","uuid":"0bbbbb03-7bcd-305b-ca5e-f058b7ff5bb4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.01742,18.05,2.97268],"to":[-10.36742,18.95,3.67268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[11,120,12,121],"texture":0},"east":{"uv":[120,11,121,12],"texture":0},"south":{"uv":[12,120,13,121],"texture":0},"west":{"uv":[120,12,121,13],"texture":0},"up":{"uv":[14,121,13,120],"texture":0},"down":{"uv":[121,13,120,14],"texture":0}},"type":"cube","uuid":"fdad1047-07ba-c7f6-deb1-005943d3b357"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.01742,18.05,2.17268],"to":[-10.36742,18.95,2.87268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[14,120,15,121],"texture":0},"east":{"uv":[120,14,121,15],"texture":0},"south":{"uv":[15,120,16,121],"texture":0},"west":{"uv":[120,15,121,16],"texture":0},"up":{"uv":[17,121,16,120],"texture":0},"down":{"uv":[121,16,120,17],"texture":0}},"type":"cube","uuid":"63e86aa5-0394-218d-e0f8-9ed00cbd7b9d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.21742,18.55,2.97268],"to":[-11.56742,19.45,3.67268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[17,120,18,121],"texture":0},"east":{"uv":[120,17,121,18],"texture":0},"south":{"uv":[18,120,19,121],"texture":0},"west":{"uv":[120,18,121,19],"texture":0},"up":{"uv":[20,121,19,120],"texture":0},"down":{"uv":[121,19,120,20],"texture":0}},"type":"cube","uuid":"dc122ad0-1ea8-4da9-420f-0e2874ed1f0d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-12.21742,18.65,1.37268],"to":[-11.56742,19.45,2.07268],"autouv":0,"color":6,"rotation":[0,0,45],"origin":[-11.29242,18.9375,3.32268],"faces":{"north":{"uv":[20,120,21,121],"texture":0},"east":{"uv":[120,20,121,21],"texture":0},"south":{"uv":[21,120,22,121],"texture":0},"west":{"uv":[120,21,121,22],"texture":0},"up":{"uv":[23,121,22,120],"texture":0},"down":{"uv":[121,22,120,23],"texture":0}},"type":"cube","uuid":"3e61372b-b54f-9344-36e1-5186b613c788"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.075,1.5,0.725],"to":[5.125,7.25,3.775],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[3.6,9.725,2.25],"faces":{"north":{"uv":[77,62,80,68],"texture":0},"east":{"uv":[77,68,80,74],"texture":0},"south":{"uv":[6,78,9,84],"texture":0},"west":{"uv":[9,78,12,84],"texture":0},"up":{"uv":[55,102,52,99],"texture":0},"down":{"uv":[102,57,99,60],"texture":0}},"type":"cube","uuid":"0646db02-a200-9b1f-229c-08c15715b008"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.825,0,-2.025],"to":[4.325,1.5,0.5],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[3.6,11.725,-1.275],"faces":{"north":{"uv":[107,13,110,15],"texture":0},"east":{"uv":[15,107,18,109],"texture":0},"south":{"uv":[107,17,110,19],"texture":0},"west":{"uv":[26,107,29,109],"texture":0},"up":{"uv":[102,63,99,60],"texture":0},"down":{"uv":[82,99,79,102],"texture":0}},"type":"cube","uuid":"f3622b46-ec20-0528-a9a7-46b0dd0cbc5b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.325,1.5,-0.275],"to":[4.875,2.275,2.275],"autouv":0,"color":0,"origin":[3.6,13.725,0],"faces":{"north":{"uv":[115,88,118,89],"texture":0},"east":{"uv":[115,89,118,90],"texture":0},"south":{"uv":[90,115,93,116],"texture":0},"west":{"uv":[115,91,118,92],"texture":0},"up":{"uv":[85,102,82,99],"texture":0},"down":{"uv":[88,99,85,102],"texture":0}},"type":"cube","uuid":"95ac3da4-49bd-6f19-ca05-4972ba65cb16"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.6,0.775,-0.275],"to":[5.9,2.4,3.275],"autouv":0,"color":0,"rotation":[0,0,-22.5],"origin":[5.1,1.975,1.5],"faces":{"north":{"uv":[117,8,118,10],"texture":0},"east":{"uv":[101,35,105,37],"texture":0},"south":{"uv":[117,10,118,12],"texture":0},"west":{"uv":[101,37,105,39],"texture":0},"up":{"uv":[90,115,89,111],"texture":0},"down":{"uv":[100,111,99,115],"texture":0}},"type":"cube","uuid":"4a69a736-4e03-2708-8e12-d859066dde6b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.825,0,-0.525],"to":[5.375,1.5,4.525],"autouv":0,"color":0,"origin":[3.6,11.725,2.25],"faces":{"north":{"uv":[101,6,105,8],"texture":0},"east":{"uv":[55,95,60,97],"texture":0},"south":{"uv":[101,17,105,19],"texture":0},"west":{"uv":[95,72,100,74],"texture":0},"up":{"uv":[77,44,73,39],"texture":0},"down":{"uv":[44,73,40,78],"texture":0}},"type":"cube","uuid":"a4f58cf6-bc79-dd90-4c51-6ecad2650c26"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.825,1.925,2.975],"to":[5.375,3.375,5.175],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[3.6,2.5,5.5],"faces":{"north":{"uv":[112,95,116,96],"texture":0},"east":{"uv":[116,113,118,114],"texture":0},"south":{"uv":[112,96,116,97],"texture":0},"west":{"uv":[7,117,9,118],"texture":0},"up":{"uv":[105,21,101,19],"texture":0},"down":{"uv":[105,24,101,26],"texture":0}},"type":"cube","uuid":"67aa1f25-afaf-291e-ac2e-8431eb1b480a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.3,0.775,-0.275],"to":[2.6,2.4,3.275],"autouv":0,"color":0,"rotation":[0,0,22.5],"origin":[2.1,1.975,1.5],"faces":{"north":{"uv":[12,117,13,119],"texture":0},"east":{"uv":[101,39,105,41],"texture":0},"south":{"uv":[13,117,14,119],"texture":0},"west":{"uv":[101,45,105,47],"texture":0},"up":{"uv":[104,115,103,111],"texture":0},"down":{"uv":[5,112,4,116],"texture":0}},"type":"cube","uuid":"b70e23c8-c0f5-91d4-399f-6351986144cd"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.25,0,1.05],"to":[4.175,0.5,4.725],"autouv":0,"color":0,"rotation":[0,-22.5,0],"origin":[2.525,0.75,3.575],"faces":{"north":{"uv":[83,115,86,116],"texture":0},"east":{"uv":[112,85,116,86],"texture":0},"south":{"uv":[115,83,118,84],"texture":0},"west":{"uv":[112,86,116,87],"texture":0},"up":{"uv":[43,95,40,91],"texture":0},"down":{"uv":[49,91,46,95],"texture":0}},"type":"cube","uuid":"513276dc-9049-753a-13bf-2a5cf4f197ab"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.05,0,1.05],"to":[5.975,0.5,4.725],"autouv":0,"color":0,"rotation":[0,22.5,0],"origin":[4.7,0.75,3.575],"faces":{"north":{"uv":[87,115,90,116],"texture":0},"east":{"uv":[112,93,116,94],"texture":0},"south":{"uv":[115,87,118,88],"texture":0},"west":{"uv":[112,94,116,95],"texture":0},"up":{"uv":[58,95,55,91],"texture":0},"down":{"uv":[94,55,91,59],"texture":0}},"type":"cube","uuid":"54e9970f-49f9-29a9-04d1-2c8e7099bf55"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.125,5.125,1.775],"to":[6.075,7.9,2.725],"autouv":0,"color":0,"rotation":[0,0,-22.5],"origin":[4.6,6.925,2.25],"faces":{"north":{"uv":[98,68,101,71],"texture":0},"east":{"uv":[115,66,116,69],"texture":0},"south":{"uv":[76,98,79,101],"texture":0},"west":{"uv":[67,115,68,118],"texture":0},"up":{"uv":[71,116,68,115],"texture":0},"down":{"uv":[118,69,115,70],"texture":0}},"type":"cube","uuid":"bbfd3b01-4e82-5915-0284-5ec05be23f5a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.125,5.125,1.775],"to":[4.075,7.9,2.725],"autouv":0,"color":0,"rotation":[0,0,22.5],"origin":[2.6,6.925,2.25],"faces":{"north":{"uv":[99,48,102,51],"texture":0},"east":{"uv":[79,115,80,118],"texture":0},"south":{"uv":[49,99,52,102],"texture":0},"west":{"uv":[115,79,116,82],"texture":0},"up":{"uv":[83,116,80,115],"texture":0},"down":{"uv":[118,82,115,83],"texture":0}},"type":"cube","uuid":"0775f99c-e0db-1022-ceb4-58e90e3c33f4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.125,4.75,-0.15],"to":[4.075,7.525,2.8],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[4.6,6.55,0.325],"faces":{"north":{"uv":[115,70,116,73],"texture":0},"east":{"uv":[98,76,101,79],"texture":0},"south":{"uv":[71,115,72,118],"texture":0},"west":{"uv":[98,79,101,82],"texture":0},"up":{"uv":[73,118,72,115],"texture":0},"down":{"uv":[116,73,115,76],"texture":0}},"type":"cube","uuid":"7e816966-7fdf-ee4e-f702-6982983edc0a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.125,4.75,1.7],"to":[4.075,7.525,4.65],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[4.6,6.55,4.175],"faces":{"north":{"uv":[76,115,77,118],"texture":0},"east":{"uv":[98,82,101,85],"texture":0},"south":{"uv":[115,76,116,79],"texture":0},"west":{"uv":[90,98,93,101],"texture":0},"up":{"uv":[78,118,77,115],"texture":0},"down":{"uv":[79,115,78,118],"texture":0}},"type":"cube","uuid":"e85ce7b0-711b-5558-279c-95322808712b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.4,12.65,0.05],"to":[5.8,14,4.45],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[3.6,12.875,2.25],"faces":{"north":{"uv":[111,89,115,90],"texture":0},"east":{"uv":[112,2,116,3],"texture":0},"south":{"uv":[112,16,116,17],"texture":0},"west":{"uv":[112,22,116,23],"texture":0},"up":{"uv":[35,84,31,80],"texture":0},"down":{"uv":[39,80,35,84],"texture":0}},"type":"cube","uuid":"a6d01e53-e044-783f-db4d-4fafd64dcc16"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.15,14,-0.2],"to":[6.05,15.35,4.7],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[3.6,14.225,2.25],"faces":{"north":{"uv":[110,40,115,41],"texture":0},"east":{"uv":[110,41,115,42],"texture":0},"south":{"uv":[110,42,115,43],"texture":0},"west":{"uv":[110,50,115,51],"texture":0},"up":{"uv":[72,36,67,31],"texture":0},"down":{"uv":[72,55,67,60],"texture":0}},"type":"cube","uuid":"5c46d5ab-7804-c02b-767c-a261e30d4ed5"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.325,7.9,0.125],"to":[4.875,9.65,2.35],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[3.6,7.625,0.325],"faces":{"north":{"uv":[106,106,109,108],"texture":0},"east":{"uv":[61,112,63,114],"texture":0},"south":{"uv":[107,0,110,2],"texture":0},"west":{"uv":[112,62,114,64],"texture":0},"up":{"uv":[110,4,107,2],"texture":0},"down":{"uv":[110,4,107,6],"texture":0}},"type":"cube","uuid":"0edb8eca-2de0-0bed-b9a2-1cb5665cb3e0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.325,7.9,2.15],"to":[4.875,9.65,4.375],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[3.6,7.625,4.175],"faces":{"north":{"uv":[6,107,9,109],"texture":0},"east":{"uv":[63,112,65,114],"texture":0},"south":{"uv":[107,9,110,11],"texture":0},"west":{"uv":[65,112,67,114],"texture":0},"up":{"uv":[110,13,107,11],"texture":0},"down":{"uv":[15,107,12,109],"texture":0}},"type":"cube","uuid":"c7656138-efa9-f632-69ce-d6c344f64e6d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4.9,16.6,1.3],"to":[6.3,18.7,3.2],"autouv":0,"color":0,"rotation":[0,0,-22.5],"origin":[3.6,15.575,2.25],"faces":{"north":{"uv":[2,117,3,119],"texture":0},"east":{"uv":[59,112,61,114],"texture":0},"south":{"uv":[117,3,118,5],"texture":0},"west":{"uv":[112,60,114,62],"texture":0},"up":{"uv":[6,119,5,117],"texture":0},"down":{"uv":[7,117,6,119],"texture":0}},"type":"cube","uuid":"2a01c2d2-7cd6-7b7b-2271-a69360c97bbb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[6.825,13.85,-0.5],"to":[6.825,18.1,5.5],"autouv":0,"color":1,"origin":[4.8,18.85,2.5],"faces":{"north":{"uv":[0,0,0,4],"texture":0},"east":{"uv":[70,5,76,9],"texture":0},"south":{"uv":[0,0,0,4],"texture":0},"west":{"uv":[70,9,76,5],"texture":0},"up":{"uv":[0,6,0,0],"texture":0},"down":{"uv":[0,0,0,6],"texture":0}},"type":"cube","uuid":"6e6bfdbd-594d-cad5-b4b0-7b3fe365077e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.825,13.85,-0.5],"to":[-6.825,18.1,5.5],"autouv":0,"color":1,"origin":[-4.8,18.85,2.5],"faces":{"north":{"uv":[0,0,0,4],"texture":0},"east":{"uv":[72,4,78,0],"texture":0},"south":{"uv":[0,0,0,4],"texture":0},"west":{"uv":[72,0,78,4],"texture":0},"up":{"uv":[0,6,0,0],"texture":0},"down":{"uv":[0,0,0,6],"texture":0}},"type":"cube","uuid":"cca29179-e0a1-fc3e-5ddd-31cbb7c1ec41"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.5,16.75,-0.75],"to":[5.5,18,5.75],"autouv":0,"color":1,"origin":[0.5,31.5,3.25],"faces":{"north":{"uv":[89,6,100,7],"texture":0},"east":{"uv":[101,41,108,42],"texture":0},"south":{"uv":[90,41,101,42],"texture":0},"west":{"uv":[102,23,109,24],"texture":0},"up":{"uv":[44,17,33,10],"texture":0},"down":{"uv":[44,17,33,24],"texture":0}},"type":"cube","uuid":"948e9537-9de2-64e9-517b-27eeca22ebff"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.5,13.75,-0.75],"to":[5.5,18,-0.75],"autouv":0,"color":1,"origin":[0.5,31.5,3.25],"faces":{"north":{"uv":[44,22,55,26],"texture":0},"east":{"uv":[0,0,0,4],"texture":0},"south":{"uv":[55,22,44,26],"texture":0},"west":{"uv":[0,0,0,4],"texture":0},"up":{"uv":[11,0,0,0],"texture":0},"down":{"uv":[11,0,0,0],"texture":0}},"type":"cube","uuid":"919f6ad0-5645-985f-2e89-4f499936da91"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.5,13.58882,5.75793],"to":[5.5,17.83882,5.75793],"autouv":0,"color":1,"origin":[0.5,31.33882,9.75793],"faces":{"north":{"uv":[59,34,48,38],"texture":0},"east":{"uv":[0,0,0,4],"texture":0},"south":{"uv":[48,34,59,38],"texture":0},"west":{"uv":[0,0,0,4],"texture":0},"up":{"uv":[11,0,0,0],"texture":0},"down":{"uv":[11,0,0,0],"texture":0}},"type":"cube","uuid":"9069559d-0e29-81b5-612a-496ee632e27a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.3,15.35,-0.45],"to":[-0.9,16.7,4.95],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[-3.6,15.575,2.25],"faces":{"north":{"uv":[110,73,115,74],"texture":0},"east":{"uv":[110,74,115,75],"texture":0},"south":{"uv":[110,75,115,76],"texture":0},"west":{"uv":[76,110,81,111],"texture":0},"up":{"uv":[73,25,68,20],"texture":0},"down":{"uv":[26,68,21,73],"texture":0}},"type":"cube","uuid":"1c3c5f85-a621-e9b2-0336-471cbcc6e18b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.3,16.6,1.3],"to":[-4.9,18.7,3.2],"autouv":0,"color":0,"rotation":[0,0,22.5],"origin":[-3.6,15.575,2.25],"faces":{"north":{"uv":[17,117,18,119],"texture":0},"east":{"uv":[74,112,76,114],"texture":0},"south":{"uv":[117,17,118,19],"texture":0},"west":{"uv":[112,97,114,99],"texture":0},"up":{"uv":[19,119,18,117],"texture":0},"down":{"uv":[20,117,19,119],"texture":0}},"type":"cube","uuid":"8a7a2efd-5ca7-a58c-d13c-a0c9d491963a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.8,12.65,0.05],"to":[-1.4,14,4.45],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[-3.6,12.875,2.25],"faces":{"north":{"uv":[112,104,116,105],"texture":0},"east":{"uv":[112,105,116,106],"texture":0},"south":{"uv":[112,106,116,107],"texture":0},"west":{"uv":[112,107,116,108],"texture":0},"up":{"uv":[56,85,52,81],"texture":0},"down":{"uv":[85,79,81,83],"texture":0}},"type":"cube","uuid":"20579d70-7fd9-9640-758b-9cb91bf8c8c2"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.05,14,-0.2],"to":[-1.15,15.35,4.7],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[-3.6,14.225,2.25],"faces":{"north":{"uv":[110,76,115,77],"texture":0},"east":{"uv":[110,77,115,78],"texture":0},"south":{"uv":[81,110,86,111],"texture":0},"west":{"uv":[110,91,115,92],"texture":0},"up":{"uv":[73,41,68,36],"texture":0},"down":{"uv":[73,41,68,46],"texture":0}},"type":"cube","uuid":"b81b6460-4ec1-dccd-76bd-c6aee50e3af8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.625,8.6,0.225],"to":[-1.575,12.65,4.275],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[-3.6,15.575,2.25],"faces":{"north":{"uv":[48,82,52,86],"texture":0},"east":{"uv":[56,82,60,86],"texture":0},"south":{"uv":[82,56,86,60],"texture":0},"west":{"uv":[60,82,64,86],"texture":0},"up":{"uv":[86,78,82,74],"texture":0},"down":{"uv":[87,12,83,16],"texture":0}},"type":"cube","uuid":"6a259483-478e-e732-e39f-95f07ae3adbf"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.875,7.9,0.125],"to":[-2.325,9.65,2.35],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[-3.6,7.625,0.325],"faces":{"north":{"uv":[29,107,32,109],"texture":0},"east":{"uv":[111,112,113,114],"texture":0},"south":{"uv":[40,107,43,109],"texture":0},"west":{"uv":[113,3,115,5],"texture":0},"up":{"uv":[46,109,43,107],"texture":0},"down":{"uv":[61,107,58,109],"texture":0}},"type":"cube","uuid":"0b9618b4-fc3f-7310-9d4d-e38f2fe541fe"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.875,7.9,2.15],"to":[-2.325,9.65,4.375],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[-3.6,7.625,4.175],"faces":{"north":{"uv":[64,107,67,109],"texture":0},"east":{"uv":[5,113,7,115],"texture":0},"south":{"uv":[67,107,70,109],"texture":0},"west":{"uv":[113,8,115,10],"texture":0},"up":{"uv":[73,109,70,107],"texture":0},"down":{"uv":[110,71,107,73],"texture":0}},"type":"cube","uuid":"aaff0afc-ccb6-ee65-cc65-691c3ab20d47"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.075,7.25,-0.225],"to":[-1.125,8.6,4.725],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[-3.6,15.575,2.25],"faces":{"north":{"uv":[110,92,115,93],"texture":0},"east":{"uv":[95,110,100,111],"texture":0},"south":{"uv":[111,6,116,7],"texture":0},"west":{"uv":[111,7,116,8],"texture":0},"up":{"uv":[48,73,43,68],"texture":0},"down":{"uv":[53,68,48,73],"texture":0}},"type":"cube","uuid":"30868f8a-27c4-db95-7b51-d3227b252417"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.075,5.125,1.775],"to":[-3.125,7.9,2.725],"autouv":0,"color":0,"rotation":[0,0,22.5],"origin":[-4.6,6.925,2.25],"faces":{"north":{"uv":[99,85,102,88],"texture":0},"east":{"uv":[93,115,94,118],"texture":0},"south":{"uv":[99,88,102,91],"texture":0},"west":{"uv":[94,115,95,118],"texture":0},"up":{"uv":[118,93,115,92],"texture":0},"down":{"uv":[98,115,95,116],"texture":0}},"type":"cube","uuid":"f32e0837-cf2b-2fd2-2d0a-a4f0b02f63f5"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.075,4.75,-0.15],"to":[-3.125,7.525,2.8],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[-4.6,6.55,0.325],"faces":{"north":{"uv":[98,115,99,118],"texture":0},"east":{"uv":[93,99,96,102],"texture":0},"south":{"uv":[99,115,100,118],"texture":0},"west":{"uv":[99,94,102,97],"texture":0},"up":{"uv":[116,103,115,100],"texture":0},"down":{"uv":[102,115,101,118],"texture":0}},"type":"cube","uuid":"b40b0cc3-9809-2839-b031-5124f77b3721"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.075,4.75,1.7],"to":[-3.125,7.525,4.65],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[-4.6,6.55,4.175],"faces":{"north":{"uv":[102,115,103,118],"texture":0},"east":{"uv":[96,99,99,102],"texture":0},"south":{"uv":[103,115,104,118],"texture":0},"west":{"uv":[99,97,102,100],"texture":0},"up":{"uv":[110,118,109,115],"texture":0},"down":{"uv":[111,115,110,118],"texture":0}},"type":"cube","uuid":"5f5bd476-ae04-dfcc-0637-40fddcbb54ba"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.075,5.125,1.775],"to":[-1.125,7.9,2.725],"autouv":0,"color":0,"rotation":[0,0,-22.5],"origin":[-2.6,6.925,2.25],"faces":{"north":{"uv":[100,0,103,3],"texture":0},"east":{"uv":[115,111,116,114],"texture":0},"south":{"uv":[3,100,6,103],"texture":0},"west":{"uv":[113,115,114,118],"texture":0},"up":{"uv":[118,104,115,103],"texture":0},"down":{"uv":[117,115,114,116],"texture":0}},"type":"cube","uuid":"30d51b39-09ac-67d2-92c7-c7f791abdaa4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.125,1.5,0.725],"to":[-2.075,7.25,3.775],"autouv":0,"color":0,"rotation":[0,45,0],"origin":[-3.6,9.725,2.25],"faces":{"north":{"uv":[19,78,22,84],"texture":0},"east":{"uv":[22,78,25,84],"texture":0},"south":{"uv":[25,78,28,84],"texture":0},"west":{"uv":[28,78,31,84],"texture":0},"up":{"uv":[103,6,100,3],"texture":0},"down":{"uv":[9,100,6,103],"texture":0}},"type":"cube","uuid":"4b2bd505-e7b7-881c-fe09-1452c0fb24fa"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.325,0,-2.025],"to":[-1.825,1.5,0.5],"autouv":0,"color":0,"rotation":[0,-45,0],"origin":[-3.6,11.725,-1.275],"faces":{"north":{"uv":[73,107,76,109],"texture":0},"east":{"uv":[107,73,110,75],"texture":0},"south":{"uv":[88,107,91,109],"texture":0},"west":{"uv":[107,91,110,93],"texture":0},"up":{"uv":[12,103,9,100],"texture":0},"down":{"uv":[103,9,100,12],"texture":0}},"type":"cube","uuid":"19245183-7ac5-9d1b-aa96-e962912f2d8d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.375,0,-0.525],"to":[-1.825,1.5,4.525],"autouv":0,"color":0,"origin":[-3.6,11.725,2.25],"faces":{"north":{"uv":[101,51,105,53],"texture":0},"east":{"uv":[95,74,100,76],"texture":0},"south":{"uv":[101,53,105,55],"texture":0},"west":{"uv":[0,96,5,98],"texture":0},"up":{"uv":[48,78,44,73],"texture":0},"down":{"uv":[52,73,48,78],"texture":0}},"type":"cube","uuid":"4a289af4-8561-0f5e-93c9-d96eb96dc343"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.175,0,1.05],"to":[-1.25,0.5,4.725],"autouv":0,"color":0,"rotation":[0,22.5,0],"origin":[-2.525,0.75,3.575],"faces":{"north":{"uv":[115,114,118,115],"texture":0},"east":{"uv":[113,10,117,11],"texture":0},"south":{"uv":[0,116,3,117],"texture":0},"west":{"uv":[113,11,117,12],"texture":0},"up":{"uv":[61,95,58,91],"texture":0},"down":{"uv":[64,91,61,95],"texture":0}},"type":"cube","uuid":"8c07b076-c295-d254-3ff8-54610dafefb3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.975,0,1.05],"to":[-3.05,0.5,4.725],"autouv":0,"color":0,"rotation":[0,-22.5,0],"origin":[-4.7,0.75,3.575],"faces":{"north":{"uv":[116,0,119,1],"texture":0},"east":{"uv":[113,17,117,18],"texture":0},"south":{"uv":[116,1,119,2],"texture":0},"west":{"uv":[113,18,117,19],"texture":0},"up":{"uv":[93,95,90,91],"texture":0},"down":{"uv":[95,27,92,31],"texture":0}},"type":"cube","uuid":"3214c9af-3f64-9d60-ba4b-5af50d31e81d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.375,1.925,2.975],"to":[-1.825,3.375,5.175],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[-3.6,2.5,5.5],"faces":{"north":{"uv":[113,19,117,20],"texture":0},"east":{"uv":[117,19,119,20],"texture":0},"south":{"uv":[113,26,117,27],"texture":0},"west":{"uv":[20,117,22,118],"texture":0},"up":{"uv":[105,57,101,55],"texture":0},"down":{"uv":[105,65,101,67],"texture":0}},"type":"cube","uuid":"b61b47e5-aef0-c7b6-d356-aba62813221d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.875,1.5,-0.275],"to":[-2.325,2.275,2.275],"autouv":0,"color":0,"origin":[-3.6,13.725,0],"faces":{"north":{"uv":[116,2,119,3],"texture":0},"east":{"uv":[116,6,119,7],"texture":0},"south":{"uv":[116,7,119,8],"texture":0},"west":{"uv":[116,12,119,13],"texture":0},"up":{"uv":[103,15,100,12],"texture":0},"down":{"uv":[20,100,17,103],"texture":0}},"type":"cube","uuid":"b00f9f74-7073-84e7-5351-080096405a56"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.9,0.775,-0.275],"to":[-4.6,2.4,3.275],"autouv":0,"color":0,"rotation":[0,0,22.5],"origin":[-5.1,1.975,1.5],"faces":{"north":{"uv":[22,117,23,119],"texture":0},"east":{"uv":[101,67,105,69],"texture":0},"south":{"uv":[23,117,24,119],"texture":0},"west":{"uv":[101,69,105,71],"texture":0},"up":{"uv":[22,116,21,112],"texture":0},"down":{"uv":[37,112,36,116],"texture":0}},"type":"cube","uuid":"9f80b25e-feab-7894-4534-3b6a5d570273"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2.6,0.775,-0.275],"to":[-1.3,2.4,3.275],"autouv":0,"color":0,"rotation":[0,0,-22.5],"origin":[-2.1,1.975,1.5],"faces":{"north":{"uv":[24,117,25,119],"texture":0},"east":{"uv":[75,101,79,103],"texture":0},"south":{"uv":[25,117,26,119],"texture":0},"west":{"uv":[101,75,105,77],"texture":0},"up":{"uv":[42,116,41,112],"texture":0},"down":{"uv":[49,112,48,116],"texture":0}},"type":"cube","uuid":"ab201e6d-7749-1af6-e5c5-cc3e1f2dd006"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.5,28.35,-1.75],"to":[-1,32.2,-0.75],"autouv":0,"color":1,"rotation":[-15,0,0],"origin":[-2.5,30.2,-1.25],"faces":{"north":{"uv":[0,80,4,84],"texture":0},"east":{"uv":[30,111,31,115],"texture":0},"south":{"uv":[80,3,84,7],"texture":0},"west":{"uv":[58,111,59,115],"texture":0},"up":{"uv":[115,34,111,33],"texture":0},"down":{"uv":[115,59,111,60],"texture":0}},"type":"cube","uuid":"c4d4556c-7a64-9699-2301-d8aa058e4096"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.75,32.5,-2.75],"to":[0.75,34,-2.25],"autouv":0,"color":3,"rotation":[0,0,45],"origin":[0,33.25,-2.75],"faces":{"north":{"uv":[70,111,72,113],"texture":0},"east":{"uv":[9,88,10,90],"texture":0},"south":{"uv":[72,111,74,113],"texture":0},"west":{"uv":[46,89,47,91],"texture":0},"up":{"uv":[118,108,116,107],"texture":0},"down":{"uv":[113,116,111,117],"texture":0}},"type":"cube","uuid":"d5105f83-0c6e-5ab9-a5c1-d78f73dfffff"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.5,7.5,7.45],"to":[5.5,32,7.45],"autouv":0,"color":3,"rotation":[0,0,-22.5],"origin":[0,31.25,7.45],"faces":{"north":{"uv":[11,25,0,50],"texture":0},"east":{"uv":[0,0,0,25],"texture":0},"south":{"uv":[0,25,11,50],"texture":0},"west":{"uv":[0,0,0,25],"texture":0},"up":{"uv":[11,0,0,0],"texture":0},"down":{"uv":[11,0,0,0],"texture":0}},"type":"cube","uuid":"f2a2c076-3830-827c-a89d-9f32cb8332e2"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-5.5,7.5,7.45],"to":[5.5,32,7.45],"autouv":0,"color":3,"rotation":[0,0,22.5],"origin":[0,31.25,7.45],"faces":{"north":{"uv":[22,0,11,25],"texture":0},"east":{"uv":[0,0,0,25],"texture":0},"south":{"uv":[11,0,22,25],"texture":0},"west":{"uv":[0,0,0,25],"texture":0},"up":{"uv":[11,0,0,0],"texture":0},"down":{"uv":[11,0,0,0],"texture":0}},"type":"cube","uuid":"e151b038-dd4d-7183-9426-8978e675de2c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,7.25,7.5],"to":[4,32,7.5],"autouv":0,"color":3,"origin":[0,31.25,7.5],"faces":{"north":{"uv":[27,25,19,50],"texture":0},"east":{"uv":[0,0,0,25],"texture":0},"south":{"uv":[19,25,27,50],"texture":0},"west":{"uv":[0,0,0,25],"texture":0},"up":{"uv":[8,0,0,0],"texture":0},"down":{"uv":[8,0,0,0],"texture":0}},"type":"cube","uuid":"21560076-f4db-b654-055a-793913997dea"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-8,0,-3],"to":[8,54,8],"autouv":1,"color":3,"visibility":false,"origin":[0,0,0],"faces":{"north":{"uv":[0,0,16,16]},"east":{"uv":[0,0,11,16]},"south":{"uv":[0,0,16,16]},"west":{"uv":[0,0,11,16]},"up":{"uv":[0,0,16,11]},"down":{"uv":[0,0,16,11]}},"type":"cube","uuid":"06fba819-151e-afac-85b3-405b6bb18348"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.29242,18.9375,-1.67732],"to":[11.04242,19.6875,6.32268],"autouv":0,"color":4,"rotation":[0,0,45],"origin":[11.29242,19.9375,3.32268],"faces":{"north":{"uv":[106,118,107,119],"texture":0},"east":{"uv":[100,93,108,94],"texture":0},"south":{"uv":[118,106,119,107],"texture":0},"west":{"uv":[101,8,109,9],"texture":0},"up":{"uv":[1,109,0,101],"texture":0},"down":{"uv":[2,101,1,109],"texture":0}},"type":"cube","uuid":"c087230a-862d-9d6d-e4cb-c9d00e17a4d1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.66742,18.4125,6.32268],"to":[11.91742,19.6625,7.57268],"autouv":0,"color":4,"origin":[11.91742,19.6625,4.07268],"faces":{"north":{"uv":[112,118,113,119],"texture":0},"east":{"uv":[118,112,119,113],"texture":0},"south":{"uv":[113,118,114,119],"texture":0},"west":{"uv":[118,113,119,114],"texture":0},"up":{"uv":[115,119,114,118],"texture":0},"down":{"uv":[119,114,118,115],"texture":0}},"type":"cube","uuid":"8977ffc9-8598-74f8-bf1d-68b5cd2583eb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.66742,18.6375,8.27268],"to":[11.91742,19.3125,8.94768],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[11.29242,19.0375,8.67268],"faces":{"north":{"uv":[119,86,120,87],"texture":0},"east":{"uv":[87,119,88,120],"texture":0},"south":{"uv":[119,87,120,88],"texture":0},"west":{"uv":[88,119,89,120],"texture":0},"up":{"uv":[120,89,119,88],"texture":0},"down":{"uv":[90,119,89,120],"texture":0}},"type":"cube","uuid":"f3f5b091-43dd-29d8-afc7-d5b805eeb9cb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.91742,18.6625,6.57268],"to":[12.16742,19.4125,7.32268],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[11.29242,19.0375,6.94768],"faces":{"north":{"uv":[119,2,120,3],"texture":0},"east":{"uv":[3,119,4,120],"texture":0},"south":{"uv":[119,3,120,4],"texture":0},"west":{"uv":[4,119,5,120],"texture":0},"up":{"uv":[120,5,119,4],"texture":0},"down":{"uv":[6,119,5,120],"texture":0}},"type":"cube","uuid":"a74a46b7-034f-fa99-4db3-6c56e6f71589"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.41742,18.6625,6.57268],"to":[10.66742,19.4125,7.32268],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[9.79242,19.0375,6.94768],"faces":{"north":{"uv":[119,83,120,84],"texture":0},"east":{"uv":[84,119,85,120],"texture":0},"south":{"uv":[119,84,120,85],"texture":0},"west":{"uv":[85,119,86,120],"texture":0},"up":{"uv":[120,86,119,85],"texture":0},"down":{"uv":[87,119,86,120],"texture":0}},"type":"cube","uuid":"95560117-7d00-3533-3923-c0492750ea03"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.91742,19.6625,6.57268],"to":[11.66742,19.9125,7.32268],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[11.29242,19.6625,6.94768],"faces":{"north":{"uv":[115,118,116,119],"texture":0},"east":{"uv":[118,115,119,116],"texture":0},"south":{"uv":[116,118,117,119],"texture":0},"west":{"uv":[118,116,119,117],"texture":0},"up":{"uv":[118,119,117,118],"texture":0},"down":{"uv":[119,117,118,118],"texture":0}},"type":"cube","uuid":"4ff64c77-291b-3c79-e625-bc7ebe3a8bc5"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.91742,18.1625,6.57268],"to":[11.66742,18.4125,7.32268],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[11.29242,18.1625,6.94768],"faces":{"north":{"uv":[118,118,119,119],"texture":0},"east":{"uv":[0,119,1,120],"texture":0},"south":{"uv":[119,0,120,1],"texture":0},"west":{"uv":[1,119,2,120],"texture":0},"up":{"uv":[120,2,119,1],"texture":0},"down":{"uv":[3,119,2,120],"texture":0}},"type":"cube","uuid":"3ba8c767-b1af-b6b4-a90c-2853d99a5911"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.39242,19.0375,7.57268],"to":[10.94242,19.5875,8.57268],"autouv":0,"color":4,"rotation":[0,0,45],"origin":[11.29242,19.9375,5.57268],"faces":{"north":{"uv":[109,118,110,119],"texture":0},"east":{"uv":[118,109,119,110],"texture":0},"south":{"uv":[110,118,111,119],"texture":0},"west":{"uv":[118,110,119,111],"texture":0},"up":{"uv":[112,119,111,118],"texture":0},"down":{"uv":[119,111,118,112],"texture":0}},"type":"cube","uuid":"8b5ad391-f368-2fec-1d47-55410fd47190"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.91742,18.6625,-2.67732],"to":[12.16742,19.4125,-1.92732],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[11.29242,19.0375,-2.30232],"faces":{"north":{"uv":[119,80,120,81],"texture":0},"east":{"uv":[81,119,82,120],"texture":0},"south":{"uv":[119,81,120,82],"texture":0},"west":{"uv":[82,119,83,120],"texture":0},"up":{"uv":[120,83,119,82],"texture":0},"down":{"uv":[84,119,83,120],"texture":0}},"type":"cube","uuid":"9d609d71-99c7-9a40-eeac-222423bcb4e8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.01742,19.6625,-2.57732],"to":[11.56742,20.9125,-2.02732],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[11.29242,19.6625,-2.30232],"faces":{"north":{"uv":[119,74,120,75],"texture":0},"east":{"uv":[75,119,76,120],"texture":0},"south":{"uv":[119,75,120,76],"texture":0},"west":{"uv":[76,119,77,120],"texture":0},"up":{"uv":[120,77,119,76],"texture":0},"down":{"uv":[78,119,77,120],"texture":0}},"type":"cube","uuid":"1c221324-8e5a-d711-e550-a1f5e3038d8a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.66742,18.4125,-2.92732],"to":[11.91742,19.6625,-1.67732],"autouv":0,"color":4,"origin":[11.91742,19.6625,-5.17732],"faces":{"north":{"uv":[119,8,120,9],"texture":0},"east":{"uv":[9,119,10,120],"texture":0},"south":{"uv":[119,9,120,10],"texture":0},"west":{"uv":[10,119,11,120],"texture":0},"up":{"uv":[120,11,119,10],"texture":0},"down":{"uv":[12,119,11,120],"texture":0}},"type":"cube","uuid":"ac2d484e-6396-ee68-ed13-3a4b61d4b29b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.41742,18.6625,-2.67732],"to":[10.66742,19.4125,-1.92732],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[9.79242,19.0375,-2.30232],"faces":{"north":{"uv":[119,5,120,6],"texture":0},"east":{"uv":[6,119,7,120],"texture":0},"south":{"uv":[119,6,120,7],"texture":0},"west":{"uv":[7,119,8,120],"texture":0},"up":{"uv":[120,8,119,7],"texture":0},"down":{"uv":[9,119,8,120],"texture":0}},"type":"cube","uuid":"89c33bc3-1e54-c13e-afef-9a0ca6ef63e3"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.29242,18.9375,-18.92732],"to":[11.04242,19.6875,-2.92732],"autouv":0,"color":4,"rotation":[0,0,45],"origin":[11.29242,19.9375,-5.92732],"faces":{"north":{"uv":[107,118,108,119],"texture":0},"east":{"uv":[80,7,96,8],"texture":0},"south":{"uv":[118,107,119,108],"texture":0},"west":{"uv":[80,8,96,9],"texture":0},"up":{"uv":[5,96,4,80],"texture":0},"down":{"uv":[6,80,5,96],"texture":0}},"type":"cube","uuid":"80fdca4d-b28b-c0f6-507c-625123d48c14"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.16742,17.6625,-23.55232],"to":[12.41742,20.4125,-19.05232],"autouv":0,"color":4,"origin":[11.91742,19.6625,-22.55232],"faces":{"north":{"uv":[0,109,2,112],"texture":0},"east":{"uv":[87,27,92,30],"texture":0},"south":{"uv":[15,109,17,112],"texture":0},"west":{"uv":[87,52,92,55],"texture":0},"up":{"uv":[47,100,45,95],"texture":0},"down":{"uv":[49,95,47,100],"texture":0}},"type":"cube","uuid":"a2f6ed5c-0f4c-374a-5435-973f5eb5f187"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.66742,18.4125,-16.67732],"to":[11.91742,19.6625,-15.92732],"autouv":0,"color":4,"rotation":[22.5,0,0],"origin":[11.29242,19.0375,-16.30232],"faces":{"north":{"uv":[119,65,120,66],"texture":0},"east":{"uv":[66,119,67,120],"texture":0},"south":{"uv":[119,66,120,67],"texture":0},"west":{"uv":[67,119,68,120],"texture":0},"up":{"uv":[120,68,119,67],"texture":0},"down":{"uv":[69,119,68,120],"texture":0}},"type":"cube","uuid":"ddfd78e6-b1d2-bd51-ec01-c77415cba75d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.66742,18.4125,-17.92732],"to":[11.91742,19.6625,-17.17732],"autouv":0,"color":4,"rotation":[22.5,0,0],"origin":[11.29242,19.0375,-17.55232],"faces":{"north":{"uv":[119,68,120,69],"texture":0},"east":{"uv":[69,119,70,120],"texture":0},"south":{"uv":[119,69,120,70],"texture":0},"west":{"uv":[70,119,71,120],"texture":0},"up":{"uv":[120,71,119,70],"texture":0},"down":{"uv":[72,119,71,120],"texture":0}},"type":"cube","uuid":"2faff165-0bac-727f-f020-671fa6b6a2e7"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.66742,18.4125,-15.17732],"to":[11.91742,19.6625,-14.42732],"autouv":0,"color":4,"rotation":[22.5,0,0],"origin":[11.29242,19.0375,-14.80232],"faces":{"north":{"uv":[119,71,120,72],"texture":0},"east":{"uv":[72,119,73,120],"texture":0},"south":{"uv":[119,72,120,73],"texture":0},"west":{"uv":[73,119,74,120],"texture":0},"up":{"uv":[120,74,119,73],"texture":0},"down":{"uv":[75,119,74,120],"texture":0}},"type":"cube","uuid":"2831f117-3c8b-9a36-df4d-9ecad1568fed"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.41742,18.3875,-19.42732],"to":[12.16742,19.3125,-18.50232],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[11.29242,19.0375,-18.77732],"faces":{"north":{"uv":[46,117,48,118],"texture":0},"east":{"uv":[119,92,120,93],"texture":0},"south":{"uv":[48,117,50,118],"texture":0},"west":{"uv":[93,119,94,120],"texture":0},"up":{"uv":[52,118,50,117],"texture":0},"down":{"uv":[119,50,117,51],"texture":0}},"type":"cube","uuid":"d19b1c81-1041-090e-b271-57d212ce1fcf"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.16742,20.4125,-24.55232],"to":[13.41742,23.1625,-18.05232],"autouv":0,"color":4,"origin":[11.91742,22.4125,-22.55232],"faces":{"north":{"uv":[10,94,14,97],"texture":0},"east":{"uv":[72,15,79,18],"texture":0},"south":{"uv":[19,94,23,97],"texture":0},"west":{"uv":[72,25,79,28],"texture":0},"up":{"uv":[4,69,0,62],"texture":0},"down":{"uv":[8,62,4,69],"texture":0}},"type":"cube","uuid":"e0147d4e-cb1c-6164-a545-637ffef444b6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.16742,14.9125,-24.55232],"to":[13.41742,17.6625,-18.05232],"autouv":0,"color":4,"origin":[11.91742,16.9125,-22.55232],"faces":{"north":{"uv":[94,21,98,24],"texture":0},"east":{"uv":[72,28,79,31],"texture":0},"south":{"uv":[30,94,34,97],"texture":0},"west":{"uv":[72,31,79,34],"texture":0},"up":{"uv":[12,69,8,62],"texture":0},"down":{"uv":[16,62,12,69],"texture":0}},"type":"cube","uuid":"50527203-69db-5358-9c03-7ce2948218c8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.66742,23.1625,-24.05232],"to":[12.91742,23.6625,-18.55232],"autouv":0,"color":4,"origin":[11.91742,22.4125,-22.55232],"faces":{"north":{"uv":[51,116,54,117],"texture":0},"east":{"uv":[106,22,112,23],"texture":0},"south":{"uv":[116,56,119,57],"texture":0},"west":{"uv":[107,19,113,20],"texture":0},"up":{"uv":[42,84,39,78],"texture":0},"down":{"uv":[45,78,42,84],"texture":0}},"type":"cube","uuid":"c5d62892-6810-ea9d-d775-bb4365179fc1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.66742,14.4125,-24.05232],"to":[12.91742,14.9125,-18.55232],"autouv":0,"color":4,"origin":[11.91742,13.6625,-22.55232],"faces":{"north":{"uv":[116,57,119,58],"texture":0},"east":{"uv":[108,45,114,46],"texture":0},"south":{"uv":[116,58,119,59],"texture":0},"west":{"uv":[108,84,114,85],"texture":0},"up":{"uv":[81,49,78,43],"texture":0},"down":{"uv":[48,78,45,84],"texture":0}},"type":"cube","uuid":"ba7003e9-c068-e689-cd00-b2d418bfc31b"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.49242,19.1375,-25.42732],"to":[10.84242,19.4875,-23.42732],"autouv":0,"color":4,"rotation":[0,0,45],"origin":[11.29242,19.9375,-26.42732],"faces":{"north":{"uv":[108,118,109,119],"texture":0},"east":{"uv":[117,42,119,43],"texture":0},"south":{"uv":[118,108,119,109],"texture":0},"west":{"uv":[43,117,45,118],"texture":0},"up":{"uv":[118,45,117,43],"texture":0},"down":{"uv":[46,117,45,119],"texture":0}},"type":"cube","uuid":"af406e05-afd9-6e24-703e-55b85b6a73ac"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.91742,18.8875,-25.70232],"to":[11.66742,19.3125,-25.27732],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[11.29242,19.0375,-25.55232],"faces":{"north":{"uv":[119,89,120,90],"texture":0},"east":{"uv":[90,119,91,120],"texture":0},"south":{"uv":[119,90,120,91],"texture":0},"west":{"uv":[91,119,92,120],"texture":0},"up":{"uv":[120,92,119,91],"texture":0},"down":{"uv":[93,119,92,120],"texture":0}},"type":"cube","uuid":"0fa4c566-f097-47cf-20bc-c8e93923dbeb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[12.41742,17.8875,-22.17732],"to":[12.91742,19.8125,-20.25232],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[13.04242,19.0375,-21.02732],"faces":{"north":{"uv":[117,51,118,53],"texture":0},"east":{"uv":[114,14,116,16],"texture":0},"south":{"uv":[52,117,53,119],"texture":0},"west":{"uv":[15,114,17,116],"texture":0},"up":{"uv":[54,119,53,117],"texture":0},"down":{"uv":[118,53,117,55],"texture":0}},"type":"cube","uuid":"3c008bd4-bf5c-033f-d1dd-55987d3bc51c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[9.66742,17.8875,-22.17732],"to":[10.16742,19.8125,-20.25232],"autouv":0,"color":4,"rotation":[45,0,0],"origin":[10.29242,19.0375,-21.02732],"faces":{"north":{"uv":[55,117,56,119],"texture":0},"east":{"uv":[19,114,21,116],"texture":0},"south":{"uv":[56,117,57,119],"texture":0},"west":{"uv":[114,20,116,22],"texture":0},"up":{"uv":[60,119,59,117],"texture":0},"down":{"uv":[61,117,60,119],"texture":0}},"type":"cube","uuid":"84dc7bab-eb3e-3c92-766f-674f5fe8147d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,21.4125,-19.80232],"to":[13.66742,22.1625,-19.05232],"autouv":0,"color":4,"origin":[11.91742,22.4125,-22.55232],"faces":{"north":{"uv":[119,17,120,18],"texture":0},"east":{"uv":[18,119,19,120],"texture":0},"south":{"uv":[119,18,120,19],"texture":0},"west":{"uv":[19,119,20,120],"texture":0},"up":{"uv":[120,20,119,19],"texture":0},"down":{"uv":[21,119,20,120],"texture":0}},"type":"cube","uuid":"fc170399-36d0-a3cb-d49e-befa5de166a6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,21.4125,-23.55232],"to":[13.66742,22.1625,-22.80232],"autouv":0,"color":4,"origin":[11.91742,22.4125,-26.30232],"faces":{"north":{"uv":[119,20,120,21],"texture":0},"east":{"uv":[21,119,22,120],"texture":0},"south":{"uv":[119,21,120,22],"texture":0},"west":{"uv":[22,119,23,120],"texture":0},"up":{"uv":[120,23,119,22],"texture":0},"down":{"uv":[24,119,23,120],"texture":0}},"type":"cube","uuid":"f3848d14-f6d0-c14a-9ee7-197983dbb509"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,21.4125,-22.30232],"to":[13.66742,22.1625,-21.55232],"autouv":0,"color":4,"origin":[11.91742,22.4125,-25.05232],"faces":{"north":{"uv":[119,23,120,24],"texture":0},"east":{"uv":[24,119,25,120],"texture":0},"south":{"uv":[119,24,120,25],"texture":0},"west":{"uv":[25,119,26,120],"texture":0},"up":{"uv":[120,26,119,25],"texture":0},"down":{"uv":[27,119,26,120],"texture":0}},"type":"cube","uuid":"f1a6a1a1-1fbf-b2f6-69cc-0b02165f442f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,21.4125,-21.05232],"to":[13.66742,22.1625,-20.30232],"autouv":0,"color":4,"origin":[11.91742,22.4125,-23.80232],"faces":{"north":{"uv":[119,62,120,63],"texture":0},"east":{"uv":[63,119,64,120],"texture":0},"south":{"uv":[119,63,120,64],"texture":0},"west":{"uv":[64,119,65,120],"texture":0},"up":{"uv":[120,65,119,64],"texture":0},"down":{"uv":[66,119,65,120],"texture":0}},"type":"cube","uuid":"4d32635b-7169-63ea-8edd-2058b2f5844c"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,15.9125,-22.30232],"to":[13.66742,16.6625,-21.55232],"autouv":0,"color":4,"origin":[11.91742,16.9125,-25.05232],"faces":{"north":{"uv":[119,35,120,36],"texture":0},"east":{"uv":[36,119,37,120],"texture":0},"south":{"uv":[119,36,120,37],"texture":0},"west":{"uv":[37,119,38,120],"texture":0},"up":{"uv":[120,38,119,37],"texture":0},"down":{"uv":[39,119,38,120],"texture":0}},"type":"cube","uuid":"84ee9ba3-52b6-09b8-d529-4a2cbb570f77"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,15.9125,-23.55232],"to":[13.66742,16.6625,-22.80232],"autouv":0,"color":4,"origin":[11.91742,16.9125,-26.30232],"faces":{"north":{"uv":[119,32,120,33],"texture":0},"east":{"uv":[33,119,34,120],"texture":0},"south":{"uv":[119,33,120,34],"texture":0},"west":{"uv":[34,119,35,120],"texture":0},"up":{"uv":[120,35,119,34],"texture":0},"down":{"uv":[36,119,35,120],"texture":0}},"type":"cube","uuid":"46417fff-471d-3791-5732-884a2289abf4"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,15.9125,-21.05232],"to":[13.66742,16.6625,-20.30232],"autouv":0,"color":4,"origin":[11.91742,16.9125,-23.80232],"faces":{"north":{"uv":[119,29,120,30],"texture":0},"east":{"uv":[30,119,31,120],"texture":0},"south":{"uv":[119,30,120,31],"texture":0},"west":{"uv":[31,119,32,120],"texture":0},"up":{"uv":[120,32,119,31],"texture":0},"down":{"uv":[33,119,32,120],"texture":0}},"type":"cube","uuid":"90eb0ce1-ff48-4351-bf47-d88694dcb183"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[13.41742,15.9125,-19.80232],"to":[13.66742,16.6625,-19.05232],"autouv":0,"color":4,"origin":[11.91742,16.9125,-22.55232],"faces":{"north":{"uv":[119,26,120,27],"texture":0},"east":{"uv":[27,119,28,120],"texture":0},"south":{"uv":[119,27,120,28],"texture":0},"west":{"uv":[28,119,29,120],"texture":0},"up":{"uv":[120,29,119,28],"texture":0},"down":{"uv":[30,119,29,120],"texture":0}},"type":"cube","uuid":"60aa03cc-070d-71a6-406f-6d50e0d1361a"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,15.9125,-22.30232],"to":[9.16742,16.6625,-21.55232],"autouv":0,"color":4,"origin":[7.41742,16.9125,-25.05232],"faces":{"north":{"uv":[119,59,120,60],"texture":0},"east":{"uv":[60,119,61,120],"texture":0},"south":{"uv":[119,60,120,61],"texture":0},"west":{"uv":[61,119,62,120],"texture":0},"up":{"uv":[120,62,119,61],"texture":0},"down":{"uv":[63,119,62,120],"texture":0}},"type":"cube","uuid":"881889d5-8955-b00a-ef17-f4cad22b51d9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,15.9125,-23.55232],"to":[9.16742,16.6625,-22.80232],"autouv":0,"color":4,"origin":[7.41742,16.9125,-26.30232],"faces":{"north":{"uv":[119,56,120,57],"texture":0},"east":{"uv":[57,119,58,120],"texture":0},"south":{"uv":[119,57,120,58],"texture":0},"west":{"uv":[58,119,59,120],"texture":0},"up":{"uv":[120,59,119,58],"texture":0},"down":{"uv":[60,119,59,120],"texture":0}},"type":"cube","uuid":"7ddf7986-5648-d1ef-1cf5-592925b33ae6"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,15.9125,-21.05232],"to":[9.16742,16.6625,-20.30232],"autouv":0,"color":4,"origin":[7.41742,16.9125,-23.80232],"faces":{"north":{"uv":[119,53,120,54],"texture":0},"east":{"uv":[54,119,55,120],"texture":0},"south":{"uv":[119,54,120,55],"texture":0},"west":{"uv":[55,119,56,120],"texture":0},"up":{"uv":[120,56,119,55],"texture":0},"down":{"uv":[57,119,56,120],"texture":0}},"type":"cube","uuid":"d878a78b-eeb5-f72a-ed3b-7ec05126ba3e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,15.9125,-19.80232],"to":[9.16742,16.6625,-19.05232],"autouv":0,"color":4,"origin":[7.41742,16.9125,-22.55232],"faces":{"north":{"uv":[119,50,120,51],"texture":0},"east":{"uv":[51,119,52,120],"texture":0},"south":{"uv":[119,51,120,52],"texture":0},"west":{"uv":[52,119,53,120],"texture":0},"up":{"uv":[120,53,119,52],"texture":0},"down":{"uv":[54,119,53,120],"texture":0}},"type":"cube","uuid":"4731abb8-728a-e9c5-a444-84b2ceb44bde"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,21.4125,-19.80232],"to":[9.16742,22.1625,-19.05232],"autouv":0,"color":4,"origin":[7.41742,22.4125,-22.55232],"faces":{"north":{"uv":[119,47,120,48],"texture":0},"east":{"uv":[48,119,49,120],"texture":0},"south":{"uv":[119,48,120,49],"texture":0},"west":{"uv":[49,119,50,120],"texture":0},"up":{"uv":[120,50,119,49],"texture":0},"down":{"uv":[51,119,50,120],"texture":0}},"type":"cube","uuid":"017686be-6b99-fc6e-430f-51e630c857fb"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,21.4125,-21.05232],"to":[9.16742,22.1625,-20.30232],"autouv":0,"color":4,"origin":[7.41742,22.4125,-23.80232],"faces":{"north":{"uv":[119,44,120,45],"texture":0},"east":{"uv":[45,119,46,120],"texture":0},"south":{"uv":[119,45,120,46],"texture":0},"west":{"uv":[46,119,47,120],"texture":0},"up":{"uv":[120,47,119,46],"texture":0},"down":{"uv":[48,119,47,120],"texture":0}},"type":"cube","uuid":"39bfba34-edbb-7c44-d1b8-c277995bf5d8"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,21.4125,-22.30232],"to":[9.16742,22.1625,-21.55232],"autouv":0,"color":4,"origin":[7.41742,22.4125,-25.05232],"faces":{"north":{"uv":[119,41,120,42],"texture":0},"east":{"uv":[42,119,43,120],"texture":0},"south":{"uv":[119,42,120,43],"texture":0},"west":{"uv":[43,119,44,120],"texture":0},"up":{"uv":[120,44,119,43],"texture":0},"down":{"uv":[45,119,44,120],"texture":0}},"type":"cube","uuid":"42dbda59-442e-cce4-8243-bae66ee979b9"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[8.91742,21.4125,-23.55232],"to":[9.16742,22.1625,-22.80232],"autouv":0,"color":4,"origin":[7.41742,22.4125,-26.30232],"faces":{"north":{"uv":[119,38,120,39],"texture":0},"east":{"uv":[39,119,40,120],"texture":0},"south":{"uv":[119,39,120,40],"texture":0},"west":{"uv":[40,119,41,120],"texture":0},"up":{"uv":[120,41,119,40],"texture":0},"down":{"uv":[42,119,41,120],"texture":0}},"type":"cube","uuid":"b7510d87-7ad4-243c-b939-ece13db6e1db"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.76742,20.9125,-2.82732],"to":[11.81742,21.4125,-1.77732],"autouv":0,"color":4,"origin":[11.91742,19.6625,-5.17732],"faces":{"north":{"uv":[119,11,120,12],"texture":0},"east":{"uv":[12,119,13,120],"texture":0},"south":{"uv":[119,12,120,13],"texture":0},"west":{"uv":[13,119,14,120],"texture":0},"up":{"uv":[120,14,119,13],"texture":0},"down":{"uv":[15,119,14,120],"texture":0}},"type":"cube","uuid":"304765d9-7128-9347-1e49-087cdc5da139"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.01742,17.1625,-2.57732],"to":[11.56742,18.4125,-2.02732],"autouv":0,"color":4,"rotation":[0,45,0],"origin":[11.29242,17.1625,-2.30232],"faces":{"north":{"uv":[119,77,120,78],"texture":0},"east":{"uv":[78,119,79,120],"texture":0},"south":{"uv":[119,78,120,79],"texture":0},"west":{"uv":[79,119,80,120],"texture":0},"up":{"uv":[120,80,119,79],"texture":0},"down":{"uv":[81,119,80,120],"texture":0}},"type":"cube","uuid":"98bf52c3-2eb1-5d25-58bd-72b0e6a5fe1e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[10.76742,16.6625,-2.82732],"to":[11.81742,17.1625,-1.77732],"autouv":0,"color":4,"origin":[11.91742,15.4125,-5.17732],"faces":{"north":{"uv":[119,14,120,15],"texture":0},"east":{"uv":[15,119,16,120],"texture":0},"south":{"uv":[119,15,120,16],"texture":0},"west":{"uv":[16,119,17,120],"texture":0},"up":{"uv":[120,17,119,16],"texture":0},"down":{"uv":[18,119,17,120],"texture":0}},"type":"cube","uuid":"fb35b537-e58f-2fa0-f887-d6d019c2932d"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.54242,18.6875,0.32268],"to":[-10.79242,19.4375,5.32268],"autouv":0,"color":9,"origin":[-10.79242,18.4375,3.32268],"faces":{"north":{"uv":[23,120,24,121],"texture":0},"east":{"uv":[111,24,116,25],"texture":0},"south":{"uv":[120,23,121,24],"texture":0},"west":{"uv":[111,25,116,26],"texture":0},"up":{"uv":[4,93,3,88],"texture":0},"down":{"uv":[105,109,104,114],"texture":0}},"type":"cube","uuid":"0e653ee1-fb8a-4e45-7455-91bd2048c061"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-13.21742,18.6875,-1.67732],"to":[-12.46742,19.4375,1.32268],"autouv":0,"color":9,"rotation":[0,45,0],"origin":[-12.84242,19.0625,-1.17732],"faces":{"north":{"uv":[24,120,25,121],"texture":0},"east":{"uv":[116,80,119,81],"texture":0},"south":{"uv":[120,24,121,25],"texture":0},"west":{"uv":[81,116,84,117],"texture":0},"up":{"uv":[85,119,84,116],"texture":0},"down":{"uv":[86,116,85,119],"texture":0}},"type":"cube","uuid":"76cf3cc6-7ee4-f915-d4c4-4282c96d6969"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-11.78258,18.6875,4.89768],"to":[-11.03258,19.4375,7.89768],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[-11.40758,19.0625,5.39768],"faces":{"north":{"uv":[25,120,26,121],"texture":0},"east":{"uv":[116,81,119,82],"texture":0},"south":{"uv":[120,25,121,26],"texture":0},"west":{"uv":[116,85,119,86],"texture":0},"up":{"uv":[88,119,87,116],"texture":0},"down":{"uv":[89,116,88,119],"texture":0}},"type":"cube","uuid":"97f742d7-a697-80c9-1d9f-397363f4952e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.24242,12.6875,-4.67732],"to":[-14.74242,22.4375,0.32268],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[-15.04242,20.0625,-0.17732],"faces":{"north":{"uv":[60,95,61,105],"texture":0},"east":{"uv":[33,42,38,52],"texture":0},"south":{"uv":[61,95,62,105],"texture":0},"west":{"uv":[38,42,43,52],"texture":0},"up":{"uv":[23,115,22,110],"texture":0},"down":{"uv":[24,110,23,115],"texture":0}},"type":"cube","uuid":"29321ff2-c3b6-8f41-2e32-b6f9c6af8d06"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.54242,14.6875,0.02268],"to":[-15.04242,23.4375,5.62268],"autouv":0,"color":9,"origin":[-14.74242,18.4375,3.32268],"faces":{"north":{"uv":[58,97,59,106],"texture":0},"east":{"uv":[38,24,44,33],"texture":0},"south":{"uv":[59,97,60,106],"texture":0},"west":{"uv":[27,42,33,51],"texture":0},"up":{"uv":[39,114,38,108],"texture":0},"down":{"uv":[9,109,8,115],"texture":0}},"type":"cube","uuid":"73d20e87-3c4e-65b7-ab04-82c6632ac983"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.24242,12.6875,5.32732],"to":[-14.74242,22.4375,10.32732],"autouv":0,"color":9,"rotation":[0,45,0],"origin":[-15.04242,20.0625,5.82732],"faces":{"north":{"uv":[62,95,63,105],"texture":0},"east":{"uv":[43,33,48,43],"texture":0},"south":{"uv":[63,95,64,105],"texture":0},"west":{"uv":[43,43,48,53],"texture":0},"up":{"uv":[25,115,24,110],"texture":0},"down":{"uv":[26,110,25,115],"texture":0}},"type":"cube","uuid":"916a6cdb-a6e7-7646-77fb-cd5299df3d75"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.56742,23.3375,0.02268],"to":[-15.06742,26.3375,5.62268],"autouv":0,"color":9,"rotation":[0,0,-22.5],"origin":[-15.31742,23.3375,2.82268],"faces":{"north":{"uv":[116,77,117,80],"texture":0},"east":{"uv":[78,0,84,3],"texture":0},"south":{"uv":[80,116,81,119],"texture":0},"west":{"uv":[79,9,85,12],"texture":0},"up":{"uv":[32,115,31,109],"texture":0},"down":{"uv":[91,109,90,115],"texture":0}},"type":"cube","uuid":"210cd302-529f-f9ad-2284-5372cce7b1ac"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.24242,9.9375,-4.67732],"to":[-14.74242,12.6875,-1.67732],"autouv":0,"color":9,"rotation":[0,-45,0],"origin":[-15.04242,8.3125,-0.17732],"faces":{"north":{"uv":[41,116,42,119],"texture":0},"east":{"uv":[55,100,58,103],"texture":0},"south":{"uv":[54,116,55,119],"texture":0},"west":{"uv":[64,100,67,103],"texture":0},"up":{"uv":[58,119,57,116],"texture":0},"down":{"uv":[66,116,65,119],"texture":0}},"type":"cube","uuid":"2f10273e-7625-2f00-37c7-7c953f6059ad"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-15.24242,9.9375,7.32732],"to":[-14.74242,12.6875,10.32732],"autouv":0,"color":9,"rotation":[0,45,0],"origin":[-15.04242,8.3125,5.82732],"faces":{"north":{"uv":[66,116,67,119],"texture":0},"east":{"uv":[67,100,70,103],"texture":0},"south":{"uv":[73,116,74,119],"texture":0},"west":{"uv":[70,100,73,103],"texture":0},"up":{"uv":[75,119,74,116],"texture":0},"down":{"uv":[117,74,116,77],"texture":0}},"type":"cube","uuid":"0973a951-8178-58a9-9352-f3fd079a3124"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.5,18,-0.75],"to":[3.5,24,5.75],"autouv":0,"color":1,"origin":[0.5,31.5,3.25],"faces":{"north":{"uv":[0,50,7,56],"texture":0},"east":{"uv":[7,50,14,56],"texture":0},"south":{"uv":[14,50,21,56],"texture":0},"west":{"uv":[48,50,55,56],"texture":0},"up":{"uv":[51,7,44,0],"texture":0},"down":{"uv":[51,7,44,14],"texture":0}},"type":"cube","uuid":"40ca8419-4070-3278-78e1-6d76eb6624a1"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.29242,11.21393,-38.03278],"to":[11.29242,27.21393,-14.03278],"autouv":0,"color":9,"origin":[11.29242,19.08893,-38.03278],"faces":{"north":{"uv":[0,0,2,2],"texture":null},"east":{"uv":[0,0,32,32],"rotation":90,"texture":2},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":2},"up":{"uv":[0,0,2,2],"texture":null},"down":{"uv":[0,0,2,2],"texture":null}},"type":"cube","uuid":"751aa955-ee70-e57a-5860-8db70824e547"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[3.29242,18.96393,-38.28278],"to":[19.29242,18.96393,-14.28278],"autouv":0,"color":9,"origin":[11.29242,19.08893,-38.28278],"faces":{"north":{"uv":[0,0,0,2],"texture":null},"east":{"uv":[0,0,0,0],"rotation":90,"texture":null},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":null},"up":{"uv":[0,0,32,32],"texture":2},"down":{"uv":[32,0,0,32],"rotation":180,"texture":2}},"type":"cube","uuid":"12dc434f-721c-d2c1-c9c3-379413c4712f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[5.29242,18.96393,-34.28278],"to":[17.29242,18.96393,-18.28278],"autouv":0,"color":9,"rotation":[-22.5,0,0],"origin":[11.29242,19.08893,-34.28278],"faces":{"north":{"uv":[0,0,0,2],"texture":null},"east":{"uv":[0,0,0,0],"rotation":90,"texture":null},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":null},"up":{"uv":[0,0,32,32],"texture":2},"down":{"uv":[32,0,0,32],"rotation":180,"texture":2}},"type":"cube","uuid":"3d1af7a5-df34-b320-f067-da1b3d2f8cd0"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[5.29242,18.96393,-34.28278],"to":[17.29242,18.96393,-18.28278],"autouv":0,"color":9,"rotation":[22.5,0,0],"origin":[11.29242,19.08893,-34.28278],"faces":{"north":{"uv":[0,0,0,2],"texture":null},"east":{"uv":[0,0,0,0],"rotation":90,"texture":null},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":null},"up":{"uv":[0,0,32,32],"texture":2},"down":{"uv":[32,0,0,32],"rotation":180,"texture":2}},"type":"cube","uuid":"5f3c900f-a480-cb33-b171-ecc6111d8927"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.29242,13.21393,-34.78278],"to":[11.29242,25.21393,-18.78278],"autouv":0,"color":9,"rotation":[0,22.5,0],"origin":[11.29242,19.08893,-34.78278],"faces":{"north":{"uv":[0,0,2,2],"texture":null},"east":{"uv":[0,0,32,32],"rotation":90,"texture":2},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":2},"up":{"uv":[0,0,2,2],"texture":null},"down":{"uv":[0,0,2,2],"texture":null}},"type":"cube","uuid":"ca06a670-d403-c657-79e8-0824cc3fdc41"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.29242,13.21393,-34.28278],"to":[11.29242,25.21393,-18.28278],"autouv":0,"color":9,"rotation":[0,-22.5,0],"origin":[11.29242,19.08893,-34.28278],"faces":{"north":{"uv":[0,0,2,2],"texture":null},"east":{"uv":[0,0,32,32],"rotation":90,"texture":2},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":2},"up":{"uv":[0,0,2,2],"texture":null},"down":{"uv":[0,0,2,2],"texture":null}},"type":"cube","uuid":"9c0d5db7-e69b-cf72-399f-66cf71bbddba"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.29242,13.21393,-38.03278],"to":[11.29242,25.21393,-2.03278],"autouv":0,"color":9,"origin":[11.29242,19.08893,-38.03278],"faces":{"north":{"uv":[0,0,2,2],"texture":null},"east":{"uv":[0,0,32,32],"rotation":90,"texture":2},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":2},"up":{"uv":[0,0,2,2],"texture":null},"down":{"uv":[0,0,2,2],"texture":null}},"type":"cube","uuid":"a53e20df-9f48-7aa4-7c3b-335394525a55"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[5.29242,18.96393,-38.28278],"to":[17.29242,18.96393,-2.28278],"autouv":0,"color":9,"origin":[11.29242,19.08893,-38.28278],"faces":{"north":{"uv":[0,0,0,2],"texture":null},"east":{"uv":[0,0,0,0],"rotation":90,"texture":null},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,32,32,0],"rotation":90,"texture":null},"up":{"uv":[0,0,32,32],"texture":2},"down":{"uv":[32,0,0,32],"rotation":180,"texture":2}},"type":"cube","uuid":"195c9da8-bf7e-278b-5861-541e642e9e06"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[11.04242,19.03893,-25.05778],"to":[11.54242,35.03893,-9.05778],"autouv":0,"color":0,"origin":[11.29242,19.03893,-21.05778],"faces":{"north":{"uv":[0,0,0,0],"texture":null},"east":{"uv":[32,0,0,32],"texture":1},"south":{"uv":[0,0,0,0],"texture":null},"west":{"uv":[0,0,32,32],"texture":1},"up":{"uv":[0,0,0,2],"texture":null},"down":{"uv":[0,0,0,0],"texture":null}},"type":"cube","uuid":"5e478285-5bb1-597c-ab98-f0ba7c72ca3f"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-16.54242,10.9375,-4.17732],"to":[-9.54242,24.9375,9.82268],"autouv":1,"color":2,"visibility":false,"origin":[-15.54242,18.9375,2.82268],"faces":{"north":{"uv":[0,0,7,14]},"east":{"uv":[0,0,14,14]},"south":{"uv":[0,0,7,14]},"west":{"uv":[0,0,14,14]},"up":{"uv":[0,0,7,14]},"down":{"uv":[0,0,7,14]}},"type":"cube","uuid":"a97b80b2-addb-5b18-4657-99a47dd5af59"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1,55.54164,1.11016],"to":[1,57.54164,3.11016],"autouv":1,"color":5,"visibility":false,"origin":[0,55.54164,2.11016],"faces":{"north":{"uv":[0,0,2,2]},"east":{"uv":[0,0,2,2]},"south":{"uv":[0,0,2,2]},"west":{"uv":[0,0,2,2]},"up":{"uv":[0,0,2,2]},"down":{"uv":[0,0,2,2]}},"type":"cube","uuid":"2d6272a2-dad4-f12b-0a50-c5cdbd2ec49e"},{"name":"cube","box_uv":false,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-21,0,-18.75],"to":[21,0,23.25],"autouv":1,"color":6,"visibility":false,"origin":[0,0,2.25],"faces":{"north":{"uv":[0,0,42,0]},"east":{"uv":[0,0,42,0]},"south":{"uv":[0,0,42,0]},"west":{"uv":[0,0,42,0]},"up":{"uv":[0,0,42,42]},"down":{"uv":[0,0,42,42]}},"type":"cube","uuid":"00c5e440-634b-654f-a9d9-95f08060aea3"}],"outliner":[{"name":"tag_name","origin":[0,56.54164,2.11016],"color":0,"uuid":"669dcb38-e598-cb35-ec4c-55b67efeecce","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["2d6272a2-dad4-f12b-0a50-c5cdbd2ec49e"]},{"name":"body","origin":[0,0,2.25],"color":0,"uuid":"a633f85e-6f17-f4be-fc15-e8561725fc8d","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":[{"name":"upper_body","origin":[0,24.00119,2.34697],"color":0,"uuid":"788aa374-e94c-c383-6565-af12949da09e","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["a6fd2059-bc63-6b1e-1adb-b110a04138fe","56c157e4-61fb-c7ec-de55-bc79f81ffd66","26ce26ab-08fe-cbe9-470c-982100b16eeb","d5105f83-0c6e-5ab9-a5c1-d78f73dfffff","02fddf8c-7397-7a63-4a7c-c467b91d5448","b4c05bb6-f96e-2f4d-93ec-7e5dd4a9252d","9a75352d-e331-7910-3b95-f3aa4d6fda3e","17175f5b-35b1-3d5b-3d47-33e3b89cfcf7","004b5756-3745-4694-ac32-533f6ec57441","bcdd576f-aa6b-f54b-c58b-1143876ab139","5990e492-34e4-0cf6-bf08-baf0fb599311","e3e2d9f0-1441-b200-dd73-7e161d346cb0","fefb8a0c-4dc9-eb37-14ea-19ed11636ce9","30860483-4b6d-b211-99f0-8b5163830762","c5de4175-68a1-cce8-dda1-6f7519bc1b2c","976142e5-835f-0783-2b66-047893b31e8d","d6db19a7-5456-61a3-c464-720c60d0ad72","80406cd6-c894-2499-2966-49eeeb4d05b9","20981f6a-ae7b-25df-06bc-490e3d7ff703","c4d4556c-7a64-9699-2301-d8aa058e4096","b413c645-64a0-6eeb-9cfc-b24522d8f63a","c668b80a-6004-f47e-6897-f60fe8fae744","1652a13e-4cf6-7d97-cf8f-1de6a42a62f9","bb38f1ab-01f0-5aff-e1a3-8322fb354dda","ca345538-c5b6-cc10-73dd-3f9acc846f95","c1a5837c-09d2-f191-8927-3147276487e0","550e740c-3b6c-a212-ebfd-d12fbacfee1c","233e6b71-b925-8a60-e09d-c2c1d3dcc613","2e873e0e-30e9-2e8c-0ec5-76ae8cbbfc1c","91c6c002-aa48-c40c-eb94-c2d2c6ce9387","f4bd8628-fad1-fde9-fbb6-68b207058fad","4fd10ae8-f304-3b56-048e-f387a9b4c279","04c0418d-5912-cd30-2b8b-0ad684dbc87f",{"name":"h_head","origin":[0,36.54164,1.11016],"color":0,"uuid":"e3e41a2b-7718-8509-a501-c13a87118853","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["820916df-84dd-05b4-f88d-bbe80c9c07bf","b75f3760-7df4-f694-3187-05e86def041a","d2d8ae28-2190-b3e0-8982-4c98ab732c72","60360fc3-03e2-1007-9b85-358674639cf6","a7aba758-5ffe-e499-3d63-2584906072f5","58cc8b17-a41a-147f-cca7-e3435f2929d4","ece57724-7e60-507a-ab93-1f99a2f0eb03","623ac13d-7644-fa69-45e2-fdefbd5ef451","07318316-9d72-9ddb-064d-bbf4bf2ae85b","1242df13-8c4c-5466-4de7-c98d18085b08","fe3c2995-33c1-f5d9-bd1b-d6bef0b142c1","15067340-dab6-b719-c5db-ccddd8c13f0d","997d2204-820b-a00f-3097-9963e1100c6a","0e146668-2a18-534a-dd8b-05002a4f7f5c","1a7991e4-745f-7efc-444b-299209fd9f79","2a05fb72-2e28-bf6e-8f2a-9450b726fefd","da8540f3-6a51-c071-0867-38b58f566207","f0b034ea-a228-d9e8-8bc5-94e6d27857a7","41431638-6882-7e35-c079-f79f0a85c310","b15ccc49-db06-5d54-8e86-936d7d405485","ed613350-2d17-57fa-a4d2-b97cb9cdd58a","2f5d276f-2374-36e5-0d78-d927dd2f992b","db0e95b6-4b6c-acca-df2c-3ac9b66cb68e","c1a05c0c-3ab3-9553-a6c4-6e0d418cd875","d11eb44e-4f69-fbf5-844a-7c4cecd2193f","ca91afbe-2f07-a0dd-e8fe-da988c1e6e68","26039549-7222-b70e-e174-6a9ef3e572c4","8291fbf4-6863-c022-2659-40bc06c7fe75","b954a18e-ac4f-0766-3526-c851081a0c17","1348e076-4cc5-4f8c-f4e2-a261ff98ab5b","6e105804-4673-4479-eaed-0423f5918b41","331420c7-b85d-798a-9191-a0d65b746238","74341beb-c746-c557-18e6-3bdcc8579260","9f6c769f-5d82-d995-6b33-028a7b5730d4","01a2d1ed-ed02-b637-51a2-5cf940965937","cf4d790b-d4b4-7a65-6206-b6c09a260faa","f8fa9e70-c280-fbed-5a29-37340fa87ce8","f36f47e6-b78d-00d0-582d-beb80e4b41c4","46051a90-1e40-4677-7aa0-1c2f182902fc","2ca2424c-e508-d492-3fe5-e9222fc5921f","5365895f-d7bd-c3f7-e1a3-73eac3124234","f68c9e09-063b-d45d-67ba-5784f7f12563","cdca1aaf-a948-f853-f008-51da7819ece6","dc68f85b-829a-93b5-feba-70599a0a5929","aeb4b18d-0dfa-fbba-3213-36a2b8301fbe","89820f44-93e4-0e7c-484b-8fa0e83e56ec"]},{"name":"cloak","origin":[0,32.04192,7.5],"rotation":[-10,0,0],"color":0,"uuid":"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["21560076-f4db-b654-055a-793913997dea","e151b038-dd4d-7183-9426-8978e675de2c","f2a2c076-3830-827c-a89d-9f32cb8332e2"]},{"name":"right_arm","origin":[8.08957,34.33184,2.87003],"color":0,"uuid":"8909e1fb-858b-2f76-7079-1923d78bfb4b","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["9681954d-8e83-f105-06c4-e9fa51ed312b","4dc33995-1196-37e1-bf4c-f3fe418da321","bdf0da6b-28a5-f3e1-7130-ad8588783047","0721e534-324b-d2ee-e49b-171b151cd7b6","dfc17fef-2439-97eb-2f60-f03d53a9b1c0","b2a87c88-5035-521e-27bc-4aeac5271565","18eb35ac-628a-8ef8-3c6e-8dbac690df22","22bbe239-0026-6040-7756-3fa9d0c984b8","3a2af0ee-d802-79a2-cb5a-99891015755e","29a5e95c-df63-f7b3-7495-1a02c37f40b8","8d01d4a6-df49-be4c-8e8a-7b488fc41a79","f193499c-22db-5035-ce51-f61078f27294","84f643b0-2711-5778-7f8c-81ccbc28a2b1","137bc3e4-445b-981d-20fd-f1039c3c2cbf","6952340a-c4cc-6f9f-7cae-86bf966078b4","34e10c22-6551-673a-cd19-e2d348ba1e23","d97eb0a1-681c-6e0f-ae41-2dff2b52b9e4","7acdfb4e-2563-277e-8412-a2a4d2fab162","953becdc-ed25-d2ba-a418-ea805624ad64","560d51d1-acc3-bfae-e3cd-a81b839151ae","fa971384-8820-d365-fcb5-a99f37f4f75f","47499439-5a44-785c-6e79-f143d2ce5073","16fc3b7f-37f9-d76e-bafd-e1397a513ffd","419267b6-458e-9b4f-83d2-43b0e6c69f8d","b58b8a38-dee1-2ef6-f951-1f08a07bdbf4",{"name":"right_sub_arm","origin":[11.88585,29.64991,2.88267],"color":0,"uuid":"a512dfcd-47cd-1831-ffcc-611e43a564d9","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["67a5e263-dd1e-b00e-9b95-a310c307f747","52345605-2265-faad-f6ad-eca1be3dec6c","0da7fcdb-b43d-8b27-971b-6df95829641c","639ce11a-9c6a-6c42-3379-700283fa19d8","ccf114a8-f053-924b-d8ca-24b0a70368da","fdc93b97-72a5-e783-9db3-5a032ac3608a","3251f438-779f-574a-a206-d180ea007ccb","fa153ca0-6723-e9f0-8cfb-7a3617eedafe","73685916-cd25-a75a-fa4e-c1371475dee6",{"name":"right_hand","origin":[11.19883,21.58754,2.91553],"color":0,"uuid":"dcf45c2b-cc07-3246-e68f-1319556212c0","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["757cfe40-f697-b3d4-3ab5-931a0f300a16","f8c3aa34-a09e-a0a1-10df-c672519263e3","70d08c0b-6a6f-3aec-4064-a6b8dc9beb84","fb17833e-914e-9268-4340-f5603d6a03e4","1a73524e-26f4-7fb3-97e2-8624cd92cede","b4965020-4279-25d4-0eb4-1f7cccba52fe","e369a8bd-344b-ef80-43d3-765cbfcf672c","a9292015-e3c2-6dc9-675c-6234fe1a6667","bdd6d7a9-0e80-b7c7-aab3-b5f075817d54","a6fa060e-d211-974f-c460-2142b9213f67","564eb56f-b9b3-74a8-a7bf-fec1f711bca9","b9e03e5e-eb23-1c48-294a-0003ec580bed","69aa58aa-ca4d-d8ff-032f-fab509531f5a","c5f19499-a1b1-8a9b-1503-6f6e12280777",{"name":"hammer","origin":[11.29242,19.03893,2.94222],"color":0,"uuid":"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["c087230a-862d-9d6d-e4cb-c9d00e17a4d1","80fdca4d-b28b-c0f6-507c-625123d48c14","af406e05-afd9-6e24-703e-55b85b6a73ac","8b5ad391-f368-2fec-1d47-55410fd47190","8977ffc9-8598-74f8-bf1d-68b5cd2583eb","4ff64c77-291b-3c79-e625-bc7ebe3a8bc5","3ba8c767-b1af-b6b4-a90c-2853d99a5911","a74a46b7-034f-fa99-4db3-6c56e6f71589","89c33bc3-1e54-c13e-afef-9a0ca6ef63e3","ac2d484e-6396-ee68-ed13-3a4b61d4b29b","304765d9-7128-9347-1e49-087cdc5da139","fb35b537-e58f-2fa0-f887-d6d019c2932d","a2f6ed5c-0f4c-374a-5435-973f5eb5f187","e0147d4e-cb1c-6164-a545-637ffef444b6","fc170399-36d0-a3cb-d49e-befa5de166a6","f3848d14-f6d0-c14a-9ee7-197983dbb509","f1a6a1a1-1fbf-b2f6-69cc-0b02165f442f","60aa03cc-070d-71a6-406f-6d50e0d1361a","90eb0ce1-ff48-4351-bf47-d88694dcb183","46417fff-471d-3791-5732-884a2289abf4","84ee9ba3-52b6-09b8-d529-4a2cbb570f77","b7510d87-7ad4-243c-b939-ece13db6e1db","42dbda59-442e-cce4-8243-bae66ee979b9","39bfba34-edbb-7c44-d1b8-c277995bf5d8","017686be-6b99-fc6e-430f-51e630c857fb","4731abb8-728a-e9c5-a444-84b2ceb44bde","d878a78b-eeb5-f72a-ed3b-7ec05126ba3e","7ddf7986-5648-d1ef-1cf5-592925b33ae6","881889d5-8955-b00a-ef17-f4cad22b51d9","4d32635b-7169-63ea-8edd-2058b2f5844c","c5d62892-6810-ea9d-d775-bb4365179fc1","ba7003e9-c068-e689-cd00-b2d418bfc31b","50527203-69db-5358-9c03-7ce2948218c8","ddfd78e6-b1d2-bd51-ec01-c77415cba75d","2faff165-0bac-727f-f020-671fa6b6a2e7","2831f117-3c8b-9a36-df4d-9ecad1568fed","1c221324-8e5a-d711-e550-a1f5e3038d8a","98bf52c3-2eb1-5d25-58bd-72b0e6a5fe1e","9d609d71-99c7-9a40-eeac-222423bcb4e8","95560117-7d00-3533-3923-c0492750ea03","f3f5b091-43dd-29d8-afc7-d5b805eeb9cb","0fa4c566-f097-47cf-20bc-c8e93923dbeb","d19b1c81-1041-090e-b271-57d212ce1fcf","3c008bd4-bf5c-033f-d1dd-55987d3bc51c","84dc7bab-eb3e-3c92-766f-674f5fe8147d",{"name":"vfx_pierce","origin":[11.29242,19.09131,-38.08726],"color":0,"uuid":"f16e4292-9e8d-235c-1fc2-b1d02dd31366","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["751aa955-ee70-e57a-5860-8db70824e547","a53e20df-9f48-7aa4-7c3b-335394525a55","12dc434f-721c-d2c1-c9c3-379413c4712f","195c9da8-bf7e-278b-5861-541e642e9e06","ca06a670-d403-c657-79e8-0824cc3fdc41","9c0d5db7-e69b-cf72-399f-66cf71bbddba","5f3c900f-a480-cb33-b171-ecc6111d8927","3d1af7a5-df34-b320-f067-da1b3d2f8cd0"]},{"name":"vfx_slash","origin":[11.29242,19.03893,-17.05778],"color":0,"uuid":"6b1142e6-7581-b73e-4a67-e134ad4585c6","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["5e478285-5bb1-597c-ab98-f0ba7c72ca3f"]},{"name":"hammer_tip","origin":[11.29242,19.03893,-21.30778],"color":0,"uuid":"f2778196-fbf7-7f1d-e39b-d4f62da49e44","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":[]}]}]}]}]},{"name":"left_arm","origin":[-7.83957,34.33184,2.87003],"color":0,"uuid":"28aa58e8-cc75-0593-0775-3690cbb1dd9c","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["d603d704-1925-f44c-266d-72cc47243394","508cda1a-e3ba-807d-6286-d5d1cc9fb6e4","54c37abf-dbde-9f65-0956-990938e33023","722703c7-5013-9ab7-215d-48e7c06ff787","ef1ded35-14f3-8696-879a-601eae597e63","ef81dede-8278-e312-7aad-6a074c2d8d48","de921633-70f2-da2a-1f66-363321a64837","845173dc-0d83-f68f-e654-bf3ca32bf352","e006de73-92a6-ca4a-8d6d-4690f7579d0c","fdb96169-284e-3bbd-53d1-03b4d5fb6897","7fd3fcca-9ba4-2adc-2762-35fde297b11c","3a0e61a6-2d75-adaa-7062-4259da88a4e0","91149954-6324-2c6c-09e2-731c212abb72","b8894893-a530-25de-6c35-cbd6b147abe3","58b6c4b7-3e97-b3c7-1227-a0ed6ed79a99","df480e42-eb3d-2448-bd27-bfe5d6b68cc1","dad8a33f-b4fd-6f3e-6c94-b04e0b7fb511","7187327e-ad38-ae9f-8f99-2ad9529ac9a3","f877ff54-6c50-4e2a-11c0-e426a27ef36c","59c92544-ec84-5abb-6d63-a1d70a6d7b06","9ec2de2a-8f71-a7e2-674e-8d9e602defde","ec32ecb8-e42d-396e-9431-867be23ad41f","e25b8ff0-483c-0dbb-7459-b417b0e56469","45704531-8633-32b8-8a22-59a1514f7b9b","50a5b6d1-8c29-200c-2a12-ba217ea71245",{"name":"left_sub_arm","origin":[-11.88585,29.64991,2.88267],"color":0,"uuid":"c1a7024c-c2d5-f677-558b-549f2d02cdbd","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["fdddcb40-2248-3a33-3221-dfca397c82fc","c44306cd-d399-8e82-5bd3-e47320ea1792","718c9191-dce2-cf36-c73b-b759257f64f4","2d700834-8646-abd2-38c7-074d7eff5448","9c60e29d-b8ef-986c-fb03-327be81416a6","62206ed9-99dd-90ed-2d52-cd55447fb334","f004b9df-bebc-3520-d00f-ae5b0be298c0","60f5e47b-26aa-497f-5a67-2da702d98cb6","a4707f71-4a31-7c53-b88e-7483a893a6e3",{"name":"left_hand","origin":[-11.19883,21.58754,2.91553],"color":0,"uuid":"08fa9aea-2f3a-ad98-9647-65d992767b55","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["47b56504-f1d3-9686-415e-a4f0709dfafd","5ceffcd8-205b-97a8-bfeb-be8bbcb0cfca","63ad2ccc-25ba-c115-dbf5-20ab848089b9","8acf7fca-4399-fc24-c932-72bf5ca8da68","8d0f979c-d198-2689-ad2d-5351f49c68a7","1779dea4-7ee2-5b9f-985c-8616cbc88867","939e2423-ada1-5d47-2c16-e0f6e92b53dd","4ca72344-81c5-e89d-19f4-dda145e7d3ae","f0e925fc-4f13-150a-775c-1c5d29164e3c","0bbbbb03-7bcd-305b-ca5e-f058b7ff5bb4","fdad1047-07ba-c7f6-deb1-005943d3b357","63e86aa5-0394-218d-e0f8-9ed00cbd7b9d","dc122ad0-1ea8-4da9-420f-0e2874ed1f0d","3e61372b-b54f-9344-36e1-5186b613c788",{"name":"shield","origin":[-11.29242,18.9375,3.32268],"color":0,"uuid":"621973ec-d68a-5564-596e-d9cd80a4a0f6","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["0e653ee1-fb8a-4e45-7455-91bd2048c061","29321ff2-c3b6-8f41-2e32-b6f9c6af8d06","2f10273e-7625-2f00-37c7-7c953f6059ad","916a6cdb-a6e7-7646-77fb-cd5299df3d75","0973a951-8178-58a9-9352-f3fd079a3124","73d20e87-3c4e-65b7-ab04-82c6632ac983","210cd302-529f-f9ad-2284-5372cce7b1ac","76cf3cc6-7ee4-f915-d4c4-4282c96d6969","97f742d7-a697-80c9-1d9f-397363f4952e",{"name":"shield_tip","origin":[-15.54242,18.9375,2.82268],"color":0,"uuid":"ced16314-0730-1aa9-0196-c7a0c2f8177d","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":false,"autouv":0,"selected":false,"children":["a97b80b2-addb-5b18-4657-99a47dd5af59"]}]}]}]}]}]},{"name":"under_body","origin":[0,24.04289,2.51243],"color":0,"uuid":"820eda84-2205-a32c-17c7-d80c17b133ed","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["fc34e1fb-b43d-b65a-94a7-9137ff36bdb2","f9e2b3b4-83be-9820-5fd2-6cc5671c0d06","174a8673-5e4f-54fb-8845-de9053585a39","7069c39d-7c58-42b2-26d5-f9812eb9eece","efd0ecbf-1a4e-dc52-e0be-fe4c9b79e954","9f7af667-56fc-1051-3344-31768975a667","722552e7-6fd6-e7ea-9dab-4d9bc3971265","40ca8419-4070-3278-78e1-6d76eb6624a1","b6c79338-054b-389c-c5fb-b4847f74bdf7","43e12532-2dd1-ac2b-45a8-200ffb0c32eb","00ac818e-4fca-67c4-cb16-1bf156ea1ec8","3a929634-fa05-3210-ff3e-5605ad95f727","92fea33d-fa66-6a60-cefd-7ddb861f07de","08acc518-8e41-a360-e3fb-99702198e77e","61a1fc5e-ca0c-2440-50ad-c9ed748263f8","caaf69a3-d39a-aa71-72dc-fd4da017e228","f78e3b4c-9ec3-6e3f-34b0-f82d64ce42f3","108f8152-a871-a04e-3694-ee8429454f4d","a8cf07da-f774-cb70-728f-fddd7dd86939","4f38d578-03f6-6f4a-c0cf-9ade07a86ed9","d6f505c5-44ae-c646-2aff-fbf44961b32d","3904e256-e178-2e6c-d063-3e84a6ce82b6","17e9cc5d-478c-6abb-ce1d-6510a8f229e5","6d219fa0-914b-4c91-2184-7e3e52b423bd","ed0088d8-cbda-85d1-1d53-989379b69dbe","ee2d6ac7-e504-ec74-8603-62e046365557","82e0d2c1-d8ff-f573-2089-cb24e3f5066a","58509bf9-3d6a-868c-d637-ff183a6cdc0b","db3d2176-8b10-9f62-b5ba-2582cd79869e","a9a3fdae-afce-dfd5-2b6d-f2eab998494f","c7ff5c49-b255-2025-50e2-7dbc631d35b1","948e9537-9de2-64e9-517b-27eeca22ebff",{"name":"front_armor","origin":[0,18.025,-0.75],"rotation":[40,0,0],"color":0,"uuid":"81c98f4b-5ade-166e-be9a-8cbee4eddc3a","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["919f6ad0-5645-985f-2e89-4f499936da91"]},{"name":"back_armor","origin":[0,17.81382,5.75793],"rotation":[-20,0,0],"color":0,"uuid":"6e67f127-d75b-e721-1019-14fc3600d26f","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["9069559d-0e29-81b5-612a-496ee632e27a"]},{"name":"right_armor","origin":[6.825,18.075,2.5],"rotation":[0,0,40],"color":0,"uuid":"5371e06c-b54b-154b-b1c7-cfce2a9b20d4","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["6e6bfdbd-594d-cad5-b4b0-7b3fe365077e"]},{"name":"left_armor","origin":[-6.825,18.075,2.5],"rotation":[0,0,-37.5],"color":0,"uuid":"3a05cd31-c803-0a4c-716f-337c916a2ee5","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["cca29179-e0a1-fc3e-5ddd-31cbb7c1ec41"]},{"name":"right_leg","origin":[3.67015,16.62216,2.25],"rotation":[0,0,5],"color":0,"uuid":"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["fa05b37a-8d36-473a-80b6-04517fb820f1","2a01c2d2-7cd6-7b7b-2271-a69360c97bbb","a6d01e53-e044-783f-db4d-4fafd64dcc16","5c46d5ab-7804-c02b-767c-a261e30d4ed5","1f0c2fcb-f77e-bb94-21d8-46a5aa7b0dd5","0edb8eca-2de0-0bed-b9a2-1cb5665cb3e0","c7656138-efa9-f632-69ce-d6c344f64e6d","d9a8de97-fd96-8be7-1d32-234b444005dc","bbfd3b01-4e82-5915-0284-5ec05be23f5a","7e816966-7fdf-ee4e-f702-6982983edc0a","e85ce7b0-711b-5558-279c-95322808712b","0775f99c-e0db-1022-ceb4-58e90e3c33f4",{"name":"right_sub_leg","origin":[3.6018,7.24722,1.98421],"rotation":[0,0,-5],"color":0,"uuid":"76bfe555-a273-5b15-5684-5779b6fddf7f","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["0646db02-a200-9b1f-229c-08c15715b008",{"name":"right_feet","origin":[3.60202,2.2325,1.95098],"color":0,"uuid":"436ebcaf-7570-8c3d-368e-80184e593a90","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["4a69a736-4e03-2708-8e12-d859066dde6b","67aa1f25-afaf-291e-ac2e-8431eb1b480a","a4f58cf6-bc79-dd90-4c51-6ecad2650c26","513276dc-9049-753a-13bf-2a5cf4f197ab","54e9970f-49f9-29a9-04d1-2c8e7099bf55","b70e23c8-c0f5-91d4-399f-6351986144cd","f3622b46-ec20-0528-a9a7-46b0dd0cbc5b","95ac3da4-49bd-6f19-ca05-4972ba65cb16"]}]}]},{"name":"left_leg","origin":[-3.67015,16.62216,2.25],"rotation":[0,0,-5],"color":0,"uuid":"b2409710-4f5e-2feb-9aee-fcdb2d9320fa","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["1c3c5f85-a621-e9b2-0336-471cbcc6e18b","8a7a2efd-5ca7-a58c-d13c-a0c9d491963a","20579d70-7fd9-9640-758b-9cb91bf8c8c2","b81b6460-4ec1-dccd-76bd-c6aee50e3af8","6a259483-478e-e732-e39f-95f07ae3adbf","0b9618b4-fc3f-7310-9d4d-e38f2fe541fe","aaff0afc-ccb6-ee65-cc65-691c3ab20d47","30868f8a-27c4-db95-7b51-d3227b252417","f32e0837-cf2b-2fd2-2d0a-a4f0b02f63f5","b40b0cc3-9809-2839-b031-5124f77b3721","5f5bd476-ae04-dfcc-0637-40fddcbb54ba","30d51b39-09ac-67d2-92c7-c7f791abdaa4",{"name":"left_sub_leg","origin":[-3.6018,7.24722,1.98421],"rotation":[0,0,5],"color":0,"uuid":"d21a5df4-acaf-a697-c2f5-1e490af18700","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["4b2bd505-e7b7-881c-fe09-1452c0fb24fa",{"name":"left_feet","origin":[-3.60202,2.23749,1.95098],"color":0,"uuid":"c942401f-8d13-b8e4-8a4a-173d0e84ed99","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["19245183-7ac5-9d1b-aa96-e962912f2d8d","4a289af4-8561-0f5e-93c9-d96eb96dc343","8c07b076-c295-d254-3ff8-54610dafefb3","3214c9af-3f64-9d60-ba4b-5af50d31e81d","b61b47e5-aef0-c7b6-d356-aba62813221d","b00f9f74-7073-84e7-5351-080096405a56","9f80b25e-feab-7894-4534-3b6a5d570273","ab201e6d-7749-1af6-e5c5-cc3e1f2dd006"]}]}]}]},{"name":"shadow","origin":[0,0,2.25],"color":0,"uuid":"13f14e6e-cb30-77bd-8dad-1a4a126c67b7","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"selected":true,"children":["00c5e440-634b-654f-a9d9-95f08060aea3"]}]},{"name":"hitbox","origin":[0,27,2.5],"color":0,"uuid":"eda9ab8a-65b4-2f01-0572-e038295f98fa","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":false,"autouv":0,"selected":false,"children":["06fba819-151e-afac-85b3-405b6bb18348"]}],"textures":[{"path":"","name":"knight.png","folder":"block","namespace":"","id":"0","group":"","width":128,"height":128,"uv_width":128,"uv_height":128,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"9ecc68d2-cfec-2635-1845-a32d430a1c11","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAIABJREFUeF7tfQecJFW1/lepc+6Z6clx8y67S1iQoEQXUUAeoPKe4lMExIT5qQ+V8EyIOYEBRHwIRhRRkOSKBAGBZdnA5pmdnZync6i6f8+tqunqmu6eHliQ9/95denp7qrbt+797jnnnijgBbR71y9l5W5LaAxJVeVfXbhtv1Br1+fG6sv2V+7+34yMVez3Ay2NNfXz7YHhsn3cuWbFvPsfTCbBL6b/MP5/3hqd+nPSJ6IoQNP0dz/YP1Ey7He1xCpOw7Z0DqPZLJhq3Ew/w4pDuH92tuKzdjud/MJ92Sy/5u2dzUwSgB/vH5y7J3vSMnbO5lHcPT1dsZ+aF8l8ikqLT99zAGgan6u3bd1Xc9+vZAD8OZmE1wF0HNEGgWnY+eQA8hrQ6WHwiIDiFiHKAjJJDUxl+PLusZoBsD2Tx0gmUwKAeKEACPrUPZFI1AyAN3e1MKcI3Nk7PPf7M6oqEFBMkJRDYs2LVDMA/j+jAPfE4/ApAmSJQZEETGcYcqqAVV6gySEALhGCDGgpDZk8w5W7SgGwtr6+IgXwqIV5FGAgl5u7vtPtLLn337z+uffXjo7yv98Y8qN7WR3yozOYSBTw+9lZOEUBfakC+o/vxsrH+uaoxKEBwFFLGYhiFanWXL9JjSHx/xkF+N3MLERjm9BrQdO5gYgcchrDzQMTJZvIJM3mpFQDgFdTMZHNoEd2zM3hd0dG5vrb4POWsKSLw5G56/4OFY1hN2SXC96Baf75xwYG+L3fb2tjG5vq0exJ4L3bpnDjWGW2uSgKcO/xBu8vAMjPB8H/jzLAh3bvL9k4q3yekvd3TUyVzOEpsRgLyOC7cDrPkGfAg5ZFtd58UWsj01ge3oJcFgB2MFlJ+TtamljU74LicsAxPMPvv2ZYl21eOgCcbAAgC4D+kRwkGmNnQEL9vykD2Ceanujs5gaTFetCIOGdAc/PxEsAcM9kqYB1XH09iygC/DIwW2CYzAOPVdmB1Nn7YrG5nW6lANUA8MP2NuYPKvBG3VBlGZkDk3jHnoG5sWU0TSAh8NzNo/jDoRIC733tUgba/bT4GQCS8Y9AUAASWYZk4f/eKaAcAM5qqYNLEhBbWgdP0I29Tx5AKi9g28wsSNqmpjJg03SppL4uHGZBWUC7W8BQlmE8x9BRjvlW+KzF68afxkpPEqs8LrR4RExmGS48aTVYVoPqcaPv8R1wKiL8SxswtWMYU1mGKweLJxwCwCGVAe49xeD/JKcQEEhGUYwjUh5IJhgSuf97p4ByANjYFIVbBhSHBDWnIqsCWQ14fmoWDU4JecYwmWNISCokSeCEUBYESKLAASKh+Hc1PnuqP4rOY7vACkAmmcHYrhF8Z3d/CTyWuJzwygL//TObGyArDqiijOHBEfgcgEMS4JAAOkG+d+9QyTHw0FKAEw0A0OIT2yIAEBWgF5cDM8NZJFMqFxBfjB6gdYmAWIeA3h0ME4NFOcghArcPVRZoyi0kjW19KFB1D26enp33/SmxyJzwRxNLo6DXbdM6AAqMYSKnIUIMv0rTGOOCI12vMoa8pnHKkdcY7/Mt9TGEPA64HBJyuTzSmTxuHtElfP16hgIj4ZPkCQZVY3h87Vp0PfH03EJ/paWFT9KD6SSaXCJiqoIpRUI2pmJkb/YQsoBjDBmAFt0FuLoiyI7NgOVVOMJ+TPbNIpnUIKjA25574XqA+lYBigtQ88BInw4A2lV1DgHXHxhd8GxsX48XAgBT4Lq0rY7JYGACsC+pL55XEhB1Crgg5jNAog+J/kuL9u/P6ry4IxiEIgp8Ad30MEY7yuctGeJzs8m59/szxFv1ttzroV+GYEghp7qLAqgp8dN1n23UFWCPZTJwSwJ8GhD0ujkANm2dPnTHwHs3LGU6rQPgFqA0eJCfSvIndzYEMbFvGsm4yk8IF26tXRNYdQst4ssXSgFaXfruOiMw/8z+p9kxTnrHshqShvKPJjmoCHh/i59L+3zhRUDSgBz18/RBPurDGqKIuRWM5zRkE8VFrhUAUaWUupzhK+oBtkgCAjKDQ1Pxy8HJklm6NBrFdaOjwkVHxtihpQB0DKSnlQHBJUFwi9CyOrJ9q1ox8vd+JOIahDzwti21U4BFrHHVS18oALq9gFMAjnCFOVk2tL789bbxceQ0IEVb39JIJ/ClJSF4JRFZBUgrgIdkozzDSU8e4FeuaYxiedCDfakCMhY2UysA7Bo8Ot6ZQ/gF0+AVVCiFPH49VKqL+HhDA2sL+tC6XsSN940fQhZw6lLGeb4KOFuCyE3Mghm6ayXiw9TBuE4BcsCFz70yKMBooYCEqtak77hpeY/OmC1Xf3xvX0XQTRQKVftdFY2wdp8DSZWBJYukXTFUvWbHfcn03G9MkCrYaKTKtf64FQDv7u/n332sJcoInPcQJbbYBujv37xpCfvYnf2HjgXc9/rlzNUaQaZvAkqDH7nx2bnJkjwOzIxnkSQKkF2cLeClogC0+NRqBcC3lnQWAWAA4epenZyXawsBoCMQYG5Zgqpp2D09M7eYJ0dCJeSkEgCicikL2NjoxI54DjvjBQzndfBd0hBhJGP8xZAj/quzdW6oDWuFQ0sBNr3tSKYmMlBCXmTHaPerECQRgijCUe/HZO8s4mNZfkRczClgMQC4oqudT54ightpQMcgJ0BrnUkBmZyusKH21GyCC49OUURQATKaLrnfa9Pemb9/Xbfet7V96cDgCwbAEXV1TIIu/T89MfmiAdDuFRFWRGyL57ErleP9neDz6SeAE3ogeHy4cVvRGHTIAXDvaUuZ4JD4YmeHp/kRUJQlCIoENZVDcpYhkTBOAYuwBr4QALgkoC7AIAcAj18g6QuJKWB0SkDGoKL7sylAE+CTJdQ7gcm8hpFsAb8ZKS6G9bfLyRBfObsNis8NQRLgagihENfJdWp4Cuf+ck9VFnBccxOrk4FcvoB7LMfXW1cvYU6PA+7GEERRRK5QwPT+Sfzn9l0l/bU5HGXN2/05ffGtALhtQzsaZAE/PlBkNQSAQ8oCuCKIBEBZBlMLXA0seZ3cnFkgAEwDiYTKD83/uf2lkQGu2djMBncwtK4U4HYD3oiA1AxDNCYgMcGQSANDexk0UtykCiioIhyiBDKV+hQVg5k8bhqoHQAfOCk2JxII3O7PIEkiNFXDnm3Fo105ED8lygiJKtLZHDYNjc8t2vUrOpnLIcPldkBSHEjmCpiZTuNDu/YuGgCfaq5noYgbPp8De/fPYJlF5/GSUAACAFeM0D8RkEMeqPEMCmkNqbiGVFrXmrxjW+2ngMM9npocOWiSe0JevriCCDjdQCQmcLktlwGSMwx5YwPQ+EiyD8siwHRtlSiqmC4UcIvNgmcuXjkKcM4RRQucfZFzpQq7eRi4P69B0FTk8nl8aUUjgl31YC4XJI0hM5VGIZXGpl0j6F7TBMXlRGE2jeRUEl/cvLekL7swSF9ax1rutJB1KXh3i3xozcF/PG0JI4UIF/yNJXPEgshNpVCI55CKM6TpzATgXTt6a5K86drFAOD1sbq5yXHK5IwhQ5BkfhohTZpK/gjGyWRSTSFAu5UBU3lgVlWRUjXcNVZqwasGgMu7otiUMC1e+pXdCnH2+e20gBfOJj8KM1nOEj9wYAiMNH+qik/VB6EoEgqCAociQ9M0FHI5PJbKoD7kJsUxRE2DouXx7b1DiwKA9eRAN365uRkmAK5z+/Dpu4taQ/uoa14kuvH3py5lmqDvcBI9mUC7j4FpgJYVkE0zpEnvSdLpIljACwEACXchl4hgTyOcLgecfgd2PdaLdDY/dzTdnk6QvwZECHBLwFBG42f6+yoIgeUowEVtUTydI74n8H5pQXscKqbzpM4FSD1NfVO/FxzZg9Q+XSnjX9+ML9z7HKKKwIVPup5A8466EEJtIbgifmhqAdmJOGYHZ/Gj/qLwdklDCIFmP5hLwdiuSVzcpx/5nrx4TVlKueFHW0vWkfQAdP3nVoUOrT/Ab07sYRpRAMMhhHYWV1VqArQsQyZH3jL6Seqyl4gCHB0J8gmmJ/Y6RHgcEgqQoAgM0xkVWbJGGhTg+weLhpFyPNr+2TdXNLNv2nz66JoNrS3wKyIKBQ2zuQKWyWmu29+f0hBWSMjUbf/vOG41krvGebeBw5txzT2bEZAEpDWiQPq8XBAOwOdVIMkSZGgQ83k+7l8MFi2AbwuHuCmaaxg1IBbwQm51g4Se6T0T6M8VUN8YgOwQcfVft/Lfe/70tfwotOKB7XOPtePYjkPLAn5+vK4o4fyfVl8QQMYORnNeAPJ5hizTXQTe/RJRgC5/qR6djqCiIPAJU1VtbvfTLFRzIC0HiGt6mtnNB0tNsRwAHW1o9MhIZfMYSeaw1TDW2Pv49ooeuLvCyA7MQsup+PzAMFpdAibyulmYWjyV4q8XHK2zMlPv9MjmokHqvMZ6DCMHX3MEuak0RhI5vMohQHYp0CQFu2czcDocEAQBP9q9TwfAxsMwEU/i+Mf099QIAN8IhfGJ3z5ekdIvigXcekQ3X3dzh3FLiE4GuHaQnjFneLi+5/m+mvteDAs4MRbVKYCxQwiM9DexBFEE8mSMNAjlN/brpNPeTgx5iHthMK1yQ40sAC1uiQNpf7KoiTPvW9HUBI8iIV8ocBDsnZiAqWQyr2mQ5aoaN/sYbjq7q4ScX3Rn6anpfR3NLBz2cZlqJp3Du08tb9G8775RRJc1IL5vDLNZDf87VgQSAeCQuoT972HdnLjO6coNgZC/J5MlY8i8xBTghIYInBLgkgFPwAnJIUMqFOBwK9wzZnZgGrM5AiLw7d6DZQHwxnof52LPx/PcukfgWeqT4RIFbJ6Zf7SzS9k+SSrLi2vVOHLq9KYlzNsSRWpoklNTu07h7HC44snozqn5Quwf1q0quf5je/uw+agWODeV6hbsQKx5l9KNP11dilp7Z5wCGA7yi6EAS10udkwoiGN8XuxMZ/DH8Ql8eEk997oVQhJHnDatIplneHRGg5MWXwKcHhlaXuNgSKsCRIcEMZPDZBbIFoAfHCj6yFvHelLIQx7cGM6qHAA0CVGHCKckYCBt+vsX77AD4NxYiNU5RPygv7w+oRzVqfTZVUtbWaglgkDEC5bJoLdvGpmxouWQ7tujZtHgEjGU1nDnZFGlfOYpJ88DyfZHHi1hAQsC4KdrunknDkGATxQwZ6igWTHgsfHp3TUBpVrMAP3GZTt0K1kt7YM99WhyCZCiJIEzsGkNibSGv05rnOSTKthcPBqcLOkCEz0MsQFqN/aXB8Aylx5UQeTfbNQHcbTdGT3Qwmy0Ux1OB9d3kLyh+N18x87sGVxQE1jLc17a1sgC0QCEApDNZpFM5xAh8mVp2/JZeCQBiQLDPRabwqEBgLGraeF9ksiBwBeedCdkixCBjY++QABYj8/a4gBw7eoG7m8vBGWdSecZ1FkVb37iQE1gpPlbKODELiR+wuKcac5/+3onmjQRKUH3hndB4M4ho9DwgT/VPpZawEDXWI+ibR532duaHeSHB9xmREnd3NXGgiEZ+ZzGHXIOr9flpJXe1MIsgCgAzSgHgEkB6ANy9zJdvjRg46aFQXDv4Ya/gDlsAhGBwBASL9taOwW47rgmIkv6tqRG/SQ0nPeX2hVMhwIA+W4RToMU0kjcJPcAmGEMX32s+jHzrOWlfJzu296rnwKolYvYsQOgzS2i3S1iV0LDmKFkswPAzv9bHHqcQW0AMCmAKMCnkN7coAAEMtPpkwEb76sBAOQyRlSDnpS2C80YjYX+zgGXbV4EAE5t0akQPwwb/3Iaznvw5QWAc6kEt6C7dZEBlky7dNwgpdDBUQeW9NRDlARs2dyPn9v0Dqf1BBlNp1MSuX6E/vUNFo01tQCgHAkwAfC4zZfR2t/YcRtYwDFTAwU4rJsfiRRSaDgEKKTaIm2fBDii5ILEkJtO8Pcb764Ogvtft5JppBCgRiFumq4p5LuXWMCjiwDA8U066Vd0Sx/9YxkN5z9eO9k9FBTgmA1eRFRBx7BIwqYGhywhDQ13bCsg6nGikM9jIpWdB4DTl4SZIDDDL5Bxy+TO/uoUwLrgJ4ZL/QbM796zYSW8rn/ofD0yfv/gNg7O85obS7BytNeL2gCwpot7+ShOAbHVMTi8CpKGLpqcncn0S35/StiHE295sir/ffDN65izMYDknmFOBRwRPzdwaHkdFJf9uXYAfHlVA3c5owhMNqvpelcNOP/Z8mf7WnjsQoBYOhflUuzt2Nf44W3SDUJarsBd4ESHjMxEHD98NMEtgyQUko6gx+bDZx/TA9OzyBpzUYkF1PIc3zlsFSPNZ15VsW96grusvz5WGoW83i+jTs7USAHI/90poOmIFhQmZrjtOz+jH0UkpwNaugBHJIATb60OgHvPsMkAptHIOFFc9sAiALCiwdDuGOK6IbG/lAAopzn871e3MJIBiBsRV9Sj4hhxNDy8PaVroRjjGtH1FYQ2c1F3pjLcbbvDI2KCHAnLtO9VOLlYL71hmX5y+9X4FP/4w21NJT151zRh9f7ZGinAYUUKUL+8DtrULLxLYkjsGuS7mPz9NYJYhuG1D+ysSgHmAcCwGTgaAtx9bDEU4IvLG3j4YZYx7pVL3ICE1P/Y8tJRgHIAII1dvSrApQlIigwBTUBKZBiVGO58uro/wEVhP1fXUjtry/aSuXtvW3NZRY8VADet6mR1hzVhYv8U3vnE83P3LwSAvMeBo0WGqJKujQKQ4OZwCvD7JMgKILoVKEE3siMznOyKggwtrmLjw9W1SvMAQPOTI6cRBzePXvZY7RSgnID05ZUtTKGEDBanyg9u1yNia2kLsYByACA9QKAhxC2KGXJESOUgel1IjU3jlseKAl253397NIDwYY2ALOLkm+8vGedNK5cwr1eBryUIJio8CGJszwTeub0oZ/1gRSfzBRRkMgwXbSl6H5kAuGxXdZ8LCg2rQRFkUACXgFCDBzJUaIUCXC1hDgCKQ5MdLrAccOq9z1WnAK8z4gaM2XCEAlAnM2BpDVqmsKhTQDkAfH1lE/NLIpymroL8/uL6kCg0inMaQTfRkpKIJHB6Ja0gvW7LZNDiceBgTkNdI4Pk0DDbL8KlKBjMqHhkYL7i6MNtzWw/eb5Y2iluhauPKWcAiSmJAkXuCLh8d6k+/6drVzKfV+YhY2c9uqVk7r68pIMF3BKcXjcymghBzWN6NoNP7i6eci5qjfI4Q7I6kgtch1vEbAG4/kDRu8gclv0oWLMQeMuabiYqJP0TBRCgkLMCuXsRzeVWP1KviVDcbpx657PVAXDqUiZH3dByeb7gXHQuAM5IANn+WWx8fOGjZLWd/M1VLSwqiTxmjxv6BWDToAqPQkctYMURTUhlNWRSeQhTcXijHowfmMFEFkjkgGeyBQTId0AS0N2oYTxdwPi4AJfTiYlsAZsMu7t1DCYAiJTTgYTONad5ZXhlBs+SBp7SJb5/HDMZ4H07S4+o31m5lLWuaYTHK2HTw3vR1hWGy+2ENxbGWN8EkEkjM5pGtC3CPVslQcP0yCx29o6izivB0xhE795J7MwVWc0DM0VV8GkRXc9glwFCx3ei69mB2ljALau7GHlMUY6CQNDB9euFdJZPrm9lM+I7h7iQ4wj58NpfbK4OgOOWMsEvguvMzCsLgLcrhuTzo9j41+osZCEy/s1VzSyiiPAEyBok8qPhn/bnOCAoSJK2PP3PIQsQVQ3TJLGpDKTeJ/Xw46ksom4n6t0iAgEVO4dzmMwIqHcrcIsMyzURXy8jhJ3b3s7IeTOiAC6BYQWZn0lNIuvHw2ye4vcEfNBGAb6ypJORv0K9B7jnwAx8ThlupwLZ5eQBHZOpHDo8CiSvG4ogQiDn0HQeW0dn4JF1V7cUObJa8gaVA8D9k6XGod8cu56d+9hmHh6+IAu4mTSBlOvGIcAbkODviCLdN8bVnXLYw4WY/FSKn+fPur+6F+y9pAgylUemSzspcQzVQC3axIUoQJ1bhKtBhiCL0KYKuLsvz8k+kXsi1CZ3oIXhFkpLJpMnkkks9SmYVYEppmIiTvK8gHqXDMLUOsVdFgBndXQwRZZ5OBhRx9EJPSGDvd1tW4ijLBk+/p5Ilt08X1veyZq7IpBlEWOjKTgyaTw5WJqD4AYiU0b7vOG63sIYN4qF1zZh6JkhEJF4Qk1yr6f/bGzgV5/TgIWFwB+v6iSpilMAj0eEK+iEmsrq5/8ZUncYs8iANz5YHQAPvfUYlhmeLImsMdkAvW58+MWygGZW75fhjOnoYmMqzn+sst+BSxTLStrHB918jAdTKo8JpBZS6Hgm4Y7R+YmZTm9rY2GPA2FFwmgyg5QtSYS5ODuTRSUPfRax6AUqAeCqng7mouhgpwvJTA5iPoe+iYUB0C4wOENuOJwS0pqI2YFZ3JVKIKMy7E3rruvkD7AgBbhptU4BdAAI8DQFkR2dhac1hPRoAs46LzLDs9AKDOc+tAAFMEPH+OoYKmAznxCpkx96sQBoYfURGc4wOeEBbDyP85+qfCysBIDTox7uNLIzkedaNNpebW4JjW4Jtw8ZUqVlewdt9v/jggF4A+QjqLvHpZM6iF4IAD7a0cw6l9ZxD59EQcBs/ziGxxYGQLPG4A8rcIU9yELC+O5J/DWfxHhWw67UIgBw40qdAkgOwOUWEVkZQ2LfKNwtEWQGp7lHq6vOj8xQHOc/Wuq3bieBFDjiaPRzCqKS1GWkkPGtaEZi+yA2PvDiAPCtNc2sPuLgR1aWIBKu4d1byzt90NgqAeBwn4tTNooSMr2HiAJQ1O8jM6l5pNoOgKu62vVHpystNObb/aVRRLVQgIua6lks4oHHpUCWHZidimNqshgrSD9jZQH/buRUXCpIqG8PIuiWuKZ2bCqLLbkcJnOLBMCPyBgkCPz873IL8LeFkBmLQ3I4kItnQLpsb3sEiQOTePPDCwCAUsiYxiDOj0Uw8o13SDyHQC0GpWoywLdWt7A6n6SbrDMaJgsMl74AALQouhbOzh9oPU/0+eYN4dfTehYus5kAaFjXzANEJraPcHVwNQA0K8VMYNWecaHvvA6d/a2UJMSafXA5JcxMZjA5ncUtk7pm0Gw1sYAfEgUgAUoROAXwNniRT2YhShIKs1ke/OHpjCB9YBJvqQEArvYIssMzYAUVStALNZnlegVqgakCgrKIST+QlYFoEtAyDPfOZObcDM3BH+aWudMHKWDMnbYtnUeEYgEEgfM6WsCLXwAAKIFSpYm+KBqdJzf8dmaag44ETbJLXdHRVvb2z/XqkSI/WbWEh3xpTMCPKgiMdN1vD1amXtYf+NKqHtbQE0UGAv76hO70uZrC3SJOuH0OjA7EEc8DPzUAsLyOLEXAb5fGFpYBfrCqi1sDKdulyyXA4XfwCZdcTmRGE9wyKEjkaMnw1kera57uJgpg2v/JZ56MQYn0HADC0wWEG90YdahIFFQ0Q0Z+KIutUJHPkHcxQ1rT/x3hdYCUIBIdh+hEkgcmyevX2LmUo4/GdNYzla2DlVjAYgHwUGoWbR6Zg/FguoAj/UH8rErK2ocuPInJThnjzwzjppFS6mFd2FoB8KGeLhbwyJhNpNHqdqNjbRMm947j88/u4d0dsMQK0vszWoLsxs7O2vQAN6zo4CxAIvOvW4Tk0HcYWb7IfMmVYBRdRflo/1YdAL87aQkDCd7cxYTBXRfmpmSVUwCGFRQ4XOfAqEdDOqOiWZPh/ocgt9Wj6Ra/rIZJlWGyoOEor4KoIiIdlCD2eOHangA52JOBXYu5MHEgwV3QqwFgIXJa7vtyFOAA0+CwqaDt+QGtfd1x5HKWE53IawL6pkrJsvW6K/bVZtpe6XbPUaW3tDbB7VI4xfzeLj2H4SmRUMmjjEhZ3D0wU5se4PqVXdxpgVyqHQ4BpJbmBi7S5Rge33RKoBFc+Hh1APzquB49gYThxOHvaUCidxyM++ACHQ4g45MQ9wKFjIqQQ4FnNI8+SQOboWhOVgSAz4GIQ8Rkg0g+25B6U2iaAgoOAfmwAxP9KW6bf7kAwDQVoiDqmfEE4A+2/IDWFXjsM29kkwMp9P99ABOJREUcVgLAB2N1LOwUkcgz/u8vyaKT6C1v7Snb33fv0hNLme267o7arIHXEwXgDyXA4RAhyYYDB9/EFjGXARf+vXrE761H9ugUgHsVGZSAXglRGkNdTEBBEcDcElhGhcfrgDaZw8x4XvfQVMGlWJMChJwiRhslUHC/NJFDbBaYCogQ3RJy+9M8N++hBkCl1Toh4GMSaet4CjjgAVt+QOt9txzWyeKqzOWUj+6sLjj/V1cL6zmshUcwzw5MYnYmhT0TCfgppIxsTwWGtzTH8PPhYg7i/zxlHWfZThfD5T//G//pzanS08tnmpvZp5f5FpYBrl/VZVJ57g4m8vBvPfKXLz+tH5kDKPXbM9UpAI8bIKMMURDz1ZThNKA5JiBPRzinBEllcHkU5McymKbUckaQydS0ivG0iqN8DtAuGI4JEMNO+A9mORWKh0TdCaMvi3zq0FOASgA4KeRjfllEvKCnbXt4Zr6+wB5b+I2lXXPdnb21aM61/sYlrY0s6HHA6VCQy+mBJ8sdLuzMZ9GyrBGhqBfbn+jDJHllGe2s7nYeki+oWVy7W7ewvqlRjzS6cWBk7rqaTgElFMBIcsghzvWqFoWOunDmr7m4AUNu4H2YhjQVWBsUkfdKXA6QBtJwkZVOIL/3AgTSxRYYpkdVjKULXAYIukSM14mQPDKaBgoY9RMLEUGpKOXhHAqzLx8ATo/4eSzAwYzKd+WTZVS75QCgBFwQFBGv+0v5CF0yNpkr9txMcZFXR3yIkipeA0biWXxnX3U/iCs6Wnk/t1koRW0AWKUHe9Dp0jzq8B1cPH0ZWRIJAAtQgDWWyCG6n2QH88ClAUf79DeUUk10Sfwfm8zh/mQOsaDEr+cUIKNTAGIBibAIV50Lwo4kWEBEvN2JTCIPaazdrcOZAAAgAElEQVQAdUp92ViAddeeYVjh7NTCrgn85rIuuJUCxKAHp/y51Bxs3vuxdh0AZK62AqBasYhyVIoyi1PsQKtb4gquFsmF760OL8wC2rylKcmtnfcnyxsw6Jpq970xPD+O7Y9jk7iVFCeMcXmDrM38jE8JDhM5xHRbKybzpacAeCUo7V7Mbp2hQwmEo4KYGUpCmFRRmK0OgJtXLGFBDwMp97h3rkL5CwREbMmXaAyVSHS5ya4VAN9a1gl31AlJBJ5PZRCMhZFnGvd0SpOX8Og04pMpPhfTZK62kHk7AKIyCWfAxmiYD+lVbhmRoIyCx43+vllu9Ppf48Rh+lLUZA18oQDoMpIT0aisWhV6f2aomNDQnMBn4qXhTvT5IzN6ouWbD2tllFY9pelW3ik6BvocqJN1qbtARzAISBF4fCLiDnDyn0stDADrAnqcDLmCADfIZF2aD/C8bbWbqisBIKzIuKA+CpF0J+SUQuFqApWUAR5NJhDwOHncYjydRSZXgMg0/MLw6zu3p5XnA57Mqni4v8jHafxW5xhTFUyawPo6Nzw+BVODccxkGX5iKIJ2nNCD924Zq40CdFehAPuqUIAVdJ/hEGl/Pa0MBSgHgMNtod70sNWA8uiGTkb1eZjIeJwg7aJqpwCiAOV2MFGhuWYg4aIFpHVrP+ZupM9Wut3wywKa3RKGMyre29MBl1+E4pNx3c5iqLZ5/9kon3LmN3mV5wxIpbPoHS2tOlIOAMe4ZfI0g+x1ouB2YvTADA56Ze6v8PWeKM55/AB+u75hYRawxlzIMjO1tUrNmnVVgPPqQwyAoQzlp5/f3tgQLWu/N68sBwCXwjBKHiI2EnD5ntoDTqwAWO5283SxBAKql/Bfq7sgZbOQBBVXj43wmAC3ogeGUKLof1fKl5D5UTzNfS8KhQJ2j+tJJszWbET60PuPnHM0lFwOu7cN46f79JzEXz58OQLLY/jBQ89DYSq+3hXCJZuHagPAUV4vMz1XqTOKeDHfP1kFAEfTfTaDivn+6JcJAOfGovhKhQhgzlpsFMDtIF96AUPpYl0ec5I/XkYrR1K93+NBSBbQ6BQwmGUImuZD48YRI1Wu+eyf7WyFq94NF3K45sCgvkuJHWh6Iuk/7dELTHzI5hU8oMjwu5yYzmt4YF9vRQC8vqMZLoeImXgWdw7oaWU+s6qHn70veUb3PDarhdVEAY4zeHm5HfZoFQBUu++IGmWAWllAJQrwjtYoPru3fASwCYCQl2HakGXNfBbX7C+f3uukMpbAx1UVMaeAFpeEvUkVARsAxkhlbmlXdrZymUiRNLz3+cppZs9u0AM4zdYriQi7FAxnNZynafCQi157EFO9MxjxCPCG3OjvncEX+suXvLP2tXnDes7jaooNLLfw5uRRyDX5hFqf+R3Pl3cKeWdLlPQ7CCoikmTqs7UXIwNUAkC5mnwkbdvbCo9uHbM2q8LE/LwcAB7L57CUdBcaw56UimaS6IyINbKZNLtEDFNupLyGkENAssB4GPfD8eqKIjsAHsnkIEsi8irDJS4XpxqmV/PTuQzPc0j6su8PV66XQHUPm15VnPu1e3ILywCVAEDZLBVZPzZZZaZKAHh9XZjxXPWSAL843/b9fxUAD2YzCMkidx2jGkDLFZEvDj0r8f6RvMo/D4sSGl0iTwY1ldPw6AIAKDfvVmHPPGmY9QJSlpCy19bpR8FP79eNSWacAMkKBAClPoj82AxeFAAqSdCVAHBho56azNpiVFTCqJ71zOz8nfliWcDLQQHusxRwoGc73C3xZA3xgm6afj5JjqVAvSxzVbpeGQQoJz9VSmdvzpkVAPaScR46VxptIQCY1/3TAZC0ko4ykG8uE0xZ7Rho78L0kLV+fqhZwE0Tpbn4TV98+k3aflEH5SAUsGWm9KTyVBX5yTpe64nCmn3cDpbPHNUNt1vC4J4ppMhBA8BNg5X1BWeEQqwmIbASC/jV+h5GErMh5M5dthgKsBAAJkQRzR4FE3kN6VwBjQrDrCbAqVDePwGteTpaabhxqHwihhcDgHKmWLP0inVOzFp85mevCgbmKB0BICBTwSbyqZzvSmbtp5LzhxUAxEKIhfanVbiMRTb7uKgpwpViqQLwP2XyH/7+Da9iU0MJpCy1B97ZJL1wGYBYgCIV8+2YAzmUABiXFTR4nUgXKKFyFpQQxaWIiHoc3C7x9/5JZDQNmyvspn8GAA6zVfM0taDdIZ0vV2q1AGBD2AEKfNmRyHPVsLXtt+Uusv/Oj49awzLkf2nRmdQEAAp+tHb28MH5Klvr9ye0ennEsOxxIjsZx32PpeAWBYxZwpfM6xeiALtUxiNlYjIpQPIYIp8/lwMRMhNrAh7s03PmVkq/9s8AALGARCEPMg2TJpLiHUiFS4WkZvIMr4uWeueYc1FOz0DfWSnAxgZd+t88nefxhpUA8OMzjmVsYgZT5LMZ12Urs4iUlXXUZA18IQCwDuzRxzOIKQq2WLxWzO/fVDdf7Wk1unQGAoxs+3UUYqVpGM0WoEgSZFnmx67hWT3p4WLy75UrAV9uV5Yz/tTCAggAWa2ADreE0azGPXYoXSx5L1FUzuk29yzzt2MdQTi8Tri8bqSnUkil8zwt3CeeKaqLF6pAYvZ17fqVTKSklZk8qIAHNZNVLRoAd1ywjFnr1y9IAdp8c5lCFZ8Lf7xvEh5RxKil6nU1ANhr8dK1VscJ895yC/SW1ibWVueFy6tgT+8Ubh+YrxT51SoqbWc3T81/f76tOAP9bi0AoOvWeNwsIItQmVE/gJJriQKyKsObGsrr+jVyd5IVOFwOkD0jmchA1Aq47kBRkKsVAOVOE1RhLOwQeb7DoYyKA+l8bZFB937kWJbo140PnqYI7n2yehL8M17djfh+fdCOoBfSk2lu8cqruhs0NXPxyu3GcgD48pL5xVXPL2Odu6C9lYXcpGNgmE7ncPuB+bkBqPDTwssPRI3YACt1uGOo1AhD3/3Ydgqgz7pcLkYh3ySoukkxZGkXVABAJRbwntZmVidLeFoD6hXyvFLRYEtXG436UNca4urf7jUxTCdUjM+kcf2Dz/JfLhdKT6eAph4nbnqqWI28HCUUrCxAVGQ8tL984KN5M8kA1sa2k58cA3e1NxTi5u6qFQCfthQ6MvsuZ507q6WJyWQYoYqauRzustTJNe/7Zk/nnI3C1M9bX0mUJjfFJgrUsF3wh5H5iaLLAaDDqSeapKWnlGyULZyOgiQDnLFIGYAAQH3tlB1wQuOy0HpbosiC3w2XIiORycHnlpHVRKRzefxpeIxTXpNFfrGjkf1wuOiFfNKa0OIAQANZkAXYADCzZb7F9Z2GabVWAHzImuPGsNKVs86dWB9lkqQXa6Bgk7+Mz0/VWq7wkxWwzgDdCwTz83P0PDQ+34e/GgCo3zaH7r0cUgQuA0wZxbM5S+ksBpDYWZrdGPSkqEBTVQ7so4x0u+a477fIV3vS6TkC95GuOkbC55MTecRcIlpECbdTJTejLQoA7sYw0sNTNQFAciqc/KdHp3Fws54IwmpdHaTCPbY2Khe4oWNnvDSCli57T4slw5XRUTmSWY5H9+fn5+mxLtpyb7Eczc5k0XO2HKXYPF0alFmJBWzw+Zip9VPIP5ECVwwZIG24wNO9X9ywDNkxirCe73FkB8BWiz/gEov5l/qpBIAPd9Xxso3bpgucCkUg4s/xol9hTQCoNEHl+IX9MxJGPnV8DNOl1kuMJ+fb768dKc+L7AKNjypBWVqA6LXRTqEiQbb2QgFQjlJsNzKjWX+iHAU4NuDniSpISUUVaqwtYwHAtStbUchrKAgOvOGZbSXhaHYA3D9ZZL2n+txo9UmIrW3BM4/14+sWA9CVK7tZ0OOE1BjE9i16ZNA5J67D7J4xvOVvRb9DmteXBQDWhz8+6uO7oaEw3xr4SgPAMdEwa3AI3JBDBpwt07qdvpZ2uM/LqCBVAQK8osidQUgnwP38LAC47vgVPOaSiRJOurN80YZPdrSyGVXDnyeKvPsot4vvaEUSMJvT8BNLiZsPL2lnHoeMZE5DPBPnGdROqNcp6Mz0JL6/WBngxVIA64StDXr0ZEbafGvgKw0Aa0MhRmAlUzeR6MUAYKnbzUzHmZgicwGQ0vhmean3YkqS/+lpmTuQvu7Z8j6HphB4/cHKfg3lQEnskBJjhV0CVx7NZhkGsyp+Z0kf+7JTAL+DAiiBjZ75evFXIgCsE7sYAJinALqfrIDkQWUyqr8bamsri6l0BLTnHeC7uMY6x9/obOKq+ozsgD/ixnj/NHan8/iLJRTtkADgnFY94MBsXRSpYLTfjZYem+zn0dc1RvkuI2MPOTN0eUuPkIlUEvtsaVXsMkDCSHdi/qa9RJq9vq7dT7AShSMKcCgAUGfTJ5hWwEMFgEuaw9zfn8rX/2y0yKaokHSgJwpn2IPk4AxSg7OggtLXbVjJ1FwBvpgfT08MLHwMXIgFEABI4nWQ7rvA0G4kgyZvlTts52Y7AF4bCzNSk/amGZIFDcuDQT3y2EitmkgmXjEAaKeIT0vbnkjxukCLUUNb77cC4HpLuJb1GjuYy1GA8+pCjLLmk8Pp7y3VQm5b2sbIYyuT130OSe6++EC/sPO4HjbY0oZ33fkYDhkFaPHIaHBL2JdmiKQzXNDzK4C9wpYdAMfWhViPR0JvWsVYjuFV9WE4ZQkZXn5NRSFZeuzSBepipMF7mmKwaw5fKgrQXKrQo9g/PpqXGwD/092B7VPFszyNwZoixqw8fnHEj+CKGD9lZA5OIh5XcYkBgGcyHvxkXz9q0gQuRAHOa2tm7X4HUiIlMdIwM1ZK9qvFyS8L+LjOnOrb07/XNNXB73bCwQronc0hHY/PeSGT6xNFt1j1CR9qbVo0AFb5PCU7uc5Fvw/4ZeAARZ4YECNHDvpH+vzv948LJ4V0O7/pOHqAciUuotnBb6UAN41OYIctlvBkWyp4yai2Rj9p1wOUA8DZATefK19jAOnROGSBYXdcxXuW+LF5WncYeUsZe4f9kYSLWqKMjkJU+NDazOSDZ7Y0sZBLQU6UucOjOlOa8OCeKnHy9qijtXV1CFFyO6ZiMp1HhgssxR2vWiRoGsvH2loWDYBlXpuuQKBF1msMES+lBSaVLf1Nef/Ir48AcIzfx8h9LegQePDnvtR81/FqeLADwDTzVjLw2AGQtPj8HWdzq+/2OBFaGkN2aAY/26+byE90uuB3CDxXAZ0GJtIM+7J5fHJ5CMuNZF4XHRljC9oC/i0WYrSwBAJrExnjpJ7StVAj40deY/ARw7G0TZY4+aP9Ptbg1DOMULqyOqfIeT+FVNNEU8p/uj3m1MOs4/SBpekeZEUa8J0LuiBJAlSV4ShLedS/X7yGKY1h5IancO1do2htDkDSNMwU0thj82egc7LZ2jwSn7CRjMaPbgFFwCRlG52YFszgSkrnTnqB3cnFAeDYUID3R3mKTw0ULYKVYg7tALB6Pp9gA8ASn5tXGSej2+54CnWU2bQhgCseKg04fWc0yggAezQfvvN8X20sYJ3fy9WJdo0+xeXRjqDJoN1DigmKOtUTgOiVQOjV6v26we/jGjLaZQQoc6EJYPT5bEHjsfVEepP/SJ5gB4B9h/3ord1wt0SRGZzE2huKD/vMpYcxV0cM6d4RXPN7yqvrgEOSeL7fA5PTJeObsejVyYmDxkz5hUjkIzDQeJ4q48G7COqPc4LBkum7tLOR51Mg9lJr0OlpgaKrmT0w9ES/f67/pZEgwl4nNFHCV7eW6ha+2BFj57b4sB0B3LX7IG4cq+xCbj6fcEFbC1u+OgYysWx9dgC7MnFuJROzIiSu39a4tytRgxmVwcvz8RabNU5+LUUL8YwSuoKFfOXIsYMaUQCabKIiNPH08RZbVgv7RH7h0hVwNtchuW8Ia79djK/f+rFXMeeyFuT3DeGDN+2C0+mAw+FAJpfD6NRMyfjGLcaZuZJ4ttXdZTGwLGbhzWvt4/6v01ci3TsFOsGe+Vz1GgtmH7UCoL+M38UXe3Rz+iOTk9i36rC5R/i1NrCwT+CbuzpYvUcBy+cwnMiiLxvnu2Q6qeoKDiNUjIolkZTely2tq2edMFNDNocu4/5K7/dkMiVnL/tEfumdPRDrQshNxbHmq8VqJVvfv57JLfVQB8bxntv7uAcRpWWjMf55eLSkT7LdL7So+23jWOh6+/f2cX/4yHaoigPJwSTOeq58ZhB7H9UAcMtZRzOv14nh50bw1T3zo42u7NKtjpvGpjC2bu3iAPD61mZGZ3xSHqQLecyqlBwSGI7Pr6FLPVcDgFVDVssk2vuyT+RVb2mH0+tCNpXB+u8XaxU8dcka5u9uRGL/CC6/rZezJ6JSs3kNm2ZLI3Je7fcz4s0kw1DcHXnNkOl2IK1x3X27x1U15Vstz2Ef9yXtjZz8U6vEAuxxFEOWiCY7C/jysg4qbsBT43x0a5GivCcW4eDuzWuc3d48VDSPkzGoJp/AkxrqeHAo+eSRPVqV9YUfKFND9+UGwKff3AZHfQjZ0WlssAiBT168hrna6pE9OIYP3t7HhU0yyAxmNDxoS950QsDP6p0iZ0HDGY3LM/Q3gYVYkj2Cx26d5BXBO8PIDMzybKdveLa09IsdIL9ft9pwFmHYbjHN2tXBdgDsTBTN5PZTwDcq1BF6f2OUy827syp//vsni/qDDxy3Ep+RXkBo2KsbKGOIMA8AZzT6kC4ANw2UBkrYJ8A+gdGAX7eXQ0OSn8dFfhp4dGx+1QtrX2YCCvosahFRryb3MUnPZff1fX2YyeXnZBTa0U/YXMhN2z3tRzLW2Js9gmceAFYuKfpAht3YuOmpqlbDO45Yy9wRNxzI4Kl9xSPzQgB40pKB3HoKoBHfOFg0pRP4y1Glj/yyl6eHIWCT2vgwt6e2BBH2zk5q9PPMoX2Tpceg7yzvhMrrAAI7U0W02h/MPoErGqM8eIIVNPRnCujyyphRBdzVV936VQkAXz/nCAiahNm+OL62Yx+mbO7oRbMMsMzrwU5LoES5ibNH8NjHf8Nxa5A3kjeLThlnPFE+14/Z961HrmN+B4OoZrFjvOgY0xr1wR1wwh3x8CosiZEkhEIO5/2tKCNY5YB92VJFVDm/v3LPY60nXJMQaO/k1CY/IyFwH5XgtrSbjuiCEPVhti+B5y0GnIUA0N5YjyVeCbPpLHeZWhl0YUwTcceu6gkZKgHgk8ta4HSTxM/Qa8mrb47DWqSBXLL0YF4BV+4rFqzirlpGdhM7jy7HAqgqubM5wCX7hVjAd9esZO1HNMFdyOCZvx2cm0EPBD5u2emAmsshk8rx4hMf6Cs6tlLOBfOGcSO/svm+EgDsFkXTOEaZS35TS4YQOwBOb9bPtLsnSoM5b1jbBZEMQgltUQAIRUJocIgYoRTyTOBKjDQTMWyxW+8vE/lDAOCl4ihfsEVh9KXXrULa4cLEtmGMxYsgLQeAK7raEFpRD9kl48N3PDH3qFd3t0IOe3kO5Nx4iidx0jSGK/cVF+wtlvj9n1usnld2d0CgNDWkO1FVKKLGQXb6Zv1M/tVl3YzLGZKG2XRRC1XJJGyd/xWWlLA5m1+gFQAmSM3PLmqOMNpcj1rYCAGhJiHQumMajz4a6s6n+ZjeW9fKeR8dA19zbwrPvLOep7lIDKdKALDCS7p3uk4oyxq6gkFGPvN5lXLzM8iUlJiETosvXyUAkGDT7ZFwcLbo93dxSx2lDOYLoFp4ejkAXN7ZBp9fAVXZvHqLnleX2peWNQE+DyjjoNgQ5Ampp3eM4uq9esoVapUA8NWzjoSzzoexRw9AyOXhjjihBJ14zZ36MfXKpfWMqnzdYshK58cijGL7b7VI6NZFf5+lYvlOi+m7GguwA+CsaJhRVtI4y8ArizxC2ac5a6MAlQBwaWMHIutiUCIuLP3gZjxztov7/pMcYJUB1sX8fAelZwvYYeG35oKY0bSmbcF8v9fCu+0AqBRG3WxJ9HDbEz7cfrpu9KBWDgCXdjej++hOZP5Rweyq3xYpAHnq+OpdoIFPzKgQNJVn6bx2z8IA+Nxhbcg73EgmC/Blc/DUuaCmc3jt33ZwALyvPcooDd63+nQh98LmMCOv4SNWL4HbJcNJaJCBRDwLli3gsaeKv/kkef9yRZqA4klAV40fH/HC1xlFfDaPTz66lT/zWUE/Vh/dhpntg0jkNFx9sKj5e0MoxGpiAZUA8PaWbnjFLFcQXbevGDDx8FSxbNl9G1Ywwe+Glsgimy7PGl5uAFh31w9P6WJCRoIjLeBbu4vVPK7uauU1/4iAKKeuRHz/GKZ2j+B7vXrOnWoU4H/OWocCkzGycwINeZVrHb1LIjjlt4/yBX9rU4TJIsNPBvRKXm9tDrPpnIZjI3Vwuhxg2Szy+TyyOfIoBMYt7O0BIxSO7rPbA44MBuF0yEilsvjagSKrMsfb7lZ42Rs6AZDKvkFWXhwFOKeuFY1r6uFMxvG5v+jep9SsAPjzCatYIRCAms4jP5nC85ZcgC+GAlgX0SrkHBYoupoNWhQntQpIZr9WL6LPr++E6hXAAhl88/4i0CuxgIvaGniqWordsJptTWHylEiARw3dO6FvlMP9XiqDgL50Uaayu319v719Tvh794HSFPLn14VY2CFgMK1BceRR55CwL1XACUoQPo+E+mY/L9AxlWH41MFiqNyLpgCn+ergBIMiqPippehBCQU4cR1LU1VGo5U7HlaiAK/y+7DS48aOdBqf662cB/elBsDVS1rhCbngjfh41LOazWOydwIPT5WPkra7u95pCIgmCJe53VwHuNOwL3S6XPz9pEXmWQwAzgwHmcvISNKbz/AiGvUOGa/1BkDVY+rWt2Hk6X7ui/m+fcU8CgSATIThgX1Fim3dWObfgulhYn7gMmoTNAky15yRt6vV190KAHuH1godZlWOSgBY4XEjKEmY/EcWrmoesVYAWCfOKie8GApwaVsjZFmCw0FFmyRQKUgis3vLxDbQ89oB8KxFg0ff9xo73bQvmLaIWgFw5dgwN5nz0rRisT4BfUbH2d8tXYmuJ4qGMfsxsNmtJ8TqlhXcvGop6h+tXvG9IgAytuJZl5/eAGfED1ESkRqewrm/nJ8tjOIMaRcpXhcyE7M8d8Buo4SZXQg8fIVr7ixOQk+iD2WBUAsAPmsYQ+yA/ODuouRv/c7KAjrsDiS2Tv5sc3ixB3TYAbDFUP+ajiCmYwhJ5mY7vynEdSK0uWihKQv5rUMTglURtCZY6kD7jf7yWVLsz7zSSGBxyAHwxmOCfPGpUaWsD90zv2DjN16nF58wm3mNNa8OkTPKu3f4Gl/R/0oU8NjmLBqcDnx9f+VCkOVIGH1WKaFVOQA8cUxnSTeXPT8/INR6gR0A1u/Ip9+qFKPvKgHA6hl0XizCyCGGhFACQMQh4LahSeFoS87GtYFSAPzIog6+9phVLBD0YDqTx6ceKq3nbM61U2O4YU0L2h4ujUiyz6FQzjf9P5piJWd9Lsys0F2tTPNw/bgLTZ1hXnHSE3Si77lBDAUSJZlGv/aYjlorAEhJQnzsza+OwN8Z4+XWEgdG8auHE4i5nC8IANYsG9YHtO468/NDCQAbVSkBv50CWAFwqsWBZIWrmMOQTgHm/D5fxUfhIz1tzKGQi56GDZoGp98BWS3AARVnGVXUSAaoiQWUA8BRloyZ1gHGLGnWC04J4aALvEhQvoCZTB5SuqiwOSLim3OGWGLRcH28vpgr95bJybk57A76UecoUgCrZHztiJ6PoBKvrwSANZY4wx8cptfTtbef9xWl89ss4/n3SNGtyzpOa3TuoQaAVRFUrV6AVf75RIP+XBQTYN1sRAFqAsDxFrJjepsstUxcJQDESKNntM8P6+fniy2L++oVdTj5AZ081QKAo+r8cEkOXLtXZwFWADzmFHhJFUrA+Ivd84tWvJwA6LQEglgXyT4GOwU43FP0VubWNqNZ5/dJS1TPCU116FoZg1Nm2LZlCN/cW6wxWA0AZr81U4CXCgBH13lhxsPVAoBjG8hs7MCXygDgTpFhWcDBBafbd80vXHVJU5jROZnMvVYVKsXum+1QUYCIBfjjlqOdNV6hXHiXlb8HbFlFzDEetPR3WnM9Aj43CtkcppIZ/HABIdB6Ams2QPr4kWsXPgVYAWBNd2JFd4tT4WdZ8ucbyRfm7VCr8uKG5T1zvPCyGnLwV7reivK6YAAhB2XGYPhLmawgpwR9jOIOSKiyWtH+a0k917ZNqRr+LTI/tJwmnsLWqD4h3f9QPIOwJPLqoOu9CpfSx/MqHm/WEFEAZbsD3zuou2VTsxpsFgKA1dBjBUDRKR44Ihamiri4ubdyRdHL26OM8ghSxvtdhuq926DY909PC+a8nRwI4EtLOxcGQBPlUBeAoVSuRAq1OlYcGXAyMjKQRuuRKT1DxY1Hr2SBZj8KY3H8+yO6HpzaSwGAhWzhlaxoH+wheUOvRFoJAAM5lae5I4HxiUSe/002PCpdSz6QKU3DPaECXAKQfS6HqEvk5eCoQsef4sXIpoUAYN2hnU6nXRTh71/TFOXh3j+pAoAPtIfZttkCT1U7YcQSlAMA9VcTBWjzKLzyR38yJ1gn0iqFrvc72RKvgvGcik2TOgC+t7yTeTwKL77y9meKOoF/BgDKzea3lnazjJzlFkhqlQCwyyK4biOXJyMF7Gq3POeH9F2y0jGG6KgKp6THNhJ1mLQkr6D77EEyJMfYVbtlV9748JRYAx2KsNbvROe6drBCAbs292OrxdXrkTKm89dHQjweg3wCF00BunxOTgH2x0u9fbtceiIks80a7tWmcGO32FE27cW0flu9W7q3kjBnPUK9vTnKKKagEQq+WyHrCPX1etKhKwKGUxpI0Lq8p3wq1/vHVZAHT6AdyE4B8WlK+S5A6AzDJUoY6JvCXZO6ruCpUd3AYzZ7Qmd7KTkCwJCaA6XR/9SBhfP8rwuH+e+U0AwAAA2cSURBVCSeVufXQ+gkDf2TaWyZKPr6lQPA68JkLxBx20jRKfRDq7rYFaG6hVnAEr++0HteJAAqkenvLOtifkXjfoFJlUKxKCBExPvLCHO1AOB8imRSGbolVwkArIImHdVW+zyM4gCiolwVAGYY1QnGaeg1fh9XzuScEs9iOp1V8fcx3bfvYcvus26ALutZfkbXvdNZ/83BIEa0HNyygI/3ztfk2dXZVv3AA0Y/5rjaLWzjZ5bUde+tq2ORkBN1YRcOHJjF3Ya3VjZfqI0FrAjqfvNvDegpTj9TpiLFNS0t7JvGWbwcBSg3Ade0NnJydr+R/KjS5FmBUwsAyLpGPPoEj68qAEwdfLfTWQIAc8HpWa0k2pzo08M+nvr1I/uLC2Z+90IA0JvPcUB9rozweigA8KFYHYs6RbhkcLnk1zO6v+Zxbg/oNPA/g9V9L4XVIRc/lr4tEObhXp/qm0+qCACfHRgQzFf6gYV2wFc6mxiFYP1xUs9aVQ0A/xGNzvEPK7rpvt+tXsHeuG1+cAV50rzu8GbujjW2Yxzv2F9qUTTzCD84McMBcPe0ngLOCrhyADg16OOu45+2bIRyALjhyJUsUOfB1FgSP9tVtM+bJNqkAHYlVrVn/enSThZoi0CSZJx53xOckpi/fU4wOMd6RmIBnk9ZdQiY3j6IrqUR5MbjGJnJ4xcGu6DnfFd9PVsoPExYG9bNf2e7g9wZ5fNDtQGAg8CtsE6PXighLOq669tGdK+UzzY38p26aXY+AL7W2sKpA8UQfuTggFBtUqw8l7JimO+35HJY45H57vqHww8ut2UNtQPAKi+YFTaETAFWIe0rLS28f14RZK7mLfDtqREcGXRyj+ardulywFdX9TC3U0YilcfvBoqVuy9vb+ZD/MHACIgFWPsnMFuLQNvB/u3ONiYrMrdKOrsjvJh3//YRLJOdeGqimMNwqs7PI5tT2QLkqTiaeqJIH5zERIbhk0/qquX6tv7FAWCLcbyzTrj5dzkKQN81OWQe/08kc7lbrxZqAoD+vvvcVzNPzAdtOo2Tb9s0J0B9paORJXKAVwE+bhGOvvPqdSwUdMPbFMS//fBP8/zv7QCwLqq54GYtILOaiEkBagEAJV4iQAWcAjxqMdPZfw8NYnXAiRVeCT/on18L6AunHcGETA4TyRw2GO4RBIAfLe8pMd2a82lGBq8K+dG5JIoDu0fxh6GiUeqBdavx80yaF5ZMZXK4areu/LL7Apr9kTAaVkS0uEX0pVQ8OJ3g11916jp2laGNLbeu9Jlw5vIwjw7+g4Fs+tCqhj2pVQHpy00WcLOlpt3qTl29+a1gF/67T88xbAXALauXM5dLgcOpYKvxgGadG7qW0plYefJnejqZizIf/SMU/Yod5YtT0X0/X7WM71RrAgQ7AExSf/M5r2E+FwOlP7hz0/N8jCcG9cqmRAHGDI32NsNsbR3/Z5Z1sqaOCKbjefzomV38nnLC7kfXLGEUnTyVyOBkQ1gjAJy5YRkCTiCtiHj/b4tp4kwArKyLoM4nY2o6hT8MFr2R3nbsCjSG3di/bRg7Wrtw14N/rgqAo31e1uCi6CgRB9MqPtbaikv37cURTi+xvnkbyQoGYWNPiJGGr2+waBS59rT1iPdNQvE6cbSUrgoAiv7deUAPgPhErJjxk4Bjbb8c0g1F1QDQ7nDwhT0qHMRvDFZSDrkmAD6+R89QeSCXEyoB4JqVSxl5BecLBewwEkE+MqUnZbyisbEqAD7c08aolh+dOu7Yr/N5Mr443RIibX4obgWj28bQn8sj7BQQO7wF+/9+EIks8BxT0RYKQGIa0tk8ru8ravdMAJzocqGlJ4zMRBJKPs9Z4j2zBTT4PfC5nMhmc9jfVhkA6z16FtQA3/0SZvIaz8twRUcbB4BXEysa0Mx5Fc5cplOAHQeK0T4myi89vIF91O3Hp/bO8gUhVmBSgPWhAPKhAle/7ujT771q7RJouQKy02mc1FIeAJsM3/WAIuOLPQG873ndInjf6uXoeFx3SS8HABMcfcccgV9M6PeYALCDhK4R/vq3kl1D4zX1byYA6LrPH9SdRU0K8LgRr/D4ngYMDBaw/hj9NGDXe5j69oThv++z6fcbvO45EN/U2cYu6u2fU9IELPYE6nuzJUz+3Fg9GzVUvBeGw/j90iWYfuJJPsYNASdXQN09pctVZj+zRrrSS5pC+FTviECyRs0UwESRdRDmhJ7fXM8+3xmYA4B1ImhCN1uCO6wDek80ipPaSp2n3rVFN+m6HDow7AAwPYboO5oEKwX4cWcbu9IokNRuGHgGDcMJgdUEhzluszr4M0euRc8Tz/CPabynO3QBKW4NujDcsOlzqrrhkyR2mM+Lb//Ri5ERFW84tzTc3ATCYgDw29esYec8tHXRALikIYxfdhYBsNrr4Or4Bw1K9t66Oq6h/N6ELj+cEfLjuyO6Ozqlhxnam12YBZQDgCkNP1rI8V1qUgCOVqMq5TUHBuYAYFIMs68PNNbjhMbSdLEmAE6t12vrPJfK8L6tMoC5gHYA3L6sjV2wSz/mmcciKwDeYStZt9k4eVzW2shTn1y2e59AfZoAGCiTYJr6HpRyGEoXU76/3xC+2KtfxUyKYo7RPElctks3T1/cEmF073/E6vglv56cnqMAdxyzjP3b47sEUtlSYaibBnWNHbEtouH7Mwlu6fzj5DQfp09UMZbR8LaoH7e1L0HDzqc5paVMKwMZDUvrwshpApb+Q0Ck9lB2lt9/nMeLOoO6TLQJ2LR1emEWsNztYo1OkVeqokaLaWbl3srUqgCoxKdvX9fDDi/1aEI6rwteNw/rR6YDuXxFAJiTbL6aJJTe2xf7ZltCCOu9X+vp5DzyI3t7+cRuDBhl7Q2x6M7xSV7skd7SBNJxlqyCJxo5eszKYjcs7eb97Mxk5opV2wFwdjTMKDr5PKNgxM508dqbu9oY6SleGwpxD9/fG7l/r+jQk3BuSyb54t43PS1Qiv2fjY7w1DxvCvhxcZ8O/FsPX8kaju7Aa79/j/DuZS1sLMuw0alTtFtHh3n5mp3pYsKNmikAAYCSC5D0aALAnMRfrl7GvC6GC5/dD1MD+C4jDo3Qal73C0Mqf7ORluyW7jb29n36wM8g1DtFfLC1nV/+zYMHeNr4u437TeHNWsaN7qFkTWTjp9j/VytePKUwLow9OTKOmEsvK0elUXZYHtoOHPuxyQSEx/Bt/PXoJDwUuczAk0XQAtj7oPfmfYP5XNVi1dZ7P9zWzL5eIa6frruus5F9vLeoczHfEwDMGINr2mPsswf00PAb1q1ikizjkqeK0cl2EJq/v//oI1izJ4FzNo8uzAKsLmGXx2IItYfx0b/pjoQ/WbGEogPx0T193IX7yjXd+FXvMDeH0mR9plmvdrEkoB8Hr7YUZTbZwmmhEKOEDDuSKXyysRG/i89yuz7ZrukecxfcNlw8BvW43aCdQppEMnJc3RPGF0YzoJwV2w0AkHsqof7tS1pRyKpw+pw4+Ygi2wlsT+OMp3TJ3RyL1VJpXayTjTrOxI7KuZubx9VrepqZvVg1ffdRlw5ua1u6eyd2L13OP9r+yKP81WkUyqScgR9rqmdfGSqGcv13Sz37wsAYpwAmAD7RWs9+OaYbgqz3mr9z16/1aq1nnlcqp5gAWPlY38IswOyMlCwHVRXRnjq4e6e4j9lVLTEWdACf65/gAPh0Tzv2TMaR1zRcNzo6B4BWg+/QPR9v0AeliJQIgjJy6K+/npnBlau6sE4uLeS8/snNQrlzvTmuH3R0sBNbZGw6mMdv1WJO/bsHdKPLWct1C5rZftVUD8gyelNZdFICPZq8TXrkbvak5WwSIkaSpfZ4t6Lb9QkApoxD4zL7tOsrrL9H35mJGZmUx//W617HZ+3Zi98v6eF/lzvHU0o3M7EzV9vWRdmN4xOCVd64uC7KHjTczPceffjcz5ryCEn79KFdVU4AoNgBqhu0oB7ADoBIdwSevhkOADOVOV3zp7EJXL22B3fvGUKLoqAt6MNYUtcdWAFAdvjLd+8T/iMcZnQdCVz0SgC4am0PvrijDw+tXcXvM4MWxo7bULKI9Ln52aaohLUTY3jaGcBpRlXyt/ftwi0dy/Ct3gGQMBhqCuCOnQfx7jPW4YLpBBpRQG9ORafHhXhBRd2m5/nO3nHiUmi5HM7fPMqdPsz2jVU6CSCKsdzwrqGJo3voPX1vCqs0qXSt6cB595Gt+GFSwNTgDE5b0YxbFd2Z9F3RPG6c0E887Gn9JEJRUBS3X+3vxjLFrFpWN+KaTcUcSea4Y26FXX9mB+7apwP4en/RXkDva2IB2ZN0rZrZaLfQZ+Yrff6eeHHnndltCFLWmwC8YUzkO83s71D8fSj6oGFW6sf6nfUZbY827y1l3yQhi76gv835osAYCpixz6m9g/OHxnBTdAn/2NwEg8evYwpz8PcDJ6xlDs3J/x46fj2TmcLBfuvEBM7uaUZ8LIE/z85ysJpArPT3Qt5UnEzQQMxzLf1NnZtIXWgy6HsTtYEGH2ZHdSVFXWcE1z5ZW468Wn7jX9e8NDNQVU/80vzkv3p9Jc3AvwDwSlqNf8JY/gWAf8Kkv5J+8l8AeCWtxj9hLP8CwD9h0l9JP/kvALySVuOfMJZ/AeCfMOmvpJ/8f0ni4co0jdltAAAAAElFTkSuQmCC"},{"path":"","name":"knight_slash.png","folder":"","namespace":"","id":"1","group":"","width":32,"height":32,"uv_width":32,"uv_height":32,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"8bb06706-12e7-2f99-b8f7-ab39d69162b0","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA0dJREFUWEfVlr1rFFEUxX/TmCJETGUM4kcwahXbJIJutNDKWJpOm2gbK8Fis2BjY1oNFvoHKKYTBF0Fo2UgjSgmUREjIoIhSAKy7nk7d7wzmTUTsxgcGOa93TfvnnvOufdNxBZf0RbHpzCAEpSqUG014P8HQKszt/0KM6AXxmFcMrRSij8CkO4KbAEFQHcr2WgKIGs6C/zPAGSzVOAylCOKV04Rpgp5wIJrw1YA8OyuCyAJXoKomp/9ZnpELgDb0PQulyjLiZb9ZgJmZWnKwK0Oboy2MxZeONwohTz6vSn/xqC5AALtXZRDYGD6HVOP5pnJC+D9MQRDG+0RawAkG3YBpxuZ31/m9uwXesfjvmA0FvHHepWQy0ANaghAmoEd9ZModEJ54Mkx3vKa93nybKRhJQCc8aplOJ4AmAM+AytpDzzfz4PBvQzzCqJFIm9MJdCsXG2dPdcwELJvA0aABVAAFqECT00CGbS/m76+AU7ysAEgK0vRfpECkNI/pt8BqNiBVOuiZvJ4BpSVdcsipao1KQBXYeZaG0fYCZwAHjfor6yIgMahNNLBmdFOxugB9gFz7Ime8cHAqWc0a1ieJauoNdR58yH9vwHtMLnMxKclvvfv5typAxwKm8X6S7ZKUAnWA5A91JoDUHbygG6N3wA/gW0xO8DkPSYuLnHZADSjP2tQgU0xkLTcNsqJ+YyvLBD7vZ59ZZFKaNMvYLqbqcEfDHPwtyRFTtQ0A677hU8RY0A7aRw+T+Kx5FkFloFOCJ5oXBeiKnd8cDGge1cH20N7dyDzAShry1xP/y1sv1sEk0jzuGdEK0QmizWlstiVuXvg5VeuD8xyRa8EAIYw6f8+ew9EwbLSaG5rYgCT25hQpmbckLlVTny2HJ3nbAJAg9CA9LWnbD0DFtTTb1nb07Oi0tWlMq43qeATL+1CcrZ8FDuBgdCAHEUp+rMm1Nx84MGZTAZe8xJM32VqcJVhep2HbjaApQEI5SXHgFHrGbHA9jSpPEgvSZYhrdd+sVRqcFFSglkA2cytCixTC25gLHM/11jr9e75eGwg4jMmAEjRn83KMspu7N2f5xuTyp4qTI3NS9qvLkUol6T9+j99IW9m7IHaPo7FX4d/VkKdCxtXAAAAAElFTkSuQmCC"},{"path":"","name":"kinght_pierce.png","folder":"","namespace":"","id":"2","group":"","width":32,"height":32,"uv_width":32,"uv_height":32,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"f723476e-ee1c-003a-f7e4-4c487e274851","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAx9JREFUWEe9lj9v01AUxX8WLKgbEgxMGdj5ACmSBz6IWUBiAYkM/CmyLbVqJSQqBiqBGPJBGDIk34KhG8qExNANjM+Lr3tj4tgOap9s2U7sd88599z7XsR/jAyyFD5G8HPXaaJdP9R3n+H0yZwX7DPK4bEADZ1vJwAKpLOYU/AduA/5Pvm1AkjhjDlLvgIxnCQcvYaDa1MgnZOyX4aT6KNV2ChhsKKDP1CgT/D+2ZQJ5xVfXafh/kYEf4aosBOAYkpRB/fRRisVzCN9gPQGUBsP7gLLf/w+W3khyq4aQEZRM7MUyAO61zWDvDz6VsQgBYLzjX0zuFBdOYCMNLC34CaFKVCB+JJx+gN+9VGhtwKF5T65ZKqcB9Y6Kw+E66x/GnoBqHp+Gown9joV3O5NfvPBCD5kHL+EN12V0AtAqPuMSWDnZbd79QAp45/VmMKxfXS+oM8LHV5uKaGAXgmvgu5XjelmBL+3QdgKQNLfgb2a/QgWU9SBoyKmUO71rDEWQPOB+aIsyS4VOhWwQL7EFPARpOMMFtUCHEBJKZ+Kc8hn23vCVgDm/EXFMJqtciplAoAYFs4X3yBPY9JaCb1cVsQ2FVoB1M6XtJXjo+mlqeZQSIFQciVLA6bfgxoJhdIzTkDfta0PrQDk/AcwCbmthlcgbEiS1aK0EYAZt1JB7dlAelO2AvDOFwNj5idRGvQs6X3XsxR5cw4CEHKfsBQ7sbYJJW2zpATMAMQQz8qk1CtnpZB8MobbOTxvtueNCmhSQ2+yNwPXQWIKe8cD0PvBkDZa2vMagMp4Z569D2wBfBrShNTM2fxfKTJDSs2jGYcH8K7VAwFAQuql95L5AL4cN6XGp61WtGzPzb3CmgKh58dMNLny1px4E4C2JbdZdmpomrNp2DUAhlSBvbm60tAGwnvC/NAkVnc27XYWsBzDvRye2qRN1kNUsHd9c9Ii5TvjWmsV0y7ZdwFgCh7D4UN46+MEAAXcWsDFJvZVI/M7gbWK7LMF96B9mjVR7YETOHpVobMITbbNXmCV0LX38/PULR72IrjoXI43BR36W5OIJ/sXYhpiMBkEWqYAAAAASUVORK5CYII="}],"animations":[{"uuid":"0568e895-3c91-5d89-99db-a8cd152c6bc6","name":"idle","loop":"loop","override":false,"length":2,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9aaeddf8-969e-96ec-499b-e177f3ca9bdd","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5\n","y":"0","z":"0"}],"uuid":"b41191dd-a827-1d70-2481-57ecaab86d85","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"c1a96e4f-e4d5-8b01-bc63-24a30810abda","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"95977c77-ae25-228d-e7ef-c26e45121bdd","time":2,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"dea0f803-3228-b1d7-1079-45f536051fbb","time":0,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5fae9431-7c30-479b-a141-8afd7b912528","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"2"}],"uuid":"de7b1cb6-e4af-26a0-6f0f-25934a6777e7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"3"}],"uuid":"ddc3fa9b-478a-cae4-d2b7-52aa90d05217","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"1"}],"uuid":"2b591754-f4e4-0b7a-86ae-4641bf978234","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2e609dad-b848-67a0-7c7a-814c1291eec5","time":2,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2c4a5034-e8aa-1479-eab7-bfdf5f817950","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2","y":"0","z":"0"}],"uuid":"afc9efd5-7991-5479-4685-6974030808fd","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"3","y":"0","z":"0"}],"uuid":"524c421a-15af-6a4d-acf9-cd8468e01d74","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"1","y":"0","z":"0"}],"uuid":"a8694bb3-e773-87e7-d336-851a4ec119c7","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cdc3fb8f-aa14-4576-8035-d5a854b8a72d","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"22e9dcff-6d62-025e-d201-7b315f096fd0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"fc74a17f-7364-b098-e1db-da334555f52f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.25","z":"0"}],"uuid":"14d06730-eb9b-1523-a2b4-39ff6e7a92bb","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ec87811f-65d5-17ce-810e-a189a5ac1d9c","time":2,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"26eb55a4-bfde-c9d0-95c9-a69453b52429","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"de9d485e-dab5-8181-4935-9f26a595ed51","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"5285314c-5162-09c4-5484-49628cf0a3ca","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.5","y":"0","z":"17.5"}],"uuid":"3f6f8f72-c792-940e-e5a6-61b045bdf30a","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-44.5","y":"0","z":"17.5"}],"uuid":"9a4eddc4-31bc-941e-6095-94b12644a5fa","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"72ad90bd-c715-fd60-9527-9d16fc5b64e8","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"a134ca79-b6e8-cf51-79c6-c252707c393b","time":2,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"ec847310-4f84-e7d7-d766-fb53ab5849aa","time":0,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"f3ce685d-3702-b666-1d52-f2c3dbb7a967","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"346de168-6db2-96ee-3256-63587cb1baf3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"2"}],"uuid":"59467291-3096-8f20-998b-e30b3563af4a","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"3"}],"uuid":"e54a1cff-07e9-d81e-1cb6-e664311fa8bd","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"1"}],"uuid":"c9a0049c-04d5-ace2-8062-8e6f6c2f884e","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fa70244b-00fa-e279-2f52-16909df98bd6","time":2,"color":-1,"interpolation":"catmullrom"}]},"5371e06c-b54b-154b-b1c7-cfce2a9b20d4":{"name":"right_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cc9bae98-3244-1324-0c0e-d3dd2ecf8ad0","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5"}],"uuid":"6edd2d20-911f-34af-f68b-c35efd9f0e78","time":0.5,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"2.5"}],"uuid":"0ea0f311-8824-837e-7ff8-c3c3730f2853","time":1,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"861a5003-238d-31cf-70bc-950490161a4f","time":2,"color":-1,"interpolation":"linear"}]},"3a05cd31-c803-0a4c-716f-337c916a2ee5":{"name":"left_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"892e5c98-765f-aa7a-ea28-f13f7ae43c9a","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-5"}],"uuid":"53be47a5-8837-d908-1ed9-98a68646afe5","time":0.5,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-2.5"}],"uuid":"efe765f2-d9e1-0a5b-c650-ee3c1df06d51","time":1,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"258ac48f-c894-68c1-5296-4db0bcbca6c9","time":2,"color":-1,"interpolation":"linear"}]}}},{"uuid":"510cb280-3696-1b43-8e58-1de159ee2e73","name":"death","loop":"once","override":false,"length":2.5,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5"}],"uuid":"8020a4d2-86c4-c7df-3226-15f2fa672aa8","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b0ba3966-f18f-dd38-f025-671f517f8e04","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"340cf0b5-d2bc-5ecd-53e1-cf305e74b2e5","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"4.9952651976","y":"0.2178207497","z":"-2.4904987465"}],"uuid":"4caff30f-3d14-14b5-9d9c-2798952df1f2","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"89.9762987955","y":"0.2165792068","z":"-7.490534542"}],"uuid":"ae3284d0-537d-1369-f4fa-72bb074ff2a4","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"92.176738467","y":"-0.1017084386","z":"0.0217524291"}],"uuid":"ca2fec36-6322-efd3-a594-9b84a342965c","time":1.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7fcd76c2-f8a4-c148-ddf8-af17ae796f6b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-5","z":"0"}],"uuid":"b0762dcc-23a4-098d-8170-e6d66d7bd03c","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-7","z":"0"}],"uuid":"95ec6482-0272-9859-3396-1951277b89a1","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"6","z":"0"}],"uuid":"7e13a2d2-7312-c1c3-43fd-38c54af47820","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-7","z":"0"}],"uuid":"21b95278-b341-2bc6-8029-c207da55f910","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"5","z":"0"}],"uuid":"4364873b-23f9-3f6e-c786-663c4bbdc991","time":1.54167,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"eae7ff44-ab85-27d8-b667-736f16e09902","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"12f39d56-99dc-9d96-2620-1da6e5f5f028","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"bc207ecb-14cf-fff2-a7fd-96b0a5317891","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"6efe5b7e-e780-eabd-e076-4c8b0c4e0ce4","time":0,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cc0147ff-e3ba-d298-3733-76be4918fc46","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"79b0ab3c-3ef4-c14a-b2fd-7b89d05c07c0","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"5f0025c2-7396-1cff-c649-5b15c91e684d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"d1930a1d-f6c4-3fa9-ec4d-f30be9285883","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"913410bc-781c-8159-47d8-759d83b6cb13","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"12.5","y":"0","z":"0"}],"uuid":"5f879aee-67da-ff07-c674-c491a3b5ea65","time":1.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.5","y":"0","z":"0"}],"uuid":"d6218785-1b3f-6809-dfc6-3f547e03fc81","time":2.5,"color":-1,"interpolation":"catmullrom"}]},"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"17.5","y":"0","z":"0"}],"uuid":"12497260-82b3-fe10-467f-701a208d25a4","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"675d5f30-711d-6b7d-bdf8-f2a67ec27bf0","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"bee90a8e-63cb-4223-950d-89611312e340","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"b5f74ae8-343e-e32f-8b1b-7dc876f8307c","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5","y":"0","z":"0"}],"uuid":"97883f34-e784-3e6f-33f0-e2a7b3db80f7","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"eab89d5c-1e5a-b97b-d624-a0af5526be9b","time":0,"color":-1,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-110.7533267908","y":"33.5046869781","z":"-4.7014465817"}],"uuid":"7b73720f-f242-3a95-0830-4f6443f781f1","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-134.3128187158","y":"8.8034834933","z":"8.9092819994"}],"uuid":"e0b04727-7bf8-a821-8234-57ed39e723db","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-166.8128187158","y":"8.8034834933","z":"8.9092819994"}],"uuid":"c338f603-760a-e403-2703-9276aedb6d07","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"d357b2b5-23e9-c300-f8ec-f87d248d8ff3","time":0,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"4e2212d7-bd4d-f37b-871c-2ae0bda3205d","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-60","y":"0","z":"0"}],"uuid":"a89eb7f9-c62f-2fb2-5e80-41c7ca811c8c","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"b0b8c9e5-2cbe-a327-c5e5-a2edd78a4745","time":1.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.4368798415","y":"-80.0381493561","z":"0.4368798422"}],"uuid":"a3f30f24-5c48-ae1b-2832-6cfb89770745","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.4393978432","y":"-80.0371962438","z":"0.4366846869"}],"uuid":"0b4a8cd7-e8da-0d56-0644-e7ed84d89212","time":2.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"bdd1cd1d-d74a-ece9-874c-bf429b5abffd","time":0,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"39.7004210349","y":"26.1121805722","z":"20.0727326499"}],"uuid":"d1c6f13c-0dad-e5ed-48f0-3356a0e78718","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"65","y":"0","z":"0"}],"uuid":"b8847624-dabc-912b-6fb4-23e6c9af3bdf","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"77.5","y":"0","z":"0"}],"uuid":"1e5daf38-a347-0353-dbd4-35c0a36d513e","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"20dfaa41-1955-d6db-a4aa-4dd3601dedb0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"17b70c8d-01c1-3e8f-d2fe-bfbce7dd52b2","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"a8c57466-0c8f-0b1a-94af-81dec6cd81ee","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"5f00bf5f-83d7-4833-524b-dd637117dd7b","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"51299b95-d594-0125-9e3f-bc3689241e20","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"0","z":"0"}],"uuid":"4b3b863b-74b2-639a-a7ab-e95f80b55e83","time":1.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"116e320d-52d5-9564-aca1-1da1cf1bfd9f","time":0,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5e0b9dec-1093-a954-b9f1-20d5edcb1415","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.3593155127","y":"2.2494361891","z":"7.1565621013"}],"uuid":"7034adff-e4cd-3746-93ca-c9998cdf21e4","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.6005110996","y":"5.44649138","z":"11.0100837016"}],"uuid":"0457095f-c9e7-c678-b46c-65e00255d3a5","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-19.6005110996","y":"5.44649138","z":"11.0100837016"}],"uuid":"e1bac75a-8490-94ec-9989-13bc46e04c18","time":1.375,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"24eb8803-b446-6b07-679e-d0c8d9422232","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"90","y":"0","z":"0"}],"uuid":"d8b914e9-3451-0090-c33a-9c041982516d","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"122.4556059038","y":"-0.4677121805","z":"-0.0471610168"}],"uuid":"f5b6a89f-c418-c043-341d-0e6cb1684b74","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.4556059038","y":"-0.4677121805","z":"-0.0471610168"}],"uuid":"e81fcb8a-4e06-d660-fab1-32971d5892a7","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"19.9556059038","y":"-0.4677121805","z":"-0.0471610168"}],"uuid":"24a9e4a4-c548-1a5e-8930-1c8acaccdd07","time":1.54167,"color":-1,"interpolation":"catmullrom"}]},"436ebcaf-7570-8c3d-368e-80184e593a90":{"name":"right_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e3170697-624f-2399-e57b-f36ec554de0a","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"72.5","y":"0","z":"0"}],"uuid":"cb338767-44ba-b0f2-2ebd-067d2c98b7d8","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fa3c79c2-0028-b8e1-8c70-3982af355be0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-14.877386585","y":"-1.9359724559","z":"-7.2472078555"}],"uuid":"c7229567-fb75-7276-bc3a-433c37989c76","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.1355863577","y":"-4.6057413366","z":"-11.4820643647"}],"uuid":"19014854-9d05-1139-c36e-8941717defbb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-9.6355863577","y":"-4.6057413366","z":"-11.4820643647"}],"uuid":"d747c85e-ce00-50c3-a6fb-c286e720b3be","time":1.375,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9b962340-0489-07f8-3f0d-4d8713e2cde8","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"102.5","y":"0","z":"0"}],"uuid":"99e9f35d-b476-7db6-c4a1-f588b808b79c","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"120","y":"0","z":"0"}],"uuid":"7c34c4d8-bce2-781c-486a-50e07fa81024","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"f3d0a1ab-dc80-7ba5-4d0c-21d3de246bf0","time":1.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"a8a570db-7e6a-e661-a79f-fe6e6630f4ff","time":1.54167,"color":-1,"interpolation":"catmullrom"}]},"c942401f-8d13-b8e4-8a4a-173d0e84ed99":{"name":"left_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"a356cdb2-c0f0-acc4-50b6-3548e534bfb0","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e0d54c45-2db2-7ace-b8f3-434018d1f4c7","time":0,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f22cd20f-7fd0-cba2-ee86-2ffd436a291d","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"67db9f71-991f-0caa-c480-9f841d70d07e","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"b31c3388-4100-ca50-0493-e30ac3af7eaa","time":0,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2f6cd6b8-e7f0-212e-4daa-da63a04ae071","time":0,"color":-1,"interpolation":"linear"}]},"5371e06c-b54b-154b-b1c7-cfce2a9b20d4":{"name":"right_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"771d32bd-dd3a-7bf0-1410-15cbd01564cf","time":0,"color":-1,"interpolation":"linear"}]},"3a05cd31-c803-0a4c-716f-337c916a2ee5":{"name":"left_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f71eac59-fff1-04a7-3e5e-0ac8a9322789","time":0,"color":-1,"interpolation":"linear"}]}}},{"uuid":"4642b4ed-f48c-2563-1140-dbf58b985fe1","name":"walk","loop":"loop","override":false,"length":1.5,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"9aaeddf8-969e-96ec-499b-e177f3ca9bdd","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"60822f88-85f7-a6f6-6815-eab0eec41c32","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"d4d369bd-34ea-2687-bbd9-26ed2bb20341","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"78bda604-b1c7-3d1b-a7c1-ff5295a3dcf1","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"c3377665-d618-dcea-7e29-c69bbb2bdda9","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"dea0f803-3228-b1d7-1079-45f536051fbb","time":0,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5fae9431-7c30-479b-a141-8afd7b912528","time":0,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2c4a5034-e8aa-1479-eab7-bfdf5f817950","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"22e9dcff-6d62-025e-d201-7b315f096fd0","time":0,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"26eb55a4-bfde-c9d0-95c9-a69453b52429","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"de9d485e-dab5-8181-4935-9f26a595ed51","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"-5","z":"0"}],"uuid":"d0705793-56ba-753f-ac10-5cf57b1f0324","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"2.5","z":"0"}],"uuid":"52638576-eb9c-f6dd-7f5b-04e47403bfb6","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"5","z":"0"}],"uuid":"352f9338-ebb0-9683-4313-5ecb0cff41f5","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-2.5","z":"0"}],"uuid":"d3fa48e6-eda4-b933-94dc-3cba7deac0bc","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-5","z":"0"}],"uuid":"e9a500bf-a132-1092-3e54-c0d70a47ceeb","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.75","z":"0"}],"uuid":"9e0f4761-8e81-54da-4fa4-28f80d4b51d0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"6d493461-ad31-a3ca-86d1-b594348d976e","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.75","z":"0"}],"uuid":"ea98ce1e-29e2-b66f-2c11-9e1751577cda","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"ed34e37c-9f69-37ce-ca7b-a99b3e33d4ae","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.75","z":"0"}],"uuid":"f5ef19fe-073a-838d-ae21-ce153ed2a529","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"5285314c-5162-09c4-5484-49628cf0a3ca","time":0,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"ec847310-4f84-e7d7-d766-fb53ab5849aa","time":0,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"91d34a2e-312c-0a63-74f0-0142d5294391","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"346de168-6db2-96ee-3256-63587cb1baf3","time":0,"color":-1,"interpolation":"catmullrom"}]},"81c98f4b-5ade-166e-be9a-8cbee4eddc3a":{"name":"front_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"9ab72de0-8948-bb5f-8b41-9a1161fb4091","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"fde09f44-b0bd-a56b-a445-cb4ce43e6e23","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"9525c60a-f019-24a7-87e6-ee6d749afb8d","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"70afea31-76be-75d9-46d3-43d028bcfe7c","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"8db2f7fe-8f10-d022-039e-4663c30b40ba","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"6e67f127-d75b-e721-1019-14fc3600d26f":{"name":"back_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"51b1e6a8-28f0-b252-d8e9-aea000a03c02","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"3534eed3-8978-e92a-6d2f-d663fff7a203","time":0.375,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"6abad1ca-822f-6280-faf9-c49623ecdbbe","time":0.75,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"d5222dfa-c52a-aaa9-1811-0fed2e95a0a3","time":1.125,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"9304ec04-03f8-78e1-f29c-6c69f42cf0ad","time":1.5,"color":-1,"interpolation":"linear"}]},"5371e06c-b54b-154b-b1c7-cfce2a9b20d4":{"name":"right_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cc9bae98-3244-1324-0c0e-d3dd2ecf8ad0","time":0,"color":-1,"interpolation":"catmullrom"}]},"3a05cd31-c803-0a4c-716f-337c916a2ee5":{"name":"left_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"892e5c98-765f-aa7a-ea28-f13f7ae43c9a","time":0,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-22.5","y":"0","z":"0"}],"uuid":"4073217e-05e6-bd60-58fb-c3e0139d6379","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"8bd0c01e-36dc-2b14-1c7d-e7497c86e92a","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"ded4e1d9-1a5f-01e6-a033-7599a98283ad","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"0","z":"0"}],"uuid":"6c935ae0-2d69-12b6-d39d-4ef8c12f3c90","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-22.5","y":"0","z":"0"}],"uuid":"4d42860c-b1ae-52b4-b73a-beef85c1cc94","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"f2c87eee-443b-0e50-bf0a-9ee7303828a4","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"69c9d864-76ab-4ca3-308f-8baeb422cc96","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"f81b8860-566d-743c-7b9e-30c1a690c13b","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"eb139803-e6c6-232b-79ec-d0f06a32d027","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"1fa6d108-f49b-da21-81b0-5c4bd018a52c","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"436ebcaf-7570-8c3d-368e-80184e593a90":{"name":"right_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"741619fb-c331-624f-091c-0b623f5de04b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"5b213e97-2bb8-7c37-630c-a1e7aaa0d842","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"3c925edd-4b6c-df59-9de2-fd46e659893a","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"7abf5873-1ea1-11e1-e36b-25d9fa312338","time":1.125,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"182dc758-48d0-137d-7a68-4d7b3a155dac","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"0","z":"0"}],"uuid":"d912926a-c050-b5dd-6282-ee299bb55ea6","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-22.5","y":"0","z":"0"}],"uuid":"ac422e86-edce-fea2-29a6-653f655af0b1","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.5","y":"0","z":"0"}],"uuid":"ada3c732-4aa4-4549-03f0-896ff112aa58","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"132b7f44-474a-ffc4-561d-593b28910874","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"531d539b-24aa-89e2-a9c1-a05de88dd2f7","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.5","y":"0","z":"0"}],"uuid":"edfcb841-4b36-29bb-05c3-9032b24325cb","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"43d87a42-cb2a-5375-2e88-4511b4743b7e","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"a7d96d8c-3faf-278f-b298-87943ef779eb","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"d18acf2a-399b-460f-a7e8-1cdc41bbe0fe","time":1.5,"color":-1,"interpolation":"catmullrom"}]},"c942401f-8d13-b8e4-8a4a-173d0e84ed99":{"name":"left_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"439749d0-1563-1ad8-5da9-97e8cdcd345d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"547ed0ff-908b-c20f-d4a0-f29393bd92eb","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"23227e6a-0f2e-b88f-e678-00bae5850529","time":1.125,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"4106bf4d-64cb-7fcb-e313-93f3c0f91322","name":"run","loop":"loop","override":false,"length":0.91667,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"45","y":"0","z":"0"}],"uuid":"9aaeddf8-969e-96ec-499b-e177f3ca9bdd","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"9ec65425-bb1c-2d1a-62da-16ebd057b61d","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"d9ba50bf-b192-2983-d4d1-1472501b8877","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"45","y":"0","z":"0"}],"uuid":"87889298-b035-0863-6bdf-7de30281300a","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"3e8c50d0-6ea8-dc6e-0077-a1e2e505002f","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"5fb98c66-a182-097c-c2f8-8f4dbda173f2","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"45","y":"0","z":"0"}],"uuid":"c540bb50-0723-d9a5-8c45-b3225ce8600d","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"dea0f803-3228-b1d7-1079-45f536051fbb","time":0,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5fae9431-7c30-479b-a141-8afd7b912528","time":0,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"7.5","y":"5","z":"0"}],"uuid":"2c4a5034-e8aa-1479-eab7-bfdf5f817950","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"2.5","z":"0"}],"uuid":"45fea26f-99a1-5ffd-2231-a669a559728b","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"-2.5","z":"0"}],"uuid":"bd7462be-9a4e-0725-9a96-9ccac3e05e8b","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"-5","z":"0"}],"uuid":"687e1408-c6fd-021b-2ae0-f2ea5dedb295","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"2.5","z":"0"}],"uuid":"63288b3d-abd4-6c2f-3c5c-0fa7e9b08dcd","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"5","z":"0"}],"uuid":"873eeba7-b3da-087b-5f42-37d37ab23d71","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"-2.5","z":"0"}],"uuid":"7787f115-9bfc-1011-3c5b-e251e73f6065","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"22e9dcff-6d62-025e-d201-7b315f096fd0","time":0,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"26eb55a4-bfde-c9d0-95c9-a69453b52429","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"de9d485e-dab5-8181-4935-9f26a595ed51","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"5285314c-5162-09c4-5484-49628cf0a3ca","time":0,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"ec847310-4f84-e7d7-d766-fb53ab5849aa","time":0,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"f3ce685d-3702-b666-1d52-f2c3dbb7a967","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"346de168-6db2-96ee-3256-63587cb1baf3","time":0,"color":-1,"interpolation":"catmullrom"}]},"5371e06c-b54b-154b-b1c7-cfce2a9b20d4":{"name":"right_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cc9bae98-3244-1324-0c0e-d3dd2ecf8ad0","time":0,"color":-1,"interpolation":"catmullrom"}]},"3a05cd31-c803-0a4c-716f-337c916a2ee5":{"name":"left_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"892e5c98-765f-aa7a-ea28-f13f7ae43c9a","time":0,"color":-1,"interpolation":"catmullrom"}]},"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"d709fc5a-8350-da8c-53b3-a40c25f3b016","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"35ecf87a-bedc-7a14-a40b-e6b4ba66e969","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"9116ff90-5872-ab22-e384-e0ceaf3436a5","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"2e5cd626-b44b-5969-6fc4-43ac37037752","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"1f7bfd87-80e9-ed24-1012-03461aa4698d","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"1db6e464-dce2-3fd9-0e57-6bfc17d7b86c","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"f2f4fc25-983e-d1da-dc88-f7a38e4636a8","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"c81a8133-a45a-784e-1087-0837e69535f7","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-7.5","y":"-5","z":"0"}],"uuid":"768cc4b1-7942-604e-1d3e-03d80679b22f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-2.5","z":"0"}],"uuid":"076ea76f-053f-1b63-d26d-4afe34124582","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"2.5","z":"0"}],"uuid":"98cbc9e9-ac63-d22b-5a8a-08180c794768","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"5","z":"0"}],"uuid":"d55c9330-6818-e5be-ea2a-d06ab4ebb670","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"2.5","z":"0"}],"uuid":"25107aa3-6be0-53a1-3a1d-05bfe1c09bf2","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-2.5","z":"0"}],"uuid":"2babf29d-3a10-5f11-8636-bb42e5e97bf4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-5","z":"0"}],"uuid":"47b6bdc0-7d28-3758-dfee-8b3a98979461","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"81c98f4b-5ade-166e-be9a-8cbee4eddc3a":{"name":"front_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"1f5d185f-1021-6c5b-9a6f-3922e7635e31","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"753af197-18c9-3e96-28c1-d5e25d26fcb6","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"d824f74a-8325-1c7e-4f67-50dcb987ead2","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"7417cc1d-0a41-7e51-f928-32277c8435eb","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"6c2f94c8-d72e-1b32-a8ea-6ccc7a405e3d","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"50e37f7e-5aae-4ca6-c510-3d032630e525","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"f8433ded-8c06-f647-6afd-2467559ed44e","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"922a3145-26b4-70ca-a930-e8ba98f73a5f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"20da063d-9109-9c89-706b-7a1cec5ead3e","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"0f82d10a-efd2-c76d-f44e-ab9716d03163","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"43af1291-643d-e435-e345-18aba110484e","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"3a738583-a65b-e679-6513-a9d958a7cd73","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"de720af0-262a-d1f2-8542-8c02548e9713","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"2f5e4d2a-8fc4-662f-bfa2-a9178ea0a040","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"746977ac-4ef3-5fce-33bc-5c7935ab1f78","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"6e25dfbe-df32-e0a4-c058-4d12d8fbb111","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"52d82805-3f0e-c083-2507-0ed9c09227e5","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"f279a7fc-ea92-6386-68a4-48601531e998","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"b23177b6-3233-0d50-d425-87055cbb8d69","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"39783afa-11d6-239e-9691-de4a55f1b627","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"9a63dd9a-4d60-8b4a-c9e1-c6422f2ac540","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"436ebcaf-7570-8c3d-368e-80184e593a90":{"name":"right_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"5ed78388-81f4-b52d-ed0a-c07bb3f41a97","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"ff65fd54-ffb2-822b-3d46-64111fb00f31","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"1bd77986-ca36-d1cc-d7b3-9d76da8ec2af","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"5d94846d-bf39-0ee8-1e6f-fccafecb58a5","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"1efeb39c-fcd1-c586-9b8c-e353e002c6ab","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"29fbec2b-d6a9-4378-e3f0-b1323dbd9f99","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"e5813c2c-417a-e9f0-f884-f24ba7e935df","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"f1afb47e-c3b7-eb28-58b3-e186f5f50326","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"0a77b47d-db3b-0396-bcf1-4d28c5f678de","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"9293d127-897d-ccd1-79af-68c3466c051f","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"6209ac0b-10f9-16e8-fb88-8ff12c4da021","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"4b7973c2-dece-e433-d49b-0d91b25e4b9d","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"c05bf0ae-689a-84d3-e888-cf771fc004f4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"915bde67-8104-9f0d-64d6-0e8f7b8884e3","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"4737d868-1811-5746-dc7f-5a021d215b17","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"1b037746-788a-fac5-53df-8a483be5e6de","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"5506fdcb-071f-9f05-8461-1126d926e475","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"30247f7a-363c-971c-abc3-d28b3d8fdb81","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"68c69f55-6ef0-a7e2-b760-8734009d5fae","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"9bd6c318-0662-cf37-37f3-b45fe1e71ad5","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"d3b21f76-0334-afe3-6e31-39c04633f835","time":0.91667,"color":-1,"interpolation":"catmullrom"}]},"c942401f-8d13-b8e4-8a4a-173d0e84ed99":{"name":"left_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"525671c2-57b1-b81e-fddb-3c3a9d9ee81e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"087222c9-19d7-52ec-d96a-176c26680920","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"7a0e9bc7-6ecf-99c1-445c-d0e8e0601afd","time":0.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"39bfc63d-774c-5316-0b96-9a85d7bd1583","time":0.45833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"8f13e430-9bb3-9880-7127-1757bf63c8af","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"1bf32ab8-3394-953c-b746-7ef81e416e73","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"f0051431-d00b-5bd2-1084-d63d4b7da6b2","time":0.91667,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"e9ddab9f-74b7-2fb1-5c04-ba511e9d0b8c","name":"guard","loop":"loop","override":false,"length":1.25,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3d26cb97-e5ce-3f17-43cd-643a71443391","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"971eea94-f544-0922-d057-eed67a3cccb6","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"12.5","y":"0","z":"0"}],"uuid":"f40ff047-9154-08db-c669-bf9c7c0e5b2a","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"ac566804-2035-411e-1aca-87c763b96092","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"12.5","y":"0","z":"0"}],"uuid":"f0451f27-00ac-18e3-232b-5f042461c53c","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0ea521b7-7dd8-8902-6a82-8b80533e9667","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"dea0f803-3228-b1d7-1079-45f536051fbb","time":0,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"10"}],"uuid":"5fae9431-7c30-479b-a141-8afd7b912528","time":0,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"13.7453807011","y":"24.3683005175","z":"5.7632011063"}],"uuid":"2c4a5034-e8aa-1479-eab7-bfdf5f817950","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"22e9dcff-6d62-025e-d201-7b315f096fd0","time":0,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"26eb55a4-bfde-c9d0-95c9-a69453b52429","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"de9d485e-dab5-8181-4935-9f26a595ed51","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"7.5","z":"17.5"}],"uuid":"5285314c-5162-09c4-5484-49628cf0a3ca","time":0,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"ec847310-4f84-e7d7-d766-fb53ab5849aa","time":0,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"f3ce685d-3702-b666-1d52-f2c3dbb7a967","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-54.7711766863","y":"-17.5581677785","z":"-0.5641567311"}],"uuid":"346de168-6db2-96ee-3256-63587cb1baf3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-54.7711766863","y":"-17.5581677785","z":"-0.5641567311"}],"uuid":"07abdd8d-91c4-b952-c78c-c12513a72de4","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55.2586332172","y":"-22.4395396341","z":"0.5841097705"}],"uuid":"ea1776a7-c481-6e51-6b62-16ab802a4cc4","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55.5495253525","y":"-24.8764419773","z":"1.1913415952"}],"uuid":"a4898274-e46d-031e-d5f0-f590cc959f6a","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55","y":"-20","z":"0"}],"uuid":"c95c2520-73b6-66f2-436f-b14f914b43d3","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-54.7711766863","y":"-17.5581677785","z":"-0.5641567311"}],"uuid":"9e65bf4d-5fdc-8811-775b-dde8a3c5163e","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"-2.5","z":"0"}],"uuid":"f34d68fa-4fe8-de16-e32b-f84d314a87f9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-2.5","z":"0"}],"uuid":"139d2723-f811-92f3-87b0-2ab0d94d4ca8","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"5","z":"0"}],"uuid":"9167b23f-c09f-fd28-7c3a-bd26fb0fa90d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"7.5","z":"0"}],"uuid":"43bb8d4d-e46b-dd86-1585-e5d55b7550ae","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"de8df2d8-013f-9ba7-ba6c-08578f9a094d","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-2.5","z":"0"}],"uuid":"e470b839-3cdf-1b9d-8543-532586599311","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.75","z":"0"}],"uuid":"d3e16565-42b1-d549-f8a7-1ca80ef64adc","time":0,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-7.5","y":"-25","z":"0"}],"uuid":"ff8900c4-b6d7-2cd5-686b-c0d1fa7e89f8","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-25","z":"0"}],"uuid":"a810309d-a926-376f-44b0-effeb8288be2","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-30","z":"0"}],"uuid":"ee5927f1-0b2b-ac7d-22e6-05a5faa6f524","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-32.5","z":"0"}],"uuid":"a6a894f3-af64-7649-7d63-62b822a9c412","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-27.5","z":"0"}],"uuid":"1c3c9cac-143e-d53c-2b14-de3b6be57c05","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"-25","z":"0"}],"uuid":"df0e4a33-bfc5-588f-be03-2d15c236086d","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"08fa9aea-2f3a-ad98-9647-65d992767b55":{"name":"left_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-85.4374286751","y":"-36.1324307237","z":"79.7006674603"}],"uuid":"2ce710fb-f885-ee5d-0f82-0cef47337413","time":0,"color":-1,"interpolation":"catmullrom"}]},"621973ec-d68a-5564-596e-d9cd80a4a0f6":{"name":"shield","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"65"}],"uuid":"a4f9efde-a87e-d679-6054-dc24c6e785fa","time":0,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"7.5"}],"uuid":"58be1892-c208-24fa-9f69-39859b610a0f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"5"}],"uuid":"55f2562e-6c6c-f724-fd73-b86e5b1ac42f","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"-5"}],"uuid":"e6b6a8c2-8be9-8341-cba5-505fd1a2efcb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"-7.5"}],"uuid":"115bd3f7-234b-a5e7-9806-bcae53842ea3","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"5"}],"uuid":"5fe6181f-cbd5-848b-98d1-e8152f8532a4","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"7.5"}],"uuid":"8a463b24-6c8d-a44c-f55e-377e12a64a3e","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"35b4882a-31ab-a98d-4865-17b7572a0f8a","time":0,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"-7.5"}],"uuid":"e52942b8-0916-f087-090d-0485d99289d5","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"-5"}],"uuid":"fa3c206b-d3a2-a21f-fc76-e7317209c85a","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"5"}],"uuid":"c391f3a4-8bb4-6c91-1fe8-ad129c430095","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"7.5"}],"uuid":"4db2de23-12cd-dc86-e7dc-fe2d5c63feda","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"-5"}],"uuid":"6040e07c-fea3-f464-5a06-3c3a42d431a0","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"-7.5"}],"uuid":"2a083383-9eca-c82b-a401-b9830a957626","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"617859b4-9b4f-d736-4db1-8e36b5fd2eeb","time":0,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"4a4d08eb-0c10-51e8-6dde-e173838f1957","name":"hammer_attack_1","loop":"once","override":false,"length":1.25,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1b1b7dec-ba79-d599-a3db-11a766e66cf9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.6696299537","y":"-19.9853884562","z":"-2.5880725782"}],"uuid":"786a0300-3f6a-d89d-015f-f695a9753917","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.7136409398","y":"-32.4846643084","z":"-2.716354566"}],"uuid":"4fbdef4c-a87a-a437-015c-206e2342f954","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"17.5","z":"0"}],"uuid":"0ba86cb3-9d82-615f-6fc4-7cc3ceda9f6b","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"22.5","z":"0"}],"uuid":"73d2cef0-0cb3-414e-34fa-762c7750e570","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.75","z":"0"}],"uuid":"220a4b15-cecf-9c21-39c2-05f78068c80a","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7d930b96-bad1-0866-7acf-9ecc87f16b48","time":0,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"7.5","y":"5","z":"0"}],"uuid":"c78982e4-af8f-37fe-0318-7d89d4d3f859","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"9.8086313282","y":"-2.4255811716","z":"-0.8807534581"}],"uuid":"e3832921-14fd-900d-6e48-f5f9bacdd243","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5.2462486798","y":"9.8553006608","z":"1.6477784446"}],"uuid":"27fbf510-1854-9d2a-a80e-7caeb72cabe6","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5.6733365898","y":"9.6173935822","z":"4.1728100336"}],"uuid":"5b631dfe-f46f-3d38-49ab-7c138ab3d9e8","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ddfe426e-bf46-2d44-4ce1-08a981a67d78","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5f9f87f1-907e-d611-63fc-d565e9595f46","time":0,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9eaa2782-2fa4-4eda-cbfb-ee5f6b6205f7","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"1f44ac96-e70e-7b77-fe61-6784672e2d61","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5.0767330166","y":"9.9615580981","z":"-0.8804470158"}],"uuid":"2f3e7959-a76f-7401-b03e-df0a27a289d3","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5.5138520612","y":"-24.8983739694","z":"2.3272993503"}],"uuid":"7b657bb8-ca8f-b8da-a4cf-80b605cfc7a7","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5.7686322235","y":"-29.8742012582","z":"2.8806590864"}],"uuid":"4b9148a2-f734-64f3-769a-a36daabd7748","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"36f74993-13cc-8807-2fcb-fda1ca9b127d","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"9b495d0c-129d-009f-6a15-e30f7baba72d","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"e78c6bc7-9646-cf70-b054-f0dde92d2ddb","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"e47353aa-4064-349e-ee79-94840880db87","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fd3ea7d0-8537-40ea-b93b-c72a9d897c1b","time":0,"color":-1,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-89.841262567","y":"-7.0453261831","z":"-2.5781617804"}],"uuid":"6eb13775-5af9-65f7-dc8a-659325059ba9","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-87.341262567","y":"-7.0453261831","z":"-2.5781617804"}],"uuid":"42066c40-7d16-9890-706c-fd9137e7a5da","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.6570134652","y":"63.4220934291","z":"46.1580740926"}],"uuid":"86e08d4a-e039-98f2-8814-7488a93aef9b","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.1937755234","y":"64.2455098641","z":"45.5178564722"}],"uuid":"d74af142-54ce-90c8-cbc6-bce2c3396947","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"e8f00e41-be0a-1bbf-1680-3e2ebde0bbf6","time":0,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"60","z":"-20"}],"uuid":"0e010224-4c62-9495-1839-491b3a502a8c","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"fc434329-f147-42a3-d3c3-331d7b5cbff8","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-32.5","z":"-20"}],"uuid":"79057b83-c498-3ecf-7fcf-0f5502c17670","time":0.70833,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"-65","z":"0"}],"uuid":"c7205370-0742-bdf2-2f03-5dc04cb0fed1","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-52.5","z":"0"}],"uuid":"01d3604c-f9f6-0565-e903-0f4e2a7538d9","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.5","y":"-72.5","z":"0"}],"uuid":"fb38379e-eb9d-6143-4146-e3b2d007b113","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"62.5","y":"-72.5","z":"0"}],"uuid":"7a1ad48f-8618-38f8-f5d7-cb55c4ca9e7d","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"30cc3c4d-3f8b-4a38-64d1-d8593fe6d38d","time":0,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-17.5"}],"uuid":"dc638d5e-24cb-7a35-f001-f99aa8d0f0b9","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"8.9188017707","y":"-4.4192464358","z":"20.2873425764"}],"uuid":"9295b6f2-d756-a453-b47b-44ae122382ed","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"16.6017897738","y":"-11.3133541722","z":"33.3441109825"}],"uuid":"7ba320e6-56ee-83b0-0519-06b14938d274","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0fa74dd9-0fa8-9f7d-923d-cf13e136c071","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"e9b8500e-748c-6723-520f-89f17d13ce3e","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"35c4d8ab-7dad-96bf-339a-de8d6e95c5ad","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"62cffffd-8f61-de42-9113-dd6e828a972e","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-11.74","y":"0","z":"0"}],"uuid":"f6289bec-1d33-3b34-cf82-679bb34a7acc","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"bda2abfd-d6b9-f857-b331-0ede0ac430cf","time":0,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-32.5","y":"0","z":"0"}],"uuid":"a741be0a-5165-4ce2-6662-6671dd53d9bc","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"85016fbc-43e6-d741-c0ab-d50471214e38","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-38.4268434977","y":"25.4967141976","z":"28.4834661178"}],"uuid":"05ec89f7-c93e-d091-6558-51a7b425bf61","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.3985022075","y":"22.0779417413","z":"24.7639715819"}],"uuid":"be37b7b2-c34c-a12c-fdd4-0b80b67b9cd4","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0b6665ea-0009-3118-3e74-d022c4cd4683","time":0,"color":-1,"interpolation":"linear"}]},"621973ec-d68a-5564-596e-d9cd80a4a0f6":{"name":"shield","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5"}],"uuid":"cf2ad0bb-4211-41df-af74-f79e757fef2b","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"622019b8-f799-e3b7-8e3c-185f7614fe20","time":0,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f09c202e-fb53-4a38-63cc-1e5c978cd5bf","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5462389525","y":"4.8811889482","z":"-1.08482395"}],"uuid":"d09f6d36-356e-efe3-d4d7-5a2ddba1e23d","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-19.4009377236","y":"18.3210986848","z":"-0.6039670971"}],"uuid":"8bf8de06-c288-e7e7-9263-ab55e5e10bb9","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"16.7268205668","y":"-5.6526197535","z":"13.8547690382"}],"uuid":"9b9cce11-631d-656e-a4b8-6fa3b2fb8f7e","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"24.2268205668","y":"-5.6526197535","z":"13.8547690382"}],"uuid":"81588c09-34c5-e0f0-4a38-688ff32acf07","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"12.5","y":"0","z":"0"}],"uuid":"4034aadd-ac15-4222-7441-375f4f0fc834","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"41e491fc-fff7-a044-c42d-e5e0c0f7df2a","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"ba86adb5-6c3a-5688-2fad-cfcd19578e1d","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.0175412737","y":"2.349144295","z":"0.8555298152"}],"uuid":"7d77986f-0a6e-979b-25b7-d8e1cb229d50","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"12.5","y":"0","z":"0"}],"uuid":"61311066-9e6b-41ea-ae18-df8c531c8efb","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"07416e53-9f01-d6df-d09a-5b9cd1447fed","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.9409310696","y":"5.7357552465","z":"-11.1250111054"}],"uuid":"e18e5d62-99ea-f277-1b69-cda940d99b38","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-19.3445693673","y":"-25.265343289","z":"3.2804683392"}],"uuid":"bc6f5eaa-28c1-5e43-c178-95a1741507f9","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-26.8445693673","y":"-25.265343289","z":"3.2804683392"}],"uuid":"4e24d6b8-b0de-73d3-8759-947bd05753e9","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"d4aa0a4b-d1b1-986b-c5e8-e9940bdd73bd","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"78958494-e126-049c-3d91-e36a104b36f3","time":0.70833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"19.842303908","y":"2.5586821925","z":"-7.0523929293"}],"uuid":"8b5037df-3a80-55df-ad10-da2159966be6","time":0.875,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.342303908","y":"2.5586821925","z":"-7.0523929293"}],"uuid":"171c02bb-b3ad-aac7-b9ff-9fbecf1de0f8","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"1","y":"2.5","z":"1"}],"uuid":"72ac3b66-e62b-f26d-5f69-3311be6b17b1","time":0.875,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"b2a515a2-421b-228b-0674-610e4526b0f3","time":1.25,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"e50f560b-5445-2865-134b-afa36dd5329b","time":0.70833,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"bfe4aa0a-998a-f8da-4357-46864057679b","time":1.04167,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0f95b084-4bac-29b7-391f-0f02394ab19b","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"22552919-6050-7754-acd1-d0cd20c47e0e","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"5371e06c-b54b-154b-b1c7-cfce2a9b20d4":{"name":"right_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"a9ca584d-8130-8424-fbbe-6d61c4d1450a","time":0,"color":-1,"interpolation":"linear"}]},"3a05cd31-c803-0a4c-716f-337c916a2ee5":{"name":"left_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e82803e7-4fbb-f7dd-1b65-5093476920f3","time":0,"color":-1,"interpolation":"linear"}]}}},{"uuid":"7acd538f-4e3c-22ad-ec93-0ed59151a482","name":"hammer_attack_2","loop":"once","override":false,"length":1.25,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5","z":"0"}],"uuid":"73d2cef0-0cb3-414e-34fa-762c7750e570","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"22.5","z":"0"}],"uuid":"69801aaa-6d29-af98-94a9-9d9fab4b3ee7","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-0.1091175935","y":"-2.4976190449","z":"2.5023786868"}],"uuid":"3db2f3f5-32ab-7653-3f36-509bd04c35c5","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"22.5","z":"0"}],"uuid":"a7662d40-adb4-6560-57ed-097fc3c8718e","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1092217464","y":"-4.9976190427","z":"-0.0023854959"}],"uuid":"5419dcdb-10a7-dd4b-8e56-150cc349e623","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f5ea03a5-2acc-3086-6661-5539130fafe3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"8d3c759b-46f9-939c-902d-b11e18c85640","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"df30836b-251f-82da-81d7-b8e67c48534e","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"8d8181fe-2f28-965b-6a40-cbb1fbf9872e","time":0.83333,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"5.6733365898","y":"9.6173935822","z":"4.1728100336"}],"uuid":"5b631dfe-f46f-3d38-49ab-7c138ab3d9e8","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5.9373275637","y":"19.5663908517","z":"5.2167003589"}],"uuid":"d7c163b0-5fed-a9c8-322f-f1358e30fa7c","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"11.1507296471","y":"24.5386408206","z":"5.7845420461"}],"uuid":"41d820f2-fe7e-76ec-4ea5-83eda9bac168","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.7710390435","y":"-19.7205168855","z":"-2.5682502902"}],"uuid":"584f2e37-9bf5-3386-3d2e-bd1bc6086ccc","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"13.2710390435","y":"-19.7205168855","z":"-2.5682502902"}],"uuid":"a51a1766-1f3d-8988-50b4-7fc728b93c7f","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-5.7686322235","y":"-29.8742012582","z":"2.8806590864"}],"uuid":"4b9148a2-f734-64f3-769a-a36daabd7748","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.3787161594","y":"-47.2626005229","z":"5.433274539"}],"uuid":"73d92e2d-f637-27d9-df51-5588d75375fa","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.7102375661","y":"-52.1340621627","z":"7.1775562076"}],"uuid":"ab6964e1-4250-21fe-1b8c-63ecd9d1bbbe","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-9.0158790017","y":"21.8194246308","z":"-7.0989933162"}],"uuid":"3730f41f-cff2-2a4b-2579-dfb6ffede68d","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-14.0158790017","y":"21.8194246308","z":"-7.0989933162"}],"uuid":"2489eec9-8732-9f85-3239-7515cef5033f","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"e47353aa-4064-349e-ee79-94840880db87","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"8e7165d1-5889-d88f-085b-ada0a74c47b7","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"6a930b54-9396-600b-7072-2c19d5c1901c","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"37.5","y":"0","z":"0"}],"uuid":"a736f3fd-eb5f-5bd1-6e0c-7de3e5ce4a76","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2916e978-c2fd-1ff4-2932-d9dffd2154e6","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-24.1937755234","y":"64.2455098641","z":"45.5178564722"}],"uuid":"d74af142-54ce-90c8-cbc6-bce2c3396947","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"80.1423824502","y":"73.0563319589","z":"100.4673277231"}],"uuid":"1afbd003-15e6-205f-be19-9864c2035905","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"55.9593666764","y":"63.5254267789","z":"74.0015760198"}],"uuid":"3696b7fa-ae6a-04fb-b173-61821aa06556","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-60","y":"0","z":"0"}],"uuid":"bfb71df5-ee13-b7e5-3137-48e09eacb8ec","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-72.5","y":"0","z":"0"}],"uuid":"34ee0766-a837-e045-b206-23d8a3b9d6fe","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-32.5"}],"uuid":"911076a9-a54c-9b69-38b3-87590a8884b6","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3ae9bfc0-89d3-26c7-2d4a-48021345c7ad","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"-65"}],"uuid":"e9808c47-8709-b384-ddb5-267d720b3fa4","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"65","z":"0"}],"uuid":"19fe472a-9e8d-a41e-4f14-e6393a8f1d6c","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-42.1862611809","y":"55.2245633302","z":"-47.8137388188"}],"uuid":"8d0fc26a-9901-62f8-40bc-c9f77ce0998d","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"62.5","y":"-72.5","z":"0"}],"uuid":"7a1ad48f-8618-38f8-f5d7-cb55c4ca9e7d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"132.4496748596","y":"-70.3622198506","z":"-167.1044344205"}],"uuid":"e6457e70-4684-6244-ade5-83974bb722e4","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"183.6473435932","y":"-62.882775119","z":"-165.6886667993"}],"uuid":"fe5ecf53-6ee1-1cd4-9d11-f04fbf5bdba5","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"69.0215502875","y":"-61.7777128461","z":"-50.5143631535"}],"uuid":"f8ae30c1-da2e-c946-afc7-534cf4cbe392","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.5215502875","y":"-61.7777128461","z":"-50.5143631535"}],"uuid":"794f1018-dec9-57b8-c197-77904accdac0","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"16.6017897738","y":"-11.3133541722","z":"33.3441109825"}],"uuid":"7ba320e6-56ee-83b0-0519-06b14938d274","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.0622974315","y":"-4.2453013066","z":"11.7678224621"}],"uuid":"dc7e6d1f-c7a3-11cd-8acb-384dd50d3cda","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"1.8700552083","y":"5.078545816","z":"-14.1327227497"}],"uuid":"d5aee4bc-23dd-df08-52fe-95d7c5a19fbe","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"12.3064507049","y":"4.897691423","z":"-9.1143093073"}],"uuid":"168f3874-1de5-c2e3-95d5-59cbfc583d20","time":0.83333,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"62cffffd-8f61-de42-9113-dd6e828a972e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-14.5108186991","y":"-3.8409657163","z":"-14.5108186991"}],"uuid":"993a0a10-1f13-7f73-e4e6-213fb22e252a","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-31.1497318285","y":"-6.2796719244","z":"-24.2476877954"}],"uuid":"cf6ece02-367a-e5c6-b54d-1c91304eed70","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-38.4268434977","y":"25.4967141976","z":"28.4834661178"}],"uuid":"05ec89f7-c93e-d091-6558-51a7b425bf61","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.686261181","y":"17.3877183348","z":"18.2489023831"}],"uuid":"581599ed-9106-f316-2859-a1ebb4c46754","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"1.7430527801","y":"17.9074425817","z":"39.2405275584"}],"uuid":"f071053f-0601-1b5f-e367-d7fd0818b505","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"621973ec-d68a-5564-596e-d9cd80a4a0f6":{"name":"shield","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"5"}],"uuid":"cf2ad0bb-4211-41df-af74-f79e757fef2b","time":0,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"24.2268205668","y":"-5.6526197535","z":"13.8547690382"}],"uuid":"81588c09-34c5-e0f0-4a38-688ff32acf07","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"24.23","y":"-5.65","z":"13.85"}],"uuid":"779e4f0e-5633-73da-ed00-d06be443b094","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.7824927505","y":"-0.7771014437","z":"-0.8689597174"}],"uuid":"0e11b7bf-d2c1-e003-0bf3-f186d2f5502a","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.7824927505","y":"-0.7771014437","z":"-0.8689597174"}],"uuid":"714e6d29-aa96-1be4-39be-e352922679e3","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"20.0175412737","y":"2.349144295","z":"0.8555298152"}],"uuid":"7d77986f-0a6e-979b-25b7-d8e1cb229d50","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.02","y":"2.35","z":"0.86"}],"uuid":"94873de5-bae7-2ae4-bf73-c02569337dcf","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.02","y":"2.35","z":"0.86"}],"uuid":"ffc19709-b900-6a98-a2de-45b060df351e","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.52","y":"2.35","z":"0.86"}],"uuid":"c8224408-7093-6a60-7cb0-705ee5d1e29c","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"436ebcaf-7570-8c3d-368e-80184e593a90":{"name":"right_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4a9bfe70-2191-670c-84be-64863e24150e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"20b23c03-80f9-5165-1a38-119818e01545","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"7ddbfeab-7e8c-525c-8981-e0a400bf1a34","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-26.8445693673","y":"-25.265343289","z":"3.2804683392"}],"uuid":"4e24d6b8-b0de-73d3-8759-947bd05753e9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.8445693673","y":"-25.265343289","z":"3.2804683392"}],"uuid":"ea344538-76f9-883c-dae4-5f884e250bba","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.3445693673","y":"-25.265343289","z":"3.2804683392"}],"uuid":"28c1fc67-fb29-812d-982c-2ff9fa2e3726","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.2773272072","y":"4.0215500522","z":"-6.3358608564"}],"uuid":"475b96f5-e83d-f59c-486d-259b42ea531e","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"19.7773272072","y":"4.0215500522","z":"-6.3358608564"}],"uuid":"8254f5d6-49be-c3aa-7086-d531d03888dd","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"27.342303908","y":"2.5586821925","z":"-7.0523929293"}],"uuid":"171c02bb-b3ad-aac7-b9ff-9fbecf1de0f8","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.342303908","y":"2.5586821925","z":"-7.0523929293"}],"uuid":"538156c8-36da-6c4e-ab53-8fc2cd859422","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.34","y":"2.56","z":"-7.05"}],"uuid":"fe5e9a50-6505-eefc-2a3f-e1cf8cd4edf1","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"19.84","y":"2.56","z":"-7.05"}],"uuid":"6adf892a-a4ae-e27c-79a2-4b7f48e0e009","time":0.83333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"9.7824020671","y":"1.7555897191","z":"-7.3111717268"}],"uuid":"7ff94733-d327-a447-2899-a10026bdbba4","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"c942401f-8d13-b8e4-8a4a-173d0e84ed99":{"name":"left_feet","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cff4e154-7491-0984-60a5-eac79e4b1bab","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"326b623b-2b43-6c49-d977-4095567cecf0","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"be24df1f-f7e3-c63b-fec5-20ae98caeef9","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"7092b32c-17a5-8b3d-6d83-3ba0b6bbb5f6","time":0.83333,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"b2a515a2-421b-228b-0674-610e4526b0f3","time":0,"color":-1,"uniform":false,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"829f03e6-f064-6f0a-a12f-098251a8ee6e","time":0,"color":-1,"uniform":true,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"db30d203-8b09-6d1c-5cf7-67f604f949d6","time":0.66667,"color":-1,"uniform":true,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"6aead5b7-6b9a-3567-5b7d-ea9757392256","time":1,"color":-1,"uniform":true,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"2","y":"2","z":"2"}],"uuid":"0c42d890-1644-e968-a462-29d0f7f7d316","time":0.83333,"color":-1,"uniform":true,"interpolation":"linear"}]}}},{"uuid":"68929995-de66-3381-35d2-c6647ca6e67d","name":"hammer_attack_3","loop":"once","override":false,"length":2.25,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0.1092217464","y":"-4.9976190427","z":"-0.0023854959"}],"uuid":"5419dcdb-10a7-dd4b-8e56-150cc349e623","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5.11","y":"-5","z":"0"}],"uuid":"d0f1225e-ba33-daa3-0967-632cefc0ea9b","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.6474230458","y":"-4.9238497548","z":"-0.870384675"}],"uuid":"1f4f41a8-43b6-328e-6bb7-d2029414fa15","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.65","y":"-4.92","z":"-0.87"}],"uuid":"e3c1ae52-6f22-b758-b05f-a1d0139b6803","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"0","z":"0"}],"uuid":"97241568-f93f-1e8c-ecc9-c8790855e6b9","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.3907782536","y":"-4.9976190427","z":"-0.0023854959"}],"uuid":"88714e3a-5fe9-7e4f-6583-3c9833558bbe","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.27","y":"-5","z":"0.06"}],"uuid":"0f4dddc2-dfb8-403b-9946-5b7f8e4e528e","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10.15","y":"-5.01","z":"0.09"}],"uuid":"6da355c3-1780-f742-e6f2-6d6049c2eb93","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4c8660c3-4d74-64fd-1d0f-f49dd9ec198d","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"df30836b-251f-82da-81d7-b8e67c48534e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"ef5008f2-5d53-a29f-cfb1-6773b9c23368","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2.5","z":"0"}],"uuid":"cf1a90c9-f65d-609c-cd90-2a8a4bebc073","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3.5","z":"0"}],"uuid":"d2f276cc-9f5d-b832-38ad-ba0bc1a99a1f","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3.5","z":"0"}],"uuid":"c61ffef9-abab-b6ea-1f84-4ad8158d4976","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-0.5","z":"0"}],"uuid":"dcb15320-11dc-ff08-49d4-ed8495527e6d","time":1.75,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"13.2710390435","y":"-19.7205168855","z":"-2.5682502902"}],"uuid":"a51a1766-1f3d-8988-50b4-7fc728b93c7f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.7710390435","y":"-19.7205168855","z":"-2.5682502902"}],"uuid":"11052c0f-2480-f2f3-8eb9-498d8c51a9d7","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5.9934708057","y":"-4.2747370108","z":"2.7150195278"}],"uuid":"11c940c3-1427-2168-a941-9f5a29690e04","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10.9644846511","y":"-19.7205168855","z":"-2.5682502902"}],"uuid":"c3809dc2-dc2b-8271-10ba-5b4f39cbbd4c","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-11.8746557255","y":"9.6866902376","z":"4.5488344014"}],"uuid":"f0f40104-422a-824e-69ec-d362da3e04ad","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.2254119804","y":"-4.4280254989","z":"7.9986462362"}],"uuid":"19efc726-2c51-fb26-e988-9708cbee9e2a","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"33.2926333821","y":"-11.8214595291","z":"-3.6332869614"}],"uuid":"b860f581-ee54-ac34-059f-6cac99cdbeff","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"33.29","y":"-14.32","z":"-3.63"}],"uuid":"40eb45e7-4d96-2308-16d4-adcf99c1863b","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"43.29","y":"-14.32","z":"-3.63"}],"uuid":"9eef37bf-699e-24fc-42ba-a69852b8042f","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.9060405348","y":"0.9816687976","z":"-0.0251436091"}],"uuid":"8ea26349-b439-1823-3fa9-0455b21d3401","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-14.0158790017","y":"21.8194246308","z":"-7.0989933162"}],"uuid":"2489eec9-8732-9f85-3239-7515cef5033f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-3.0281240726","y":"22.4040808767","z":"-4.4753047279"}],"uuid":"c0adc41e-db2d-78a5-b9b9-43d9ed9b4838","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"19.4718759274","y":"22.4040808767","z":"-4.4753047279"}],"uuid":"42d31442-70b9-a55c-99e4-e7f4a2db8db6","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"11.9718759274","y":"22.4040808767","z":"-4.4753047279"}],"uuid":"0f4902fb-f80d-3d9e-e652-cdc907f56013","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"1.9718759274","y":"22.4040808767","z":"-4.4753047279"}],"uuid":"2077bbd7-27ff-f1cc-db12-f2495a9f4319","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-8.03","y":"22.4","z":"-4.48"}],"uuid":"e8dc0cb3-5776-b082-c493-df41b1a9adab","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-3.03","y":"22.4","z":"-4.48"}],"uuid":"8acf989a-892d-757e-df9c-3420adc48d7a","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.03","y":"22.4","z":"-4.48"}],"uuid":"b4bd0a4a-bc68-433e-76a6-24761c62df78","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-3.03","y":"7.4","z":"0.52"}],"uuid":"d83ee307-1871-0082-1f39-d161aee2687b","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2916e978-c2fd-1ff4-2932-d9dffd2154e6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.5","y":"0","z":"0"}],"uuid":"ae26a00d-d136-03b1-41fe-59dfee4dce75","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"50","y":"0","z":"0"}],"uuid":"9370ace8-772e-e156-5e99-f6c3b61217db","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"7987262d-ced0-a5f9-d03b-3b34ad78349e","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"fbab7eb6-22bf-07fa-8cbd-48498916e252","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"0"}],"uuid":"ffa33c7f-1a8a-6f0a-4d95-04c032d39e13","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"07ddc4c6-648a-4e75-20ea-5994b7ddf947","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"90","y":"0","z":"0"}],"uuid":"6d2dd852-7c8e-6f70-fbf1-57f421cedd66","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"0","z":"0"}],"uuid":"bbcebcdf-c37b-06c3-50ec-af598fec6431","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"25","y":"0","z":"0"}],"uuid":"e611f0ce-7104-2158-d034-c65a45490c99","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b9eff2f4-042f-5cdd-e302-16bd68c3f8f8","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-72.5","y":"0","z":"0"}],"uuid":"34ee0766-a837-e045-b206-23d8a3b9d6fe","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-127.5","y":"0","z":"0"}],"uuid":"0b9a6a49-f395-94f1-8312-a532f8b9b6f3","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-127.9244467882","y":"-7.9185000792","z":"6.1267309018"}],"uuid":"27ebc863-901a-b6ab-5065-5c8bc4ea5d1e","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-137.9244467882","y":"-7.9185000792","z":"6.1267309018"}],"uuid":"f797a277-ccfd-db57-d9b2-01e5fa9d9c08","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-178.0233407839","y":"3.3225807566","z":"16.9537500901"}],"uuid":"b4a1b140-65f2-c832-e8b2-bbb1fa048581","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-128.1204456239","y":"-9.2593718846","z":"2.5980158114"}],"uuid":"701050d2-358e-358e-e193-6f374beb5952","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-52.2962837048","y":"1.7610109902","z":"-29.3488032387"}],"uuid":"ca9fedd5-30f9-2b06-6f41-c2148eb9f502","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-52.2962837048","y":"1.7610109902","z":"-29.3488032387"}],"uuid":"216d527b-699e-90fb-1998-4d7aaa8eb5c3","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"f7a3336b-809e-0966-af6d-7d9156208a4e","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-42.1862611809","y":"55.2245633302","z":"-47.8137388188"}],"uuid":"8d0fc26a-9901-62f8-40bc-c9f77ce0998d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-44.7185386925","y":"53.5046204069","z":"-50.9288531115"}],"uuid":"25a5fe4a-6165-24eb-15b8-c3473940ad5b","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50.8894896827","y":"47.9365935671","z":"-58.8864388912"}],"uuid":"e48f7ba2-fc70-464f-a01f-b9701fd480d6","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15.6376942806","y":"63.9686690589","z":"-17.3026653259"}],"uuid":"fa1e5c2f-41a4-adef-6e4e-bf9f58f128a4","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.2832219061","y":"39.8578808548","z":"-45.6531679612"}],"uuid":"3d99cde7-c9e8-afa3-bad1-5738a704aba1","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-29.9566499018","y":"59.9476088103","z":"-30.410358294"}],"uuid":"c09f05f4-a465-a87d-fec0-fde8e43e67b1","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-41.2009622532","y":"49.2853322068","z":"-40.6851409241"}],"uuid":"ff8f58bf-4c5a-7284-a864-80b38665cbe5","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"32e6287e-adf4-e930-1c15-9ae9be48a01e","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"26.5215502875","y":"-61.7777128461","z":"-50.5143631535"}],"uuid":"794f1018-dec9-57b8-c197-77904accdac0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"86.9513138835","y":"-73.8651018995","z":"-75.0888002117"}],"uuid":"da77cdcb-7ad1-5c0b-a29a-998b0664fb05","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.2353527438","y":"-51.1150674833","z":"-9.2501158675"}],"uuid":"44f36723-c0f8-ff49-a41b-6de828531f36","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"68.7353527438","y":"-51.1150674833","z":"-9.2501158675"}],"uuid":"49bd31d3-fafa-ddf2-5986-112e32336c03","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95.1074979228","y":"-54.0308419195","z":"-42.1153172543"}],"uuid":"ddac85ce-dbe0-297a-9897-31b05308c743","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"80.4922695548","y":"-55.944438858","z":"-54.7759816792"}],"uuid":"8516fe30-1a65-1ade-3093-26eb5d9cfd23","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"58.8705045819","y":"-22.8693080051","z":"-38.4385602338"}],"uuid":"4c2198de-9c28-73f4-7468-ddfbc75cbec6","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.8701229845","y":"-19.5470956865","z":"-31.4164080838"}],"uuid":"455b252f-766d-2a54-0166-8e3ed51a2687","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"56.8554393194","y":"-14.7598210701","z":"-23.6078658164"}],"uuid":"e89a29ef-88a7-45e8-a4aa-c0c2416fba4c","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.8701229845","y":"-19.5470956865","z":"-31.4164080838"}],"uuid":"b23209e2-b71c-4aec-7418-94be4949d493","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"125f6596-2eb7-55af-69eb-8677927fe476","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"65"}],"uuid":"6750bd21-6ca9-0af1-1990-691231ec7d5e","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7ba40dd8-629c-ba37-7163-1d70e0fd4649","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"57.5"}],"uuid":"02008661-ad77-9147-214e-d2edce0f9e65","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"75"}],"uuid":"4a85b6ff-c083-4ee4-20f3-64284b8ff8c7","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"75"}],"uuid":"7de32ba5-4c82-5d29-4d8f-b2e7c88fc615","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"3c3b5d3f-20cc-696a-f796-069f871b7185","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"8cc1ae4b-a887-d93e-a134-48eb5015515c","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3a5a200e-5b23-de6e-f6fc-1503688a765c","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"8ef8ba69-28fb-de6e-2705-59239bd37edb","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"62.0092548231","y":"-0.4316912615","z":"-4.7675517875"}],"uuid":"bfcf3c88-9ae6-cf4d-4815-cc1253d64802","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"0128b596-c461-0be0-3e87-ae99c43616b7","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"71.7180308119","y":"-0.4920358582","z":"-6.9015946924"}],"uuid":"9f1fec70-bfe8-521c-82fc-95c8bf0100c4","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"83.5952619136","y":"-0.2620600682","z":"-10.8773772113"}],"uuid":"1fb1e754-10db-852f-dd4e-847cfa05f47e","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.5331663206","y":"19.3369807941","z":"6.474997315"}],"uuid":"0f898036-bd46-54c8-2922-1bf7e71b61bd","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ce0a2261-dc9a-b477-2b1d-8c924a13c61c","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-32.5","y":"0","z":"0"}],"uuid":"77766224-4c56-ead7-8336-ff23a97eb3d9","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"8941e3e9-30df-496b-1a44-9aa82008a633","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"f1037af0-08b6-96ab-b8a9-9f604591d5e2","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-59.419604523","y":"4.4112625392","z":"1.8609367826"}],"uuid":"e75f6bf7-92b2-ff34-6b1e-d895fea1ffc7","time":1.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"515c5559-b19e-1a6c-d194-29a0226d431f","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.1345380161","y":"0.0343648853","z":"0.9349334418"}],"uuid":"bf173ded-0269-b11a-89a0-579adb2ed085","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-35.561839418","y":"-2.6973273112","z":"-0.2733372638"}],"uuid":"5c803596-624f-2feb-5737-43fb585c8364","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-35.561839418","y":"-2.6973273112","z":"-0.2733372638"}],"uuid":"edfe0c9e-41db-a123-d6aa-bc3fbf3b6c9c","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"75992dfc-ca12-468b-4ad8-22a335c6c870","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-12.7824927505","y":"-0.7771014437","z":"-0.8689597174"}],"uuid":"714e6d29-aa96-1be4-39be-e352922679e3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5.2824927505","y":"-0.7771014437","z":"-0.8689597174"}],"uuid":"027f3663-0314-bb9e-f17f-4909219a363b","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"9.7175072495","y":"-0.7771014437","z":"-0.8689597174"}],"uuid":"8c4217d6-5580-f9f2-1db1-14c195fe823d","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2175072495","y":"-0.7771014437","z":"-0.8689597174"}],"uuid":"ce372cd1-74cf-c009-45b7-ccfd6f7500ae","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.22","y":"-0.78","z":"-0.87"}],"uuid":"06fe5ad8-49b5-c0c3-1481-c840cf8d0c97","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.0854773535","y":"-2.0710910703","z":"-4.3046531819"}],"uuid":"fe4b2b06-210e-6818-9182-6a8199343c06","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.9257249471","y":"-6.3276235401","z":"-5.592924586"}],"uuid":"36366c3a-ac04-5c04-e1b2-0666166ddd82","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.9257249471","y":"-6.3276235401","z":"-5.592924586"}],"uuid":"ffc459e4-6cc4-6c5e-a7aa-011aa2bfb47a","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-19.57","y":"-6.33","z":"-5.59"}],"uuid":"0942847c-6696-72b8-0627-1047c824d420","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c2783087-740b-9efa-26a8-a97cebef2d46","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"7.52","y":"2.35","z":"0.86"}],"uuid":"c8224408-7093-6a60-7cb0-705ee5d1e29c","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.52","y":"2.35","z":"0.86"}],"uuid":"52df1888-d866-d09c-1bdd-97264544ffef","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"50.02","y":"2.35","z":"0.86"}],"uuid":"b7d624cd-9b83-5246-29fe-f8c589aea371","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.02","y":"2.35","z":"0.86"}],"uuid":"f7cf5d1f-76a7-2c2b-cb64-ee48f34f975e","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.02","y":"2.35","z":"0.86"}],"uuid":"87bace8c-7d34-8ab1-fab8-b8c16aaf77e0","time":1,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.4306519248","y":"2.512107265","z":"-0.4054368633"}],"uuid":"92aece4b-e73e-5faf-344f-0100549a4270","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"46.9944148989","y":"3.877620433","z":"-3.708398832"}],"uuid":"1e047db9-0f4d-4fcd-4285-878e216fe2ac","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"71.9944148989","y":"3.877620433","z":"-3.708398832"}],"uuid":"89c102f5-3f1c-f870-7620-8fd3190806be","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"29.49","y":"3.88","z":"-3.71"}],"uuid":"3eb6b73d-3fc8-486d-f466-8614d2b892b3","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"dc2edf12-1919-0c47-f85b-ccd8f5180958","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"19.7773272072","y":"4.0215500522","z":"-6.3358608564"}],"uuid":"8254f5d6-49be-c3aa-7086-d531d03888dd","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.6544374225","y":"3.9742628642","z":"6.1946635266"}],"uuid":"0cc840b0-5edb-34d7-050d-460146350550","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-21.8455625775","y":"3.9742628642","z":"6.1946635266"}],"uuid":"d761792a-955d-9859-62a0-18780da551e4","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-9.3455625775","y":"3.9742628642","z":"6.1946635266"}],"uuid":"56a0a4f9-80c4-7617-13ba-48bb31647ea8","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-44.3045028717","y":"4.8460261594","z":"6.5670128269"}],"uuid":"7270293e-9aae-244f-2f12-c6bbf41813d7","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-56.8001045578","y":"5.1896248202","z":"6.563506006"}],"uuid":"ab789810-81de-abf7-0628-4afac167871c","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-61.8001045578","y":"5.1896248202","z":"6.563506006"}],"uuid":"65f926cb-8824-30c2-0531-f99a34cc9007","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-31.8","y":"5.19","z":"6.56"}],"uuid":"bdb13ba5-e132-92ff-8c5c-91f628a8b80d","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e6bd6ecd-3205-7c82-d745-afaf885a3e01","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"9.7824020671","y":"1.7555897191","z":"-7.3111717268"}],"uuid":"7ff94733-d327-a447-2899-a10026bdbba4","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.2824020671","y":"1.7555897191","z":"-7.3111717268"}],"uuid":"dba230b0-7807-c71f-8414-e2f10e993710","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.2824020671","y":"1.7555897191","z":"-7.3111717268"}],"uuid":"06bc60ab-4def-54b8-6b4c-6560b2fc6268","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"9.7824020671","y":"1.7555897191","z":"-7.3111717268"}],"uuid":"1c7d220d-2ccf-b1a3-929e-260ad7b9a3d3","time":0.79167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"37.2885661048","y":"2.291634426","z":"-6.9105191417"}],"uuid":"59a67d47-dd2b-98f8-ec90-9f814a68cff8","time":1.20833,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"42.2910410363","y":"2.3655123276","z":"-6.8126584981"}],"uuid":"752161ac-c921-9c53-681c-a6d41b4bf3eb","time":1.29167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"54.7910410363","y":"2.3655123276","z":"-6.8126584981"}],"uuid":"8e6ddb58-25f0-a57d-8271-35e014a24eb0","time":1.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"29.79","y":"2.37","z":"-6.81"}],"uuid":"ee809423-dae6-ba11-c322-457825be6017","time":1.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f76eb944-e985-a29c-2c7f-4e1975efa04b","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"766720b6-ef18-a906-fe7c-91cec9d74c1e","time":0,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"631db0ee-affe-ab1f-d8d2-bd817de1bcae","time":1.125,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"1","z":"1"}],"uuid":"518284dd-d754-d4c9-c282-8a06fbc547c1","time":1.20833,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"2","z":"1"}],"uuid":"2ec267ed-686c-0c6c-f5c2-dca2b30c2694","time":1.29167,"color":-1,"uniform":false,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"1","y":"0","z":"1"}],"uuid":"83c5925e-251f-d60a-1ab4-f8bd8ff3b4c5","time":1.5,"color":-1,"uniform":false,"interpolation":"linear"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e1751804-1587-d53e-510c-119440896dd5","time":0,"color":-1,"uniform":true,"interpolation":"linear"}]}}},{"uuid":"278631b6-ea97-aabe-b020-7abbb28d5986","name":"shield_attack_1","loop":"once","override":false,"length":1.25,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9aaeddf8-969e-96ec-499b-e177f3ca9bdd","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"f9e80793-6624-05b4-105c-257503d29e7e","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"50","y":"0","z":"0"}],"uuid":"cb8f4bf7-c06a-b918-ee5c-2e4b7170c95f","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1ed31989-274f-9c7e-b970-15726691e047","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"dea0f803-3228-b1d7-1079-45f536051fbb","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-85","y":"0","z":"-25"}],"uuid":"489bee7f-cb6e-d8d8-d87e-4e53de84239d","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-85.7312453482","y":"2.509721274","z":"-46.1135416426"}],"uuid":"507cd94b-6004-036e-4485-01cc973ab316","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-85.7312453482","y":"2.509721274","z":"-46.1135416426"}],"uuid":"f8855233-7dda-2590-7b61-ab8f71530006","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5fae9431-7c30-479b-a141-8afd7b912528","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"7ccc4a6c-0c15-665b-3118-81eb0d6d7bfc","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"9c6ef684-aad2-7864-73e0-9ae4d53e5878","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"50"}],"uuid":"db56b344-e93a-8f36-d40a-6e6489c2a2e8","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"2c4a5034-e8aa-1479-eab7-bfdf5f817950","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"-10","z":"0"}],"uuid":"da6aa0f0-ef10-59f8-b704-a2f0f83289cf","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.8674260512","y":"32.7690834759","z":"14.528800838"}],"uuid":"348df97a-6aaf-ae86-cc2f-deba55b8c3d5","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"5","z":"0"}],"uuid":"51adbc86-d17e-650f-5bd3-5a1d8628b7a0","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"22e9dcff-6d62-025e-d201-7b315f096fd0","time":0,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"26eb55a4-bfde-c9d0-95c9-a69453b52429","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"de9d485e-dab5-8181-4935-9f26a595ed51","time":0,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"5285314c-5162-09c4-5484-49628cf0a3ca","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-22.5","y":"0","z":"17.5"}],"uuid":"05e82c1a-5979-7d52-5be2-369ab8643cda","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.5","y":"27.5","z":"17.5"}],"uuid":"fc917305-0287-b8b8-5649-587928e07692","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.5","y":"27.5","z":"17.5"}],"uuid":"7ccd1019-3235-dee8-b3ae-2c71892d7f92","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"ec847310-4f84-e7d7-d766-fb53ab5849aa","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"492109e8-e3d2-65d6-5b0c-681b79d2debe","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-43.1891941881","y":"-41.0646509681","z":"24.2317823278"}],"uuid":"3cbfe7b4-1976-b18f-bfee-61edf32073c1","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-43.1891941881","y":"-41.0646509681","z":"24.2317823278"}],"uuid":"82699b8e-1eec-f4f1-3e25-9c8b1093a340","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"f3ce685d-3702-b666-1d52-f2c3dbb7a967","time":0,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"346de168-6db2-96ee-3256-63587cb1baf3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5","y":"0","z":"0"}],"uuid":"a9fed4ad-3e38-46da-fe48-276bf92818ae","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-110.3703136516","y":"-40.5010889301","z":"20.6863595327"}],"uuid":"c63a6b87-e980-b2cf-2243-2069b5ba2e10","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-70","y":"2.5","z":"47.5"}],"uuid":"4785fe2c-5b6f-1e75-123b-eec62343bff6","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"5371e06c-b54b-154b-b1c7-cfce2a9b20d4":{"name":"right_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cc9bae98-3244-1324-0c0e-d3dd2ecf8ad0","time":0,"color":-1,"interpolation":"linear"}]},"3a05cd31-c803-0a4c-716f-337c916a2ee5":{"name":"left_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"892e5c98-765f-aa7a-ea28-f13f7ae43c9a","time":0,"color":-1,"interpolation":"linear"}]},"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5b120357-d3e7-3f99-af0e-5a40659e76e5","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"-20","z":"0"}],"uuid":"09cd0ad4-ab0e-9580-2da6-78dbb3407c58","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"12.5","z":"0"}],"uuid":"91700c73-6d2b-d3ae-1b90-210a6e6ff0db","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"12.5","z":"0"}],"uuid":"35aab2f7-88f1-f91c-6a34-e484bf4fabdc","time":1.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f904d334-10b2-a2a1-b90e-7f22c8409e28","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"29815afa-d487-9580-ce8f-3de867ea32a9","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3","z":"0"}],"uuid":"9c9ce216-9bd0-89fb-df3b-62167ab42e10","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3","z":"0"}],"uuid":"932ea191-fbc2-d364-73e9-d05bdc3315b5","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cb886698-fb4c-9055-5729-e52f5c72c77f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"95f80753-e55c-3b7c-bf53-b7769cb84a8f","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"-10","z":"0"}],"uuid":"ae9bb722-adfc-5f39-e19f-713b29b399cd","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"-10","z":"0"}],"uuid":"de1e6170-350f-a3e0-dfe4-0e164fde910f","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"08fa9aea-2f3a-ad98-9647-65d992767b55":{"name":"left_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"accba002-32fb-d416-bd9a-332895956d14","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"cdd63cc4-92fe-bccd-2a10-112b927e9181","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-72.48030955","y":"-50.8350819403","z":"88.5344358877"}],"uuid":"df2306ab-e8e3-c85a-e8f9-86f0b3b0c345","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"22.5"}],"uuid":"c865a1ea-1c13-8071-86cc-2f614ee83406","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"621973ec-d68a-5564-596e-d9cd80a4a0f6":{"name":"shield","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ac932628-12e7-ab77-0349-9f001b096922","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"6fadd96e-eaf5-033d-7436-9edadbf463c7","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.7450989839","y":"3.6869834395","z":"59.7113435627"}],"uuid":"a4cad7a8-8204-780e-5891-7038456a8949","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.7450989839","y":"3.6869834395","z":"59.7113435627"}],"uuid":"902bc1d6-7047-cda6-fe52-1345084e33dc","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"81c98f4b-5ade-166e-be9a-8cbee4eddc3a":{"name":"front_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7949e715-0052-8077-3d65-a2d9e8615439","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"bf889c9b-01ef-50a9-076e-351c81b6f3b4","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"0b74d257-b19a-7279-ef7c-08f7a2a0814b","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"4ffce021-dcca-9b55-bf39-5918b08eb91c","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"05b3e52d-6142-bd4c-f19b-18c831466b1c","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"829d0c77-bf21-ccf2-0f06-c4f1310e55c6","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"d0f7b7d0-2fdb-08b5-eb30-05f7d9d58a87","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"e4736043-be0c-cae6-3ef2-6160a8bccb1f","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"eb502aed-a9ef-5add-ff01-ddaeff23efdc","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"0caf1631-e726-c826-1302-5468c9542adf","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"10","y":"0","z":"0"}],"uuid":"38cab1f7-c219-4596-3c62-f1bb8d7c9300","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"e05604bb-d2c8-c8f1-44b4-2a1c7981f668","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0698b021-dae2-4205-7f11-32c9c689293f","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"5b44b36f-9b2b-2656-e9c8-822e37140667","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"7e282a11-9070-f49b-2f94-fea84a93b2fd","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"947c81dc-763a-9e6f-02a7-c201f88b866c","time":1.25,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7a656745-dcf1-4a7a-c5cc-d29a586ffd49","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"d80f3713-3f15-8a74-2112-93cce2f0b62a","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"d77e07e4-f3c2-cdf8-9b7b-9eb570ffdb20","time":0.54167,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"815d4e79-bae7-6ba8-aa59-2c9d202f371b","time":1.25,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"64329f10-02a0-0046-020a-75f5d0e2528f","name":"shield_attack_2","loop":"once","override":false,"length":2.25,"snapping":24,"selected":false,"anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"c5eedd4c-1c07-3567-ca9a-1d47e13b8a95":{"name":"cloak","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4cce6b4b-729e-b202-76c7-d545c951ca43","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"37.5","y":"0","z":"0"}],"uuid":"f35dd944-fc43-3bbd-5e5b-d3573cf07d4d","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5","y":"0","z":"0"}],"uuid":"156e98f5-d57a-9abe-f501-36ec6d176cae","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"0","z":"0"}],"uuid":"a17a67fd-7b6d-6962-b8a1-40b85e433c3c","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"45","y":"0","z":"0"}],"uuid":"b3f0e60d-db3a-5e36-00e6-05551de70020","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"07297c90-9e98-a6c4-66bc-c1c968bfca92","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"0","z":"0"}],"uuid":"53f038db-6138-27bd-02d3-c6e5d077a1d2","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"a512dfcd-47cd-1831-ffcc-611e43a564d9":{"name":"right_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-85.7312453482","y":"2.509721274","z":"-46.1135416426"}],"uuid":"1f9392bf-48cf-786b-8920-a075e6e8dfb1","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-85.7312453482","y":"2.509721274","z":"-46.1135416426"}],"uuid":"46b5461c-c66b-5e44-4e9a-5331406de8b4","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-103.23","y":"2.51","z":"-46.11"}],"uuid":"36a41a70-c9ff-8be7-0328-e37a73e2781e","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-75","y":"0","z":"-25"}],"uuid":"97ce081e-6aef-6ada-bd91-69504d07cebe","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-103.23","y":"2.51","z":"-46.11"}],"uuid":"70de3c33-8368-4f19-ffdf-9d1b14830429","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"c1a7024c-c2d5-f677-558b-549f2d02cdbd":{"name":"left_sub_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"50"}],"uuid":"1f4d34c3-543f-0de1-84d3-259b8aba497e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"8e0a9552-ca9d-4960-201d-8601e1fbd4f2","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.5","y":"0","z":"0"}],"uuid":"04610b3e-9c47-9c4e-bbb6-78da8de2852b","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-52.5","y":"0","z":"0"}],"uuid":"30a13d3e-45ee-a557-e294-cdd2a10676d9","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.4309112331","y":"0.4673957558","z":"1.2447027674"}],"uuid":"4fc4ae7f-c6d7-db82-41b0-4ea2632488ee","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"80fd2cd7-a062-a706-6209-57a3e3c7919a","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30.1670030659","y":"0.7231543205","z":"8.5484839094"}],"uuid":"293c3c83-af35-182e-f766-63070661d01a","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"788aa374-e94c-c383-6565-af12949da09e":{"name":"upper_body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"27.5","y":"5","z":"0"}],"uuid":"8feaadc2-d2df-a26f-5e72-12f1ffb746c8","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"5","z":"0"}],"uuid":"9d957068-47d4-145c-bd64-1df921c011cf","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"5","z":"0"}],"uuid":"a6ace2a0-5804-d2a4-3ec4-205a5cb832c4","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"5","z":"0"}],"uuid":"95b62c9a-7f52-875d-8d49-91e96e0b21af","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"50.8227165896","y":"13.3763646802","z":"26.800620538"}],"uuid":"3b9898ed-9d5d-dce2-95ce-50a75ac48b1d","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"70cfec07-220c-0ff5-f558-161b8eba5bfe","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"35.8227165896","y":"-14.1236353198","z":"26.800620538"}],"uuid":"632564dc-2b4e-84fd-65f2-e40685d62e21","time":1.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"162c587c-e220-1d1f-273f-19ef6077bc40","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"6b1142e6-7581-b73e-4a67-e134ad4585c6":{"name":"vfx_slash","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"06f5ff61-3784-a42b-599d-f35578fd6e3e","time":0,"color":-1,"uniform":true,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1fd8b4f5-f36a-533d-4613-8ff29fcf0828","time":2.25,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"f16e4292-9e8d-235c-1fc2-b1d02dd31366":{"name":"vfx_pierce","type":"bone","keyframes":[{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"d1a5e2a3-48ec-ed24-3e06-8f0df92631a7","time":0,"color":-1,"uniform":true,"interpolation":"linear"},{"channel":"scale","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"34775f4a-1325-0779-3cb2-50ae577fa369","time":2.25,"color":-1,"uniform":true,"interpolation":"catmullrom"}]},"8909e1fb-858b-2f76-7079-1923d78bfb4b":{"name":"right_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-67.5","y":"27.5","z":"17.5"}],"uuid":"91e49ccc-fe06-a45d-a0ee-fb8ec13e85f6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.5","y":"27.5","z":"17.5"}],"uuid":"f5cae43a-8ca1-2b3c-242b-2a85d0b6c113","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55","y":"27.5","z":"17.5"}],"uuid":"4bbec5ff-9cf9-5177-f5e1-4e6963640cce","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.5","y":"27.5","z":"17.5"}],"uuid":"f73645da-31ab-1db3-9599-6ac3148ceb9b","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"17.5"}],"uuid":"1520d75d-b1c8-7a2b-828c-09a2795d4fa7","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30","y":"27.5","z":"17.5"}],"uuid":"551592cb-4c7a-711b-eca3-3290ab32a0b7","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"dcf45c2b-cc07-3246-e68f-1319556212c0":{"name":"right_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-43.1891941881","y":"-41.0646509681","z":"24.2317823278"}],"uuid":"c53dcb8b-2f62-f929-3e01-3e16839e2e52","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-43.1891941881","y":"-41.0646509681","z":"24.2317823278"}],"uuid":"20f7d2e8-e9bd-bb80-9318-f52cd56cfdee","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.6891941881","y":"-26.0646509681","z":"24.2317823278"}],"uuid":"7453bd06-3947-8102-7d49-23bec9e0b74c","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"8f6061d3-ef98-da1e-5f4a-33b4ff2874f1":{"name":"hammer","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"a47ba037-7059-cae3-820a-1a06208232df","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18","y":"0","z":"0"}],"uuid":"3af14301-1080-4f14-0934-4d145a50929d","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"28aa58e8-cc75-0593-0775-3690cbb1dd9c":{"name":"left_arm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-70","y":"2.5","z":"47.5"}],"uuid":"35514051-5fa8-457d-17e6-7471bd968df5","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50","y":"0","z":"0"}],"uuid":"ca727ce7-b9d5-2e9a-e84b-a4cccfa81ebe","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-130","y":"0","z":"0"}],"uuid":"27970128-e0e9-fd73-4cef-5efccc959217","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-140","y":"0","z":"0"}],"uuid":"1585e58c-3e94-1805-fd04-4e738dc4d1ad","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-45.540998577","y":"-25.9440256268","z":"-12.5519268326"}],"uuid":"3dd0f933-1d0d-ea30-a29c-8a7a5caa1cc0","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"a6dafaa3-3e35-1f7b-1703-719c20bbe4cf","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.2017292565","y":"-4.7672682729","z":"0.5611677721"}],"uuid":"736c24c9-4186-7f03-fd86-7554bac562bc","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"a633f85e-6f17-f4be-fc15-e8561725fc8d":{"name":"body","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-2.5","y":"12.5","z":"0"}],"uuid":"d97a9efc-fb72-2d03-5e40-a5a4a6ac042b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-5","y":"12.5","z":"0"}],"uuid":"d855d5f7-b44a-8364-cdbe-f0e98f45f8fa","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"12.5","z":"0"}],"uuid":"5d1a5505-55cb-9430-b0ea-cc754a94abc7","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"12.5","z":"0"}],"uuid":"44a0d3b4-27d7-28b5-ff15-35e8fa66019d","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"12.5","z":"0"}],"uuid":"8b754d68-6de3-3dc4-d563-9a5791b8b01f","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.1019253534","y":"11.9127617839","z":"-3.8139723926"}],"uuid":"a61e1172-b5e5-c32f-a779-6168e17b91ff","time":1.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4b093467-69f1-22e2-9ba5-7c9259ca9a2e","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3","z":"0"}],"uuid":"c06ee386-a6e8-7c1c-dba0-a167a7572f08","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"a8e58795-463c-7890-7bd9-357eb23abecc","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"22","z":"0"}],"uuid":"62ecf1f6-b6de-c3fd-0a86-ceecaad925bb","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-5","z":"0"}],"uuid":"c2176fd8-7476-c979-7a48-ff077d41519b","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-2","z":"0"}],"uuid":"6c4d0e7d-04fa-7fd2-34eb-f4efee907e78","time":1.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"5bfd368e-2c5d-b8ac-9617-f257151323d9","time":2.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"16.81","z":"0"}],"uuid":"4aeb4c03-096b-6699-bb00-89a5785c1007","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"e3e41a2b-7718-8509-a501-c13a87118853":{"name":"h_head","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-15","y":"-10","z":"0"}],"uuid":"25f29464-5d40-3025-d491-39dc29307ac4","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"-10","z":"0"}],"uuid":"db24203d-e173-098d-25d2-727613f59747","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"-10","z":"0"}],"uuid":"635ded11-f5ab-0a5b-4cd2-f84ed1184d66","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-53.7147145384","y":"-23.9403918278","z":"14.1313484524"}],"uuid":"503a5cf9-b4f6-f2d9-d8cf-2bc7ec54a6d3","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.4554275644","y":"0.4206975165","z":"-15.2568188401"}],"uuid":"b7c9f7d8-db2d-5c73-2aee-c7a4e91646bf","time":1.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4d7e3a42-035b-42a0-456c-cedb90aa7009","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"08fa9aea-2f3a-ad98-9647-65d992767b55":{"name":"left_hand","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"22.5"}],"uuid":"4108e05a-0873-5acb-45b3-bf95519d3fc5","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-90","z":"22.5"}],"uuid":"17006c9c-f43b-3186-a6db-206a97798532","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"-90","z":"22.5"}],"uuid":"f5459780-9df5-c20c-cf4e-2c6fd9ace467","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"-85","z":"22.5"}],"uuid":"05edd0a2-c26c-6784-8a80-c9741a28feae","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"-10","z":"22.5"}],"uuid":"a2005296-d1a2-7bb4-b283-9ab16ff5269c","time":1.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"0836c3d7-8701-9ea6-ae35-1edcce3ab7fc","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"621973ec-d68a-5564-596e-d9cd80a4a0f6":{"name":"shield","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0.7450989839","y":"3.6869834395","z":"59.7113435627"}],"uuid":"4c0947c3-3917-0434-f6e0-506674a43e1e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.75","y":"1.19","z":"59.71"}],"uuid":"9eb60096-33f1-9883-6c5e-77742120f633","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.75","y":"1.19","z":"87.21"}],"uuid":"d6fc1a8a-097d-4ac8-0c56-7c42748d7560","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.69787198","y":"-2.8660043568","z":"32.0274704597"}],"uuid":"551437e9-07e7-0bbb-1d3b-9bdeb35b9fe2","time":1.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"76ae89ce-e4d9-9b1f-5c30-881ca954df9c","time":2.25,"color":-1,"interpolation":"catmullrom"}]},"81c98f4b-5ade-166e-be9a-8cbee4eddc3a":{"name":"front_armor","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"5fa55e7b-8762-8b5c-0322-14775d2d7bcc","time":0,"color":-1,"interpolation":"catmullrom"}]},"9bbcfe86-35fe-5dd7-1fba-8d89aadd07f5":{"name":"right_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"b11bd6c5-c01f-76e2-35f5-321ca26785de","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"355d15a3-0dcc-aa89-881d-21ce53f7b23c","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"2cf44481-f502-ae24-046a-3da12b0fe27c","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50","y":"0","z":"0"}],"uuid":"4dce3c70-3b31-25a4-7bcc-8a0396600c9e","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-87.5","y":"0","z":"0"}],"uuid":"537f4db4-3b7b-0500-cb45-3690c7ceedf9","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9eda75a4-7db9-6c1e-9df2-a271be12d6af","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-41.1975167366","y":"-4.1841018364","z":"8.43806997"}],"uuid":"c7f90c84-0378-733c-8194-6bc8e160f51d","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"76bfe555-a273-5b15-5684-5779b6fddf7f":{"name":"right_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"6d1d768c-34d4-a634-feb5-f277b4d1aeaf","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"a9b7c7e7-786c-31eb-e5a4-249e548ee6b4","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"42.5","y":"0","z":"0"}],"uuid":"f2dbde6d-76ad-afbe-2f9f-52c6967f75e1","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"64.8506594855","y":"-2.6331213387","z":"-0.1715498031"}],"uuid":"0940ba3d-d217-5bff-5aa9-9756b63c3b6c","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"-2.6331213387","z":"-0.1715498031"}],"uuid":"2659eeda-a2f1-e944-8e0b-7465bbbcb232","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"25.1356479959","y":"2.233482251","z":"0.0756133347"}],"uuid":"49048782-5b44-a81a-837f-f0eceef600a6","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"b2409710-4f5e-2feb-9aee-fcdb2d9320fa":{"name":"left_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"35995a1b-8c59-f78b-0227-772acead331d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"30","y":"0","z":"0"}],"uuid":"6481733e-1df7-1392-5119-c690e32b9af0","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"40","y":"0","z":"0"}],"uuid":"be544dbd-d083-9ef4-148e-61a262ac1300","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"37.5","y":"0","z":"0"}],"uuid":"6671222b-4d44-8002-42ef-05f90bd4ec53","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"75862e9a-da7a-772b-d39d-4632851fb942","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"13.4980418595","y":"-4.90689418","z":"-4.1197178199"}],"uuid":"68b897b6-1a51-529e-843b-8fe42cda8a73","time":1.41667,"color":-1,"interpolation":"catmullrom"}]},"d21a5df4-acaf-a697-c2f5-1e490af18700":{"name":"left_sub_leg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"32.5","y":"0","z":"0"}],"uuid":"ecca4479-9be1-ef6b-1da5-0b70e4ab3c8e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"50","y":"0","z":"0"}],"uuid":"018c12a3-a241-8b52-3190-72547640c08a","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"15","y":"0","z":"0"}],"uuid":"649abd67-89c8-f7d3-d874-d78f7ceac15b","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.5","y":"0","z":"0"}],"uuid":"8fca8ba5-63d6-4225-6ff3-f106676983a3","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.5","y":"0","z":"0"}],"uuid":"e9d28f97-5ae6-1c53-e64d-8623aaf8d356","time":0.91667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"99d32996-b1ca-1954-4b35-2fc604bbcc74","time":2,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"34.8218133931","y":"1.9560482195","z":"1.0226853405"}],"uuid":"ebfa545c-a035-e8b2-5273-2cc2898849e0","time":1.41667,"color":-1,"interpolation":"catmullrom"}]}}}]} ================================================ FILE: core/src/main/resources/steve.bbmodel ================================================ {"meta":{"format_version":"5.0","model_format":"free","box_uv":false},"name":"steve","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":64,"height":64},"elements":[{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,11.25,-1.875],"to":[3.75,15,1.875],"autouv":0,"color":1,"origin":[0,11.25,0],"uv_offset":[16,24],"faces":{"north":{"uv":[20,28,28,32],"texture":0},"east":{"uv":[16,28,20,32],"texture":0},"south":{"uv":[32,28,40,32],"texture":0},"west":{"uv":[28,28,32,32],"texture":0},"up":{"uv":[28,28,20,24],"texture":0},"down":{"uv":[36,16,28,20],"texture":0}},"type":"cube","uuid":"ea42f7f7-a6f1-4479-c43f-48211bab5ed2"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,15,-1.875],"to":[3.75,18.75,1.875],"autouv":0,"color":2,"origin":[0,15,0],"uv_offset":[16,20],"faces":{"north":{"uv":[20,24,28,28],"texture":0},"east":{"uv":[16,24,20,28],"texture":0},"south":{"uv":[32,24,40,28],"texture":0},"west":{"uv":[28,24,32,28],"texture":0},"up":{"uv":[28,24,20,20],"texture":0},"down":{"uv":[28,28,20,32],"texture":0}},"type":"cube","uuid":"5ea74bdb-ba28-b8e3-103b-9be6ff2262da"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,18.75,-1.875],"to":[3.75,22.5,1.875],"autouv":0,"color":3,"origin":[0,18.75,0],"uv_offset":[16,16],"faces":{"north":{"uv":[20,20,28,24],"texture":0},"east":{"uv":[16,20,20,24],"texture":0},"south":{"uv":[32,20,40,24],"texture":0},"west":{"uv":[28,20,32,24],"texture":0},"up":{"uv":[28,20,20,16],"texture":0},"down":{"uv":[28,24,20,28],"texture":0}},"type":"cube","uuid":"dc1510db-a719-17b4-e253-c992a92c5d25"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,18.76563,-1.85937],"to":[3.73438,22.48438,1.85938],"autouv":0,"color":3,"inflate":0.25,"origin":[0,18.75,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,28,40],"texture":0},"east":{"uv":[16,36,20,40],"texture":0},"south":{"uv":[32,36,40,40],"texture":0},"west":{"uv":[28,36,32,40],"texture":0},"up":{"uv":[28,36,20,32],"texture":0},"down":{"uv":[8,0,0,4],"texture":0}},"type":"cube","uuid":"d51a8665-a2bc-af6e-1230-5acba07248e7"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,15.01563,-1.85937],"to":[3.73438,18.73438,1.85938],"autouv":0,"color":2,"inflate":0.25,"origin":[0,15,0],"uv_offset":[16,36],"faces":{"north":{"uv":[20,40,28,44],"texture":0},"east":{"uv":[16,40,20,44],"texture":0},"south":{"uv":[32,40,40,44],"texture":0},"west":{"uv":[28,40,32,44],"texture":0},"up":{"uv":[8,4,0,0],"texture":0},"down":{"uv":[8,0,0,4],"texture":0}},"type":"cube","uuid":"f5b9f499-b26b-f912-1a54-151d702e13ed"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,11.26563,-1.85937],"to":[3.73438,14.98438,1.85938],"autouv":0,"color":1,"inflate":0.25,"origin":[0,11.25,0],"uv_offset":[16,40],"faces":{"north":{"uv":[20,44,28,48],"texture":0},"east":{"uv":[16,44,20,48],"texture":0},"south":{"uv":[32,44,40,48],"texture":0},"west":{"uv":[28,44,32,48],"texture":0},"up":{"uv":[8,4,0,0],"texture":0},"down":{"uv":[36,32,28,36],"texture":0}},"type":"cube","uuid":"357ebf82-23ba-edb1-081f-dca75d94b83c"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.75,16.875,-1.875],"to":[7.5,22.5,1.875],"autouv":0,"color":4,"origin":[5.15625,21.5625,0],"uv_offset":[40,16],"faces":{"north":{"uv":[44,20.1,48,26.1],"texture":0},"east":{"uv":[40,20,44,26],"texture":0},"south":{"uv":[52,20,56,26],"texture":0},"west":{"uv":[48,20,52,26],"texture":0},"up":{"uv":[48,20.1,44,16.1],"texture":0},"down":{"uv":[48,26.1,44,30.1],"texture":0}},"type":"cube","uuid":"53d40d2e-0941-29f9-00ed-1b19c941dcd8"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.76563,16.89063,-1.85937],"to":[7.48438,22.48438,1.85938],"autouv":0,"color":4,"inflate":0.25,"origin":[5.15625,21.5625,0],"uv_offset":[40,32],"faces":{"north":{"uv":[44,36,48,42],"texture":0},"east":{"uv":[40,36,44,42],"texture":0},"south":{"uv":[52,36,56,42],"texture":0},"west":{"uv":[48,36,52,42],"texture":0},"up":{"uv":[48,36,44,32],"texture":0},"down":{"uv":[56,32,52,36],"texture":0}},"type":"cube","uuid":"b17452ef-afbe-e010-f2c9-e0ba945faa1f"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.5,16.875,-1.875],"to":[-3.75,22.5,1.875],"autouv":0,"color":4,"origin":[-5.15625,21.5625,0],"uv_offset":[32,48],"faces":{"north":{"uv":[36,52,40,58],"texture":0},"east":{"uv":[32,52,36,58],"texture":0},"south":{"uv":[44,52,48,58],"texture":0},"west":{"uv":[40,52,44,58],"texture":0},"up":{"uv":[40,52,36,48],"texture":0},"down":{"uv":[40,58,36,62],"texture":0}},"type":"cube","uuid":"addb9f66-a2a4-46f7-b6af-f5a23c00fe70"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.48437,16.89063,-1.85937],"to":[-3.76562,22.48438,1.85938],"autouv":0,"color":4,"inflate":0.25,"origin":[-5.15625,21.5625,0],"uv_offset":[48,48],"faces":{"north":{"uv":[52,52,56,58],"texture":0},"east":{"uv":[48,52,52,58],"texture":0},"south":{"uv":[60,52,64,58],"texture":0},"west":{"uv":[56,52,60,58],"texture":0},"up":{"uv":[56,52,52,48],"texture":0},"down":{"uv":[64,48,60,52],"texture":0}},"type":"cube","uuid":"5e7dc31c-c64a-ff15-90d9-52a6f77cb14b"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.75,11.25,-1.875],"to":[7.5,16.875,1.875],"autouv":0,"color":8,"origin":[5.15625,15.9375,0],"uv_offset":[40,22],"faces":{"north":{"uv":[44,26,48,32],"texture":0},"east":{"uv":[40,26,44,32],"texture":0},"south":{"uv":[52,26,56,32],"texture":0},"west":{"uv":[48,26,52,32],"texture":0},"up":{"uv":[48,26,44,22],"texture":0},"down":{"uv":[52,16,48,20],"texture":0}},"type":"cube","uuid":"e02c395d-e1bc-1375-a8ed-729e19544ce9"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.76563,11.26563,-1.85937],"to":[7.48438,16.85938,1.85938],"autouv":0,"color":8,"inflate":0.25,"origin":[5.15625,15.9375,0],"uv_offset":[40,38],"faces":{"north":{"uv":[44,42,48,48],"texture":0},"east":{"uv":[40,42,44,48],"texture":0},"south":{"uv":[52,42,56,48],"texture":0},"west":{"uv":[48,42,52,48],"texture":0},"up":{"uv":[44,36,40,32],"texture":0},"down":{"uv":[52,32,48,36],"texture":0}},"type":"cube","uuid":"a509c2d7-53ef-2b11-1331-f716b9c210d6"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.5,11.25,-1.875],"to":[-3.75,16.875,1.875],"autouv":0,"color":8,"origin":[-5.15625,15.9375,0],"uv_offset":[32,54],"faces":{"north":{"uv":[36,58,40,64],"texture":0},"east":{"uv":[32,58,36,64],"texture":0},"south":{"uv":[44,58,48,64],"texture":0},"west":{"uv":[40,58,44,64],"texture":0},"up":{"uv":[40,58,36,54],"texture":0},"down":{"uv":[44,48,40,52],"texture":0}},"type":"cube","uuid":"1433ee62-21ac-d72c-0fd0-37000e5eb221"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.48437,11.26563,-1.85937],"to":[-3.76562,16.85938,1.85938],"autouv":0,"color":8,"inflate":0.25,"origin":[-5.15625,15.9375,0],"uv_offset":[48,54],"faces":{"north":{"uv":[52,58,56,64],"texture":0},"east":{"uv":[48,58,52,64],"texture":0},"south":{"uv":[60,58,64,64],"texture":0},"west":{"uv":[56,58,60,64],"texture":0},"up":{"uv":[52,52,48,48],"texture":0},"down":{"uv":[60,48,56,52],"texture":0}},"type":"cube","uuid":"24bacfd6-cde8-81a0-f620-998cf5393d48"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0,5.625,-1.875],"to":[3.75,11.25,1.875],"autouv":0,"color":7,"origin":[1.40625,10.3125,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,8,26],"texture":0},"east":{"uv":[0,20,4,26],"texture":0},"south":{"uv":[12,20,16,26],"texture":0},"west":{"uv":[8,20,12,26],"texture":0},"up":{"uv":[8,20,4,16],"texture":0},"down":{"uv":[8,26,4,30],"texture":0}},"type":"cube","uuid":"b17675fc-b79b-0ad9-8f81-aa2dd1fc8c97"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.01563,5.64063,-1.85937],"to":[3.73438,11.23438,1.85938],"autouv":0,"color":7,"inflate":0.25,"origin":[1.40625,10.3125,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,42],"texture":0},"east":{"uv":[0,36,4,42],"texture":0},"south":{"uv":[12,36,16,42],"texture":0},"west":{"uv":[8,36,12,42],"texture":0},"up":{"uv":[8,36,4,32],"texture":0},"down":{"uv":[16,32,12,36],"texture":0}},"type":"cube","uuid":"2e42e483-1557-d21e-aee0-94af7bedfd40"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0,0,-1.875],"to":[3.75,5.625,1.875],"autouv":0,"color":6,"origin":[1.40625,4.6875,0],"uv_offset":[0,22],"faces":{"north":{"uv":[4,26,8,32],"texture":0},"east":{"uv":[0,26,4,32],"texture":0},"south":{"uv":[12,26,16,32],"texture":0},"west":{"uv":[8,26,12,32],"texture":0},"up":{"uv":[8,26,4,22],"texture":0},"down":{"uv":[12,16,8,20],"texture":0}},"type":"cube","uuid":"9455d16e-4bbf-4b63-881d-7daf943e782b"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.01563,0.01563,-1.85937],"to":[3.73438,5.60938,1.85938],"autouv":0,"color":6,"inflate":0.25,"origin":[1.40625,4.6875,0],"uv_offset":[0,38],"faces":{"north":{"uv":[4,42,8,48],"texture":0},"east":{"uv":[0,42,4,48],"texture":0},"south":{"uv":[12,42,16,48],"texture":0},"west":{"uv":[8,42,12,48],"texture":0},"up":{"uv":[4,36,0,32],"texture":0},"down":{"uv":[12,32,8,36],"texture":0}},"type":"cube","uuid":"16aee685-0ead-a542-5b3c-a62467ca45e3"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,5.625,-1.875],"to":[0,11.25,1.875],"autouv":0,"color":7,"origin":[-1.40625,10.3125,0],"uv_offset":[16,48],"faces":{"north":{"uv":[20,52,24,58],"texture":0},"east":{"uv":[16,52,20,58],"texture":0},"south":{"uv":[28,52,32,58],"texture":0},"west":{"uv":[24,52,28,58],"texture":0},"up":{"uv":[24,52,20,48],"texture":0},"down":{"uv":[24,58,20,62],"texture":0}},"type":"cube","uuid":"0e370edc-7b05-dccf-dd2a-a92cfe9f3e22"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,5.64063,-1.85937],"to":[-0.01562,11.23438,1.85938],"autouv":0,"color":7,"inflate":0.25,"origin":[-1.40625,10.3125,0],"uv_offset":[0,48],"faces":{"north":{"uv":[4,52,8,58],"texture":0},"east":{"uv":[0,52,4,58],"texture":0},"south":{"uv":[12,52,16,58],"texture":0},"west":{"uv":[8,52,12,58],"texture":0},"up":{"uv":[8,52,4,48],"texture":0},"down":{"uv":[16,48,12,52],"texture":0}},"type":"cube","uuid":"dc189735-0a58-619b-07ef-feec37d65e7d"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,0,-1.875],"to":[0,5.625,1.875],"autouv":0,"color":6,"origin":[-1.40625,4.6875,0],"uv_offset":[16,54],"faces":{"north":{"uv":[20,58,24,64],"texture":0},"east":{"uv":[16,58,20,64],"texture":0},"south":{"uv":[28,58,32,64],"texture":0},"west":{"uv":[24,58,28,64],"texture":0},"up":{"uv":[24,58,20,54],"texture":0},"down":{"uv":[28,48,24,52],"texture":0}},"type":"cube","uuid":"6bcf768b-beab-4c39-6d96-91266036a4e1"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,0.01563,-1.85938],"to":[-0.01562,5.60938,1.85937],"autouv":0,"color":6,"inflate":0.25,"origin":[-1.40625,4.6875,-0.00001],"uv_offset":[0,54],"faces":{"north":{"uv":[4,58,8,64],"texture":0},"east":{"uv":[0,58,4,64],"texture":0},"south":{"uv":[12,58,16,64],"texture":0},"west":{"uv":[8,58,12,64],"texture":0},"up":{"uv":[4,52,0,48],"texture":0},"down":{"uv":[12,48,8,52],"texture":0}},"type":"cube","uuid":"274282a7-bc7b-02f8-4dce-84226e9dd9c1"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,22.5,-3.75],"to":[3.75,30,3.75],"autouv":0,"color":4,"origin":[0,22.5,-1.875],"faces":{"north":{"uv":[8,8,16,16],"texture":0},"east":{"uv":[0,8,8,16],"texture":0},"south":{"uv":[24,8,32,16],"texture":0},"west":{"uv":[16,8,24,16],"texture":0},"up":{"uv":[16,8,8,0],"texture":0},"down":{"uv":[24,0,16,8],"texture":0}},"type":"cube","uuid":"7f60fbaf-510d-2e5f-b7d2-9111e08443cd"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,22.51563,-3.73437],"to":[3.73438,29.98438,3.73438],"autouv":0,"color":4,"inflate":0.5,"origin":[0,22.5,-1.875],"uv_offset":[32,0],"faces":{"north":{"uv":[40,8,48,16],"texture":0},"east":{"uv":[32,8,40,16],"texture":0},"south":{"uv":[56,8,64,16],"texture":0},"west":{"uv":[48,8,56,16],"texture":0},"up":{"uv":[48,8,40,0],"texture":0},"down":{"uv":[56,0,48,8],"texture":0}},"type":"cube","uuid":"e0f94313-bf88-492d-0c68-bd6b466a3b68"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1,35.8,-1],"to":[1,37.8,1],"autouv":1,"color":3,"visibility":false,"origin":[0,35.8,0],"faces":{"north":{"uv":[0,0,2,2]},"east":{"uv":[0,0,2,2]},"south":{"uv":[0,0,2,2]},"west":{"uv":[0,0,2,2]},"up":{"uv":[0,0,2,2]},"down":{"uv":[0,0,2,2]}},"type":"cube","uuid":"34afd0ba-f1fd-3d07-3b19-cdbe87b7bc1e"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-8,0,-8],"to":[8,0,8],"autouv":1,"color":9,"visibility":false,"origin":[0,0,0],"faces":{"north":{"uv":[0,0,16,0]},"east":{"uv":[0,0,16,0]},"south":{"uv":[0,0,16,0]},"west":{"uv":[0,0,16,0]},"up":{"uv":[0,0,16,16]},"down":{"uv":[0,0,16,16]}},"type":"cube","uuid":"ee35aceb-8b83-2bcd-dd62-f18b680f1ece"}],"groups":[{"uuid":"778fa89c-759a-8884-89d9-238c555d2dc1","export":true,"locked":false,"origin":[0,17,0],"rotation":[0,0,0],"color":9,"name":"player_root","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"678a7cf6-aa1f-be09-a547-bc4aa322ef40","export":true,"locked":false,"origin":[0,17,0],"rotation":[0,0,0],"color":0,"name":"shadow","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"9dc65952-10a9-876f-bd47-d6a7e9ec6183","export":true,"locked":false,"origin":[0,11.25,0],"rotation":[0,0,0],"color":1,"name":"phip_hip","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"e297aef6-7dfd-f100-2e7c-ab113699b922","export":true,"locked":false,"origin":[0,15,0],"rotation":[0,0,0],"color":2,"name":"pw_waist","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"a0c01522-9040-7533-fa11-f6a45d3d96ac","export":true,"locked":false,"origin":[0,18.75,0],"rotation":[0,0,0],"color":3,"name":"pc_chest","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"34097e46-c233-c03c-d8b9-aee154c9946f","export":true,"locked":false,"origin":[0,22.5,0],"rotation":[0,0,0],"color":4,"name":"h_ph_head","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"bfc2f156-b48b-dd08-1b9e-777d8ada16b2","export":true,"locked":false,"origin":[5,22,0],"rotation":[0,0,0],"color":4,"name":"pra_right_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"cf1618da-24d8-aab8-eebc-128815c02d35","export":true,"locked":false,"origin":[5,16.5,0],"rotation":[0,0,0],"color":8,"name":"prfa_right_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"fcaf8da0-0146-2587-b578-3e1af888deaa","export":true,"locked":false,"origin":[5.625,10.875,0],"rotation":[-90,0,0],"color":0,"name":"pri_right_item","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"b3135254-0351-3462-2479-e6a3286c89ff","export":true,"locked":false,"origin":[-5,22,0],"rotation":[0,0,0],"color":4,"name":"pla_left_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"1a9070b5-b8b6-b955-9f31-54f9625f8f3d","export":true,"locked":false,"origin":[-5,16.5,0],"rotation":[0,0,0],"color":8,"name":"plfa_left_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"0e94ff40-15b6-0b24-d0a0-447f000d761b","export":true,"locked":false,"origin":[-5.625,10.875,0],"rotation":[-90,0,0],"color":0,"name":"pli_left_item","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"7e8426f1-08b2-81a2-7703-cb76ff5e7003","export":true,"locked":false,"origin":[1.875,11.25,0],"rotation":[0,0,0],"color":7,"name":"prl_right_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"c6d9e946-1d10-482d-14b1-0766027adba8","export":true,"locked":false,"origin":[1.875,5.625,0],"rotation":[0,0,0],"color":6,"name":"prfl_right_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"5ef5d225-d5ae-6787-8838-b75ccb7a7a81","export":true,"locked":false,"origin":[-1.875,11.25,0],"rotation":[0,0,0],"color":7,"name":"pll_left_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"1b5cc202-c09e-faa0-5057-eb4ae60bf336","export":true,"locked":false,"origin":[-1.875,5.625,0],"rotation":[0,0,0],"color":6,"name":"plfl_left_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"310c12c3-81f7-ed2f-172e-4d4c44aaa31f","export":true,"locked":false,"origin":[0,36.8,0],"rotation":[0,0,0],"color":0,"name":"tag_name","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"9824ae07-4a7e-34a7-e7db-3dbb3327c38b","export":true,"locked":false,"origin":[0,22.75,2],"rotation":[0,0,0],"color":0,"name":"cape_cape","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true}],"outliner":[{"uuid":"778fa89c-759a-8884-89d9-238c555d2dc1","isOpen":true,"children":[{"uuid":"678a7cf6-aa1f-be09-a547-bc4aa322ef40","isOpen":true,"children":["ee35aceb-8b83-2bcd-dd62-f18b680f1ece"]},{"uuid":"9dc65952-10a9-876f-bd47-d6a7e9ec6183","isOpen":true,"children":["ea42f7f7-a6f1-4479-c43f-48211bab5ed2","357ebf82-23ba-edb1-081f-dca75d94b83c",{"uuid":"e297aef6-7dfd-f100-2e7c-ab113699b922","isOpen":true,"children":["5ea74bdb-ba28-b8e3-103b-9be6ff2262da","f5b9f499-b26b-f912-1a54-151d702e13ed",{"uuid":"a0c01522-9040-7533-fa11-f6a45d3d96ac","isOpen":true,"children":[{"uuid":"9824ae07-4a7e-34a7-e7db-3dbb3327c38b","isOpen":true,"children":[]},"dc1510db-a719-17b4-e253-c992a92c5d25","d51a8665-a2bc-af6e-1230-5acba07248e7",{"uuid":"34097e46-c233-c03c-d8b9-aee154c9946f","isOpen":true,"children":["7f60fbaf-510d-2e5f-b7d2-9111e08443cd","e0f94313-bf88-492d-0c68-bd6b466a3b68"]},{"uuid":"bfc2f156-b48b-dd08-1b9e-777d8ada16b2","isOpen":true,"children":["53d40d2e-0941-29f9-00ed-1b19c941dcd8","b17452ef-afbe-e010-f2c9-e0ba945faa1f",{"uuid":"cf1618da-24d8-aab8-eebc-128815c02d35","isOpen":true,"children":["e02c395d-e1bc-1375-a8ed-729e19544ce9","a509c2d7-53ef-2b11-1331-f716b9c210d6",{"uuid":"fcaf8da0-0146-2587-b578-3e1af888deaa","isOpen":true,"children":[]}]}]},{"uuid":"b3135254-0351-3462-2479-e6a3286c89ff","isOpen":true,"children":["addb9f66-a2a4-46f7-b6af-f5a23c00fe70","5e7dc31c-c64a-ff15-90d9-52a6f77cb14b",{"uuid":"1a9070b5-b8b6-b955-9f31-54f9625f8f3d","isOpen":true,"children":["1433ee62-21ac-d72c-0fd0-37000e5eb221","24bacfd6-cde8-81a0-f620-998cf5393d48",{"uuid":"0e94ff40-15b6-0b24-d0a0-447f000d761b","isOpen":true,"children":[]}]}]}]}]}]},{"uuid":"7e8426f1-08b2-81a2-7703-cb76ff5e7003","isOpen":true,"children":["b17675fc-b79b-0ad9-8f81-aa2dd1fc8c97","2e42e483-1557-d21e-aee0-94af7bedfd40",{"uuid":"c6d9e946-1d10-482d-14b1-0766027adba8","isOpen":true,"children":["9455d16e-4bbf-4b63-881d-7daf943e782b","16aee685-0ead-a542-5b3c-a62467ca45e3"]}]},{"uuid":"5ef5d225-d5ae-6787-8838-b75ccb7a7a81","isOpen":true,"children":["0e370edc-7b05-dccf-dd2a-a92cfe9f3e22","dc189735-0a58-619b-07ef-feec37d65e7d",{"uuid":"1b5cc202-c09e-faa0-5057-eb4ae60bf336","isOpen":true,"children":["6bcf768b-beab-4c39-6d96-91266036a4e1","274282a7-bc7b-02f8-4dce-84226e9dd9c1"]}]},{"uuid":"310c12c3-81f7-ed2f-172e-4d4c44aaa31f","isOpen":true,"children":["34afd0ba-f1fd-3d07-3b19-cdbe87b7bc1e"]}]}],"textures":[{"name":"-steve_template.png","path":"","folder":"","namespace":"","id":"0","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"44166cb6-b1bf-f227-4398-96c94f36d7f7","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAApBJREFUeF7tWztLA0EQvoCgEGsTiPhAwcbOJr12WmlhYWVlr1b+gFRqoZVVKkutLLUXwc5GUHxgIBFLBa1OJrmDzbi3O+u6SHJf2rndmftudl7fphBZfmc3T7HpkfreoXGHk/puwabDJA+t32ocGVCdHtXaeHn3EhEAlVJZK2+0mtFfALA0O661k2wj/Vk6Vta3Y5t+AGBzT3gAjgBiAIJgX6fB+eaeMc+XbzaMcfLo6sAoH1h9M8oXixWjPLT+AgFQLY3o83zrNSIDJspFrfyx+RERAMXJMa384+E5IgAGKzNa+VfjNiIALspb2nRMtpH+44VhrXzt/D0m/cM7O1r5e60Wk/6hqX2t/PN+MwYAEg/YnOt4wGmj8yGXE6/dv86JB+QeAMSAfg+CpjwUOg39exrkL8/7b97v29rL0P27r338fX/kR7X74/2+pL8P3T362gcAGAJaD1Cf6fsjwHsBHvR4rc9rex7EXIOma6/gal9WGZx+5K5S+JLV/rzW57V9WsunvQRfz8+bbT/+vG1/2360HgBkNELwgAQBHAG1G0QMQBDsngBJoiylQWSBZKSGNOg4Q5TODLM8TOKhqAMkdYBagbmWmj1fCmMewBCgfjvl4zn/LuHb1fU6cH35fF/7RPOA3AOAeYCCQC7mASk3x7k4zr1xro24NZXbc+XyJNydaX+JfaI6AAAk7Cw8gNHREhfDEVD4fcQAxwsNCIKCGxzIAoYjJolRSIOSdjj3dQDmAQoCvvx7r63XssO+7XD6/wLf+wW/Xe9iPwAIPRLzvV8Qen2bGwwZBH3nCaHXtwHwTYM+c3sTsySZ+/tWigAAHoAjgBiAIIgs4DkURRrMuCAhzeO+APp4MOqAvJfC38z1ql4UO+HHAAAAAElFTkSuQmCC"}],"animations":[{"uuid":"91f45b91-0388-39e0-52fd-e98e668d0d06","name":"roll","loop":"once","override":false,"length":0.66667,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10.3452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"4e015e1c-020c-c0cc-cf03-d0f365b91b4f","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"96a1cbef-dc1c-8cb3-893d-1844d13a8898","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55.3452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"cffaa113-7472-9a2d-879c-3b37b7918233","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.8452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"1859ff86-2a12-36c8-57b7-cf230e86e3e3","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-173.4065947693","y":"12.6083678689","z":"-8.1925230243"}],"uuid":"6c944778-0f30-a404-20be-7d985529dcc3","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-254.4306397576","y":"-5.2125464926","z":"-2.586642225"}],"uuid":"6fea6dfb-ac36-8bb7-7b6c-f1b31c3fb09c","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-299.316821318","y":"-7.7099874859","z":"-2.5994534331"}],"uuid":"19e7bcfa-34ef-ca1f-9316-34616fa523cb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-334.7684189709","y":"2.2798591799","z":"-2.5779800118"}],"uuid":"143b95c6-51d7-3d73-dab9-4afc76a18bee","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-359.7452658674","y":"0.977484586","z":"-0.7993651888"}],"uuid":"ca8b487a-5316-821c-6a0d-2b17dc09e2ce","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"739f6fd0-eeff-515d-bc2c-05459d38fb35","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"610dffa2-b064-c32e-49b0-95e662c9fbe0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f8040d86-2633-7d51-02f2-d276ac128c20","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-15","z":"-5"}],"uuid":"2613c6a2-076c-8a2d-7022-314ada456d6a","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-16","z":"3"}],"uuid":"65475ddb-0901-3649-c168-8f332b92d1c4","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-4","z":"5"}],"uuid":"d26510d5-a53d-5570-114b-1a49178fc6f7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"4"}],"uuid":"03b08547-82b6-c8ce-eb7e-8147925b4d3c","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3d14037c-74a9-abc2-b13d-3600ae6c5dd0","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3","z":"0.31"}],"uuid":"7a5c383e-b443-3773-3fc7-b41a95b66aa3","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"f218f334-8ebe-66d8-1769-41de9f9f7ad0","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"75314a09-9386-3e4d-3f96-a8e59911fe5e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e579328d-a99d-9a7f-0a05-746e42159080","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5169123047","y":"4.8873117739","z":"2.6276738824"}],"uuid":"57494920-9783-7099-8971-f8828309b07f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.2554184495","y":"1.9324031631","z":"0.6242872643"}],"uuid":"14faa28e-7b7c-4fca-04bd-fb15427d1d14","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.321532076","y":"-0.1985844587","z":"-0.7364643904"}],"uuid":"2960010b-2391-100b-9059-499f90d98595","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.7446837252","y":"-2.2894808314","z":"1.027565895"}],"uuid":"6436ac0d-b7c1-eb4c-741b-c6fec8760846","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"6cb82785-ecee-7220-9b21-7f748ca4ff73","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"554f1255-7efc-bebf-d907-e6b38e212768","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"918337ed-db4f-55f0-7a21-d10d25500ae6","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"66b19558-a367-598b-50c7-142c4211a07d","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"af84a634-f0ee-f31b-42da-a9789b3af450","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9b812e3b-ff6a-5bcf-9b14-d88149ef2a29","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.9335647827","y":"0.1910653446","z":"0.5860712492"}],"uuid":"d26f71b7-3eb6-c334-8e41-f3048cc39bfd","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.8337650092","y":"1.5719388815","z":"0.836263831"}],"uuid":"908fd1af-b92a-7c86-ec15-33aa208e85d9","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.5153158634","y":"1.5694142029","z":"1.1566234175"}],"uuid":"06e3b992-d5bc-6bd5-da5e-ec28155c8af4","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-5","y":"0","z":"0"}],"uuid":"af2c3045-852e-36da-7224-00ca89c0c360","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4e57e7e0-bff8-8f86-f5a9-14379ed8f0cc","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"7e8f0cb8-aacf-3a06-0990-319fcca439ca","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"951b375f-8b17-0bce-c1e5-b020c20b1378","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"0","z":"0"}],"uuid":"96e0a9d7-f9a1-ca27-c4c1-49b9b9ae9739","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"d691f682-5653-24b5-64cd-f593d802ae7a","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"64d27d7c-16f1-4e24-11de-1df5b650e5b7","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-44.7041455577","y":"-0.5463397482","z":"2.4016359177"}],"uuid":"cf7881c9-f9af-c342-d155-dda6ca40df0f","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-42.1987087714","y":"0.9199484607","z":"2.3855070851"}],"uuid":"1e94d2b6-12f7-f0b3-5513-e088c678cddc","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"4.7824447755","y":"-15.0272212866","z":"0.6748976469"}],"uuid":"d578b08c-e775-fb98-c9f8-42f4e180dc8f","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3a4776d8-2065-0aff-5901-b4d3d6ecf2f6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.9790874962","y":"-5.8032396565","z":"15.815264958"}],"uuid":"f9ce9b56-16a1-0ade-1b32-395dc6a6508f","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.1815443852","y":"7.1155834398","z":"18.1304160091"}],"uuid":"14d812e3-97c4-fb55-ae8c-7ec92768c5bd","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.9790874962","y":"-5.8032396565","z":"15.815264958"}],"uuid":"5afbcdab-4795-1ca8-59a9-285e3dfac932","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.9171104584","y":"-4.5000466504","z":"5.8600103802"}],"uuid":"6499750a-a4fb-5c57-4995-c4d47b630672","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"8.2765048988","y":"-2.9212329552","z":"-1.4866588636"}],"uuid":"7f018859-2372-9fd4-17e0-756d8fffc147","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"aba6447d-d798-c3c4-8579-72b987b8b41c","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.7013003421","y":"1.5426491326","z":"16.086881186"}],"uuid":"3896e9cb-f48c-75cc-888d-769cf83332f2","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"55","y":"0","z":"0"}],"uuid":"8e1409c4-941b-084c-02e5-df03cc1a4690","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"92ea27f8-0e0a-b629-1027-b86cf738d7ec","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"79.7622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"fc368334-2af0-e8a5-5d0a-5cbdb752e343","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"117.2622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"7f4efd9b-c5ba-5977-b5fa-b1b9799bbac1","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"94.7622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"7db6c762-536f-e3b9-482d-8337cffe5746","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95.0395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"bbc95573-feb7-ffe2-345c-af69f5f8f939","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"72.5395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"ab7c0856-f341-34ea-f57c-6904b0e30d92","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"9e4fcb79-0aa2-9377-3fc6-ab698d600e8a","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"63743b29-3a44-0bc8-f5a5-8903bb15d390","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"25.6848204073","y":"10.1778091184","z":"-20.173933666"}],"uuid":"7a3b3bdf-dd57-badd-a6dd-7580d30e23e0","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4323899f-7d3f-557a-beda-d05603cd2f96","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.4403124196","y":"16.3255534092","z":"-34.2402741209"}],"uuid":"744316fc-f3d8-67fd-9a68-2b2c568b639e","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.2988707081","y":"3.4553272208","z":"-6.6606725408"}],"uuid":"6cbe013b-e0b9-702c-e627-f2c374f53718","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fd38995a-2f81-eb5e-6c06-404eceec04f6","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"25","y":"0","z":"0"}],"uuid":"5f5fe94f-19c5-c019-7a47-ac924d74285c","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"339cdfba-9029-f75f-9d19-ac8567b07424","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"87d530dc-a3ca-5bc5-53fd-d0b58e67c115","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95","y":"0","z":"0"}],"uuid":"4056d3f0-9227-879d-0ccd-a715a6acefd8","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"60","y":"0","z":"0"}],"uuid":"5cb622b9-5c77-b222-ba79-a62ddbf1e801","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.9979365876","y":"19.3545961515","z":"-11.7009195082"}],"uuid":"d08bec63-419b-45f1-d3ac-4bf88c534563","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.9979365876","y":"19.3545961515","z":"-11.7009195082"}],"uuid":"aeaa276f-1181-1922-960d-7fb6845fd9f3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"13.2306976448","y":"4.1075261935","z":"1.5640491387"}],"uuid":"2416d5e6-91bb-6991-a145-7778a9ab717b","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ce1a67cc-be9b-632e-7d0f-1ea657ce8f06","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"3e549bb1-58c3-92b5-d13c-9c3c0772ea12","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c6b905c0-5196-e52b-59c7-8d3927a73397","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.5","y":"0","z":"0"}],"uuid":"e4592899-dc47-7f8c-acd9-92811b5fdd77","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.1404769782","y":"-17.3455144203","z":"2.3566635967"}],"uuid":"07d4219f-d8d7-811e-fba2-06fb667c4f62","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"31.427095159","y":"-13.5306627252","z":"-4.344153106"}],"uuid":"7a496aba-7df1-b5b5-4dbe-7e6420e13fef","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"11.427095159","y":"-13.5306627252","z":"-4.344153106"}],"uuid":"446c86e4-1c3b-eff5-a180-a62769af7bb8","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"95fe2e07-173b-a066-a66d-0be58ae890a5","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"48.3424105805","y":"1.1690538795","z":"-1.2575696869"}],"uuid":"06246a98-556c-f22c-45a0-44083c73dbe9","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.8934503775","y":"-3.8213109421","z":"17.9129813754"}],"uuid":"aead35ef-607e-93dc-f018-85e424e6a137","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"447233aa-66b2-dbd7-e45d-483f46dadbae","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"bd388449-ced9-61f6-e408-4f913fc0355d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.4009318272","y":"-4.2154088774","z":"2.6913572849"}],"uuid":"7b84f346-ab75-70f4-a8f4-67879637f9e5","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.4009318272","y":"-4.2154088774","z":"2.6913572849"}],"uuid":"724ba19e-1a1c-00e9-111a-d67ed5aa5c83","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"48.3698475508","y":"-13.3767559929","z":"0.8884687898"}],"uuid":"28e08960-9fa6-1789-1edf-5b1e443db7bd","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-0.7930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"3629a4e5-a2bf-6722-7a63-824e74693c95","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-23.2930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"fc5d0a27-b180-4a2f-5fb1-c25e9d8d0cbc","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.2930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"0471b600-cd96-fb88-b394-25b68b236191","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b3528148-886c-4981-aad9-a449912e4ff3","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"c6d9e946-1d10-482d-14b1-0766027adba8":{"name":"prfl_right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"4d65f4ff-4666-1d6e-e2a2-5754675f39d3","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"713091ea-3c5f-2107-8abf-ee97bc6176ee","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-35","y":"0","z":"0"}],"uuid":"020d854c-8f1d-eea6-0d44-f050772acad3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"747e19b0-5b0d-1bde-37c5-d61eae5ee649","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1a3a7fe0-9ac6-642d-1655-a02cbedc50c0","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-63.5104629601","y":"-0.0562419487","z":"-3.8034923955"}],"uuid":"cf494402-d95a-c9a9-0666-1105ac4ff1ba","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30.3667568215","y":"-5.6104628256","z":"0.822128921"}],"uuid":"338edd44-0be5-f9d0-495a-16b3c7a47468","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5627211181","y":"5.0414022802","z":"3.6575498102"}],"uuid":"601767a5-ac9a-bc96-4b0b-237160c36700","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50.8538632696","y":"5.4159878628","z":"2.605219904"}],"uuid":"b124849a-6e1c-463f-9163-98272fab4254","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-17.1096711992","y":"-3.7317133585","z":"-11.938445897"}],"uuid":"486ede20-d032-4d46-d9cd-95cf257c04d9","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1e3e41fd-1374-7b20-aada-7d42393cb399","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-93.8329224881","y":"-12.0025641778","z":"-18.1816740733"}],"uuid":"13a334d3-48cd-1136-e910-d86f10ecd6ff","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"25.8053212577","y":"7.0178718272","z":"-29.2161110452"}],"uuid":"7e87c3b5-9d38-b35b-df3b-aa05fc47517d","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.0037108296","y":"-5.9031625207","z":"-19.1431448415"}],"uuid":"6601f44d-7b3e-1933-edfb-aaf37ed8f707","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-14.2395920994","y":"3.5940186635","z":"-7.5169369411"}],"uuid":"284cc08b-c152-0ab3-3689-6d441335ef56","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"8085208f-1d05-3f74-9820-0173fe43fd6b","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.9519624501","y":"-7.2690960426","z":"-16.7194764292"}],"uuid":"487c4a04-c29a-1aa1-e26a-aa6dbafe7e0a","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"18.247962068","y":"7.8313214321","z":"-24.4844657083"}],"uuid":"73a7fea9-6029-f2f6-ba64-9449179096e1","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"a0002446-b908-df56-d45b-79e09a54036d","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"71dde296-8a96-6c52-d883-110fe28f6083","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18.550117808","y":"-0.4065298268","z":"4.9663131482"}],"uuid":"a9e10a80-2787-3737-8868-9c28c2cbafdb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5298058614","y":"-2.9158033357","z":"6.6597637739"}],"uuid":"3ec121c2-1295-28ed-2ca7-22374f35c684","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c43f7153-1df2-38ec-1edd-b3694bed6dc8","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.1236819605","y":"-0.9306502051","z":"1.542865843"}],"uuid":"fefea430-0ffa-21c0-738a-21ab44462a5e","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-33.1776348771","y":"-2.2590643664","z":"4.7558127555"}],"uuid":"3245ce43-4453-1563-6426-957785fa545f","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-53.9268912309","y":"-7.3104700825","z":"8.1053141589"}],"uuid":"8d32a863-19e6-66eb-dd63-bc4a4490440e","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.6010831098","y":"0.530086938","z":"2.60850085"}],"uuid":"1a5cc3fb-2736-9e73-929b-2b1f796b4824","time":0.58333,"color":-1,"interpolation":"catmullrom"}]}}}]} ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] kotlin = "2.3.21" paperweight = "2.0.0-beta.21" resourcefactory = "1.3.1" shadow = "9.4.1" minotaur = "2.9.0" hangarPublish = "0.1.4" fabric-api = "0.146.1+26.1.2" fabric-language-kotlin = "1.13.11+kotlin.2.3.21" cloud-bukkit = "2.0.0-beta.15" cloud-mod = "2.0.0-beta.16" cloud-core = "2.0.0" configurate = "4.2.0" neoform = "26.1.2-1" [libraries] kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } semver4j = "org.semver4j:semver4j:6.0.0" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" adventure-api = "net.kyori:adventure-api:4.26.1" adventure-examination = "net.kyori:examination-api:1.3.0" adventure-option = "net.kyori:option:1.1.0" adventure-platform-bukkit = "net.kyori:adventure-platform-bukkit:4.4.1" adventure-platform-fabric = "net.kyori:adventure-platform-fabric:6.9.0" asm-tree = "org.ow2.asm:asm-tree:9.9.1" fabric-loader = "net.fabricmc:fabric-loader:0.19.2" fabric-language-kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric-language-kotlin" } polymer-resource-pack = "eu.pb4:polymer-resource-pack:0.16.3+26.1.2" configurate-core = { module = "org.spongepowered:configurate-core", version.ref = "configurate" } configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } joml = "org.joml:joml:1.10.8" gson = "com.google.code.gson:gson:2.14.0" fastutil = "it.unimi.dsi:fastutil:8.5.18" guava = "com.google.guava:guava:33.6.0-jre" cloud-paper = { module = "org.incendo:cloud-paper", version.ref = "cloud-bukkit" } cloud-fabric = { module = "org.incendo:cloud-fabric", version.ref = "cloud-mod" } cloud-core = { module = "org.incendo:cloud-core", version.ref = "cloud-core" } geantyref = "io.leangen.geantyref:geantyref:2.0.1" lombok = "org.projectlombok:lombok:1.18.46" bStats = "org.bstats:bstats-bukkit:3.2.1" dynamicUV = "io.github.toxicity188:dynamicuv:1.2.2" armormodel = "io.github.toxicity188:armormodel:1.0.2" javamesh = "io.github.toxicity188:java-mesh:0.0.1" molangCompiler = "gg.moonflower:molang-compiler:3.1.1.19" libby = "net.byteflux:libby-bukkit:1.3.1" build-kotlin-jvm = { module = "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" } build-shadow = { module = "com.gradleup.shadow:com.gradleup.shadow.gradle.plugin", version.ref = "shadow" } build-minotaur = { module = "com.modrinth.minotaur:com.modrinth.minotaur.gradle.plugin", version.ref = "minotaur" } build-hangarPublish = { module = "io.papermc.hangar-publish-plugin:io.papermc.hangar-publish-plugin.gradle.plugin", version.ref = "hangarPublish" } build-resourcefactory = { module = "xyz.jpenilla:resource-factory", version.ref = "resourcefactory" } build-paperweight = { module = "io.papermc.paperweight.userdev:io.papermc.paperweight.userdev.gradle.plugin", version.ref = "paperweight" } [bundles] minecraft = [ "adventure-api", "joml", "fastutil", "gson", "guava" ] fabric = [ "fabric-loader", "fabric-language-kotlin" ] fabric-library = [ "configurate-core", "configurate-yaml" ] fabric-mod = [ "adventure-platform-fabric", "cloud-fabric", "polymer-resource-pack", ] library = [ "semver4j", "dynamicUV", "javamesh", "caffeine" ] core = [ "armormodel", "molangCompiler" ] manifestLibrary = [ "kotlin", "bStats", "molangCompiler", "cloud-paper", "cloud-core", "geantyref", "adventure-api", "adventure-examination", "adventure-option", "adventure-platform-bukkit", "asm-tree" ] shadedLibrary = [ "armormodel", "libby" ] [plugins] resourcefactory-bukkit = { id = "xyz.jpenilla.resource-factory-bukkit-convention" } resourcefactory-paper = { id = "xyz.jpenilla.resource-factory-paper-convention" } resourcefactory-fabric = { id = "xyz.jpenilla.resource-factory-fabric-convention" } hangar = { id = "io.papermc.hangar-publish-plugin" } convention-publish = { id = "publish-conventions" } convention-plugin = { id = "plugin-conventions" } convention-standard = { id = "standard-conventions" } convention-bukkit = { id = "bukkit-conventions" } convention-paperweight = { id = "paperweight-conventions" } convention-modrinth = { id = "modrinth-conventions" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ kotlin.code.style=official kotlin.daemon.jvmargs=-Xmx4g org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 project_version=3.0.2 minecraft_version=26.1.2 ================================================ FILE: gradlew ================================================ #!/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/2d6327017519d23b96af35865dc997fcb544fb40/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" "$@" ================================================ FILE: gradlew.bat ================================================ @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 with windows NT shell if "%OS%"=="Windows_NT" setlocal 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 goto fail :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 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: nms/v1_21_R3/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.convention.paperweight) } dependencies { paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") } tasks { compileJava { options.release = 21 } compileKotlin { compilerOptions.jvmTarget = JvmTarget.JVM_21 } } ================================================ FILE: nms/v1_21_R3/src/main/java/kr/toxicity/model/bukkit/nms/v1_21_R3/AbstractHitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3; import kr.toxicity.model.api.nms.HitBox; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class AbstractHitBox extends ArmorStand implements HitBox { AbstractHitBox(@NotNull Level level) { super(EntityType.ARMOR_STAND, level); } @Override //Only for provide compiler hint for Kotlin jvm public final boolean equals(@Nullable Object other) { return super.equals(other); } @Override //Only for provide compiler hint for Kotlin jvm public final int hashCode() { return super.hashCode(); } @Override public void remove(@NotNull RemovalReason reason, @Nullable org.bukkit.event.entity.EntityRemoveEvent.Cause eventCause) { super.remove(reason, eventCause); } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/BaseEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.persistence.PersistentDataHolder import org.joml.Vector3f import java.util.* import java.util.stream.Stream internal data class BaseEntityImpl( private val delegate: CraftEntity ) : BaseBukkitEntity, PersistentDataHolder by delegate { override fun customName(): AdventureComponent? = handle().run { if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { isCustomNameVisible } } override fun entity(): org.bukkit.entity.Entity = delegate override fun handle(): Entity = delegate.vanillaEntity override fun uuid(): UUID = delegate.uniqueId override fun id(): Int = handle().id override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true override fun glow(): Boolean = handle().isCurrentlyGlowing override fun onWalk(): Boolean { return handle().isWalking() } override fun scale(): Double { val handle = handle() return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 } override fun pitch(): Float = handle().xRot override fun ground(): Boolean = handle().onGround() override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = handle().yRot override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun fly(): Boolean = handle().isFlying override fun damageTick(): Float { val handle = handle() if (handle !is LivingEntity) return 0F val duration = handle.invulnerableDuration.toFloat() if (duration <= 0F) return 0F val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) return handle.invulnerableTime.toFloat() / duration * knockBack } override fun walkSpeed(): Float { val handle = handle() if (handle !is LivingEntity) return 0F if (!handle.onGround) return 1F val speed = handle.getEffect(MobEffects.MOVEMENT_SPEED)?.amplifier ?: 0 val slow = handle.getEffect(MobEffects.MOVEMENT_SLOWDOWN)?.amplifier ?: 0 return (1F + (speed - slow) * 0.2F) .coerceAtLeast(0.2F) .coerceAtMost(2F) } override fun passengerPosition(dest: Vector3f): Vector3f { return handle().passengerPosition(dest) } override fun platform(): PlatformEntity = delegate.wrap() override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } override fun location(): PlatformLocation = delegate.location.wrap() } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/BasePlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import net.minecraft.util.Mth import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.stream.Stream internal data class BasePlayerImpl( private val delegate: CraftPlayer, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { override fun entity(): Player = delegate override fun updateInventory() { delegate.handle.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = delegate.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(delegate), delegate.trackedBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = delegate.handle var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.bukkit.platform.BukkitItemStack import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack internal fun Entity.wrap() = adapt(this) internal fun LivingEntity.wrap() = adapt(this) internal fun OfflinePlayer.wrap() = adapt(this) internal fun Player.wrap() = adapt(this) internal fun Location.wrap() = adapt(this) internal fun World.wrap() = adapt(this) internal fun ItemStack.wrap() = adapt(this) internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/EntityData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import org.joml.Quaternionf import org.joml.Vector3f import java.lang.reflect.Field internal fun Field.toEntityDataAccessor() = run { isAccessible = true get(null) as EntityDataAccessor<*> } internal fun Class<*>.accessors() = declaredFields.filter { f -> EntityDataAccessor::class.java.isAssignableFrom(f.type) }.map { it.toEntityDataAccessor() } internal val DISPLAY_SET = Display::class.java.accessors() internal val SHARED_FLAG = Entity::class.java.accessors().first().id internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { it.id } internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() internal val ITEM_ENTITY_DATA = buildList { add(SHARED_FLAG) addAll(ITEM_DISPLAY_ID) add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } }.toIntSet() @Suppress("UNCHECKED_CAST") private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { SynchedEntityData.DataValue(id, serializer, 0) } @Suppress("UNCHECKED_CAST") internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor internal class TransformationData { private var _duration = 0 private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return (dest.mod as ModAnimationBundlerImpl).append(entityId) { dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(DISPLAY_INTERPOLATION_DELAY) translation.value?.let { appendPosition(it.value); add(it) } rotation.value?.let { appendRotation(it.value); add(it) } scale.value?.let { appendScale(it.value); add(it) } appendDuration(_duration); add(duration) }) } } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { _duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack() = listOf( DISPLAY_INTERPOLATION_DELAY, duration, translation.forceValue, scale.forceValue, rotation.forceValue ) private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _t: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else null val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) fun set(other: T) { if (dirtyChecker(_t, other)) return _dirty = true setter(_t, other) } } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import io.netty.buffer.Unpooled import io.papermc.paper.adventure.PaperAdventure import io.papermc.paper.configuration.GlobalConfiguration import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.tracker.EntityTrackerRegistry import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.inventory.CraftItemStack import org.bukkit.craftbukkit.util.CraftChatMessage import org.joml.Vector3f import java.util.* internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() } internal inline fun createAdaptedFieldGetter(): (T) -> R { return T::class.java.declaredFields.first { R::class.java.isAssignableFrom(it.type) }.apply { isAccessible = true }.let { getter -> { t -> getter[t] as R } } } internal fun dirtyChecked(hash: () -> H, function: (H) -> T, equalityChecker: (H, H) -> Boolean = { a, b -> a == b }): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() if (equalityChecker(h, newH)) value else synchronized(lock) { h = newH value = function(h) value } } } internal val CONFIG get() = BetterModel.config() internal val EMPTY_ITEM = VanillaItemStack.EMPTY internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() } internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) } } private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { it.type.isArray }.apply { isAccessible = true } internal fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, valueFilter: (DataValue<*>) -> Boolean = { true }, required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? = (DATA_ITEMS[this] as Array<*>) .mapNotNull map@ { val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null val value = item.value().takeIf(valueFilter) ?: return@map null item to value } .takeIf(required) ?.map { if (clean) it.first.isDirty = false it.second } internal fun Entity.isWalking(): Boolean { return controllingPassenger?.isWalking() ?: when (this) { is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { it.isRunning && when (it.goal) { is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true else -> false } } is ServerPlayer -> xMovement() != 0F || zMovement() != 0F else -> false } } internal fun ServerPlayer.xMovement(): Float { val leftMovement: Boolean = lastClientInput.left() val rightMovement: Boolean = lastClientInput.right() return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F } internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F internal fun ServerPlayer.zMovement(): Float { val forwardMovement: Boolean = lastClientInput.forward() val backwardMovement: Boolean = lastClientInput.backward() return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F } internal fun ServerPlayer.isJump() = lastClientInput.jump() internal val Entity.isFlying: Boolean get() = when (this) { is FlyingAnimal -> isFlying is FlyingMob -> true is Mob -> isNoAi is Player -> abilities.flying is LivingEntity -> isFallFlying else -> false } internal val CraftEntity.vanillaEntity: Entity get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { if (hasItemInSlot(it)) getItemBySlot(it) else null }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } internal val Player.hotbarSlot get() = inventory.selected + 36 internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) internal fun Player.toCustomisation() = entityData.get(Player.DATA_PLAYER_MODE_CUSTOMISATION).toInt() internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asAdventure(this) } else { GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) } internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asVanilla(this) } else { CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/HitBoxImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import io.papermc.paper.event.entity.EntityKnockbackEvent import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.core.BlockPos import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionHand.MAIN_HAND import net.minecraft.world.InteractionHand.OFF_HAND import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.Color import org.bukkit.Particle import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftArmorStand import org.bukkit.craftbukkit.entity.CraftLivingEntity import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityPotionEffectEvent import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.plugin.Plugin import org.joml.Vector3f import java.util.* internal class HitBoxImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractHitBox(delegate.level()) { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var collision = ifLivingEntity { collides } == true private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false val craftEntity: HitBox by lazy { object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} } val dimensions: EntityDimensions get() = source.run { EntityDimensions( (x() + z()).toFloat() / 2, y().toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale(bone.hitBoxScale()) } private val interaction by lazy { HitBoxInteraction(this) } init { moveTo(delegate.position()) isInvisible = true persist = false isSilent = true initialized = true level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) level().addFreshEntity(interaction.apply { moveTo(delegate.position()) }, CreatureSpawnEvent.SpawnReason.CUSTOM) interaction.startRiding(this) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity ifLivingEntity { collides = collision } } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f = delegate.position().run { bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { } override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) return if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity ifLivingEntity { collision = collides collides = false } } listener.handle(HitBoxMountEvent(this, entity)) } } override fun dismount(entity: PlatformEntity) { forceDismount = true if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { it.stopRiding(true) listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback( d0: Double, d1: Double, d2: Double, attacker: Entity?, cause: EntityKnockbackEvent.Cause ) { if (attacker === delegate) return ifLivingEntity { knockback(d0, d1, d2, attacker, cause) } } override fun push(pushingEntity: Entity) { if (pushingEntity === delegate) return delegate.push(pushingEntity) } override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { if (pushingEntity === delegate) return delegate.push(x, y, z, pushingEntity) } override fun isCollidable(ignoreClimbing: Boolean): Boolean { return delegate.isCollidable(ignoreClimbing) } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } override fun canCollideWithBukkit(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWithBukkit(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return ifLivingEntity { getActiveEffects() } ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null } override fun onWalk(): Boolean { return isWalking() } private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity) return val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) if (!mountController.canFly() && delegate.isFallFlying) return updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) delegate.yHeadRot = player.yRot delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) } val dy = delegate.deltaMovement.y + delegate.gravity if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed() = ifLivingEntity { getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { if (!onFly && !shouldDiscardFriction()) level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * it else it } ?: 0.0F } ?: 0.0F private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) delegate.isNoAi = fly else delegate.isNoGravity = fly onFly = fly && !delegate.onGround() if (onFly) delegate.resetFallDistance() } private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, player.bukkitEntity.wrap(), (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) override fun tick() { delegate.removalReason?.let { if (!isRemoved) remove(it) return } val controller = controllingPassenger if (jumpDelay > 0) jumpDelay-- interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) delegate.navigation.stop() mountControl(controller) } else initialSetup() yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockPos.betweenClosedStream(boundingBox).forEach { level().getBlockState(it).entityInside(level(), it, delegate) } if (isInLava) delegate.lavaHurt() firstTick = false listener.sync(craftEntity) } override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { initialSetup() listener.handle(HitBoxRemoveEvent(craftEntity)) interaction.remove(reason) super.remove(reason, cause) } override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean { return ifLivingEntity { isDeadOrDying } == true } override fun hide(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { hideEntity(plugin, bukkitEntity) hideEntity(plugin, interaction.bukkitEntity) } } override fun show(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { showEntity(plugin, bukkitEntity) showEntity(plugin, interaction.bukkitEntity) } } override fun interact(player: Player, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand)) return InteractionResult.SUCCESS } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL val interact = HitBoxInteractAtEvent( (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { MAIN_HAND -> ModelInteractionHand.RIGHT OFF_HAND -> ModelInteractionHand.LEFT }, vec.toBukkit() ) if (!listener.handle(interact)) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand, vec)) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { return ifLivingEntity { addEffect(effectInstance, cause) } == true } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause, fireEvent: Boolean ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (source.entity === delegate || delegate.isInvulnerable) return false if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false val ds = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(craftEntity, ds, amount) if (!listener.handle(event)) return false return ifLivingEntity { hurtServer(world, source, event.damage) } == true } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner === delegate) return ProjectileDeflection.NONE return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return ifLivingEntity { health } ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { return if (!initialized) { super.makeBoundingBox(vec3) } else { val scale = bone.hitBoxScale() AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ).apply { if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) } } } } override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) override fun removeHitBox() { source().task { dismountAll() remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) } } private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { return if (delegate.valid) (delegate as? LivingEntity)?.block() else null } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/HitBoxInteraction.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.nms.HitBox import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftInteraction internal class HitBoxInteraction( val delegate: HitBoxImpl ) : Interaction(EntityType.INTERACTION, delegate.level()) { init { persist = false } private val craftEntity: CraftInteraction by lazy { object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} } override fun getBukkitEntity(): CraftEntity = craftEntity override fun getBukkitEntityRaw(): CraftEntity = craftEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun tick() { val dimension = delegate.dimensions width = dimension.width height = dimension.height yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { return if (entity is Player) { entity.attack(delegate) true } else false } override fun interact(player: Player, hand: InteractionHand): InteractionResult { delegate.interact(player, hand) return InteractionResult.FAIL } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { delegate.interactAt(player, vec, hand) return InteractionResult.FAIL } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.server.MinecraftServer import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, MinecraftServer.getServer().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { (player.unwarp() as CraftPlayer).handle.connection.send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import net.minecraft.world.damagesource.DamageSource import org.bukkit.craftbukkit.util.CraftLocation internal class ModelDamageSourceImpl( private val source: DamageSource ) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/ModelDisplayImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import org.joml.Quaternionf import org.joml.Vector3d import org.joml.Vector3f import java.util.* import java.util.concurrent.atomic.AtomicBoolean internal class ModelDisplayImpl( private val pos: Vector3d, val display: ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData = display.entityData private val entityDataLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityDataLock.accessToLock { entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += addPacket } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.moveTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.unwarp().asVanilla() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean = entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else EMPTY_ITEM ) else it } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) private class DisplayTransformerImpl( source: ItemDisplay ) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.run { bundler += ClientboundSetEntityDataPacket(id, this) } } } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/ModelGameProfile.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import com.mojang.authlib.GameProfile import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin internal data class ModelGameProfile( private val gameProfile: GameProfile ) : ModelProfile { private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) private val skin by lazy { gameProfile.properties["textures"].firstOrNull()?.let { BetterModel.platform().profileManager().skin(it.value) } ?: ModelProfileSkin.EMPTY } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import net.kyori.adventure.text.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap internal class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, MinecraftServer.getServer().overworld() ).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: Component?) { display.text = component?.asVanilla() ?: VanillaComponent.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == VanillaComponent.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.moveTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/NMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup import com.mojang.authlib.GameProfile import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.TransformedItemStack import net.kyori.adventure.key.Keyed import net.minecraft.core.NonNullList import net.minecraft.core.component.DataComponents import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.resources.ResourceLocation import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerCommonPacketListenerImpl import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.level.entity.LevelEntityGetter import net.minecraft.world.level.entity.LevelEntityGetterAdapter import net.minecraft.world.level.entity.PersistentEntitySectionManager import org.bukkit.craftbukkit.CraftWorld import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Vector3d import java.util.* import java.util.function.Consumer import java.util.function.IntConsumer class NMSImpl : NMS { companion object { private const val INJECT_NAME = "bettermodel_channel_handler" //Spigot private val getGameProfile: (Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { it.type == PersistentEntitySectionManager::class.java }?.apply { isAccessible = true } @Suppress("UNCHECKED_CAST") private val ServerLevel.levelGetter get(): LevelEntityGetter { return if (BetterModelBukkit.IS_PAPER) { `moonrise$getEntityLookup`() } else { spigotChunkAccess?.get(this)?.let { (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter } ?: throw RuntimeException("LevelEntityGetter") } } private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> (g as EntityLookup)[i] } else LevelEntityGetterAdapter::class.java.declaredFields.first { net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) }.let { it.isAccessible = true { e, i -> (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity } } private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) //Spigot private val hitBoxData by lazy { ItemDisplay(EntityType.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 entityData.nonDefaultValues!! } } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) inner class PlayerChannelHandlerImpl( private val player: CraftPlayer ) : PlayerChannelHandler, ChannelDuplexHandler() { private val connection = player.handle.connection private val uuid = player.uniqueId private val base = adapt(player.wrap()) init { val pipeline = getConnection(connection).channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = getConnection(connection).channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = base override fun isModEnabled(): Boolean = player.listeningPluginChannels.contains("modelengine:bulk_data") private val playerModel get() = connection.player.id.toRegistry() private fun Int.toPlayerEntity() = toEntity(connection.player.serverLevel()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry( ifHitBox: (Entity) -> Unit = {} ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uniqueId) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(handle.id, it) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list += it } list.send(player.wrap()) } private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) is ClientboundAddEntityPacket -> { val entity = id.toPlayerEntity() ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.bukkitEntity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(player.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@ { it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) } } is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry()?.let { if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> return packet } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, (items as NonNullList).apply { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) set(connection.player.hotbarSlot, EMPTY_ITEM) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.handle.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(player.wrap()) } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { val entity = registry.entity().handle() if (entity is Entity) bundler += registry.mountPacket(entity) } private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { EntityTrackerRegistry.registry(it.uuid) == null }.map { it.id }.toIntArray()): ClientboundSetPassengersPacket { return useByteBuf { buffer -> buffer.writeVarInt(entity.id) buffer.writeVarIntArray(displays() .mapToInt { (it as ModelDisplayImpl).display.id }.toArray() + array) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( Vector3d(location.x(), location.y(), location.z()), ItemDisplay(EntityType.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 billboardConstraints = Display.BillboardConstraints.FIXED valid = true yRot = location.yaw() itemTransform = ItemDisplayContext.FIXED }, yOffset ).apply { initialConsumer.accept(this) display.entityData.packDirty() } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.unwarp().asVanilla().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb, false)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { CustomModelData(it.floats, it.flags, it.strings, it.colors .run { if (rgb == 0xFFFFFF) this else map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } .ifEmpty { listOf(rgb) }) }) }.asBukkit().wrap() } override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { val handle = entity.handle() as? Entity ?: return null return HitBoxImpl( boundingBox.center(), bone, listener, handle, mountController ).craftEntity } override fun version(): NMSVersion = NMSVersion.V1_21_R3 override fun adapt(entity: PlatformEntity): BaseBukkitEntity { val craft = entity.unwarp() as CraftEntity return BaseEntityImpl(craft) } override fun adapt(player: PlatformPlayer): BasePlayer { val craft = player.unwarp() as CraftPlayer return BasePlayerImpl( craft, dirtyChecked( { getGameProfile(craft.handle) }, { ModelGameProfile(it) }, { a, b -> a == b && a.properties["texture"] === b.properties["texture"]} ), dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return VanillaItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, ResourceLocation.parse(model)) TransformedItemStack.of(asBukkit().wrap()) } } override fun isProxyOnlineMode(): Boolean = ONLINE_MODE } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import net.kyori.adventure.key.Key import net.kyori.adventure.key.Keyed import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import org.bukkit.craftbukkit.entity.CraftPlayer private val KEY = Key.key("bettermodel") internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable, Keyed { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket = ClientboundBundlePacket(this) override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun key(): Key = KEY override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import org.bukkit.craftbukkit.entity.CraftPlayer internal data class PlayerArmorImpl( private val player: CraftPlayer ) : PlayerArmor { override fun helmet(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() } override fun leggings(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() } override fun chestplate(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() } override fun boots(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() } private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { val trim = get(DataComponents.TRIM) ArmorItem( get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, it.location().path, trim?.pattern?.value()?.assetId?.path, trim?.material?.value()?.assetName ) }?.orElse(null) } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: nms/v1_21_R3/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R3/TypeAliases.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R3 import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.world.item.ItemStack internal typealias VanillaItemStack = ItemStack internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack internal typealias ClientPacket = Packet internal typealias VanillaComponent = Component internal typealias AdventureComponent = net.kyori.adventure.text.Component ================================================ FILE: nms/v1_21_R4/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.convention.paperweight) } dependencies { paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT") } tasks { compileJava { options.release = 21 } compileKotlin { compilerOptions.jvmTarget = JvmTarget.JVM_21 } } ================================================ FILE: nms/v1_21_R4/src/main/java/kr/toxicity/model/bukkit/nms/v1_21_R4/AbstractHitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4; import kr.toxicity.model.api.nms.HitBox; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class AbstractHitBox extends ArmorStand implements HitBox { AbstractHitBox(@NotNull Level level) { super(EntityType.ARMOR_STAND, level); } @Override //Only for provide compiler hint for Kotlin jvm public final boolean equals(@Nullable Object other) { return super.equals(other); } @Override //Only for provide compiler hint for Kotlin jvm public final int hashCode() { return super.hashCode(); } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/BaseEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.persistence.PersistentDataHolder import org.joml.Vector3f import java.util.* import java.util.stream.Stream internal data class BaseEntityImpl( private val delegate: CraftEntity ) : BaseBukkitEntity, PersistentDataHolder by delegate { override fun customName(): AdventureComponent? = handle().run { if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { isCustomNameVisible } } override fun entity(): org.bukkit.entity.Entity = delegate override fun handle(): Entity = delegate.vanillaEntity override fun uuid(): UUID = delegate.uniqueId override fun id(): Int = handle().id override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true override fun glow(): Boolean = handle().isCurrentlyGlowing override fun onWalk(): Boolean { return handle().isWalking() } override fun scale(): Double { val handle = handle() return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 } override fun pitch(): Float = handle().xRot override fun ground(): Boolean = handle().onGround() override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = handle().yRot override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun fly(): Boolean = handle().isFlying override fun damageTick(): Float { val handle = handle() if (handle !is LivingEntity) return 0F val duration = handle.invulnerableDuration.toFloat() if (duration <= 0F) return 0F val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) return handle.invulnerableTime.toFloat() / duration * knockBack } override fun walkSpeed(): Float { val handle = handle() if (handle !is LivingEntity) return 0F if (!handle.onGround) return 1F val speed = handle.getEffect(MobEffects.SPEED)?.amplifier ?: 0 val slow = handle.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 return (1F + (speed - slow) * 0.2F) .coerceAtLeast(0.2F) .coerceAtMost(2F) } override fun passengerPosition(dest: Vector3f): Vector3f { return handle().passengerPosition(dest) } override fun platform(): PlatformEntity = delegate.wrap() override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } override fun location(): PlatformLocation = delegate.location.wrap() } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/BasePlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import net.minecraft.util.Mth import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.stream.Stream internal data class BasePlayerImpl( private val delegate: CraftPlayer, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { override fun entity(): Player = delegate override fun updateInventory() { delegate.handle.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = delegate.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(delegate), delegate.trackedBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = delegate.handle var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.bukkit.platform.BukkitItemStack import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack internal fun Entity.wrap() = adapt(this) internal fun LivingEntity.wrap() = adapt(this) internal fun OfflinePlayer.wrap() = adapt(this) internal fun Player.wrap() = adapt(this) internal fun Location.wrap() = adapt(this) internal fun World.wrap() = adapt(this) internal fun ItemStack.wrap() = adapt(this) internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/EntityData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import org.joml.Quaternionf import org.joml.Vector3f import java.lang.reflect.Field internal fun Field.toEntityDataAccessor() = run { isAccessible = true get(null) as EntityDataAccessor<*> } internal fun Class<*>.accessors() = declaredFields.filter { f -> EntityDataAccessor::class.java.isAssignableFrom(f.type) }.map { it.toEntityDataAccessor() } internal val DISPLAY_SET = Display::class.java.accessors() internal val SHARED_FLAG = Entity::class.java.accessors().first().id internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { it.id } internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() internal val ITEM_ENTITY_DATA = buildList { add(SHARED_FLAG) addAll(ITEM_DISPLAY_ID) add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } }.toIntSet() @Suppress("UNCHECKED_CAST") private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { SynchedEntityData.DataValue(id, serializer, 0) } @Suppress("UNCHECKED_CAST") internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor internal class TransformationData { private var _duration = 0 private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return (dest.mod as ModAnimationBundlerImpl).append(entityId) { dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(DISPLAY_INTERPOLATION_DELAY) translation.value?.let { appendPosition(it.value); add(it) } rotation.value?.let { appendRotation(it.value); add(it) } scale.value?.let { appendScale(it.value); add(it) } appendDuration(_duration); add(duration) }) } } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { _duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack() = listOf( DISPLAY_INTERPOLATION_DELAY, duration, translation.forceValue, scale.forceValue, rotation.forceValue ) private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _t: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else null val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) fun set(other: T) { if (dirtyChecker(_t, other)) return _dirty = true setter(_t, other) } } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import io.netty.buffer.Unpooled import io.papermc.paper.adventure.PaperAdventure import io.papermc.paper.configuration.GlobalConfiguration import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.tracker.EntityTrackerRegistry import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.inventory.CraftItemStack import org.bukkit.craftbukkit.util.CraftChatMessage import org.joml.Vector3f import java.util.* internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() } internal inline fun createAdaptedFieldGetter(): (T) -> R { return T::class.java.declaredFields.first { R::class.java.isAssignableFrom(it.type) }.apply { isAccessible = true }.let { getter -> { t -> getter[t] as R } } } internal fun dirtyChecked(hash: () -> H, function: (H) -> T, equalityChecker: (H, H) -> Boolean = { a, b -> a == b }): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() if (equalityChecker(h, newH)) value else synchronized(lock) { h = newH value = function(h) value } } } internal val CONFIG get() = BetterModel.config() internal val EMPTY_ITEM = VanillaItemStack.EMPTY internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() } internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) } } private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { it.type.isArray }.apply { isAccessible = true } internal fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, valueFilter: (DataValue<*>) -> Boolean = { true }, required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? = (DATA_ITEMS[this] as Array<*>) .mapNotNull map@ { val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null val value = item.value().takeIf(valueFilter) ?: return@map null item to value } .takeIf(required) ?.map { if (clean) it.first.isDirty = false it.second } internal fun Entity.isWalking(): Boolean { return controllingPassenger?.isWalking() ?: when (this) { is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { it.isRunning && when (it.goal) { is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true else -> false } } is ServerPlayer -> xMovement() != 0F || zMovement() != 0F else -> false } } internal fun ServerPlayer.xMovement(): Float { val leftMovement: Boolean = lastClientInput.left() val rightMovement: Boolean = lastClientInput.right() return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F } internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F internal fun ServerPlayer.zMovement(): Float { val forwardMovement: Boolean = lastClientInput.forward() val backwardMovement: Boolean = lastClientInput.backward() return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F } internal fun ServerPlayer.isJump() = lastClientInput.jump() internal val Entity.isFlying: Boolean get() = when (this) { is FlyingAnimal -> isFlying is FlyingMob -> true is Mob -> isNoAi is Player -> abilities.flying is LivingEntity -> isFallFlying else -> false } internal val CraftEntity.vanillaEntity: Entity get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle internal fun Entity.moveTo(vec: Vec3) = snapTo(vec) internal fun Entity.moveTo(x: Double, y: Double, z: Double, yaw: Float, pitch: Float) = snapTo(x, y, z, yaw, pitch) internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } internal val Player.hotbarSlot get() = inventory.selectedSlot + 36 internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) internal fun Player.toCustomisation() = entityData.get(Player.DATA_PLAYER_MODE_CUSTOMISATION).toInt() internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asAdventure(this) } else { GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) } internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asVanilla(this) } else { CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/HitBoxImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import io.papermc.paper.event.entity.EntityKnockbackEvent import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionHand.MAIN_HAND import net.minecraft.world.InteractionHand.OFF_HAND import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.level.BlockGetter import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.Color import org.bukkit.Particle import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftArmorStand import org.bukkit.craftbukkit.entity.CraftLivingEntity import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityPotionEffectEvent import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.plugin.Plugin import org.joml.Vector3f import java.util.* internal class HitBoxImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractHitBox(delegate.level()) { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var collision = ifLivingEntity { collides } == true private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false val craftEntity: HitBox by lazy { object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} } val dimensions: EntityDimensions get() = source.run { EntityDimensions( (x() + z()).toFloat() / 2, y().toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale(bone.hitBoxScale()) } private val interaction by lazy { HitBoxInteraction(this) } private val applier = InsideBlockEffectApplier.StepBasedCollector() init { moveTo(delegate.position()) isInvisible = true persist = false isSilent = true initialized = true level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) level().addFreshEntity(interaction.apply { moveTo(delegate.position()) }, CreatureSpawnEvent.SpawnReason.CUSTOM) interaction.startRiding(this) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity ifLivingEntity { collides = collision } } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f = delegate.position().run { bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { } override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) return if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity ifLivingEntity { collision = collides collides = false } } listener.handle(HitBoxMountEvent(this, entity)) } } override fun dismount(entity: PlatformEntity) { forceDismount = true if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { it.stopRiding(true) listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback( d0: Double, d1: Double, d2: Double, attacker: Entity?, cause: EntityKnockbackEvent.Cause ) { if (attacker === delegate) return ifLivingEntity { knockback(d0, d1, d2, attacker, cause) } } override fun push(pushingEntity: Entity) { if (pushingEntity === delegate) return delegate.push(pushingEntity) } override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { if (pushingEntity === delegate) return delegate.push(x, y, z, pushingEntity) } override fun isCollidable(ignoreClimbing: Boolean): Boolean { return delegate.isCollidable(ignoreClimbing) } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } override fun canCollideWithBukkit(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWithBukkit(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return ifLivingEntity { getActiveEffects() } ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null } override fun onWalk(): Boolean { return isWalking() } private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity) return val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) if (!mountController.canFly() && delegate.isFallFlying) return updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) delegate.yHeadRot = player.yRot delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) } val dy = delegate.deltaMovement.y + delegate.gravity if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed() = ifLivingEntity { getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { if (!onFly && !shouldDiscardFriction()) level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * it else it } ?: 0.0F } ?: 0.0F private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) delegate.isNoAi = fly else delegate.isNoGravity = fly onFly = fly && !delegate.onGround() if (onFly) delegate.resetFallDistance() } override fun tick() { delegate.removalReason?.let { if (!isRemoved) remove(it) return } val controller = controllingPassenger if (jumpDelay > 0) jumpDelay-- interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) delegate.navigation.stop() mountControl(controller) } else initialSetup() yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockGetter.forEachBlockIntersectedBetween( oldPosition(), position(), boundingBox ) { pos, step -> if (BetterModelBukkit.IS_PAPER) applier.advanceStep(step, pos) level().getBlockState(pos).entityInside(level(), pos, delegate, applier) } applier.applyAndClear(delegate) if (isInLava) delegate.lavaHurt() firstTick = false listener.sync(craftEntity) } private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, player.bukkitEntity.wrap(), (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { initialSetup() listener.handle(HitBoxRemoveEvent(craftEntity)) interaction.remove(reason) super.remove(reason, cause) } override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean { return ifLivingEntity { isDeadOrDying } == true } override fun hide(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { hideEntity(plugin, bukkitEntity) hideEntity(plugin, interaction.bukkitEntity) } } override fun show(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { showEntity(plugin, bukkitEntity) showEntity(plugin, interaction.bukkitEntity) } } override fun interact(player: Player, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand)) return InteractionResult.SUCCESS } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL val interact = HitBoxInteractAtEvent( (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { MAIN_HAND -> ModelInteractionHand.RIGHT OFF_HAND -> ModelInteractionHand.LEFT }, vec.toBukkit() ) if (!listener.handle(interact)) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand, vec)) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { return ifLivingEntity { addEffect(effectInstance, cause) } == true } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause, fireEvent: Boolean ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (source.entity === delegate || delegate.isInvulnerable) return false if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false val ds = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(craftEntity, ds, amount) if (!listener.handle(event)) return false return ifLivingEntity { hurtServer(world, source, event.damage) } == true } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner === delegate) return ProjectileDeflection.NONE return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return ifLivingEntity { health } ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { return if (!initialized) { super.makeBoundingBox(vec3) } else { val scale = bone.hitBoxScale() AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ).apply { if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) } } } } override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) override fun removeHitBox() { source().task { dismountAll() remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) } } private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { return if (delegate.valid) (delegate as? LivingEntity)?.block() else null } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/HitBoxInteraction.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.nms.HitBox import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftInteraction internal class HitBoxInteraction( val delegate: HitBoxImpl ) : Interaction(EntityType.INTERACTION, delegate.level()) { init { persist = false } private val craftEntity: CraftInteraction by lazy { object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} } override fun getBukkitEntity(): CraftEntity = craftEntity override fun getBukkitEntityRaw(): CraftEntity = craftEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun tick() { val dimension = delegate.dimensions width = dimension.width height = dimension.height yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { return if (entity is Player) { entity.attack(delegate) true } else false } override fun interact(player: Player, hand: InteractionHand): InteractionResult { delegate.interact(player, hand) return InteractionResult.FAIL } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { delegate.interactAt(player, vec, hand) return InteractionResult.FAIL } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.server.MinecraftServer import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, MinecraftServer.getServer().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { (player.unwarp() as CraftPlayer).handle.connection.send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import net.minecraft.world.damagesource.DamageSource import org.bukkit.craftbukkit.util.CraftLocation internal class ModelDamageSourceImpl( private val source: DamageSource ) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/ModelDisplayImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import org.joml.Quaternionf import org.joml.Vector3d import org.joml.Vector3f import java.util.* import java.util.concurrent.atomic.AtomicBoolean internal class ModelDisplayImpl( private val pos: Vector3d, val display: ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData = display.entityData private val entityDataLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityDataLock.accessToLock { entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += addPacket } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.moveTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.unwarp().asVanilla() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean = entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else EMPTY_ITEM ) else it } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) private class DisplayTransformerImpl( source: ItemDisplay ) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.run { bundler += ClientboundSetEntityDataPacket(id, this) } } } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/ModelGameProfile.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import com.mojang.authlib.GameProfile import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin internal data class ModelGameProfile( private val gameProfile: GameProfile ) : ModelProfile { private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) private val skin by lazy { gameProfile.properties["textures"].firstOrNull()?.let { BetterModel.platform().profileManager().skin(it.value) } ?: ModelProfileSkin.EMPTY } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import net.kyori.adventure.text.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap internal class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, MinecraftServer.getServer().overworld() ).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: Component?) { display.text = component?.asVanilla() ?: VanillaComponent.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == VanillaComponent.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.moveTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/NMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup import com.mojang.authlib.GameProfile import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.TransformedItemStack import net.kyori.adventure.key.Keyed import net.minecraft.core.component.DataComponents import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.resources.ResourceLocation import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerCommonPacketListenerImpl import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.level.entity.LevelEntityGetter import net.minecraft.world.level.entity.LevelEntityGetterAdapter import net.minecraft.world.level.entity.PersistentEntitySectionManager import org.bukkit.craftbukkit.CraftWorld import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Vector3d import java.util.* import java.util.function.Consumer import java.util.function.IntConsumer class NMSImpl : NMS { companion object { private const val INJECT_NAME = "bettermodel_channel_handler" //Spigot private val getGameProfile: (Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { it.type == PersistentEntitySectionManager::class.java }?.apply { isAccessible = true } @Suppress("UNCHECKED_CAST") private val ServerLevel.levelGetter get(): LevelEntityGetter { return if (BetterModelBukkit.IS_PAPER) { `moonrise$getEntityLookup`() } else { spigotChunkAccess?.get(this)?.let { (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter } ?: throw RuntimeException("LevelEntityGetter") } } private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> (g as EntityLookup)[i] } else LevelEntityGetterAdapter::class.java.declaredFields.first { net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) }.let { it.isAccessible = true { e, i -> (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity } } private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) //Spigot private val hitBoxData by lazy { ItemDisplay(EntityType.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 entityData.nonDefaultValues!! } } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) inner class PlayerChannelHandlerImpl( private val player: CraftPlayer ) : PlayerChannelHandler, ChannelDuplexHandler() { private val connection = player.handle.connection private val uuid = player.uniqueId private val base = adapt(player.wrap()) init { val pipeline = getConnection(connection).channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = getConnection(connection).channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = base override fun isModEnabled(): Boolean = player.listeningPluginChannels.contains("modelengine:bulk_data") private val playerModel get() = connection.player.id.toRegistry() private fun Int.toPlayerEntity() = toEntity(connection.player.serverLevel()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry( ifHitBox: (Entity) -> Unit = {} ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uniqueId) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(handle.id, it) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list += it } list.send(player.wrap()) } private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) is ClientboundAddEntityPacket -> { val entity = id.toPlayerEntity() ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.bukkitEntity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(player.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@ { it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) } } is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry { return null }?.let { if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> return packet } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, items.apply { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) set(connection.player.hotbarSlot, EMPTY_ITEM) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.handle.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(player.wrap()) } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { val entity = registry.entity().handle() if (entity is Entity) bundler += registry.mountPacket(entity) } private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { EntityTrackerRegistry.registry(it.uuid) == null }.map { it.id }.toIntArray()): ClientboundSetPassengersPacket { return useByteBuf { buffer -> buffer.writeVarInt(entity.id) buffer.writeVarIntArray(displays() .mapToInt { (it as ModelDisplayImpl).display.id }.toArray() + array) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( Vector3d(location.x(), location.y(), location.z()), ItemDisplay(EntityType.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 billboardConstraints = Display.BillboardConstraints.FIXED valid = true yRot = location.yaw() itemTransform = ItemDisplayContext.FIXED }, yOffset ).apply { initialConsumer.accept(this) display.entityData.packDirty() } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.unwarp().asVanilla().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { CustomModelData(it.floats, it.flags, it.strings, it.colors .run { if (rgb == 0xFFFFFF) this else map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } .ifEmpty { listOf(rgb) }) }) }.asBukkit().wrap() } override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { val handle = entity.handle() as? Entity ?: return null return HitBoxImpl( boundingBox.center(), bone, listener, handle, mountController ).craftEntity } override fun version(): NMSVersion = NMSVersion.V1_21_R4 override fun adapt(entity: PlatformEntity): BaseBukkitEntity { val craft = entity.unwarp() as CraftEntity return BaseEntityImpl(craft) } override fun adapt(player: PlatformPlayer): BasePlayer { val craft = player.unwarp() as CraftPlayer return BasePlayerImpl( craft, dirtyChecked( { getGameProfile(craft.handle) }, { ModelGameProfile(it) }, { a, b -> a == b && a.properties["texture"] === b.properties["texture"]} ), dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return VanillaItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, ResourceLocation.parse(model)) TransformedItemStack.of(asBukkit().wrap()) } } override fun isProxyOnlineMode(): Boolean = ONLINE_MODE } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import net.kyori.adventure.key.Key import net.kyori.adventure.key.Keyed import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import org.bukkit.craftbukkit.entity.CraftPlayer private val KEY = Key.key("bettermodel") internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable, Keyed { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket = ClientboundBundlePacket(this) override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun key(): Key = KEY override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import org.bukkit.craftbukkit.entity.CraftPlayer internal data class PlayerArmorImpl( private val player: CraftPlayer ) : PlayerArmor { override fun helmet(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() } override fun leggings(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() } override fun chestplate(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() } override fun boots(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() } private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { val trim = get(DataComponents.TRIM) ArmorItem( get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, it.location().path, trim?.pattern?.value()?.assetId?.path, trim?.material?.value()?.assets?.base?.suffix ) }?.orElse(null) } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: nms/v1_21_R4/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R4/TypeAliases.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R4 import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.world.item.ItemStack internal typealias VanillaItemStack = ItemStack internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack internal typealias ClientPacket = Packet internal typealias VanillaComponent = Component internal typealias AdventureComponent = net.kyori.adventure.text.Component ================================================ FILE: nms/v1_21_R5/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.convention.paperweight) } dependencies { paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT") } tasks { compileJava { options.release = 21 } compileKotlin { compilerOptions.jvmTarget = JvmTarget.JVM_21 } } ================================================ FILE: nms/v1_21_R5/src/main/java/kr/toxicity/model/bukkit/nms/v1_21_R5/AbstractHitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5; import kr.toxicity.model.api.nms.HitBox; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class AbstractHitBox extends ArmorStand implements HitBox { AbstractHitBox(@NotNull Level level) { super(EntityType.ARMOR_STAND, level); } @Override //Only for provide compiler hint for Kotlin jvm public final boolean equals(@Nullable Object other) { return super.equals(other); } @Override //Only for provide compiler hint for Kotlin jvm public final int hashCode() { return super.hashCode(); } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/BaseEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.persistence.PersistentDataHolder import org.joml.Vector3f import java.util.* import java.util.stream.Stream internal data class BaseEntityImpl( private val delegate: CraftEntity ) : BaseBukkitEntity, PersistentDataHolder by delegate { override fun customName(): AdventureComponent? = handle().run { if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { isCustomNameVisible } } override fun entity(): org.bukkit.entity.Entity = delegate override fun handle(): Entity = delegate.vanillaEntity override fun uuid(): UUID = delegate.uniqueId override fun id(): Int = handle().id override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true override fun glow(): Boolean = handle().isCurrentlyGlowing override fun onWalk(): Boolean { return handle().isWalking() } override fun scale(): Double { val handle = handle() return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 } override fun pitch(): Float = handle().xRot override fun ground(): Boolean = handle().onGround() override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = handle().yRot override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun fly(): Boolean = handle().isFlying override fun damageTick(): Float { val handle = handle() if (handle !is LivingEntity) return 0F val duration = handle.invulnerableDuration.toFloat() if (duration <= 0F) return 0F val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) return handle.invulnerableTime.toFloat() / duration * knockBack } override fun walkSpeed(): Float { val handle = handle() if (handle !is LivingEntity) return 0F if (!handle.onGround) return 1F val speed = handle.getEffect(MobEffects.SPEED)?.amplifier ?: 0 val slow = handle.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 return (1F + (speed - slow) * 0.2F) .coerceAtLeast(0.2F) .coerceAtMost(2F) } override fun passengerPosition(dest: Vector3f): Vector3f { return handle().passengerPosition(dest) } override fun platform(): PlatformEntity = delegate.wrap() override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } override fun location(): PlatformLocation = delegate.location.wrap() } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/BasePlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import net.minecraft.util.Mth import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.stream.Stream internal data class BasePlayerImpl( private val delegate: CraftPlayer, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { override fun entity(): Player = delegate override fun updateInventory() { delegate.handle.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = delegate.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(delegate), delegate.trackedBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = delegate.handle var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.bukkit.platform.BukkitItemStack import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack internal fun Entity.wrap() = adapt(this) internal fun LivingEntity.wrap() = adapt(this) internal fun OfflinePlayer.wrap() = adapt(this) internal fun Player.wrap() = adapt(this) internal fun Location.wrap() = adapt(this) internal fun World.wrap() = adapt(this) internal fun ItemStack.wrap() = adapt(this) internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/EntityData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import org.joml.Quaternionf import org.joml.Vector3f import java.lang.reflect.Field internal fun Field.toEntityDataAccessor() = run { isAccessible = true get(null) as EntityDataAccessor<*> } internal fun Class<*>.accessors() = declaredFields.filter { f -> EntityDataAccessor::class.java.isAssignableFrom(f.type) }.map { it.toEntityDataAccessor() } internal val DISPLAY_SET = Display::class.java.accessors() internal val SHARED_FLAG = Entity::class.java.accessors().first().id internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { it.id } internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() internal val ITEM_ENTITY_DATA = buildList { add(SHARED_FLAG) addAll(ITEM_DISPLAY_ID) add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } }.toIntSet() @Suppress("UNCHECKED_CAST") private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { SynchedEntityData.DataValue(id, serializer, 0) } @Suppress("UNCHECKED_CAST") internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor internal class TransformationData { private var _duration = 0 private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return (dest.mod as ModAnimationBundlerImpl).append(entityId) { dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(DISPLAY_INTERPOLATION_DELAY) translation.value?.let { appendPosition(it.value); add(it) } rotation.value?.let { appendRotation(it.value); add(it) } scale.value?.let { appendScale(it.value); add(it) } appendDuration(_duration); add(duration) }) } } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { _duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack() = listOf( DISPLAY_INTERPOLATION_DELAY, duration, translation.forceValue, scale.forceValue, rotation.forceValue ) private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _t: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else null val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) fun set(other: T) { if (dirtyChecker(_t, other)) return _dirty = true setter(_t, other) } } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import io.netty.buffer.Unpooled import io.papermc.paper.adventure.PaperAdventure import io.papermc.paper.configuration.GlobalConfiguration import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.tracker.EntityTrackerRegistry import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.inventory.CraftItemStack import org.bukkit.craftbukkit.util.CraftChatMessage import org.joml.Vector3f import java.util.* internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() } internal inline fun createAdaptedFieldGetter(): (T) -> R { return T::class.java.declaredFields.first { R::class.java.isAssignableFrom(it.type) }.apply { isAccessible = true }.let { getter -> { t -> getter[t] as R } } } internal fun dirtyChecked(hash: () -> H, function: (H) -> T, equalityChecker: (H, H) -> Boolean = { a, b -> a == b }): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() if (equalityChecker(h, newH)) value else synchronized(lock) { h = newH value = function(h) value } } } internal val CONFIG get() = BetterModel.config() internal val EMPTY_ITEM = VanillaItemStack.EMPTY internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() } internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) } } private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { it.type.isArray }.apply { isAccessible = true } internal fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, valueFilter: (DataValue<*>) -> Boolean = { true }, required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? = (DATA_ITEMS[this] as Array<*>) .mapNotNull map@ { val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null val value = item.value().takeIf(valueFilter) ?: return@map null item to value } .takeIf(required) ?.map { if (clean) it.first.isDirty = false it.second } internal fun Entity.isWalking(): Boolean { return controllingPassenger?.isWalking() ?: when (this) { is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { it.isRunning && when (it.goal) { is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true else -> false } } is ServerPlayer -> xMovement() != 0F || zMovement() != 0F else -> false } } internal fun ServerPlayer.xMovement(): Float { val leftMovement: Boolean = lastClientInput.left() val rightMovement: Boolean = lastClientInput.right() return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F } internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F internal fun ServerPlayer.zMovement(): Float { val forwardMovement: Boolean = lastClientInput.forward() val backwardMovement: Boolean = lastClientInput.backward() return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F } internal fun ServerPlayer.isJump() = lastClientInput.jump() internal val Entity.isFlying: Boolean get() = when (this) { is FlyingAnimal -> isFlying is Mob -> isNoAi is Player -> abilities.flying is LivingEntity -> isFallFlying else -> false } internal val CraftEntity.vanillaEntity: Entity get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle internal fun Entity.moveTo(vec: Vec3) = snapTo(vec) internal fun Entity.moveTo(x: Double, y: Double, z: Double, yaw: Float, pitch: Float) = snapTo(x, y, z, yaw, pitch) internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } internal val Player.hotbarSlot get() = inventory.selectedSlot + 36 internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) internal fun Player.toCustomisation() = entityData.get(Player.DATA_PLAYER_MODE_CUSTOMISATION).toInt() internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asAdventure(this) } else { GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) } internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asVanilla(this) } else { CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/HitBoxImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import io.papermc.paper.event.entity.EntityKnockbackEvent import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionHand.MAIN_HAND import net.minecraft.world.InteractionHand.OFF_HAND import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.level.BlockGetter import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.Color import org.bukkit.Particle import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftArmorStand import org.bukkit.craftbukkit.entity.CraftLivingEntity import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityPotionEffectEvent import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.plugin.Plugin import org.joml.Vector3f import java.util.* internal class HitBoxImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractHitBox(delegate.level()) { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var collision = ifLivingEntity { collides } == true private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false val craftEntity: HitBox by lazy { object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} } val dimensions: EntityDimensions get() = source.run { EntityDimensions( (x() + z()).toFloat() / 2, y().toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale(bone.hitBoxScale()) } private val interaction by lazy { HitBoxInteraction(this) } private val applier = InsideBlockEffectApplier.StepBasedCollector() init { moveTo(delegate.position()) isInvisible = true persist = false isSilent = true initialized = true level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) level().addFreshEntity(interaction.apply { moveTo(delegate.position()) }, CreatureSpawnEvent.SpawnReason.CUSTOM) interaction.startRiding(this) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity ifLivingEntity { collides = collision } } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f = delegate.position().run { bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { } override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) return if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity ifLivingEntity { collision = collides collides = false } } listener.handle(HitBoxMountEvent(this, entity)) } } override fun dismount(entity: PlatformEntity) { forceDismount = true if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { it.stopRiding(true) listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback( d0: Double, d1: Double, d2: Double, attacker: Entity?, cause: EntityKnockbackEvent.Cause ) { if (attacker === delegate) return ifLivingEntity { knockback(d0, d1, d2, attacker, cause) } } override fun push(pushingEntity: Entity) { if (pushingEntity === delegate) return delegate.push(pushingEntity) } override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { if (pushingEntity === delegate) return delegate.push(x, y, z, pushingEntity) } override fun isCollidable(ignoreClimbing: Boolean): Boolean { return delegate.isCollidable(ignoreClimbing) } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } override fun canCollideWithBukkit(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWithBukkit(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return ifLivingEntity { getActiveEffects() } ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null } override fun onWalk(): Boolean { return isWalking() } private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity) return val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) if (!mountController.canFly() && delegate.isFallFlying) return updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) delegate.yHeadRot = player.yRot delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) } val dy = delegate.deltaMovement.y + delegate.gravity if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed() = ifLivingEntity { getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { if (!onFly && !shouldDiscardFriction()) level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * it else it } ?: 0.0F } ?: 0.0F private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) delegate.isNoAi = fly else delegate.isNoGravity = fly onFly = fly && !delegate.onGround() if (onFly) delegate.resetFallDistance() } private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, player.bukkitEntity.wrap(), (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) override fun tick() { delegate.removalReason?.let { if (!isRemoved) remove(it) return } val controller = controllingPassenger if (jumpDelay > 0) jumpDelay-- interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) delegate.navigation.stop() mountControl(controller) } else initialSetup() yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockGetter.forEachBlockIntersectedBetween( oldPosition(), position(), boundingBox ) { pos, step -> if (BetterModelBukkit.IS_PAPER) applier.advanceStep(step, pos) level().getBlockState(pos).entityInside(level(), pos, delegate, applier) true } applier.applyAndClear(delegate) if (isInLava) delegate.lavaHurt() firstTick = false listener.sync(craftEntity) } override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { initialSetup() listener.handle(HitBoxRemoveEvent(craftEntity)) interaction.remove(reason) super.remove(reason, cause) } override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean { return ifLivingEntity { isDeadOrDying } == true } override fun hide(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { hideEntity(plugin, bukkitEntity) hideEntity(plugin, interaction.bukkitEntity) } } override fun show(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { showEntity(plugin, bukkitEntity) showEntity(plugin, interaction.bukkitEntity) } } override fun interact(player: Player, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand)) return InteractionResult.SUCCESS } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL val interact = HitBoxInteractAtEvent( (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { MAIN_HAND -> ModelInteractionHand.RIGHT OFF_HAND -> ModelInteractionHand.LEFT }, vec.toBukkit() ) if (!listener.handle(interact)) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand, vec)) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { return ifLivingEntity { addEffect(effectInstance, cause) } == true } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause, fireEvent: Boolean ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (source.entity === delegate || delegate.isInvulnerable) return false if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false val ds = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(craftEntity, ds, amount) if (!listener.handle(event)) return false return ifLivingEntity { hurtServer(world, source, event.damage) } == true } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner?.uuid == delegate.uuid) return ProjectileDeflection.NONE return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return ifLivingEntity { health } ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { return if (!initialized) { super.makeBoundingBox(vec3) } else { val scale = bone.hitBoxScale() AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ).apply { if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) } } } } override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) override fun removeHitBox() { source().task { dismountAll() remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) } } private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { return if (delegate.valid) (delegate as? LivingEntity)?.block() else null } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/HitBoxInteraction.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.nms.HitBox import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftInteraction internal class HitBoxInteraction( val delegate: HitBoxImpl ) : Interaction(EntityType.INTERACTION, delegate.level()) { init { persist = false } private val craftEntity: CraftInteraction by lazy { object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} } override fun getBukkitEntity(): CraftEntity = craftEntity override fun getBukkitEntityRaw(): CraftEntity = craftEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun tick() { val dimension = delegate.dimensions width = dimension.width height = dimension.height yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { return if (entity is Player) { entity.attack(delegate) true } else false } override fun interact(player: Player, hand: InteractionHand): InteractionResult { delegate.interact(player, hand) return InteractionResult.FAIL } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { delegate.interactAt(player, vec, hand) return InteractionResult.FAIL } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.server.MinecraftServer import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, MinecraftServer.getServer().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { (player.unwarp() as CraftPlayer).handle.connection.send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import net.minecraft.world.damagesource.DamageSource import org.bukkit.craftbukkit.util.CraftLocation internal class ModelDamageSourceImpl( private val source: DamageSource ) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/ModelDisplayImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import org.joml.Quaternionf import org.joml.Vector3d import org.joml.Vector3f import java.util.* import java.util.concurrent.atomic.AtomicBoolean internal class ModelDisplayImpl( private val pos: Vector3d, val display: ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData = display.entityData private val entityDataLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityDataLock.accessToLock { entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += addPacket } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.moveTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.unwarp().asVanilla() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean = entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else EMPTY_ITEM ) else it } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) private class DisplayTransformerImpl( source: ItemDisplay ) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.run { bundler += ClientboundSetEntityDataPacket(id, this) } } } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/ModelGameProfile.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import com.mojang.authlib.GameProfile import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin internal data class ModelGameProfile( private val gameProfile: GameProfile ) : ModelProfile { private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) private val skin by lazy { gameProfile.properties["textures"].firstOrNull()?.let { BetterModel.platform().profileManager().skin(it.value) } ?: ModelProfileSkin.EMPTY } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import net.kyori.adventure.text.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap internal class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, MinecraftServer.getServer().overworld() ).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: Component?) { display.text = component?.asVanilla() ?: VanillaComponent.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == VanillaComponent.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.moveTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/NMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup import com.mojang.authlib.GameProfile import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.TransformedItemStack import net.kyori.adventure.key.Keyed import net.minecraft.core.component.DataComponents import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.resources.ResourceLocation import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerCommonPacketListenerImpl import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.level.entity.LevelEntityGetter import net.minecraft.world.level.entity.LevelEntityGetterAdapter import net.minecraft.world.level.entity.PersistentEntitySectionManager import org.bukkit.craftbukkit.CraftWorld import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Vector3d import java.util.* import java.util.function.Consumer import java.util.function.IntConsumer class NMSImpl : NMS { companion object { private const val INJECT_NAME = "bettermodel_channel_handler" //Spigot private val getGameProfile: (Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { it.type == PersistentEntitySectionManager::class.java }?.apply { isAccessible = true } @Suppress("UNCHECKED_CAST") private val ServerLevel.levelGetter get(): LevelEntityGetter { return if (BetterModelBukkit.IS_PAPER) { `moonrise$getEntityLookup`() } else { spigotChunkAccess?.get(this)?.let { (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter } ?: throw RuntimeException("LevelEntityGetter") } } private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> (g as EntityLookup)[i] } else LevelEntityGetterAdapter::class.java.declaredFields.first { net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) }.let { it.isAccessible = true { e, i -> (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity } } private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) //Spigot private val hitBoxData by lazy { ItemDisplay(EntityType.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 entityData.nonDefaultValues!! } } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) inner class PlayerChannelHandlerImpl( private val player: CraftPlayer ) : PlayerChannelHandler, ChannelDuplexHandler() { private val connection = player.handle.connection private val uuid = player.uniqueId private val base = adapt(player.wrap()) init { val pipeline = getConnection(connection).channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = getConnection(connection).channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = base override fun isModEnabled(): Boolean = (if (BetterModelBukkit.IS_PAPER) player.channels() else player.listeningPluginChannels).contains(ModAnimationBundlerImpl.KEY) private val playerModel get() = connection.player.id.toRegistry() private fun Int.toPlayerEntity() = toEntity(connection.player.level()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry( ifHitBox: (Entity) -> Unit = {} ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uniqueId) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(handle.id, it) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list += it } list.send(player.wrap()) } private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) is ClientboundAddEntityPacket -> { val entity = id.toPlayerEntity() ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.bukkitEntity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(player.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@ { it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) } } is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry { return null }?.let { if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> return packet } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, items.apply { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) set(connection.player.hotbarSlot, EMPTY_ITEM) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.handle.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(player.wrap()) } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { val entity = registry.entity().handle() if (entity is Entity) bundler += registry.mountPacket(entity) } private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { EntityTrackerRegistry.registry(it.uuid) == null }.map { it.id }.toIntArray()): ClientboundSetPassengersPacket { return useByteBuf { buffer -> buffer.writeVarInt(entity.id) buffer.writeVarIntArray(displays() .mapToInt { (it as ModelDisplayImpl).display.id }.toArray() + array) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( Vector3d(location.x(), location.y(), location.z()), ItemDisplay(EntityType.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 billboardConstraints = Display.BillboardConstraints.FIXED valid = true yRot = location.yaw() itemTransform = ItemDisplayContext.FIXED }, yOffset ).apply { initialConsumer.accept(this) display.entityData.packDirty() } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.unwarp().asVanilla().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { CustomModelData(it.floats, it.flags, it.strings, it.colors .run { if (rgb == 0xFFFFFF) this else map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } .ifEmpty { listOf(rgb) }) }) }.asBukkit().wrap() } override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { val handle = entity.handle() as? Entity ?: return null return HitBoxImpl( boundingBox.center(), bone, listener, handle, mountController ).craftEntity } override fun version(): NMSVersion = NMSVersion.V1_21_R5 override fun adapt(entity: PlatformEntity): BaseBukkitEntity { val craft = entity.unwarp() as CraftEntity return BaseEntityImpl(craft) } override fun adapt(player: PlatformPlayer): BasePlayer { val craft = player.unwarp() as CraftPlayer return BasePlayerImpl( craft, dirtyChecked( { getGameProfile(craft.handle) }, { ModelGameProfile(it) }, { a, b -> a == b && a.properties["texture"] === b.properties["texture"]} ), dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return VanillaItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, ResourceLocation.parse(model)) TransformedItemStack.of(asBukkit().wrap()) } } override fun isProxyOnlineMode(): Boolean = ONLINE_MODE } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import net.kyori.adventure.key.Key import net.kyori.adventure.key.Keyed import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import org.bukkit.craftbukkit.entity.CraftPlayer private val KEY = Key.key("bettermodel") internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable, Keyed { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket = ClientboundBundlePacket(this) override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun key(): Key = KEY override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import org.bukkit.craftbukkit.entity.CraftPlayer internal data class PlayerArmorImpl( private val player: CraftPlayer ) : PlayerArmor { override fun helmet(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() } override fun leggings(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() } override fun chestplate(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() } override fun boots(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() } private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { val trim = get(DataComponents.TRIM) ArmorItem( get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, it.location().path, trim?.pattern?.value()?.assetId?.path, trim?.material?.value()?.assets?.base?.suffix ) }?.orElse(null) } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: nms/v1_21_R5/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R5/TypeAliases.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R5 import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.world.item.ItemStack internal typealias VanillaItemStack = ItemStack internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack internal typealias ClientPacket = Packet internal typealias VanillaComponent = Component internal typealias AdventureComponent = net.kyori.adventure.text.Component ================================================ FILE: nms/v1_21_R6/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.convention.paperweight) } dependencies { paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") } tasks { compileJava { options.release = 21 } compileKotlin { compilerOptions.jvmTarget = JvmTarget.JVM_21 } } ================================================ FILE: nms/v1_21_R6/src/main/java/kr/toxicity/model/bukkit/nms/v1_21_R6/AbstractHitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6; import kr.toxicity.model.api.nms.HitBox; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class AbstractHitBox extends ArmorStand implements HitBox { AbstractHitBox(@NotNull Level level) { super(EntityType.ARMOR_STAND, level); } @Override //Only for provide compiler hint for Kotlin jvm public final boolean equals(@Nullable Object other) { return super.equals(other); } @Override //Only for provide compiler hint for Kotlin jvm public final int hashCode() { return super.hashCode(); } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/BaseEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.persistence.PersistentDataHolder import org.joml.Vector3f import java.util.* import java.util.stream.Stream internal data class BaseEntityImpl( private val delegate: CraftEntity ) : BaseBukkitEntity, PersistentDataHolder by delegate { override fun customName(): AdventureComponent? = handle().run { if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { isCustomNameVisible } } override fun entity(): org.bukkit.entity.Entity = delegate override fun handle(): Entity = delegate.vanillaEntity override fun uuid(): UUID = delegate.uniqueId override fun id(): Int = handle().id override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true override fun glow(): Boolean = handle().isCurrentlyGlowing override fun onWalk(): Boolean { return handle().isWalking() } override fun scale(): Double { val handle = handle() return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 } override fun pitch(): Float = handle().xRot override fun ground(): Boolean = handle().onGround() override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = handle().yRot override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun fly(): Boolean = handle().isFlying override fun damageTick(): Float { val handle = handle() if (handle !is LivingEntity) return 0F val duration = handle.invulnerableDuration.toFloat() if (duration <= 0F) return 0F val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) return handle.invulnerableTime.toFloat() / duration * knockBack } override fun walkSpeed(): Float { val handle = handle() if (handle !is LivingEntity) return 0F if (!handle.onGround) return 1F val speed = handle.getEffect(MobEffects.SPEED)?.amplifier ?: 0 val slow = handle.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 return (1F + (speed - slow) * 0.2F) .coerceAtLeast(0.2F) .coerceAtMost(2F) } override fun passengerPosition(dest: Vector3f): Vector3f { return handle().passengerPosition(dest) } override fun platform(): PlatformEntity = delegate.wrap() override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } override fun location(): PlatformLocation = delegate.location.wrap() } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/BasePlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import net.minecraft.util.Mth import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.stream.Stream internal data class BasePlayerImpl( private val delegate: CraftPlayer, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { override fun entity(): Player = delegate override fun updateInventory() { delegate.handle.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = delegate.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(delegate), delegate.trackedBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = delegate.handle var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.bukkit.platform.BukkitItemStack import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack internal fun Entity.wrap() = adapt(this) internal fun LivingEntity.wrap() = adapt(this) internal fun OfflinePlayer.wrap() = adapt(this) internal fun Player.wrap() = adapt(this) internal fun Location.wrap() = adapt(this) internal fun World.wrap() = adapt(this) internal fun ItemStack.wrap() = adapt(this) internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/EntityData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import org.joml.Quaternionf import org.joml.Vector3f import java.lang.reflect.Field internal fun Field.toEntityDataAccessor() = run { isAccessible = true get(null) as EntityDataAccessor<*> } internal fun Class<*>.accessors() = declaredFields.filter { f -> EntityDataAccessor::class.java.isAssignableFrom(f.type) }.map { it.toEntityDataAccessor() } internal val DISPLAY_SET = Display::class.java.accessors() internal val SHARED_FLAG = Entity::class.java.accessors().first().id internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { it.id } internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() internal val ITEM_ENTITY_DATA = buildList { add(SHARED_FLAG) addAll(ITEM_DISPLAY_ID) add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } }.toIntSet() @Suppress("UNCHECKED_CAST") private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { SynchedEntityData.DataValue(id, serializer, 0) } @Suppress("UNCHECKED_CAST") internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor internal class TransformationData { private var _duration = 0 private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return (dest.mod as ModAnimationBundlerImpl).append(entityId) { dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(DISPLAY_INTERPOLATION_DELAY) translation.value?.let { appendPosition(it.value); add(it) } rotation.value?.let { appendRotation(it.value); add(it) } scale.value?.let { appendScale(it.value); add(it) } appendDuration(_duration); add(duration) }) } } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { _duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack() = listOf( DISPLAY_INTERPOLATION_DELAY, duration, translation.forceValue, scale.forceValue, rotation.forceValue ) private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _t: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else null val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) fun set(other: T) { if (dirtyChecker(_t, other)) return _dirty = true setter(_t, other) } } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import io.netty.buffer.Unpooled import io.papermc.paper.adventure.PaperAdventure import io.papermc.paper.configuration.GlobalConfiguration import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.tracker.EntityTrackerRegistry import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.inventory.CraftItemStack import org.bukkit.craftbukkit.util.CraftChatMessage import org.joml.Vector3f import java.util.* internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() } internal inline fun createAdaptedFieldGetter(): (T) -> R { return T::class.java.declaredFields.first { R::class.java.isAssignableFrom(it.type) }.apply { isAccessible = true }.let { getter -> { t -> getter[t] as R } } } internal fun dirtyChecked(hash: () -> H, function: (H) -> T): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() when { h === newH -> value h == newH -> value else -> synchronized(lock) { h = newH value = function(h) value } } } } internal val CONFIG get() = BetterModel.config() internal val EMPTY_ITEM = VanillaItemStack.EMPTY internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() } internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) } } private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { it.type.isArray }.apply { isAccessible = true } internal fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, valueFilter: (DataValue<*>) -> Boolean = { true }, required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? = (DATA_ITEMS[this] as Array<*>) .mapNotNull map@ { val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null val value = item.value().takeIf(valueFilter) ?: return@map null item to value } .takeIf(required) ?.map { if (clean) it.first.isDirty = false it.second } internal fun Entity.isWalking(): Boolean { return controllingPassenger?.isWalking() ?: when (this) { is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { it.isRunning && when (it.goal) { is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true else -> false } } is ServerPlayer -> xMovement() != 0F || zMovement() != 0F else -> false } } internal fun ServerPlayer.xMovement(): Float { val leftMovement: Boolean = lastClientInput.left() val rightMovement: Boolean = lastClientInput.right() return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F } internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F internal fun ServerPlayer.zMovement(): Float { val forwardMovement: Boolean = lastClientInput.forward() val backwardMovement: Boolean = lastClientInput.backward() return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F } internal fun ServerPlayer.isJump() = lastClientInput.jump() internal val Entity.isFlying: Boolean get() = when (this) { is FlyingAnimal -> isFlying is Mob -> isNoAi is Player -> abilities.flying is LivingEntity -> isFallFlying else -> false } internal val CraftEntity.vanillaEntity: Entity get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle internal fun Entity.moveTo(vec: Vec3) = snapTo(vec) internal fun Entity.moveTo(x: Double, y: Double, z: Double, yaw: Float, pitch: Float) = snapTo(x, y, z, yaw, pitch) internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } internal val Player.hotbarSlot get() = inventory.selectedSlot + 36 internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) internal fun Avatar.toCustomisation() = entityData.get(Avatar.DATA_PLAYER_MODE_CUSTOMISATION).toInt() internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asAdventure(this) } else { GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) } internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asVanilla(this) } else { CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/HitBoxImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import io.papermc.paper.event.entity.EntityKnockbackEvent import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionHand.MAIN_HAND import net.minecraft.world.InteractionHand.OFF_HAND import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.level.BlockGetter import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.Color import org.bukkit.Particle import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftArmorStand import org.bukkit.craftbukkit.entity.CraftLivingEntity import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityPotionEffectEvent import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.plugin.Plugin import org.joml.Vector3f import java.util.* internal class HitBoxImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractHitBox(delegate.level()) { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var collision = ifLivingEntity { collides } == true private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false val craftEntity: HitBox by lazy { object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} } val dimensions: EntityDimensions get() = source.run { EntityDimensions( (x() + z()).toFloat() / 2, y().toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale(bone.hitBoxScale()) } private val interaction by lazy { HitBoxInteraction(this) } private val applier = InsideBlockEffectApplier.StepBasedCollector() init { moveTo(delegate.position()) isInvisible = true persist = false isSilent = true initialized = true level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) level().addFreshEntity(interaction.apply { moveTo(delegate.position()) }, CreatureSpawnEvent.SpawnReason.CUSTOM) interaction.startRiding(this) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity ifLivingEntity { collides = collision } } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f = delegate.position().run { bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { } override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) return if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity ifLivingEntity { collision = collides collides = false } } listener.handle(HitBoxMountEvent(this, entity)) } } override fun dismount(entity: PlatformEntity) { forceDismount = true if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { it.stopRiding(true) listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback( d0: Double, d1: Double, d2: Double, attacker: Entity?, cause: EntityKnockbackEvent.Cause ) { if (attacker === delegate) return ifLivingEntity { knockback(d0, d1, d2, attacker, cause) } } override fun push(pushingEntity: Entity) { if (pushingEntity === delegate) return delegate.push(pushingEntity) } override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { if (pushingEntity === delegate) return delegate.push(x, y, z, pushingEntity) } override fun isCollidable(ignoreClimbing: Boolean): Boolean { return delegate.isCollidable(ignoreClimbing) } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } override fun canCollideWithBukkit(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWithBukkit(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return ifLivingEntity { getActiveEffects() } ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null } override fun onWalk(): Boolean { return isWalking() } private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity) return val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) if (!mountController.canFly() && delegate.isFallFlying) return updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) delegate.yHeadRot = player.yRot delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) } val dy = delegate.deltaMovement.y + delegate.gravity if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed() = ifLivingEntity { getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { if (!onFly && !shouldDiscardFriction()) level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * it else it } ?: 0.0F } ?: 0.0F private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) delegate.isNoAi = fly else delegate.isNoGravity = fly onFly = fly && !delegate.onGround() if (onFly) delegate.resetFallDistance() } private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, player.bukkitEntity.wrap(), (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) override fun tick() { delegate.removalReason?.let { if (!isRemoved) remove(it) return } val controller = controllingPassenger if (jumpDelay > 0) jumpDelay-- interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) delegate.navigation.stop() mountControl(controller) } else initialSetup() yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockGetter.forEachBlockIntersectedBetween( oldPosition(), position(), boundingBox ) { pos, step -> if (BetterModelBukkit.IS_PAPER) applier.advanceStep(step, pos) level().getBlockState(pos).entityInside(level(), pos, delegate, applier, true) true } applier.applyAndClear(delegate) if (isInLava) delegate.lavaHurt() firstTick = false listener.sync(craftEntity) } override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { initialSetup() listener.handle(HitBoxRemoveEvent(craftEntity)) interaction.remove(reason) super.remove(reason, cause) } override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean { return ifLivingEntity { isDeadOrDying } == true } override fun hide(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { hideEntity(plugin, bukkitEntity) hideEntity(plugin, interaction.bukkitEntity) } } override fun show(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { showEntity(plugin, bukkitEntity) showEntity(plugin, interaction.bukkitEntity) } } override fun interact(player: Player, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand)) return InteractionResult.SUCCESS } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL val interact = HitBoxInteractAtEvent( (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { MAIN_HAND -> ModelInteractionHand.RIGHT OFF_HAND -> ModelInteractionHand.LEFT }, vec.toBukkit() ) if (!listener.handle(interact)) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand, vec)) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { return ifLivingEntity { addEffect(effectInstance, cause) } == true } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause, fireEvent: Boolean ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (source.entity === delegate || delegate.isInvulnerable) return false if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false val ds = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(craftEntity, ds, amount) if (!listener.handle(event)) return false return ifLivingEntity { hurtServer(world, source, event.damage) } == true } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner?.uuid == delegate.uuid) return ProjectileDeflection.NONE return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return ifLivingEntity { health } ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { return if (!initialized) { super.makeBoundingBox(vec3) } else { val scale = bone.hitBoxScale() AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ).apply { if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) } } } } override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) override fun removeHitBox() { source().task { dismountAll() remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) } } private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { return if (delegate.valid) (delegate as? LivingEntity)?.block() else null } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/HitBoxInteraction.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.nms.HitBox import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftInteraction internal class HitBoxInteraction( val delegate: HitBoxImpl ) : Interaction(EntityType.INTERACTION, delegate.level()) { init { persist = false } private val craftEntity: CraftInteraction by lazy { object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} } override fun getBukkitEntity(): CraftEntity = craftEntity override fun getBukkitEntityRaw(): CraftEntity = craftEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun tick() { val dimension = delegate.dimensions width = dimension.width height = dimension.height yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { return if (entity is Player) { entity.attack(delegate) true } else false } override fun interact(player: Player, hand: InteractionHand): InteractionResult { delegate.interact(player, hand) return InteractionResult.FAIL } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { delegate.interactAt(player, vec, hand) return InteractionResult.FAIL } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.server.MinecraftServer import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, MinecraftServer.getServer().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { (player.unwarp() as CraftPlayer).handle.connection.send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import net.minecraft.world.damagesource.DamageSource import org.bukkit.craftbukkit.util.CraftLocation internal class ModelDamageSourceImpl( private val source: DamageSource ) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/ModelDisplayImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import org.joml.Quaternionf import org.joml.Vector3d import org.joml.Vector3f import java.util.* import java.util.concurrent.atomic.AtomicBoolean internal class ModelDisplayImpl( private val pos: Vector3d, val display: ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData = display.entityData private val entityDataLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityDataLock.accessToLock { entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += addPacket } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.moveTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.unwarp().asVanilla() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean = entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else EMPTY_ITEM ) else it } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) private class DisplayTransformerImpl( source: ItemDisplay ) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.run { bundler += ClientboundSetEntityDataPacket(id, this) } } } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/ModelGameProfile.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import com.mojang.authlib.GameProfile import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin internal data class ModelGameProfile( private val gameProfile: GameProfile ) : ModelProfile { private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) private val skin by lazy { gameProfile.properties["textures"].firstOrNull()?.let { BetterModel.platform().profileManager().skin(it.value) } ?: ModelProfileSkin.EMPTY } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import net.kyori.adventure.text.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap internal class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, MinecraftServer.getServer().overworld() ).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: Component?) { display.text = component?.asVanilla() ?: VanillaComponent.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == VanillaComponent.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.moveTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/NMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup import com.mojang.authlib.GameProfile import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.TransformedItemStack import net.kyori.adventure.key.Keyed import net.minecraft.core.component.DataComponents import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.resources.ResourceLocation import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerCommonPacketListenerImpl import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.level.entity.LevelEntityGetter import net.minecraft.world.level.entity.LevelEntityGetterAdapter import net.minecraft.world.level.entity.PersistentEntitySectionManager import org.bukkit.craftbukkit.CraftWorld import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Vector3d import java.util.* import java.util.function.Consumer import java.util.function.IntConsumer class NMSImpl : NMS { companion object { private const val INJECT_NAME = "bettermodel_channel_handler" //Spigot private val getGameProfile: (Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { it.type == PersistentEntitySectionManager::class.java }?.apply { isAccessible = true } @Suppress("UNCHECKED_CAST") private val ServerLevel.levelGetter get(): LevelEntityGetter { return if (BetterModelBukkit.IS_PAPER) { `moonrise$getEntityLookup`() } else { spigotChunkAccess?.get(this)?.let { (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter } ?: throw RuntimeException("LevelEntityGetter") } } private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> (g as EntityLookup)[i] } else LevelEntityGetterAdapter::class.java.declaredFields.first { net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) }.let { it.isAccessible = true { e, i -> (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity } } private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) //Spigot private val hitBoxData by lazy { ItemDisplay(EntityType.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 entityData.nonDefaultValues!! } } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) inner class PlayerChannelHandlerImpl( private val player: CraftPlayer ) : PlayerChannelHandler, ChannelDuplexHandler() { private val connection = player.handle.connection private val uuid = player.uniqueId private val base = adapt(player.wrap()) init { val pipeline = getConnection(connection).channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = getConnection(connection).channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = base override fun isModEnabled(): Boolean = (if (BetterModelBukkit.IS_PAPER) player.channels() else player.listeningPluginChannels).contains(ModAnimationBundlerImpl.KEY) private val playerModel get() = connection.player.id.toRegistry() private fun Int.toPlayerEntity() = toEntity(connection.player.level()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry( ifHitBox: (Entity) -> Unit = {} ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uniqueId) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(handle.id, it) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list += it } list.send(player.wrap()) } private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) is ClientboundAddEntityPacket -> { val entity = id.toPlayerEntity() ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.bukkitEntity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(player.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@ { it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) } } is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry { return null }?.let { if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> return packet } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, items.apply { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) set(connection.player.hotbarSlot, EMPTY_ITEM) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.handle.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(player.wrap()) } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { val entity = registry.entity().handle() if (entity is Entity) bundler += registry.mountPacket(entity) } private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { EntityTrackerRegistry.registry(it.uuid) == null }.map { it.id }.toIntArray()): ClientboundSetPassengersPacket { return useByteBuf { buffer -> buffer.writeVarInt(entity.id) buffer.writeVarIntArray(displays() .mapToInt { (it as ModelDisplayImpl).display.id }.toArray() + array) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( Vector3d(location.x(), location.y(), location.z()), ItemDisplay(EntityType.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 billboardConstraints = Display.BillboardConstraints.FIXED valid = true yRot = location.yaw() itemTransform = ItemDisplayContext.FIXED }, yOffset ).apply { initialConsumer.accept(this) display.entityData.packDirty() } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.unwarp().asVanilla().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { CustomModelData(it.floats, it.flags, it.strings, it.colors .run { if (rgb == 0xFFFFFF) this else map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } .ifEmpty { listOf(rgb) }) }) }.asBukkit().wrap() } override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { val handle = entity.handle() as? Entity ?: return null return HitBoxImpl( boundingBox.center(), bone, listener, handle, mountController ).craftEntity } override fun version(): NMSVersion = NMSVersion.V1_21_R6 override fun adapt(entity: PlatformEntity): BaseBukkitEntity { val craft = entity.unwarp() as CraftEntity return BaseEntityImpl(craft) } override fun adapt(player: PlatformPlayer): BasePlayer { val craft = player.unwarp() as CraftPlayer return BasePlayerImpl( craft, dirtyChecked({ getGameProfile(craft.handle) }, { ModelGameProfile(it) }), dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return VanillaItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, ResourceLocation.parse(model)) TransformedItemStack.of(asBukkit().wrap()) } } override fun isProxyOnlineMode(): Boolean = ONLINE_MODE } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import net.kyori.adventure.key.Key import net.kyori.adventure.key.Keyed import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import org.bukkit.craftbukkit.entity.CraftPlayer private val KEY = Key.key("bettermodel") internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable, Keyed { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket = ClientboundBundlePacket(this) override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun key(): Key = KEY override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import org.bukkit.craftbukkit.entity.CraftPlayer internal data class PlayerArmorImpl( private val player: CraftPlayer ) : PlayerArmor { override fun helmet(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() } override fun leggings(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() } override fun chestplate(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() } override fun boots(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() } private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { val trim = get(DataComponents.TRIM) ArmorItem( get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, it.location().path, trim?.pattern?.value()?.assetId?.path, trim?.material?.value()?.assets?.base?.suffix ) }?.orElse(null) } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: nms/v1_21_R6/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R6/TypeAliases.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R6 import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.world.item.ItemStack internal typealias VanillaItemStack = ItemStack internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack internal typealias ClientPacket = Packet internal typealias VanillaComponent = Component internal typealias AdventureComponent = net.kyori.adventure.text.Component ================================================ FILE: nms/v1_21_R7/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.convention.paperweight) } dependencies { paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT") } tasks { compileJava { options.release = 21 } compileKotlin { compilerOptions.jvmTarget = JvmTarget.JVM_21 } } ================================================ FILE: nms/v1_21_R7/src/main/java/kr/toxicity/model/bukkit/nms/v1_21_R7/AbstractHitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7; import kr.toxicity.model.api.nms.HitBox; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class AbstractHitBox extends ArmorStand implements HitBox { AbstractHitBox(@NotNull Level level) { super(EntityType.ARMOR_STAND, level); } @Override //Only for provide compiler hint for Kotlin jvm public final boolean equals(@Nullable Object other) { return super.equals(other); } @Override //Only for provide compiler hint for Kotlin jvm public final int hashCode() { return super.hashCode(); } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/BaseEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.persistence.PersistentDataHolder import org.joml.Vector3f import java.util.* import java.util.stream.Stream internal data class BaseEntityImpl( private val delegate: CraftEntity ) : BaseBukkitEntity, PersistentDataHolder by delegate { override fun customName(): AdventureComponent? = handle().run { if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { isCustomNameVisible } } override fun entity(): org.bukkit.entity.Entity = delegate override fun handle(): Entity = delegate.vanillaEntity override fun uuid(): UUID = delegate.uniqueId override fun id(): Int = handle().id override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true override fun glow(): Boolean = handle().isCurrentlyGlowing override fun onWalk(): Boolean { return handle().isWalking() } override fun scale(): Double { val handle = handle() return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 } override fun pitch(): Float = handle().xRot override fun ground(): Boolean = handle().onGround() override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = handle().yRot override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun fly(): Boolean = handle().isFlying override fun damageTick(): Float { val handle = handle() if (handle !is LivingEntity) return 0F val duration = handle.invulnerableDuration.toFloat() if (duration <= 0F) return 0F val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) return handle.invulnerableTime.toFloat() / duration * knockBack } override fun walkSpeed(): Float { val handle = handle() if (handle !is LivingEntity) return 0F if (!handle.onGround) return 1F val speed = handle.getEffect(MobEffects.SPEED)?.amplifier ?: 0 val slow = handle.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 return (1F + (speed - slow) * 0.2F) .coerceAtLeast(0.2F) .coerceAtMost(2F) } override fun passengerPosition(dest: Vector3f): Vector3f { return handle().passengerPosition(dest) } override fun platform(): PlatformEntity = delegate.wrap() override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } override fun location(): PlatformLocation = delegate.location.wrap() } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/BasePlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import net.minecraft.util.Mth import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.stream.Stream internal data class BasePlayerImpl( private val delegate: CraftPlayer, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { override fun entity(): Player = delegate override fun updateInventory() { delegate.handle.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = delegate.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(delegate), delegate.trackedBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = delegate.handle var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.bukkit.platform.BukkitItemStack import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack internal fun Entity.wrap() = adapt(this) internal fun LivingEntity.wrap() = adapt(this) internal fun OfflinePlayer.wrap() = adapt(this) internal fun Player.wrap() = adapt(this) internal fun Location.wrap() = adapt(this) internal fun World.wrap() = adapt(this) internal fun ItemStack.wrap() = adapt(this) internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/EntityData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import org.joml.Quaternionf import org.joml.Vector3f import java.lang.reflect.Field internal fun Field.toEntityDataAccessor() = run { isAccessible = true get(null) as EntityDataAccessor<*> } internal fun Class<*>.accessors() = declaredFields.filter { f -> EntityDataAccessor::class.java.isAssignableFrom(f.type) }.map { it.toEntityDataAccessor() } internal val DISPLAY_SET = Display::class.java.accessors() internal val SHARED_FLAG = Entity::class.java.accessors().first().id internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { it.id } internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() internal val ITEM_ENTITY_DATA = buildList { add(SHARED_FLAG) addAll(ITEM_DISPLAY_ID) add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } }.toIntSet() @Suppress("UNCHECKED_CAST") private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { SynchedEntityData.DataValue(id, serializer, 0) } @Suppress("UNCHECKED_CAST") internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor internal class TransformationData { private var _duration = 0 private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return (dest.mod as ModAnimationBundlerImpl).append(entityId) { dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(DISPLAY_INTERPOLATION_DELAY) translation.value?.let { appendPosition(it.value); add(it) } rotation.value?.let { appendRotation(it.value); add(it) } scale.value?.let { appendScale(it.value); add(it) } appendDuration(_duration); add(duration) }) } } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { _duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack() = listOf( DISPLAY_INTERPOLATION_DELAY, duration, translation.forceValue, scale.forceValue, rotation.forceValue ) private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _t: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else null val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) fun set(other: T) { if (dirtyChecker(_t, other)) return _dirty = true setter(_t, other) } } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import io.netty.buffer.Unpooled import io.papermc.paper.adventure.PaperAdventure import io.papermc.paper.configuration.GlobalConfiguration import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.tracker.EntityTrackerRegistry import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.inventory.CraftItemStack import org.bukkit.craftbukkit.util.CraftChatMessage import org.joml.Vector3f import java.util.* internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() } internal inline fun createAdaptedFieldGetter(): (T) -> R { return T::class.java.declaredFields.first { R::class.java.isAssignableFrom(it.type) }.apply { isAccessible = true }.let { getter -> { t -> getter[t] as R } } } internal fun dirtyChecked(hash: () -> H, function: (H) -> T): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() when { h === newH -> value h == newH -> value else -> synchronized(lock) { h = newH value = function(h) value } } } } internal val CONFIG get() = BetterModel.config() internal val EMPTY_ITEM = VanillaItemStack.EMPTY internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() } internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) } } private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { it.type.isArray }.apply { isAccessible = true } internal fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, valueFilter: (DataValue<*>) -> Boolean = { true }, required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? = (DATA_ITEMS[this] as Array<*>) .mapNotNull map@ { val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null val value = item.value().takeIf(valueFilter) ?: return@map null item to value } .takeIf(required) ?.map { if (clean) it.first.isDirty = false it.second } internal fun Entity.isWalking(): Boolean { return controllingPassenger?.isWalking() ?: when (this) { is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { it.isRunning && when (it.goal) { is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true else -> false } } is ServerPlayer -> xMovement() != 0F || zMovement() != 0F else -> false } } internal fun ServerPlayer.xMovement(): Float { val leftMovement: Boolean = lastClientInput.left() val rightMovement: Boolean = lastClientInput.right() return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F } internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F internal fun ServerPlayer.zMovement(): Float { val forwardMovement: Boolean = lastClientInput.forward() val backwardMovement: Boolean = lastClientInput.backward() return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F } internal fun ServerPlayer.isJump() = lastClientInput.jump() internal val Entity.isFlying: Boolean get() = when (this) { is FlyingAnimal -> isFlying is Mob -> isNoAi is Player -> abilities.flying is LivingEntity -> isFallFlying else -> false } internal val CraftEntity.vanillaEntity: Entity get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle internal fun Entity.moveTo(vec: Vec3) = snapTo(vec) internal fun Entity.moveTo(x: Double, y: Double, z: Double, yaw: Float, pitch: Float) = snapTo(x, y, z, yaw, pitch) internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } internal val Player.hotbarSlot get() = inventory.selectedSlot + 36 internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) internal fun Avatar.toCustomisation() = entityData.get(Avatar.DATA_PLAYER_MODE_CUSTOMISATION).toInt() internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asAdventure(this) } else { GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) } internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asVanilla(this) } else { CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/HitBoxImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import io.papermc.paper.event.entity.EntityKnockbackEvent import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionHand.MAIN_HAND import net.minecraft.world.InteractionHand.OFF_HAND import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.level.BlockGetter import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.Color import org.bukkit.Particle import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftArmorStand import org.bukkit.craftbukkit.entity.CraftLivingEntity import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityPotionEffectEvent import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.plugin.Plugin import org.joml.Vector3f import java.util.* internal class HitBoxImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractHitBox(delegate.level()) { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var collision = ifLivingEntity { collides } == true private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false val craftEntity: HitBox by lazy { object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} } val dimensions: EntityDimensions get() = source.run { EntityDimensions( (x() + z()).toFloat() / 2, y().toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale(bone.hitBoxScale()) } private val interaction by lazy { HitBoxInteraction(this) } private val applier = InsideBlockEffectApplier.StepBasedCollector() init { moveTo(delegate.position()) isInvisible = true persist = false isSilent = true initialized = true level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) level().addFreshEntity(interaction.apply { moveTo(delegate.position()) }, CreatureSpawnEvent.SpawnReason.CUSTOM) interaction.startRiding(this) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity ifLivingEntity { collides = collision } } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f = delegate.position().run { bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { } override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) return if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity ifLivingEntity { collision = collides collides = false } } listener.handle(HitBoxMountEvent(this, entity)) } } override fun dismount(entity: PlatformEntity) { forceDismount = true if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { it.stopRiding(true) listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback( d0: Double, d1: Double, d2: Double, attacker: Entity?, cause: EntityKnockbackEvent.Cause ) { if (attacker === delegate) return ifLivingEntity { knockback(d0, d1, d2, attacker, cause) } } override fun push(pushingEntity: Entity) { if (pushingEntity === delegate) return delegate.push(pushingEntity) } override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { if (pushingEntity === delegate) return delegate.push(x, y, z, pushingEntity) } override fun isCollidable(ignoreClimbing: Boolean): Boolean { return delegate.isCollidable(ignoreClimbing) } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } override fun canCollideWithBukkit(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWithBukkit(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return ifLivingEntity { getActiveEffects() } ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null } override fun onWalk(): Boolean { return isWalking() } private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity) return val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) if (!mountController.canFly() && delegate.isFallFlying) return updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) delegate.yHeadRot = player.yRot delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) } val dy = delegate.deltaMovement.y + delegate.gravity if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed() = ifLivingEntity { getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { if (!onFly && !shouldDiscardFriction()) level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * it else it } ?: 0.0F } ?: 0.0F private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) delegate.isNoAi = fly else delegate.isNoGravity = fly onFly = fly && !delegate.onGround() if (onFly) delegate.resetFallDistance() } private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, player.bukkitEntity.wrap(), (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) override fun tick() { delegate.removalReason?.let { if (!isRemoved) remove(it) return } val controller = controllingPassenger if (jumpDelay > 0) jumpDelay-- interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) delegate.navigation.stop() mountControl(controller) } else initialSetup() yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockGetter.forEachBlockIntersectedBetween( oldPosition(), position(), boundingBox ) { pos, step -> if (BetterModelBukkit.IS_PAPER) applier.advanceStep(step, pos) level().getBlockState(pos).entityInside(level(), pos, delegate, applier, true) true } applier.applyAndClear(delegate) if (isInLava) delegate.lavaHurt() firstTick = false listener.sync(craftEntity) } override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { initialSetup() listener.handle(HitBoxRemoveEvent(craftEntity)) interaction.remove(reason) super.remove(reason, cause) } override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean { return ifLivingEntity { isDeadOrDying } == true } override fun hide(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { hideEntity(plugin, bukkitEntity) hideEntity(plugin, interaction.bukkitEntity) } } override fun show(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { showEntity(plugin, bukkitEntity) showEntity(plugin, interaction.bukkitEntity) } } override fun interact(player: Player, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand)) return InteractionResult.SUCCESS } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { if (player === delegate) return InteractionResult.FAIL val interact = HitBoxInteractAtEvent( (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { MAIN_HAND -> ModelInteractionHand.RIGHT OFF_HAND -> ModelInteractionHand.LEFT }, vec.toBukkit() ) if (!listener.handle(interact)) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket.createInteractionPacket(delegate, player.isShiftKeyDown, hand, vec)) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { return ifLivingEntity { addEffect(effectInstance, cause) } == true } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause, fireEvent: Boolean ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (source.entity === delegate || delegate.isInvulnerable) return false if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false val ds = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(craftEntity, ds, amount) if (!listener.handle(event)) return false return ifLivingEntity { hurtServer(world, source, event.damage) } == true } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner?.uuid == delegate.uuid) return ProjectileDeflection.NONE return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return ifLivingEntity { health } ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { return if (!initialized) { super.makeBoundingBox(vec3) } else { val scale = bone.hitBoxScale() AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ).apply { if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) } } } } override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) override fun removeHitBox() { source().task { dismountAll() remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) } } private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { return if (delegate.valid) (delegate as? LivingEntity)?.block() else null } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/HitBoxInteraction.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.nms.HitBox import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftInteraction internal class HitBoxInteraction( val delegate: HitBoxImpl ) : Interaction(EntityType.INTERACTION, delegate.level()) { init { persist = false } private val craftEntity: CraftInteraction by lazy { object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} } override fun getBukkitEntity(): CraftEntity = craftEntity override fun getBukkitEntityRaw(): CraftEntity = craftEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun tick() { val dimension = delegate.dimensions width = dimension.width height = dimension.height yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { return if (entity is Player) { entity.attack(delegate) true } else false } override fun interact(player: Player, hand: InteractionHand): InteractionResult { delegate.interact(player, hand) return InteractionResult.FAIL } override fun interactAt(player: Player, vec: Vec3, hand: InteractionHand): InteractionResult { delegate.interactAt(player, vec, hand) return InteractionResult.FAIL } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.server.MinecraftServer import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, MinecraftServer.getServer().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { (player.unwarp() as CraftPlayer).handle.connection.send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import net.minecraft.world.damagesource.DamageSource import org.bukkit.craftbukkit.util.CraftLocation internal class ModelDamageSourceImpl( private val source: DamageSource ) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/ModelDisplayImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import org.joml.Quaternionf import org.joml.Vector3d import org.joml.Vector3f import java.util.* import java.util.concurrent.atomic.AtomicBoolean internal class ModelDisplayImpl( private val pos: Vector3d, val display: ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData = display.entityData private val entityDataLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityDataLock.accessToLock { entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += addPacket } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.moveTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.unwarp().asVanilla() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean = entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else EMPTY_ITEM ) else it } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) private class DisplayTransformerImpl( source: ItemDisplay ) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.run { bundler += ClientboundSetEntityDataPacket(id, this) } } } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/ModelGameProfile.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import com.mojang.authlib.GameProfile import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin internal data class ModelGameProfile( private val gameProfile: GameProfile ) : ModelProfile { private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) private val skin by lazy { gameProfile.properties["textures"].firstOrNull()?.let { BetterModel.platform().profileManager().skin(it.value) } ?: ModelProfileSkin.EMPTY } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import net.kyori.adventure.text.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap internal class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, MinecraftServer.getServer().overworld() ).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: Component?) { display.text = component?.asVanilla() ?: VanillaComponent.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == VanillaComponent.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.moveTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/NMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup import com.mojang.authlib.GameProfile import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.TransformedItemStack import net.kyori.adventure.key.Keyed import net.minecraft.core.component.DataComponents import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerCommonPacketListenerImpl import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.level.entity.LevelEntityGetter import net.minecraft.world.level.entity.LevelEntityGetterAdapter import net.minecraft.world.level.entity.PersistentEntitySectionManager import org.bukkit.craftbukkit.CraftWorld import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Vector3d import java.util.* import java.util.function.Consumer import java.util.function.IntConsumer class NMSImpl : NMS { companion object { private const val INJECT_NAME = "bettermodel_channel_handler" //Spigot private val getGameProfile: (net.minecraft.world.entity.player.Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { it.type == PersistentEntitySectionManager::class.java }?.apply { isAccessible = true } @Suppress("UNCHECKED_CAST") private val ServerLevel.levelGetter get(): LevelEntityGetter { return if (BetterModelBukkit.IS_PAPER) { `moonrise$getEntityLookup`() } else { spigotChunkAccess?.get(this)?.let { (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter } ?: throw RuntimeException("LevelEntityGetter") } } private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> (g as EntityLookup)[i] } else LevelEntityGetterAdapter::class.java.declaredFields.first { net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) }.let { it.isAccessible = true { e, i -> (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity } } private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) //Spigot private val hitBoxData by lazy { ItemDisplay(EntityType.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 entityData.nonDefaultValues!! } } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) inner class PlayerChannelHandlerImpl( private val player: CraftPlayer ) : PlayerChannelHandler, ChannelDuplexHandler() { private val connection = player.handle.connection private val uuid = player.uniqueId private val base = adapt(player.wrap()) init { val pipeline = getConnection(connection).channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = getConnection(connection).channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = base override fun isModEnabled(): Boolean = (if (BetterModelBukkit.IS_PAPER) player.channels() else player.listeningPluginChannels).contains(ModAnimationBundlerImpl.KEY) private val playerModel get() = connection.player.id.toRegistry() private fun Int.toPlayerEntity() = toEntity(connection.player.level()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry( ifHitBox: (Entity) -> Unit = {} ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uniqueId) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(handle.id, it) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list += it } list.send(player.wrap()) } private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) is ClientboundAddEntityPacket -> { val entity = id.toPlayerEntity() ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.bukkitEntity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(player.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@ { it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) } } is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry { return null }?.let { if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> return packet } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, items.apply { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) set(connection.player.hotbarSlot, EMPTY_ITEM) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.handle.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(player.wrap()) } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { val entity = registry.entity().handle() if (entity is Entity) bundler += registry.mountPacket(entity) } private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { EntityTrackerRegistry.registry(it.uuid) == null }.map { it.id }.toIntArray()): ClientboundSetPassengersPacket { return useByteBuf { buffer -> buffer.writeVarInt(entity.id) buffer.writeVarIntArray(displays() .mapToInt { (it as ModelDisplayImpl).display.id }.toArray() + array) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( Vector3d(location.x(), location.y(), location.z()), ItemDisplay(EntityType.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 billboardConstraints = Display.BillboardConstraints.FIXED valid = true yRot = location.yaw() itemTransform = ItemDisplayContext.FIXED }, yOffset ).apply { initialConsumer.accept(this) display.entityData.packDirty() } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.unwarp().asVanilla().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { CustomModelData(it.floats, it.flags, it.strings, it.colors .run { if (rgb == 0xFFFFFF) this else map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } .ifEmpty { listOf(rgb) }) }) }.asBukkit().wrap() } override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { val handle = entity.handle() as? Entity ?: return null return HitBoxImpl( boundingBox.center(), bone, listener, handle, mountController ).craftEntity } override fun version(): NMSVersion = NMSVersion.V1_21_R7 override fun adapt(entity: PlatformEntity): BaseBukkitEntity { val craft = entity.unwarp() as CraftEntity return BaseEntityImpl(craft) } override fun adapt(player: PlatformPlayer): BasePlayer { val craft = player.unwarp() as CraftPlayer return BasePlayerImpl( craft, dirtyChecked({ getGameProfile(craft.handle) }, { ModelGameProfile(it) }), dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return VanillaItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, Identifier.parse(model)) TransformedItemStack.of(asBukkit().wrap()) } } override fun isProxyOnlineMode(): Boolean = ONLINE_MODE } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import net.kyori.adventure.key.Key import net.kyori.adventure.key.Keyed import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import org.bukkit.craftbukkit.entity.CraftPlayer private val KEY = Key.key("bettermodel") internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable, Keyed { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket = ClientboundBundlePacket(this) override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun key(): Key = KEY override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import org.bukkit.craftbukkit.entity.CraftPlayer internal data class PlayerArmorImpl( private val player: CraftPlayer ) : PlayerArmor { override fun helmet(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() } override fun leggings(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() } override fun chestplate(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() } override fun boots(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() } private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { val trim = get(DataComponents.TRIM) ArmorItem( get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, it.identifier().path, trim?.pattern?.value()?.assetId?.path, trim?.material?.value()?.assets?.base?.suffix ) }?.orElse(null) } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: nms/v1_21_R7/src/main/kotlin/kr/toxicity/model/bukkit/nms/v1_21_R7/TypeAliases.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v1_21_R7 import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.world.item.ItemStack internal typealias VanillaItemStack = ItemStack internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack internal typealias ClientPacket = Packet internal typealias VanillaComponent = Component internal typealias AdventureComponent = net.kyori.adventure.text.Component ================================================ FILE: nms/v26_R1/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.paperweight) } dependencies { paperweight.paperDevBundle("26.1.1.build.+") } ================================================ FILE: nms/v26_R1/src/main/java/kr/toxicity/model/bukkit/nms/v26_R1/AbstractHitBox.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1; import kr.toxicity.model.api.nms.HitBox; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class AbstractHitBox extends ArmorStand implements HitBox { AbstractHitBox(@NotNull Level level) { super(EntityType.ARMOR_STAND, level); } @Override //Only for provide compiler hint for Kotlin jvm public final boolean equals(@Nullable Object other) { return super.equals(other); } @Override //Only for provide compiler hint for Kotlin jvm public final int hashCode() { return super.hashCode(); } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/BaseEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.persistence.PersistentDataHolder import org.joml.Vector3f import java.util.* import java.util.stream.Stream internal data class BaseEntityImpl( private val delegate: CraftEntity ) : BaseBukkitEntity, PersistentDataHolder by delegate { override fun customName(): AdventureComponent? = handle().run { if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { isCustomNameVisible } } override fun entity(): org.bukkit.entity.Entity = delegate override fun handle(): Entity = delegate.vanillaEntity override fun uuid(): UUID = delegate.uniqueId override fun id(): Int = handle().id override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true override fun glow(): Boolean = handle().isCurrentlyGlowing override fun onWalk(): Boolean { return handle().isWalking() } override fun scale(): Double { val handle = handle() return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 } override fun pitch(): Float = handle().xRot override fun ground(): Boolean = handle().onGround() override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = handle().yRot override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun fly(): Boolean = handle().isFlying override fun damageTick(): Float { val handle = handle() if (handle !is LivingEntity) return 0F val duration = handle.invulnerableDuration.toFloat() if (duration <= 0F) return 0F val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) return handle.invulnerableTime.toFloat() / duration * knockBack } override fun walkSpeed(): Float { val handle = handle() if (handle !is LivingEntity) return 0F if (!handle.onGround) return 1F val speed = handle.getEffect(MobEffects.SPEED)?.amplifier ?: 0 val slow = handle.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 return (1F + (speed - slow) * 0.2F) .coerceAtLeast(0.2F) .coerceAtMost(2F) } override fun passengerPosition(dest: Vector3f): Vector3f { return handle().passengerPosition(dest) } override fun platform(): PlatformEntity = delegate.wrap() override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } override fun location(): PlatformLocation = delegate.location.wrap() } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/BasePlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import net.minecraft.util.Mth import org.bukkit.craftbukkit.entity.CraftPlayer import org.bukkit.entity.Player import java.util.stream.Stream internal data class BasePlayerImpl( private val delegate: CraftPlayer, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { override fun entity(): Player = delegate override fun updateInventory() { delegate.handle.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = delegate.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(delegate), delegate.trackedBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = delegate.handle var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/BukkitWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.bukkit.platform.* import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt import kr.toxicity.model.api.bukkit.platform.BukkitItemStack import kr.toxicity.model.api.platform.* import org.bukkit.Location import org.bukkit.OfflinePlayer import org.bukkit.World import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack internal fun Entity.wrap() = adapt(this) internal fun LivingEntity.wrap() = adapt(this) internal fun OfflinePlayer.wrap() = adapt(this) internal fun Player.wrap() = adapt(this) internal fun Location.wrap() = adapt(this) internal fun World.wrap() = adapt(this) internal fun ItemStack.wrap() = adapt(this) internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/EntityData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import org.joml.Quaternionf import org.joml.Vector3f import java.lang.reflect.Field internal fun Field.toEntityDataAccessor() = run { isAccessible = true get(null) as EntityDataAccessor<*> } internal fun Class<*>.accessors() = declaredFields.filter { f -> EntityDataAccessor::class.java.isAssignableFrom(f.type) }.map { it.toEntityDataAccessor() } internal val DISPLAY_SET = Display::class.java.accessors() internal val SHARED_FLAG = Entity::class.java.accessors().first().id internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { it.id } internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() internal val ITEM_ENTITY_DATA = buildList { add(SHARED_FLAG) addAll(ITEM_DISPLAY_ID) add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } }.toIntSet() @Suppress("UNCHECKED_CAST") private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { SynchedEntityData.DataValue(id, serializer, 0) } @Suppress("UNCHECKED_CAST") internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor @Suppress("UNCHECKED_CAST") internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor internal class TransformationData { private var _duration = 0 private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return (dest.mod as ModAnimationBundlerImpl).append(entityId) { dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(DISPLAY_INTERPOLATION_DELAY) translation.value?.let { appendPosition(it.value); add(it) } rotation.value?.let { appendRotation(it.value); add(it) } scale.value?.let { appendScale(it.value); add(it) } appendDuration(_duration); add(duration) }) } } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { _duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack() = listOf( DISPLAY_INTERPOLATION_DELAY, duration, translation.forceValue, scale.forceValue, rotation.forceValue ) private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _t: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else null val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) fun set(other: T) { if (dirtyChecker(_t, other)) return _dirty = true setter(_t, other) } } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import io.netty.buffer.Unpooled import io.papermc.paper.adventure.PaperAdventure import io.papermc.paper.configuration.GlobalConfiguration import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.tracker.EntityTrackerRegistry import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.inventory.CraftItemStack import org.bukkit.craftbukkit.util.CraftChatMessage import org.joml.Vector3f import java.util.* internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() } internal inline fun createAdaptedFieldGetter(): (T) -> R { return T::class.java.declaredFields.first { R::class.java.isAssignableFrom(it.type) }.apply { isAccessible = true }.let { getter -> { t -> getter[t] as R } } } internal fun dirtyChecked(hash: () -> H, function: (H) -> T): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() when { h === newH -> value h == newH -> value else -> synchronized(lock) { h = newH value = function(h) value } } } } internal val CONFIG get() = BetterModel.config() internal val EMPTY_ITEM = VanillaItemStack.EMPTY internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() } internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) } } private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { it.type.isArray }.apply { isAccessible = true } internal fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, valueFilter: (DataValue<*>) -> Boolean = { true }, required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? = (DATA_ITEMS[this] as Array<*>) .mapNotNull map@ { val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null val value = item.value().takeIf(valueFilter) ?: return@map null item to value } .takeIf(required) ?.map { if (clean) it.first.isDirty = false it.second } internal fun Entity.isWalking(): Boolean { return controllingPassenger?.isWalking() ?: when (this) { is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { it.isRunning && when (it.goal) { is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true else -> false } } is ServerPlayer -> xMovement() != 0F || zMovement() != 0F else -> false } } internal fun ServerPlayer.xMovement(): Float { val leftMovement: Boolean = lastClientInput.left() val rightMovement: Boolean = lastClientInput.right() return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F } internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F internal fun ServerPlayer.zMovement(): Float { val forwardMovement: Boolean = lastClientInput.forward() val backwardMovement: Boolean = lastClientInput.backward() return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F } internal fun ServerPlayer.isJump() = lastClientInput.jump() internal val Entity.isFlying: Boolean get() = when (this) { is FlyingAnimal -> isFlying is Mob -> isNoAi is Player -> abilities.flying is LivingEntity -> isFallFlying else -> false } internal val CraftEntity.vanillaEntity: Entity get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle internal fun Entity.moveTo(vec: Vec3) = snapTo(vec) internal fun Entity.moveTo(x: Double, y: Double, z: Double, yaw: Float, pitch: Float) = snapTo(x, y, z, yaw, pitch) internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } internal val Player.hotbarSlot get() = inventory.selectedSlot + 36 internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) internal fun Avatar.toCustomisation() = entityData.get(Avatar.DATA_PLAYER_MODE_CUSTOMISATION).toInt() internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asAdventure(this) } else { GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) } internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { PaperAdventure.asVanilla(this) } else { CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/HitBoxImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import io.papermc.paper.event.entity.EntityKnockbackEvent import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionHand.MAIN_HAND import net.minecraft.world.InteractionHand.OFF_HAND import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.level.BlockGetter import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.Color import org.bukkit.Particle import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftArmorStand import org.bukkit.craftbukkit.entity.CraftLivingEntity import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityPotionEffectEvent import org.bukkit.event.entity.EntityRemoveEvent import org.bukkit.plugin.Plugin import org.joml.Vector3f import java.util.* internal class HitBoxImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractHitBox(delegate.level()) { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var collision = ifLivingEntity { collides } == true private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false val craftEntity: HitBox by lazy { object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} } val dimensions: EntityDimensions get() = source.run { EntityDimensions( (x() + z()).toFloat() / 2, y().toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale(bone.hitBoxScale()) } private val interaction by lazy { HitBoxInteraction(this) } private val applier = InsideBlockEffectApplier.StepBasedCollector() init { moveTo(delegate.position()) isInvisible = true persist = false isSilent = true initialized = true level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) level().addFreshEntity(interaction.apply { moveTo(delegate.position()) }, CreatureSpawnEvent.SpawnReason.CUSTOM) interaction.startRiding(this) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity ifLivingEntity { collides = collision } } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f = delegate.position().run { bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { } override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) return if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity ifLivingEntity { collision = collides collides = false } } listener.handle(HitBoxMountEvent(this, entity)) } } override fun dismount(entity: PlatformEntity) { forceDismount = true if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { it.stopRiding(true) listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback( d0: Double, d1: Double, d2: Double, attacker: Entity?, cause: EntityKnockbackEvent.Cause ) { if (attacker === delegate) return ifLivingEntity { knockback(d0, d1, d2, attacker, cause) } } override fun push(pushingEntity: Entity) { if (pushingEntity === delegate) return delegate.push(pushingEntity) } override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { if (pushingEntity === delegate) return delegate.push(x, y, z, pushingEntity) } override fun isCollidable(ignoreClimbing: Boolean): Boolean { return delegate.isCollidable(ignoreClimbing) } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } override fun canCollideWithBukkit(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWithBukkit(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return ifLivingEntity { getActiveEffects() } ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null } override fun onWalk(): Boolean { return isWalking() } private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity) return val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) if (!mountController.canFly() && delegate.isFallFlying) return updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) delegate.yHeadRot = player.yRot delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) } val dy = delegate.deltaMovement.y + delegate.gravity if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed() = ifLivingEntity { getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { if (!onFly && !shouldDiscardFriction()) level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * it else it } ?: 0.0F } ?: 0.0F private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) delegate.isNoAi = fly else delegate.isNoGravity = fly onFly = fly && !delegate.onGround() if (onFly) delegate.resetFallDistance() } private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, player.bukkitEntity.wrap(), (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) override fun tick() { delegate.removalReason?.let { if (!isRemoved) remove(it) return } val controller = controllingPassenger if (jumpDelay > 0) jumpDelay-- interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) delegate.navigation.stop() mountControl(controller) } else initialSetup() yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockGetter.forEachBlockIntersectedBetween( oldPosition(), position(), boundingBox ) { pos, step -> if (BetterModelBukkit.IS_PAPER) applier.advanceStep(step, pos) level().getBlockState(pos).entityInside(level(), pos, delegate, applier, true) true } applier.applyAndClear(delegate) if (isInLava) delegate.lavaHurt() firstTick = false listener.sync(craftEntity) } override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { initialSetup() listener.handle(HitBoxRemoveEvent(craftEntity)) interaction.remove(reason) super.remove(reason, cause) } override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean { return ifLivingEntity { isDeadOrDying } == true } override fun hide(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { hideEntity(plugin, bukkitEntity) hideEntity(plugin, interaction.bukkitEntity) } } override fun show(player: PlatformPlayer) { val plugin = BetterModel.platform() as Plugin player.unwarp().run { showEntity(plugin, bukkitEntity) showEntity(plugin, interaction.bukkitEntity) } } override fun interact(player: Player, hand: InteractionHand, vec: Vec3): InteractionResult { if (player === delegate) return InteractionResult.FAIL val interact = HitBoxInteractAtEvent( (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { MAIN_HAND -> ModelInteractionHand.RIGHT OFF_HAND -> ModelInteractionHand.LEFT }, vec.toBukkit() ) if (!listener.handle(interact)) return InteractionResult.FAIL (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket( delegate.id, hand, vec, player.isShiftKeyDown )) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { return ifLivingEntity { addEffect(effectInstance, cause) } == true } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true } override fun addEffect( effectInstance: MobEffectInstance, entity: Entity?, cause: EntityPotionEffectEvent.Cause, fireEvent: Boolean ): Boolean { if (entity === delegate) return false return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (source.entity === delegate || delegate.isInvulnerable) return false if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false val ds = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(craftEntity, ds, amount) if (!listener.handle(event)) return false return ifLivingEntity { hurtServer(world, source, event.damage) } == true } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner?.uuid == delegate.uuid) return ProjectileDeflection.NONE return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return ifLivingEntity { health } ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { return if (!initialized) { super.makeBoundingBox(vec3) } else { val scale = bone.hitBoxScale() AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ).apply { if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) } } } } override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) override fun removeHitBox() { source().task { dismountAll() remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) } } private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { return if (delegate.valid) (delegate as? LivingEntity)?.block() else null } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/HitBoxInteraction.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.nms.HitBox import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 import org.bukkit.Bukkit import org.bukkit.craftbukkit.CraftServer import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftInteraction internal class HitBoxInteraction( val delegate: HitBoxImpl ) : Interaction(EntityType.INTERACTION, delegate.level()) { init { persist = false } private val craftEntity: CraftInteraction by lazy { object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} } override fun getBukkitEntity(): CraftEntity = craftEntity override fun getBukkitEntityRaw(): CraftEntity = craftEntity override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun tick() { val dimension = delegate.dimensions width = dimension.width height = dimension.height yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { return if (entity is Player) { entity.attack(delegate) true } else false } override fun interact(player: Player, hand: InteractionHand, vec: Vec3): InteractionResult { delegate.interact(player, hand, vec) return InteractionResult.FAIL } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.server.MinecraftServer import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, MinecraftServer.getServer().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { (player.unwarp() as CraftPlayer).handle.connection.send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import net.minecraft.world.damagesource.DamageSource import org.bukkit.craftbukkit.util.CraftLocation internal class ModelDamageSourceImpl( private val source: DamageSource ) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/ModelDisplayImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import org.joml.Quaternionf import org.joml.Vector3d import org.joml.Vector3f import java.util.* import java.util.concurrent.atomic.AtomicBoolean internal class ModelDisplayImpl( private val pos: Vector3d, val display: ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData = display.entityData private val entityDataLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityDataLock.accessToLock { entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ITEM_SERIALIZER) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += addPacket } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.moveTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.unwarp().asVanilla() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean = entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else EMPTY_ITEM ) else it } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) private class DisplayTransformerImpl( source: ItemDisplay ) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.run { bundler += ClientboundSetEntityDataPacket(id, this) } } } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/ModelGameProfile.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import com.mojang.authlib.GameProfile import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin internal data class ModelGameProfile( private val gameProfile: GameProfile ) : ModelProfile { private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) private val skin by lazy { gameProfile.properties["textures"].firstOrNull()?.let { BetterModel.platform().profileManager().skin(it.value) } ?: ModelProfileSkin.EMPTY } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import net.kyori.adventure.text.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.server.MinecraftServer import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap internal class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, MinecraftServer.getServer().overworld() ).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: Component?) { display.text = component?.asVanilla() ?: VanillaComponent.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == VanillaComponent.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.moveTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/NMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup import com.mojang.authlib.GameProfile import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.bukkit.BetterModelBukkit import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.api.util.TransformedItemStack import net.kyori.adventure.key.Keyed import net.minecraft.core.component.DataComponents import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.resources.Identifier import net.minecraft.server.MinecraftServer import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerCommonPacketListenerImpl import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Display.ItemDisplay import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.level.entity.LevelEntityGetter import net.minecraft.world.level.entity.LevelEntityGetterAdapter import net.minecraft.world.level.entity.PersistentEntitySectionManager import org.bukkit.craftbukkit.CraftWorld import org.bukkit.craftbukkit.entity.CraftEntity import org.bukkit.craftbukkit.entity.CraftPlayer import org.joml.Vector3d import java.util.* import java.util.function.Consumer import java.util.function.IntConsumer class NMSImpl : NMS { companion object { private const val INJECT_NAME = "bettermodel_channel_handler" //Spigot private val getGameProfile: (net.minecraft.world.entity.player.Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { it.type == PersistentEntitySectionManager::class.java }?.apply { isAccessible = true } @Suppress("UNCHECKED_CAST") private val ServerLevel.levelGetter get(): LevelEntityGetter { return if (BetterModelBukkit.IS_PAPER) { `moonrise$getEntityLookup`() } else { spigotChunkAccess?.get(this)?.let { (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter } ?: throw RuntimeException("LevelEntityGetter") } } private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> (g as EntityLookup)[i] } else LevelEntityGetterAdapter::class.java.declaredFields.first { net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) }.let { it.isAccessible = true { e, i -> (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity } } private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) //Spigot private val hitBoxData by lazy { ItemDisplay(EntityType.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 entityData.nonDefaultValues!! } } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) inner class PlayerChannelHandlerImpl( private val player: CraftPlayer ) : PlayerChannelHandler, ChannelDuplexHandler() { private val connection = player.handle.connection private val uuid = player.uniqueId private val base = adapt(player.wrap()) init { val pipeline = getConnection(connection).channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = getConnection(connection).channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = base override fun isModEnabled(): Boolean = (if (BetterModelBukkit.IS_PAPER) player.channels() else player.listeningPluginChannels).contains(ModAnimationBundlerImpl.KEY) private val playerModel get() = connection.player.id.toRegistry() private fun Int.toPlayerEntity() = toEntity(connection.player.level()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry( ifHitBox: (Entity) -> Unit = {} ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uniqueId) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == SHARED_FLAG } )?.let { list += ClientboundSetEntityDataPacket(handle.id, it) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list += it } list.send(player.wrap()) } private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) is ClientboundAddEntityPacket -> { val entity = id.toPlayerEntity() ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.bukkitEntity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(player.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@ { it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) } } is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry { return null }?.let { if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> return packet } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, items.apply { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) set(connection.player.hotbarSlot, EMPTY_ITEM) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.handle.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(player.wrap()) } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { val entity = registry.entity().handle() if (entity is Entity) bundler += registry.mountPacket(entity) } private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { EntityTrackerRegistry.registry(it.uuid) == null }.map { it.id }.toIntArray()): ClientboundSetPassengersPacket { return useByteBuf { buffer -> buffer.writeVarInt(entity.id) buffer.writeVarIntArray(displays() .mapToInt { (it as ModelDisplayImpl).display.id }.toArray() + array) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( Vector3d(location.x(), location.y(), location.z()), ItemDisplay(EntityType.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 billboardConstraints = Display.BillboardConstraints.FIXED valid = true yRot = location.yaw() itemTransform = ItemDisplayContext.FIXED }, yOffset ).apply { initialConsumer.accept(this) display.entityData.packDirty() } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.unwarp().asVanilla().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { CustomModelData(it.floats, it.flags, it.strings, it.colors .run { if (rgb == 0xFFFFFF) this else map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } .ifEmpty { listOf(rgb) }) }) }.asBukkit().wrap() } override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { val handle = entity.handle() as? Entity ?: return null return HitBoxImpl( boundingBox.center(), bone, listener, handle, mountController ).craftEntity } override fun version(): NMSVersion = NMSVersion.V26_R1 override fun adapt(entity: PlatformEntity): BaseBukkitEntity { val craft = entity.unwarp() as CraftEntity return BaseEntityImpl(craft) } override fun adapt(player: PlatformPlayer): BasePlayer { val craft = player.unwarp() as CraftPlayer return BasePlayerImpl( craft, dirtyChecked({ getGameProfile(craft.handle) }, { ModelGameProfile(it) }), dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return VanillaItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, Identifier.parse(model)) TransformedItemStack.of(asBukkit().wrap()) } } override fun isProxyOnlineMode(): Boolean = ONLINE_MODE } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import net.kyori.adventure.key.Key import net.kyori.adventure.key.Keyed import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import org.bukkit.craftbukkit.entity.CraftPlayer private val KEY = Key.key("bettermodel") internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable, Keyed { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket = ClientboundBundlePacket(this) override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun key(): Key = KEY override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = (player.unwarp() as CraftPlayer).handle.connection subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import org.bukkit.craftbukkit.entity.CraftPlayer internal data class PlayerArmorImpl( private val player: CraftPlayer ) : PlayerArmor { override fun helmet(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() } override fun leggings(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() } override fun chestplate(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() } override fun boots(): ArmorItem? { return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() } private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { val trim = get(DataComponents.TRIM) ArmorItem( get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, it.identifier().path, trim?.pattern?.value()?.assetId?.path, trim?.material?.value()?.assets?.base?.suffix ) }?.orElse(null) } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: nms/v26_R1/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R1/TypeAliases.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.nms.v26_R1 import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.world.item.ItemStack internal typealias VanillaItemStack = ItemStack internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack internal typealias ClientPacket = Packet internal typealias VanillaComponent = Component internal typealias AdventureComponent = net.kyori.adventure.text.Component ================================================ FILE: platform/fabric/build.gradle.kts ================================================ import xyz.jpenilla.resourcefactory.fabric.Environment plugins { alias(libs.plugins.convention.publish) alias(libs.plugins.convention.modrinth) alias(libs.plugins.resourcefactory.fabric) id("net.fabricmc.fabric-loom") } val versionString = "${rootProject.version}+${property("minecraft_version")}" val jarName = "${rootProject.name}-$versionString-${project.name.substringAfterLast('-')}.jar" val jarDir = rootProject.layout.buildDirectory.dir("libs") sourceSets { create("testmod") { compileClasspath += main.get().compileClasspath + main.get().output runtimeClasspath += main.get().runtimeClasspath + main.get().output } } loom { // Access winder //accessWidenerPath = file("src/main/resources/bettermodel.accesswidener") // Run runs { create("testClient") { client() configName = "Test Minecraft Client" source("testmod") } create("testServer") { server() configName = "Test Minecraft Server" source("testmod") } } // Test mod //createRemapConfigurations(sourceSets["testmod"]) } dependencies { // Minecraft minecraft("com.mojang:minecraft:${property("minecraft_version")}") api(project(":bettermodel-api")); include(project(":bettermodel-api")) api(project(":bettermodel-api:bettermodel-mod-api")); include(project(":bettermodel-api:bettermodel-mod-api")) api(project(":bettermodel-core")); include(project(":bettermodel-core")) setOf( "fabric-api-base", "fabric-command-api-v2", "fabric-data-attachment-api-v1", "fabric-entity-events-v1", "fabric-events-interaction-v0", "fabric-lifecycle-events-v1", "fabric-networking-api-v1", "fabric-transitive-access-wideners-v1" ).forEach { implementation(fabricApi.module(it, libs.versions.fabric.api.get())) } implementation(libs.bundles.fabric) implementation(libs.bundles.fabric.library); include(libs.bundles.fabric.library) api(libs.bundles.fabric.mod); include(libs.bundles.fabric.mod) implementation(libs.bundles.core); include(libs.bundles.core) include(libs.bundles.library) } fabricModJson { id = "bettermodel" name = "BetterModel" description = "Modern Bedrock model engine for Minecraft Java Edition" entrypoints = listOf( mainEntrypoint("$group.impl.fabric.BetterModelFabricImpl") { adapter = "kotlin" } ) environment = Environment.ANY depends = mapOf( "minecraft" to listOf(">=${LATEST_VERSION.first()}"), "fabricloader" to listOf("*"), "fabric-language-kotlin" to listOf(">=${libs.versions.fabric.language.kotlin.get()}"), // fabric-api "fabric-api-base" to listOf("*"), "fabric-command-api-v2" to listOf("*"), "fabric-data-attachment-api-v1" to listOf("*"), "fabric-entity-events-v1" to listOf("*"), "fabric-events-interaction-v0" to listOf("*"), "fabric-lifecycle-events-v1" to listOf("*"), "fabric-networking-api-v1" to listOf("*"), "fabric-transitive-access-wideners-v1" to listOf("*"), // mod libraries "adventure-platform-fabric" to listOf("*"), "cloud" to listOf("*"), "polymer-resource-pack" to listOf("*") ) mixins = listOf( mixin("bettermodel.mixins.json") ) authors = listOf( person("toxicity188") ) contributors = listOf( person("Kouvali (Fabric Port)") ) contact { sources = "https://github.com/toxicity188/BetterModel/" issues = "https://github.com/toxicity188/BetterModel/issues" homepage = "https://modrinth.com/plugin/bettermodel" } icon("assets/icon.png") mitLicense() version = project.version.toString() } sourceSets["testmod"].resourceFactory { fabricModJson { id = "bettermodel-testmod" version = project.version.toString() entrypoints = listOf( mainEntrypoint( "$group.test.RollTest" ) ) depends = mapOf( // mod modules "bettermodel" to listOf("*") ) } } interface FsInjected { @get:Inject val fs: FileSystemOperations } val copyModJar by tasks.registering { val injected = objects.newInstance() val archiveFile = tasks.jar.flatMap { it.archiveFile } val jarName = jarName val jarDir = jarDir doLast { injected.fs.copy { from(archiveFile) rename { jarName } into(jarDir) } } } tasks { jar { from(rootProject.layout.projectDirectory.file("LICENSE.md")) from(rootProject.layout.projectDirectory.file(".idea/icon.png")) { rename { "assets/icon.png" } } manifest { attributes( mapOf( "Dev-Build" to (BUILD_NUMBER ?: -1), "Version" to versionString, "Author" to "toxicity188", "Url" to "https://github.com/toxicity188/BetterModel", "Created-By" to "Gradle $gradle", "Build-Jdk" to "${System.getProperty("java.vendor")} ${System.getProperty("java.version")}", "Build-OS" to "${System.getProperty("os.arch")} ${System.getProperty("os.name")}" ) ) } finalizedBy(copyModJar) } runServer { enabled = false } } modrinth { loaders = listOf("fabric", "quilt") uploadFile.set(jarDir.map { it.file(jarName) }) gameVersions = LATEST_VERSION dependencies { required.version("fabric-api", libs.versions.fabric.api.get()) required.version("fabric-language-kotlin", libs.versions.fabric.language.kotlin.get()) // optional.project( // "skinsrestorer" // ) } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/impl/fabric/entity/AbstractArmorStand.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; public abstract class AbstractArmorStand extends ArmorStand { public AbstractArmorStand(EntityType type, Level level) { super(type, level); } @Override public final boolean equals(@Nullable Object object) { return super.equals(object); } @Override public final int hashCode() { return super.hashCode(); } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/impl/fabric/entity/EntityHook.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity; import org.jetbrains.annotations.Nullable; public interface EntityHook { @Nullable String bettermodel$getModelData(); void bettermodel$setModelData(@Nullable String modelData); } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/impl/fabric/network/BetterModelBundlePacket.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.network; public interface BetterModelBundlePacket { boolean bettermodel$isBetterModelPacket(); void bettermodel$setBetterModelPacket(boolean isBetterModel); } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/AvatarAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.world.entity.Avatar; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = Avatar.class) public interface AvatarAccessor { @Accessor("DATA_PLAYER_MODE_CUSTOMISATION") static @NotNull EntityDataAccessor bettermodel$getDataPlayerModeCustomisation() { throw new UnsupportedOperationException("Implemented via mixin"); } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/ClientboundBundlePacketMixin.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import kr.toxicity.model.impl.fabric.network.BetterModelBundlePacket; import net.minecraft.network.protocol.game.ClientboundBundlePacket; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; @Mixin(value = ClientboundBundlePacket.class) public abstract class ClientboundBundlePacketMixin implements BetterModelBundlePacket { @Unique private boolean bettermodel$isBetterModelPacket; @Override public boolean bettermodel$isBetterModelPacket() { return bettermodel$isBetterModelPacket; } @Override public void bettermodel$setBetterModelPacket(boolean isBetterModel) { bettermodel$isBetterModelPacket = isBetterModel; } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/ConnectionAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import io.netty.channel.Channel; import net.minecraft.network.Connection; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = Connection.class) public interface ConnectionAccessor { @Accessor(value = "channel") @NotNull Channel bettermodel$getChannel(); } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/DisplayAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.world.entity.Display; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionfc; import org.joml.Vector3fc; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = Display.class) public interface DisplayAccessor { @Accessor("DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID") static @NotNull EntityDataAccessor bettermodel$getDataTransformationInterpolationStartDeltaTicksId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID") static @NotNull EntityDataAccessor bettermodel$getDataTransformationInterpolationDurationId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_POS_ROT_INTERPOLATION_DURATION_ID") static @NotNull EntityDataAccessor bettermodel$getDataPosRotInterpolationDurationId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_TRANSLATION_ID") static @NotNull EntityDataAccessor bettermodel$getDataTranslationId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_SCALE_ID") static @NotNull EntityDataAccessor bettermodel$getDataScaleId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_LEFT_ROTATION_ID") static @NotNull EntityDataAccessor bettermodel$getDataLeftRotationId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_RIGHT_ROTATION_ID") static @NotNull EntityDataAccessor bettermodel$getDataRightRotationId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_BILLBOARD_RENDER_CONSTRAINTS_ID") static @NotNull EntityDataAccessor bettermodel$getDataBillboardRenderConstraintsId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_BRIGHTNESS_OVERRIDE_ID") static @NotNull EntityDataAccessor bettermodel$getDataBrightnessOverrideId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_VIEW_RANGE_ID") static @NotNull EntityDataAccessor bettermodel$getDataViewRangeId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_SHADOW_RADIUS_ID") static @NotNull EntityDataAccessor bettermodel$getDataShadowRadiusId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_SHADOW_STRENGTH_ID") static @NotNull EntityDataAccessor bettermodel$getDataShadowStrengthId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_WIDTH_ID") static @NotNull EntityDataAccessor bettermodel$getDataWidthId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_HEIGHT_ID") static @NotNull EntityDataAccessor bettermodel$getDataHeightId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor("DATA_GLOW_COLOR_OVERRIDE_ID") static @NotNull EntityDataAccessor bettermodel$getDataGlowColorOverrideId() { throw new UnsupportedOperationException("Implemented via mixin"); } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/EntityAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.world.entity.Entity; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = Entity.class) public interface EntityAccessor { @Accessor(value = "DATA_SHARED_FLAGS_ID") static @NotNull EntityDataAccessor bettermodel$getDataSharedFlagsId() { throw new UnsupportedOperationException("Implemented via mixin"); } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/EntityMixin.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import kr.toxicity.model.impl.fabric.attachment.BetterModelAttachments; import kr.toxicity.model.impl.fabric.entity.EntityHook; import kr.toxicity.model.impl.fabric.events.ServerEntityDismountCallback; import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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(value = Entity.class) public abstract class EntityMixin implements EntityHook { @Shadow public abstract Level level(); @Override public @Nullable String bettermodel$getModelData() { return ((AttachmentTarget) this).getAttached(BetterModelAttachments.MODEL_DATA); } @Override public void bettermodel$setModelData(@Nullable String modelData) { ((AttachmentTarget) this).setAttached(BetterModelAttachments.MODEL_DATA, modelData); } @Inject( method = "removePassenger", at = @At(value = "HEAD"), cancellable = true ) private void bettermodel$invokeDismountCallbacks(@NotNull Entity passenger, @NotNull CallbackInfo ci) { if (level().isClientSide()) return; Entity vehicle = betterModel$entity(); if (vehicle != passenger.getVehicle() && !ServerEntityDismountCallback.EVENT.invoker().onDismount(passenger, vehicle) ) { ci.cancel(); } } @Unique private @NotNull Entity betterModel$entity() { return (Entity) (Object) this; } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/ItemDisplayAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.world.entity.Display; import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = Display.ItemDisplay.class) public interface ItemDisplayAccessor { @Accessor(value = "DATA_ITEM_STACK_ID") static @NotNull EntityDataAccessor bettermodel$getDataItemStackId() { throw new UnsupportedOperationException("Implemented via mixin"); } @Accessor(value = "DATA_ITEM_DISPLAY_ID") static @NotNull EntityDataAccessor bettermodel$getDataItemDisplayId() { throw new UnsupportedOperationException("Implemented via mixin"); } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/LivingEntityMixin.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import com.llamalad7.mixinextras.sugar.Local; import kr.toxicity.model.impl.fabric.events.ServerLivingEntityJumpCallback; import kr.toxicity.model.impl.fabric.events.ServerMobEffectLoadCallback; import kr.toxicity.model.impl.fabric.events.ServerMobEffectUnloadCallback; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.level.Level; import org.jetbrains.annotations.NotNull; 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 java.util.Collection; @Mixin(value = LivingEntity.class) public abstract class LivingEntityMixin extends Entity { private LivingEntityMixin(EntityType type, Level level) { super(type, level); } @Inject( method = "aiStep", at = @At( value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;jumpFromGround()V" ) ) private void bettermodel$invokeJumpCallbacks(@NotNull CallbackInfo ci) { if (level().isClientSide()) return; ServerLivingEntityJumpCallback.EVENT.invoker().onJump(bettermodel$livingEntity()); } @Inject( method = "onEffectAdded", at = @At( value = "INVOKE", target = "Lnet/minecraft/world/effect/MobEffect;addAttributeModifiers(Lnet/minecraft/world/entity/ai/attributes/AttributeMap;I)V", shift = At.Shift.AFTER ) ) private void bettermodel$invokeEffectLoadCallbacks(@NotNull MobEffectInstance effect, @NotNull Entity source, @NotNull CallbackInfo ci) { if (level().isClientSide()) return; ServerMobEffectLoadCallback.EVENT.invoker().onLoad(bettermodel$livingEntity(), effect); } @Inject( method = "onEffectsRemoved", at = @At( value = "INVOKE", target = "Lnet/minecraft/world/effect/MobEffect;removeAttributeModifiers(Lnet/minecraft/world/entity/ai/attributes/AttributeMap;)V", shift = At.Shift.AFTER ) ) private void bettermodel$invokeEffectUnloadCallbacks(@NotNull Collection effects, @NotNull CallbackInfo ci, @Local @NotNull MobEffectInstance effect) { if (level().isClientSide()) return; ServerMobEffectUnloadCallback.EVENT.invoker().onUnload(bettermodel$livingEntity(), effect); } @Unique private @NotNull LivingEntity bettermodel$livingEntity() { return (LivingEntity) (Object) this; } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/MobAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ai.goal.GoalSelector; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = Mob.class) public interface MobAccessor { @Accessor(value = "goalSelector") @NotNull GoalSelector bettermodel$getGoalSelector(); } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/ServerCommonPacketListenerImplAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.network.Connection; import net.minecraft.server.network.ServerCommonPacketListenerImpl; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(value = ServerCommonPacketListenerImpl.class) public interface ServerCommonPacketListenerImplAccessor { @Accessor(value = "connection") @NotNull Connection bettermodel$getConnection(); } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/ServerLevelEntityCallbacksMixin.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import kr.toxicity.model.impl.fabric.events.ServerMobEffectLoadCallback; import kr.toxicity.model.impl.fabric.events.ServerMobEffectUnloadCallback; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.Entity; import net.minecraft.world.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(targets = "net.minecraft.server.level.ServerLevel$EntityCallbacks") public abstract class ServerLevelEntityCallbacksMixin { @Inject( method = "onTrackingStart(Lnet/minecraft/world/entity/Entity;)V", at = @At(value = "TAIL") ) private void bettermodel$invokeLoadCallbacks(Entity entity, CallbackInfo ci) { if (entity.level().isClientSide()) return; if (entity instanceof LivingEntity livingEntity) { for (MobEffectInstance effect : livingEntity.getActiveEffects()) { ServerMobEffectLoadCallback.EVENT.invoker().onLoad(livingEntity, effect); } } } @Inject( method = "onTrackingEnd(Lnet/minecraft/world/entity/Entity;)V", at = @At(value = "HEAD") ) private void bettermodel$invokeUnloadCallbacks(Entity entity, CallbackInfo ci) { if (entity.level().isClientSide()) return; if (entity instanceof LivingEntity livingEntity) { for (MobEffectInstance effect : livingEntity.getActiveEffects()) { ServerMobEffectUnloadCallback.EVENT.invoker().onUnload(livingEntity, effect); } } } } ================================================ FILE: platform/fabric/src/main/java/kr/toxicity/model/mixin/SynchedEntityDataAccessor.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.mixin; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.SynchedEntityData; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(value = SynchedEntityData.class) public interface SynchedEntityDataAccessor { @Accessor(value = "itemsById") @NotNull SynchedEntityData.DataItem[] bettermodel$getItemsById(); @Accessor(value = "isDirty") void bettermodel$setDirty(boolean dirty); @Invoker(value = "getItem") SynchedEntityData.@NotNull DataItem bettermodel$getItem(@NotNull EntityDataAccessor key); } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelFabricImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric import eu.pb4.polymer.resourcepack.api.PolymerResourcePackUtils import eu.pb4.polymer.resourcepack.api.ResourcePackBuilder import kr.toxicity.model.BetterModelEvaluatorImpl import kr.toxicity.model.BetterModelEventBusImpl import kr.toxicity.model.BetterModelPlatformImpl import kr.toxicity.model.api.* import kr.toxicity.model.api.BetterModelPlatform.ReloadResult.* import kr.toxicity.model.api.event.PluginEndReloadEvent import kr.toxicity.model.api.event.PluginStartReloadEvent import kr.toxicity.model.api.manager.* import kr.toxicity.model.api.mod.BetterModelMod import kr.toxicity.model.api.mod.platform.ModAdapter import kr.toxicity.model.api.mod.scheduler.ModModelScheduler import kr.toxicity.model.api.nms.NMS import kr.toxicity.model.api.pack.PackResult import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.platform.PlatformAdapter import kr.toxicity.model.api.version.MinecraftVersion import kr.toxicity.model.impl.fabric.attachment.BetterModelAttachments import kr.toxicity.model.impl.fabric.command.startFabricCommand import kr.toxicity.model.impl.fabric.config.BetterModelConfigImpl import kr.toxicity.model.impl.fabric.config.toConfig import kr.toxicity.model.impl.fabric.manager.EntityManager import kr.toxicity.model.impl.fabric.manager.PlayerManagerImpl import kr.toxicity.model.impl.fabric.scheduler.FabricModelSchedulerImpl import kr.toxicity.model.manager.* import kr.toxicity.model.util.* import net.fabricmc.api.ModInitializer import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents import net.fabricmc.loader.api.FabricLoader import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.format.NamedTextColor.AQUA import net.kyori.adventure.text.format.NamedTextColor.GREEN import net.minecraft.DetectedVersion import net.minecraft.WorldVersion import net.minecraft.server.MinecraftServer import net.minecraft.server.packs.metadata.pack.PackFormat import net.minecraft.util.InclusiveRange import org.semver4j.Semver import java.io.File import java.io.InputStream import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean import java.util.function.BiConsumer import java.util.function.Consumer import java.util.jar.JarFile import kotlin.io.path.exists import kotlin.system.measureTimeMillis class BetterModelFabricImpl : ModInitializer, BetterModelPlatformImpl, BetterModelMod { private lateinit var server: MinecraftServer private val configDir: Path = FabricLoader.getInstance() .configDir .resolve(modId()).apply { toFile().mkdirs() } private val jarFile: JarFile get() = JarFile( File(javaClass.protectionDomain.codeSource.location.toURI()) ) private lateinit var config: BetterModelConfigImpl private val worldVersion: WorldVersion = DetectedVersion.tryDetectVersion() private val minecraftVersion: MinecraftVersion = MinecraftVersion.parse(worldVersion.id()) private val semver: Semver = FabricLoader.getInstance() .getModContainer(modId()) .map { modContainer -> Semver.coerce(modContainer.metadata.version.friendlyString) .ifNull { "Unable to load BetterModel's semver." } } .orElseThrow() private val nms by lazy { BetterModelNMSImpl() } private val logger = BetterModelLoggerImpl() private val evaluator = BetterModelEvaluatorImpl() private val eventBus = BetterModelEventBusImpl() private val adapter = ModAdapter() private var reloadStartTask: (PackZipper) -> Unit = { zipper -> callEvent { PluginStartReloadEvent(zipper) } } private var reloadEndTask: (BetterModelPlatform.ReloadResult) -> Unit = { result -> callEvent { PluginEndReloadEvent(result) } } private val isLoadingProvider: AtomicBoolean = AtomicBoolean() private val isFirstLoadProvider: AtomicBoolean = AtomicBoolean() private val allManagers by lazy { listOf( ArmorManager, ProfileManagerImpl, SkinManagerImpl, ModelManagerImpl, PlayerManagerImpl, EntityManager, ScriptManagerImpl ) } override fun onInitialize() { BetterModel.register(this) startFabricCommand() ServerLifecycleEvents.SERVER_STARTING.register { server -> this.server = server } PolymerResourcePackUtils.addModAssets(modId()) PolymerResourcePackUtils.markAsRequired() config = loadOrSaveConfig() val initialLoad = AtomicBoolean() PolymerResourcePackUtils.RESOURCE_PACK_CREATION_EVENT.register { builder -> val isInitialLoad = initialLoad.compareAndSet(false, true) reload { includeAssets(builder, it.packResult) if (isInitialLoad) loadLog(it) } } ServerLifecycleEvents.SERVER_STARTED.register { allManagers.forEach { it.start() } if (initialLoad.compareAndSet(false, true)) reload { loadLog(it) } } BetterModelAttachments.init() FabricModelSchedulerImpl.init() ServerLifecycleEvents.SERVER_STOPPED.register { allManagers.forEach { manager -> manager.end() } } } private fun reload(callback: (Success) -> Unit) { when (val result = reload(ReloadInfo(true, Audience.empty()))) { is Failure -> result.throwable.handleException("Unable to load mod properly.") is OnReload -> throw RuntimeException("mod load failed.") is Success -> callback(result) } } private fun includeAssets(builder: ResourcePackBuilder, packResult: PackResult) { packResult.stream().forEach { packByte -> when (val path = packByte.path.path) { "pack.png", "pack.mcmeta" -> return@forEach else -> builder.addData(path, packByte.bytes) } } packResult.meta().overlays?.entries?.forEach { entry -> if (entry.directory == "bettermodel_legacy") { return@forEach } val min = entry.minFormat.run { PackFormat(major, minor) } val max = entry.maxFormat.run { PackFormat(major, minor) } val range = InclusiveRange(min, max) builder.packMcMetaBuilder.addOverlay(range, entry.directory) } } private fun loadLog(success: Success) { info( "Mod is loaded. (${success.totalTime().withComma()} ms)".toComponent(GREEN), "Platform: Fabric".toComponent(AQUA) ) } override fun getResource(fileName: String): InputStream? { return javaClass.getResourceAsStream("/$fileName") } override fun saveResource(fileName: String) { getResource(fileName)?.use { input -> Files.copy(input, configDir.resolve(fileName)) } } override fun loadAssets(pipeline: ReloadPipeline, prefix: String, consumer: BiConsumer) { jarFile.use { jarFile -> val jarEntries = jarFile.entries() .asSequence() .filter { jarEntry -> jarEntry.name.startsWith(prefix) && jarEntry.name.length > prefix.length + 1 && !jarEntry.isDirectory } .toList() pipeline.forEachParallel( jarEntries, { it.size } ) { jarEntry -> jarFile.getInputStream(jarEntry).use { stream -> consumer.accept(jarEntry.name.substring(prefix.length + 1), stream) } } } } override fun dataFolder(): File = configDir.toFile() override fun jarType(): BetterModelPlatform.JarType = BetterModelPlatform.JarType.FABRIC override fun reload(info: ReloadInfo): BetterModelPlatform.ReloadResult { if (!isLoadingProvider.compareAndSet(false, true)) { return OnReload.INSTANCE } return runCatching { if (!info.skipConfig) { config = loadOrSaveConfig() } val zipper = PackZipper.zipper() reloadStartTask(zipper) val indicators = config().indicator().options.toIndicator(info) ReloadPipeline(indicators).use { pipeline -> val assetsTime = measureTimeMillis { allManagers.forEach { manager -> manager.reload(pipeline, zipper) } } pipeline.run { status = "Generating files..." goal = zipper.size() } val isFirstLoad = isFirstLoadProvider.compareAndSet(false, true) val packResult = config().packType().toGenerator().create(zipper, pipeline) Success( isFirstLoad, assetsTime, packResult ) } } .getOrElse { throwable -> Failure(throwable) } .also { result -> isLoadingProvider.set(false) reloadEndTask(result) } } private fun loadOrSaveConfig(): BetterModelConfigImpl { return configDir.resolve("config.yml").run { if (!exists()) saveResource("config.yml") toConfig() } } override fun isSnapshot(): Boolean = !worldVersion.stable() override fun config(): BetterModelConfig = config override fun version(): MinecraftVersion = minecraftVersion override fun semver(): Semver = semver override fun nms(): NMS = nms override fun modelManager(): ModelManager = ModelManagerImpl override fun playerManager(): PlayerManager = PlayerManagerImpl override fun scriptManager(): ScriptManager = ScriptManagerImpl override fun skinManager(): SkinManager = SkinManagerImpl override fun profileManager(): ProfileManager = ProfileManagerImpl override fun addReloadStartHandler(consumer: Consumer) { val oldHandler = reloadStartTask reloadStartTask = { zipper -> oldHandler(zipper) consumer.accept(zipper) } } override fun addReloadEndHandler(consumer: Consumer) { val oldHandler = reloadEndTask reloadEndTask = { result -> oldHandler(result) consumer.accept(result) } } override fun logger(): BetterModelLogger = logger override fun evaluator(): BetterModelEvaluator = evaluator override fun eventBus(): BetterModelEventBus = eventBus override fun server(): MinecraftServer = server override fun scheduler(): ModModelScheduler = FabricModelSchedulerImpl override fun adapter(): PlatformAdapter = adapter override fun isEnabled(): Boolean = true } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelLoggerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric import kr.toxicity.model.api.BetterModelLogger import net.kyori.adventure.text.Component import net.kyori.adventure.text.logger.slf4j.ComponentLogger import java.util.logging.Logger class BetterModelLoggerImpl : BetterModelLogger { private val logger by lazy { ComponentLogger.logger(LOGGER.name) } override fun info(vararg messages: Component) { synchronized(this) { for (message in messages) { logger.info(message) } } } override fun warn(vararg messages: Component) { synchronized(this) { for (message in messages) { logger.warn(message) } } } companion object { private val LOGGER: Logger = Logger.getLogger(modId()) } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelNMSImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mod.BetterModelMod import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.* import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.util.TransformedItemStack import kr.toxicity.model.impl.fabric.entity.* import kr.toxicity.model.impl.fabric.network.* import kr.toxicity.model.impl.fabric.profile.ModelProfileImpl import kr.toxicity.model.mixin.DisplayAccessor import kr.toxicity.model.mixin.EntityAccessor import kr.toxicity.model.util.PLATFORM import net.minecraft.core.component.DataComponents import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.resources.Identifier import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomModelData import net.minecraft.world.item.component.DyedItemColor import org.joml.Vector3d import java.util.function.Consumer class BetterModelNMSImpl : NMS { override fun create( location: PlatformLocation, yOffset: Double, initialConsumer: Consumer ): ModelDisplay { val type = EntityType.ITEM_DISPLAY val level = location.asFabric.level()!! val itemDisplay = Display.ItemDisplay(type, level).apply { billboardConstraints = Display.BillboardConstraints.FIXED entityData[DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`()] = 3 itemTransform = ItemDisplayContext.FIXED yRot = location.yaw() } val modelDisplay = ModelDisplayEntityImpl(Vector3d(location.x(), location.y(), location.z()), itemDisplay, yOffset).apply { initialConsumer.accept(this) display.entityData.packDirty() } return modelDisplay } override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) override fun inject(player: PlatformPlayer): PlayerChannelHandler = PlayerChannelHandlerImpl(player.unwarp()) override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { return itemStack.clone().unwarp().apply { set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.withMappedColors(rgb)) }.wrap() } private fun CustomModelData.withMappedColors(rgb: Int): CustomModelData { return CustomModelData( floats, flags, strings, getMappedColors(rgb) ) } private fun CustomModelData.getMappedColors(rgb: Int): List { if (colors.isEmpty()) { return listOf(rgb) } if (rgb == 0xFFFFFF) { return colors } return colors.map { color -> ARGB.multiply(color, rgb) and 0xFFFFFF } } override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { (registry.entity().handle() as? Entity)?.let { bundler += registry.mountPacket(it) } } override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { val target = registry.entity().handle() as? Entity ?: return val list = bundlerOf() target.entityData.pack( valueFilter = { it.id == EntityAccessor.`bettermodel$getDataSharedFlagsId`().id } )?.let { list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) } if (target is LivingEntity) { val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() packet?.let { list += it } } list.send(channel.player()) } override fun createHitBox( entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, controller: MountController, listener: HitBoxListener ): HitBox { return HitBoxEntityImpl( boundingBox.center(), bone, listener, entity.handle() as Entity, controller ) } override fun version(): NMSVersion = NMSVersion.V26_R1 override fun adapt(entity: PlatformEntity): BaseEntity = BaseFabricEntityImpl(entity.unwarp()) override fun adapt(player: PlatformPlayer): BasePlayer { val connection = player.unwarp() return BaseFabricPlayerImpl( connection, dirtyChecked( { connection.player.gameProfile }, { ModelProfileImpl(it) } ), dirtyChecked( { connection.player.getCustomisation() }, { PlayerSkinParts(it) } ) ) } override fun profile(player: PlatformPlayer): ModelProfile = ModelProfileImpl(player.unwarp().player.gameProfile) override fun isProxyOnlineMode(): Boolean = (PLATFORM as BetterModelMod).server().usesAuthentication() override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { return ItemStack(Items.PLAYER_HEAD).run { set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) set(DataComponents.ITEM_MODEL, Identifier.parse(model)) TransformedItemStack.of(wrap()) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Constants.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric import org.slf4j.Logger import org.slf4j.LoggerFactory fun modId() = MOD_ID private const val MOD_ID = "bettermodel" fun logger(): Logger = LOGGER private val LOGGER: Logger = LoggerFactory.getLogger(modId()) ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Entities.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ @file:Suppress("UnstableApiUsage") package kr.toxicity.model.impl.fabric import kr.toxicity.model.api.BetterModel import kr.toxicity.model.impl.fabric.entity.EntityHook import kr.toxicity.model.impl.fabric.world.entityMap import kr.toxicity.model.mixin.AvatarAccessor import kr.toxicity.model.mixin.MobAccessor import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.server.network.ServerPlayerConnection import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.goal.Goal import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import org.joml.Vector3f fun Entity.toTracker(model: String?) = toRegistry()?.tracker(model) fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) val Entity.isWalking: Boolean get() { return controllingPassenger?.isWalking ?: checkEntityWalkingState() } val Entity.isFlying: Boolean get() { return this is FlyingAnimal && isFlying || this is Mob && isNoAi || this is Player && abilities.flying || this is LivingEntity && isFallFlying } val Entity.seenBy: Set get() { val level = level() as ServerLevel val tracker = level.chunkSource.chunkMap.entityMap.get(id) ?: return emptySet() return tracker.seenBy } var Entity.modelData: String? get() { return (this as EntityHook).`bettermodel$getModelData`() } set(value) { (this as EntityHook).`bettermodel$setModelData`(value) } fun Entity.passengerPosition(dest: Vector3f): Vector3f { val point = attachments.get(EntityAttachment.PASSENGER, 0, yRot) return dest.set( point.x.toFloat(), point.y.toFloat(), point.z.toFloat() ) } private fun Entity.checkEntityWalkingState(): Boolean { return when (this) { is Mob -> navigation.isInProgress || isRangedAttacking() is ServerPlayer -> xMovement() != 0.0f || zMovement() != 0.0f else -> false } } private fun Mob.isRangedAttacking(): Boolean { return (this as MobAccessor).`bettermodel$getGoalSelector`().availableGoals.any { wrapper -> wrapper.isRunning && wrapper.goal.isRangedAttackGoal() } } private fun Goal.isRangedAttackGoal(): Boolean { return this is RangedAttackGoal || this is RangedCrossbowAttackGoal<*> || this is RangedBowAttackGoal<*> } fun Avatar.getCustomisation(): Int { return entityData.get( AvatarAccessor.`bettermodel$getDataPlayerModeCustomisation`() ).toInt() } fun ServerPlayer.xMovement(): Float { return when { lastClientInput.left() == lastClientInput.right() -> 0.0f lastClientInput.left() -> 1.0f else -> -1.0f } } fun ServerPlayer.yMovement(): Float { return when { lastClientInput.jump -> 1.0f lastClientInput.shift -> -1.0f else -> 0.0f } } fun ServerPlayer.zMovement(): Float { return when { lastClientInput.forward() == lastClientInput.backward() -> 0.0f lastClientInput.forward() -> 1.0f else -> -1.0f } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/FabricWrappers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric import kr.toxicity.model.api.mod.platform.* import kr.toxicity.model.api.mod.platform.ModAdapter.adapt import kr.toxicity.model.api.platform.* import net.minecraft.server.network.ServerPlayerConnection import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemStack val PlatformLocation.asFabric get() = this as ModLocation fun Entity.wrap() = adapt(this) fun LivingEntity.wrap() = adapt(this) fun ServerPlayerConnection.wrap() = adapt(this) fun ItemStack.wrap() = adapt(this) fun PlatformEntity.unwarp(): Entity = (this as ModEntity).source() fun PlatformLivingEntity.unwarp(): LivingEntity = (this as ModLivingEntity).source() fun PlatformPlayer.unwarp(): ServerPlayerConnection = (this as ModPlayer).source() fun PlatformItemStack.unwarp(): ItemStack = (this as ModItemStack).source() ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Functions.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric inline fun dirtyChecked(crossinline hash: () -> H, crossinline function: (H) -> T): () -> T { val lock = Any() var h = hash() var value = function(h) return { val newH = hash() when { h === newH -> value h == newH -> value else -> synchronized(lock) { h = newH value = function(h) value } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/armor/PlayerArmorImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.armor import kr.toxicity.model.api.armor.ArmorItem import kr.toxicity.model.api.armor.PlayerArmor import net.minecraft.core.component.DataComponents import net.minecraft.server.network.ServerPlayerConnection import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.item.ItemStack import net.minecraft.world.item.component.DyedItemColor import net.minecraft.world.item.equipment.EquipmentAssets import net.minecraft.world.item.equipment.trim.ArmorTrim import kotlin.jvm.optionals.getOrNull class PlayerArmorImpl(private val connection: ServerPlayerConnection) : PlayerArmor { private val player get() = connection.player override fun helmet(): ArmorItem? = player.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() override fun leggings(): ArmorItem? = player.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() override fun chestplate(): ArmorItem? = player.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() override fun boots(): ArmorItem? = player.getItemBySlot(EquipmentSlot.FEET).toArmorItem() private fun ItemStack.assetIdOrNull() = get(DataComponents.EQUIPPABLE)?.assetId?.getOrNull() private fun ItemStack.toArmorItem() = assetIdOrNull()?.let { asset -> val trim = get(DataComponents.TRIM) val tint = get(DataComponents.DYED_COLOR)?.rgb ?: if (asset === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF ArmorItem( tint, asset.identifier().path, trim?.getPath(), trim?.getPalette() ) } private fun ArmorTrim.getPath() = pattern.value().assetId.path private fun ArmorTrim.getPalette() = material.value().assets.base.suffix } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/attachment/BetterModelAttachments.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ @file:Suppress("UnstableApiUsage") package kr.toxicity.model.impl.fabric.attachment import com.mojang.serialization.Codec import kr.toxicity.model.impl.fabric.modId import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry import net.fabricmc.fabric.api.attachment.v1.AttachmentType import net.minecraft.resources.Identifier object BetterModelAttachments { @JvmField val MODEL_DATA: AttachmentType = AttachmentRegistry.create( Identifier.fromNamespaceAndPath(modId(), "model_data") ) { builder -> builder .persistent(Codec.STRING) .copyOnDeath() } internal fun init() = Unit } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/audience/AudienceCommandSource.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.audience import net.kyori.adventure.audience.Audience import net.minecraft.commands.CommandSourceStack interface AudienceCommandSource : Audience { val source: CommandSourceStack } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/audience/AudiencePlayer.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.text.Component import net.minecraft.commands.CommandSourceStack import net.minecraft.server.level.ServerPlayer data class AudiencePlayer( override val source: CommandSourceStack, val player: ServerPlayer ) : AudienceCommandSource { override fun sendMessage(message: Component) { player.sendMessage(message) } override fun showBossBar(bar: BossBar) { player.showBossBar(bar) } override fun hideBossBar(bar: BossBar) { player.hideBossBar(bar) } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/audience/AudienceSourceStack.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.text.Component import net.minecraft.commands.CommandSourceStack data class AudienceSourceStack(override val source: CommandSourceStack) : AudienceCommandSource { override fun sendMessage(message: Component) { source.sendMessage(message) } override fun showBossBar(bar: BossBar) { source.showBossBar(bar) } override fun hideBossBar(bar: BossBar) { source.hideBossBar(bar) } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/chat/Components.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.chat import net.kyori.adventure.platform.modcommon.impl.NonWrappingComponentSerializer import net.minecraft.network.chat.Component fun net.kyori.adventure.text.Component.asVanilla(): Component = NonWrappingComponentSerializer.INSTANCE.serialize(this) fun Component.asAdventure(): net.kyori.adventure.text.Component = NonWrappingComponentSerializer.INSTANCE.deserialize(this) ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/command/Commands.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.command import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.BetterModelPlatform.ReloadResult.* import kr.toxicity.model.api.animation.AnimationIterator import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.mod.platform.ModLocation import kr.toxicity.model.api.tracker.EntityHideOption import kr.toxicity.model.api.tracker.ModelScaler import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerModifier import kr.toxicity.model.command.* import kr.toxicity.model.impl.fabric.audience.AudienceCommandSource import kr.toxicity.model.impl.fabric.audience.AudiencePlayer import kr.toxicity.model.impl.fabric.audience.AudienceSourceStack import kr.toxicity.model.impl.fabric.toRegistry import kr.toxicity.model.impl.fabric.toTracker import kr.toxicity.model.impl.fabric.wrap import kr.toxicity.model.util.* import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.format.NamedTextColor.* import net.minecraft.commands.CommandSourceStack import net.minecraft.core.registries.Registries import net.minecraft.world.entity.EntitySpawnReason import net.minecraft.world.entity.EntityType import net.minecraft.world.phys.Vec3 import org.incendo.cloud.SenderMapper import org.incendo.cloud.context.CommandContext import org.incendo.cloud.execution.ExecutionCoordinator import org.incendo.cloud.fabric.FabricServerCommandManager import org.incendo.cloud.minecraft.modded.data.Coordinates import org.incendo.cloud.minecraft.modded.data.MultipleEntitySelector import org.incendo.cloud.minecraft.modded.data.SinglePlayerSelector import org.incendo.cloud.minecraft.modded.parser.RegistryEntryParser.registryEntryParser import org.incendo.cloud.minecraft.modded.parser.VanillaArgumentParsers.singlePlayerSelectorParser import org.incendo.cloud.minecraft.modded.parser.VanillaArgumentParsers.vec3Parser import org.incendo.cloud.parser.standard.BooleanParser.booleanParser import org.incendo.cloud.parser.standard.DoubleParser.doubleParser import org.incendo.cloud.parser.standard.EnumParser.enumParser import org.incendo.cloud.parser.standard.StringParser.stringParser import org.incendo.cloud.suggestion.SuggestionProvider.blockingStrings private val MODEL_SUGGESTION = blockingStrings { _, _ -> BetterModel.modelKeys() } private val LIMB_SUGGESTION = blockingStrings { _, _ -> BetterModel.limbKeys() } fun startFabricCommand() { FabricServerCommandManager( ExecutionCoordinator.simpleCoordinator(), SenderMapper.create( { stack -> stack.player?.let { player -> AudiencePlayer(stack, player) } ?: AudienceSourceStack(stack) }, { audience -> (audience as AudienceCommandSource).source } ) ).register( "bettermodel", "All-related command.", { it }, "bm", "model" ) { create( "reload", "Reloads BetterModel.", "re", "rl" ) { handler(::reload) } create( "spawn", "Summons some model to given type", "s" ) { required("model", stringParser(), MODEL_SUGGESTION) .optional("type", registryEntryParser(Registries.ENTITY_TYPE, EntityType::class.java)) .optional("scale", doubleParser(0.0625, 16.0)) .optional("location", vec3Parser(true)) .senderType(AudiencePlayer::class.java) .handler(::spawn) } create( "test", "Tests some model's animation to specific source", "t" ) { required("model", stringParser(), MODEL_SUGGESTION) .required( "animation", stringParser(), blockingStrings { ctx, _ -> ctx.nullableString("model") { BetterModel.modelOrNull(it)?.animations()?.keys } ?: emptySet() } ) .optional("source", singlePlayerSelectorParser()) .optional("location", vec3Parser(false)) .handler(::test) } create( "disguise", "Disguises self.", "d" ) { required("model", stringParser(), MODEL_SUGGESTION) .optional("scaling", booleanParser()) .senderType(AudiencePlayer::class.java) .handler(::disguise) } create( "undisguise", "Undisguises self.", "ud" ) { senderType(AudiencePlayer::class.java) .optional("model", stringParser(), blockingStrings { ctx, _ -> ctx.sender().player.toRegistry()?.trackers()?.map(Tracker::name) ?: emptyList() }) .handler(::undisguise) } create( "play", "Plays source animation", "p" ) { required("limb", stringParser(), LIMB_SUGGESTION) .required( "animation", stringParser(), blockingStrings { ctx, _ -> ctx.nullableString("limb") { BetterModel.limbOrNull(it)?.animations()?.keys } ?: emptySet() } ) .optional("loop_type", enumParser(AnimationIterator.Type::class.java)) .optional("hide", booleanParser()) .senderType(AudiencePlayer::class.java) .handler(::play) } create( "version", "Checks BetterModel's version", "v" ) { handler(::version) } // TODO NOT implemented yet // create( // "hide", // "Hides some entities from target source." // ) { // required("model", stringParser(), MODEL_SUGGESTION) // .required("source", singlePlayerSelectorParser()) // .required("entities", multipleEntitySelectorParser()) // .handler(::hide) // } // create( // "show", // "Shows some entities to target source." // ) { // required("model", stringParser(), MODEL_SUGGESTION) // .required("source", singlePlayerSelectorParser()) // .required("entities", multipleEntitySelectorParser()) // .handler(::show) // } } } private fun hide(context: CommandContext) { val sender = context.sender() val model = context.get("model") val player = context.get("source").single().connection.wrap() var success = false context.get("entities").values().forEach { if (it.toRegistry()?.tracker(model)?.hide(player) == true) success = true } if (!success) sender.warn("Failed to hide any of provided entities.") } private fun show(context: CommandContext) { val sender = context.sender() val model = context.get("model") val player = context.get("source").single().connection.wrap() var success = false context.get("entities").values().forEach { if (it.toRegistry()?.tracker(model)?.show(player) == true) success = true } if (!success) sender.warn("Failed to show any of provided entities.") } private fun disguise(context: CommandContext) { val audience = context.sender() val player = audience.player val scaling = if (context.getOrDefault("scaling", true)) ModelScaler.entity() else ModelScaler.defaultScaler() context.model("model") { return audience.warn("Unable to find this model: $it") }.getOrCreate(player.connection.wrap(), TrackerModifier.DEFAULT) { it.scaler(scaling) } } private fun undisguise(context: CommandContext) { val audience = context.sender() val player = audience.player val model = context.nullable("model") if (model != null) { player.toTracker(model)?.close() ?: audience.warn("Cannot find this model to undisguise: $model") } else player.toRegistry()?.close() ?: audience.warn("Cannot find any model to undisguise") } private fun spawn(context: CommandContext) { val audience = context.sender() val player = audience.player val model = context.model("model") { return audience.warn("Unable to find this model: $it") } val type = context.nullable>("type", EntityType.HUSK) val scale = context.nullable("scale", 1.0) val loc = context.nullable("location") type.spawn( player.level(), loc?.blockPos() ?: player.blockPosition(), EntitySpawnReason.COMMAND )?.let { entity -> model.create(entity.wrap(), TrackerModifier.DEFAULT) { tracker -> tracker.scaler(ModelScaler.entity().multiply(scale.toFloat())) } } ?: audience.warn("Entity spawning has been blocked.") } private fun version(context: CommandContext) { val sender = context.sender() sender.info("Searching version, please wait...") PLATFORM.scheduler().asyncTask { val version = LATEST_VERSION sender.infoNotNull( emptyComponentOf(), "Current: ${PLATFORM.semver()}".toComponent(), version.release?.let { version -> componentOf("Latest release: ") { append(version.toURLComponent()) } }, version.snapshot?.let { version -> componentOf("Latest snapshot: ") { append(version.toURLComponent()) } } ) } } private fun reload(context: CommandContext) { val audience = context.sender() PLATFORM.scheduler().asyncTask { audience.info("Start reloading. please wait...") when (val result = PLATFORM.reload(audience)) { is OnReload -> audience.warn("BetterModel is still on reload!") is Success -> { audience.info( emptyComponentOf(), "Reload completed. (${result.totalTime().withComma()}ms)".toComponent(GREEN), "Assets reload time - ${result.assetsTime().withComma()}ms".toComponent { color(GRAY) hoverEvent("Reading all config and model.".toComponent().toHoverEvent()) }, "Packing time - ${result.packingTime().withComma()}ms".toComponent { color(GRAY) hoverEvent("Packing all model to resource pack.".toComponent().toHoverEvent()) }, "${BetterModel.models().size.withComma()} of models are loaded successfully. (${result.length().toByteFormat()})".toComponent(YELLOW), (if (result.packResult.changed()) "${result.packResult.size().withComma()} of files are zipped." else "Zipping is skipped due to the same result.").toComponent(YELLOW), emptyComponentOf() ) } is Failure -> { audience.warn( emptyComponentOf(), "Reload failed.".toComponent(), "Please read the log to find the problem.".toComponent(), emptyComponentOf() ) audience.warn() result.throwable.handleException("Reload failed.") } } } } private fun play(context: CommandContext) { val audience = context.sender() val player = audience.player val limb = context.limb("limb") { return audience.warn("Unable to find this limb: $it") } val animation = context.string("animation") { limb.animation(it).orElse(null) ?: return audience.warn("Unable to find this animation: $it") } val loopType = context.nullable("loop_type", AnimationIterator.Type.PLAY_ONCE) val hide = context.nullable("hide") != false limb.getOrCreate(player.connection.wrap(), TrackerModifier.DEFAULT) { it.hideOption(if (hide) EntityHideOption.DEFAULT else EntityHideOption.FALSE) }.run { if (!animate(animation, AnimationModifier(0, 0, loopType), ::close)) close() } } private fun test(context: CommandContext) { val audience = context.sender() val model = context.model("model") { return audience.warn("Unable to find this model: $it") } val animation = context.string("animation") { str -> model.animation(str).orElse(null) ?: return audience.warn("Unable to find this animation: $str") } val player = context.nullable("source")?.single() ?: (audience as? AudiencePlayer)?.player ?: return audience.warn("Unable to find target source.") val location = context.nullable("location")?.position() ?: player.position() .add(Vec3(0.0, 0.0, 10.0).yRot(-Math.toRadians(player.yRot.toDouble()).toFloat())) model.create(ModLocation.of( player.level(), location.x, location.y, location.z, player.xRot, player.yRot + 180 )).run { spawn(player.connection.wrap()) animate(animation, AnimationModifier(0, 0, AnimationIterator.Type.PLAY_ONCE), ::close) } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/config/BetterModelConfigImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.config import kr.toxicity.model.api.BetterModelConfig import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.config.IndicatorConfig import kr.toxicity.model.api.config.ModuleConfig import kr.toxicity.model.api.config.PackConfig import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.mount.MountControllers import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.util.EntityUtil import kr.toxicity.model.impl.fabric.wrap import kr.toxicity.model.util.toPackName import net.minecraft.core.registries.BuiltInRegistries import net.minecraft.resources.Identifier import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items import org.spongepowered.configurate.ConfigurationNode import org.spongepowered.configurate.yaml.YamlConfigurationLoader import java.io.File import java.nio.file.Path import java.util.function.Supplier fun Path.toConfig() = BetterModelConfigImpl(YamlConfigurationLoader.builder().path(this).build().load()) class BetterModelConfigImpl(yaml: ConfigurationNode) : BetterModelConfig { private val debug = yaml.node("debug")?.let { node -> DebugConfig.from { node.node(it).getBoolean(false) } } ?: DebugConfig.DEFAULT private val indicator = yaml.node("indicator")?.let { node -> IndicatorConfig.from { node.node(it).getBoolean(false) } } ?: IndicatorConfig.DEFAULT private val module = yaml.node("module")?.let { node -> ModuleConfig.from { node.node(it).getBoolean(false) } } ?: ModuleConfig.DEFAULT private val pack = yaml.node("pack")?.let { node -> PackConfig.from { node.node(it).getBoolean(false) } } ?: PackConfig.DEFAULT private val sightTrace = yaml.node("sight-trace").getBoolean(true) private val mergeWithExternalResources = yaml.node("merge-with-external-resources").getBoolean(false) private val itemModel = yaml.node("item").getString("leather_horse_armor") private val item = runCatching { BuiltInRegistries.ITEM.getValue( Identifier.withDefaultNamespace(itemModel) ) }.getOrDefault(Items.LEATHER_HORSE_ARMOR).let { Supplier { ItemStack(it).wrap() } } private val itemNamespace = yaml.node("item-namespace").getString("bm_models").toPackName() private val maxSight by lazy { yaml.node("max-sight").getDouble(-1.0).run { if (this <= 0.0) EntityUtil.renderDistance() else this } } private val minSight = yaml.node("min-sight").getDouble(5.0) private val namespace = yaml.node("namespace").getString("bettermodel") private val packType = yaml.node("pack-type").getString("zip")?.let { runCatching { BetterModelConfig.PackType.valueOf(it.uppercase()) }.getOrNull() } ?: BetterModelConfig.PackType.ZIP private val buildFolderLocation = (yaml.node("build-folder-location").getString("BetterModel/build")).replace('/', File.separatorChar) private val followMobInvisibility = yaml.node("follow-mob-invisibility").getBoolean(true) private val versionCheck = yaml.node("version-check").getBoolean(true) private val defaultMountController = when (yaml.node("default-mount-controller").getString("walk")?.lowercase()) { "invalid" -> MountControllers.INVALID "none" -> MountControllers.NONE "fly" -> MountControllers.FLY else -> MountControllers.WALK } private val lerpFrameTime = yaml.node("lerp-frame-time").getInt(5) private val cancelPlayerModelInventory = yaml.node("cancel-player-model-inventory").getBoolean(false) private val playerHideDelay = yaml.node("player-hide-delay").getLong(3L).coerceAtLeast(1L) private val packetBundlingSize = yaml.node("packet-bundling-size").getInt(16) private val enableStrictLoading = yaml.node("enable-strict-loading").getBoolean(false) override fun debug(): DebugConfig = debug override fun indicator(): IndicatorConfig = indicator override fun module(): ModuleConfig = module override fun pack(): PackConfig = pack override fun item(): Supplier = item override fun itemModel(): String = itemModel override fun itemNamespace(): String = itemNamespace override fun metrics(): Boolean = false override fun sightTrace(): Boolean = sightTrace override fun mergeWithExternalResources(): Boolean = mergeWithExternalResources override fun maxSight(): Double = maxSight override fun minSight(): Double = minSight override fun namespace(): String = namespace override fun packType(): BetterModelConfig.PackType = packType override fun buildFolderLocation(): String = buildFolderLocation override fun followMobInvisibility(): Boolean = followMobInvisibility override fun usePurpurAfk(): Boolean = false override fun versionCheck(): Boolean = versionCheck override fun defaultMountController(): MountController = defaultMountController override fun lerpFrameTime(): Int = lerpFrameTime override fun cancelPlayerModelInventory(): Boolean = cancelPlayerModelInventory override fun playerHideDelay(): Long = playerHideDelay override fun packetBundlingSize(): Int = packetBundlingSize override fun enableStrictLoading(): Boolean = enableStrictLoading } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/BaseFabricEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import kr.toxicity.model.api.mod.entity.BaseModEntity import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.TransformedItemStack import kr.toxicity.model.impl.fabric.* import kr.toxicity.model.impl.fabric.chat.asAdventure import net.kyori.adventure.text.Component import net.minecraft.server.level.ServerPlayer import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.ai.attributes.Attributes import org.joml.Vector3f import java.util.* import java.util.stream.Stream class BaseFabricEntityImpl(private var entity: Entity) : BaseModEntity { override fun entity(entity: Entity) { this.entity = entity } override fun platform(): PlatformEntity = entity.wrap() override fun customName(): Component? { return if (entity is ServerPlayer) { (entity.customName ?: entity.name).asAdventure() } else { entity.customName?.takeIf { entity.isCustomNameVisible }?.asAdventure() } } override fun handle(): Entity = entity override fun id(): Int = entity.id override fun dead(): Boolean { val entity = entity return entity.removalReason != null || entity is LivingEntity && entity.isDeadOrDying } override fun ground(): Boolean = entity.onGround() override fun invisible(): Boolean { val entity = entity return entity.isInvisible || entity is LivingEntity && entity.hasEffect(MobEffects.INVISIBILITY) } override fun glow(): Boolean = entity.isCurrentlyGlowing override fun onWalk(): Boolean = entity.isWalking override fun fly(): Boolean = entity.isFlying override fun scale(): Double { val entity = entity return if (entity is LivingEntity) { entity.scale.toDouble() } else { 1.0 } } override fun pitch(): Float = entity.xRot override fun bodyYaw(): Float = entity.let { if (it is LivingEntity) it.yBodyRot else it.yRot } override fun yaw(): Float = entity.yRot override fun headYaw(): Float = entity.let { if (it is LivingEntity) it.yHeadRot else it.yRot } override fun damageTick(): Float { val entity = entity if (entity !is LivingEntity || entity.invulnerableTime <= 0.0f) { return 0F } val knockbackResistant = entity.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value ?: 0.0 val knockBack = 1 - knockbackResistant.toFloat() return entity.hurtTime.toFloat() / entity.hurtDuration * knockBack } override fun walkSpeed(): Float { val entity = entity if (entity !is LivingEntity) { return 0.0f } if (!entity.onGround()) { return 1.0f } val speed = entity.getEffect(MobEffects.SPEED)?.amplifier ?: 0 val slow = entity.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 return (1.0f + (speed - slow) * 0.2f).coerceIn(0.2f..2.0f) } override fun passengerPosition(dest: Vector3f): Vector3f = entity.passengerPosition(dest) override fun trackedBy(): Stream = entity.seenBy.stream().map { it.wrap() } override fun mainHand(): TransformedItemStack { val entity = entity return if (entity is LivingEntity) { TransformedItemStack.of(entity.mainHandItem.wrap()) } else { TransformedItemStack.empty() } } override fun offHand(): TransformedItemStack { val entity = entity return if (entity is LivingEntity) { TransformedItemStack.of(entity.offhandItem.wrap()) } else { TransformedItemStack.empty() } } override fun modelData(): String? { return entity.modelData } override fun modelData(modelData: String?) { entity.modelData = modelData } override fun uuid(): UUID = entity.uuid } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/BaseFabricPlayerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import kr.toxicity.model.api.mod.entity.BaseModEntity import kr.toxicity.model.api.mod.entity.BaseModPlayer import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.impl.fabric.armor.PlayerArmorImpl import kr.toxicity.model.impl.fabric.seenBy import kr.toxicity.model.impl.fabric.wrap import kr.toxicity.model.impl.fabric.xMovement import kr.toxicity.model.impl.fabric.zMovement import net.minecraft.server.network.ServerPlayerConnection import net.minecraft.util.Mth import java.util.stream.Stream class BaseFabricPlayerImpl( private val connection: ServerPlayerConnection, private val profile: () -> ModelProfile, private val skinParts: () -> PlayerSkinParts ) : BaseModPlayer, BaseModEntity by BaseFabricEntityImpl(connection.player), Profiled by ProfiledImpl(PlayerArmorImpl(connection), profile, skinParts) { override fun updateInventory() { connection.player.containerMenu.sendAllDataToRemote() } override fun platform(): PlatformPlayer = connection.wrap() override fun trackedBy(): Stream = Stream.concat( Stream.of(connection), connection.player.seenBy.stream() ).map { it.wrap() } override fun bodyYaw(): Float { val handle = connection.player var yaw = -45 * handle.xMovement() if (handle.zMovement() < 0) yaw *= -1 return Mth.wrapDegrees(handle.yHeadRot + yaw) } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/DisplayTransformerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.util.lock.SingleLock import kr.toxicity.model.impl.fabric.network.plusAssign import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.world.entity.Display import org.joml.Quaternionf import org.joml.Vector3f class DisplayTransformerImpl(source: Display.ItemDisplay) : DisplayTransformer { private val id = source.id private val entityData = TransformationData() private val entityDataLock = SingleLock() override fun transform( duration: Int, position: Vector3f, scale: Vector3f, rotation: Quaternionf, bundler: AnimationBundler ) { entityDataLock.accessToLock { entityData.transform( duration, position, scale, rotation ) entityData.packDirty(id, bundler) } } override fun sendTransformation(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack() }?.let { bundler += ClientboundSetEntityDataPacket(id, it) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/HitBoxEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.config.DebugConfig import kr.toxicity.model.api.data.blueprint.ModelBoundingBox import kr.toxicity.model.api.event.hitbox.* import kr.toxicity.model.api.mount.MountController import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.HitBoxListener import kr.toxicity.model.api.nms.ModelInteractionHand import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.impl.fabric.* import kr.toxicity.model.impl.fabric.world.damagesource.ModelDamageSourceImpl import kr.toxicity.model.util.CONFIG import net.minecraft.core.particles.DustParticleOptions import net.minecraft.network.protocol.game.ServerboundInteractPacket import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.damagesource.DamageSource import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.* import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.Projectile import net.minecraft.world.entity.projectile.ProjectileDeflection import net.minecraft.world.item.ItemStack import net.minecraft.world.level.BlockGetter import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.awt.Color import java.util.* class HitBoxEntityImpl( private val source: ModelBoundingBox, private val bone: RenderedBone, private var listener: HitBoxListener, private val delegate: Entity, private var mountController: MountController ) : AbstractArmorStand(EntityType.ARMOR_STAND, delegate.level()), HitBox { private val posCache = BoneMovement() private var initialized = false private var jumpDelay = 0 private var mounted = false private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity private var forceDismount = false private var onFly = false fun calculateDimensions(): EntityDimensions { val width = (source.x() + source.z()) * 0.5 val height = source.y() return EntityDimensions( width.toFloat(), height.toFloat(), delegate.eyeHeight, EntityAttachments.createDefault(0F, 0F), false ).scale( bone.hitBoxScale() ) } private val interaction by lazy { InteractionEntityImpl(this) } private val applier = InsideBlockEffectApplier.StepBasedCollector() init { snapTo(delegate.position()) isInvisible = true isSilent = true initialized = true level().addFreshEntity(this) interaction.snapTo(delegate.position()) interaction.startRiding(this) level().addFreshEntity(interaction) listener.handle(HitBoxCreateEvent(this)) } private fun initialSetup() { if (mounted) { mounted = false if (delegate is Mob) delegate.isNoAi = noGravity else delegate.isNoGravity = noGravity } } override fun id(): Int = id override fun uuid(): UUID = uuid override fun source(): PlatformEntity = delegate.wrap() override fun positionSource(): RenderedBone = bone override fun forceDismount(): Boolean = forceDismount override fun mountController(): MountController = mountController override fun hasMountDriver(): Boolean = controllingPassenger != null override fun mountController(controller: MountController) { this.mountController = controller } override fun relativePosition(): Vector3f { return bone.hitBoxPosition(posCache).add( delegate.x.toFloat(), delegate.y.toFloat(), delegate.z.toFloat() ) } override fun listener(): HitBoxListener = listener override fun listener(listener: HitBoxListener) { this.listener = listener } override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) = Unit override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT override fun mount(entity: PlatformEntity) { if (controllingPassenger != null) { return } entity.unwarp().startRiding(this, true, true) if (mountController.canControl()) { mounted = true noGravity = delegate.isNoGravity } listener.handle(HitBoxMountEvent(this, entity)) } override fun dismount(entity: PlatformEntity) { forceDismount = true entity.unwarp().stopRiding() listener.handle(HitBoxDismountEvent(this, entity)) forceDismount = false } override fun dismountAll() { forceDismount = true interaction.passengers.forEach { passenger -> passenger.stopRiding() listener.handle(HitBoxDismountEvent(this, passenger.wrap())) } forceDismount = false } override fun setRemainingFireTicks(remainingFireTicks: Int) { delegate.remainingFireTicks = remainingFireTicks } override fun getRemainingFireTicks(): Int { return delegate.remainingFireTicks } override fun knockback(d: Double, e: Double, f: Double) { (delegate as? LivingEntity)?.knockback(d, e, f) } override fun push(pushingEntity: Entity) { if (pushingEntity !== delegate) { delegate.push(pushingEntity) } } override fun canCollideWith(entity: Entity): Boolean { return checkCollide(entity) && delegate.canCollideWith(entity) } private fun checkCollide(entity: Entity): Boolean { return entity !== delegate && passengers.none { it === entity } && delegate.passengers.none { it === entity } && (entity !is HitBoxEntityImpl || entity.delegate !== delegate) } override fun getActiveEffects(): Collection { return (delegate as? LivingEntity)?.activeEffects ?: emptyList() } override fun getControllingPassenger(): LivingEntity? { return if (!mounted) { null } else { interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() } } override fun onWalk(): Boolean = isWalking private fun mountControl(player: ServerPlayer) { if (delegate !is LivingEntity || !mountController.canFly() && delegate.isFallFlying ) { return } val travelVector = Vec3( delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble() ) updateFlyStatus(player) val riddenInput = rideInput(player, travelVector) if (riddenInput.length() > 0.01) { delegate.yRot = player.yRot if (onFly) { delegate.yHeadRot = player.yRot } val movementVector = Vec3( riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble() ) delegate.move(MoverType.SELF, movementVector) } if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.lastClientInput.jump()) && (delegate.deltaMovement.y + delegate.gravity) in 0.0..0.01 && jumpDelay == 0 ) { jumpDelay = 10 delegate.jumpFromGround() } } private fun movementSpeed(): Float { if (delegate !is LivingEntity) { return 0.0f } val attribute = delegate.getAttribute(Attributes.MOVEMENT_SPEED) ?: return 0.0f val attributeValue = attribute.value.toFloat() if (onFly || shouldDiscardFriction()) { return attributeValue } return level() .getBlockState(blockPosBelowThatAffectsMyMovement) .block .getFriction() * attributeValue } private fun updateFlyStatus(player: ServerPlayer) { val fly = (player.lastClientInput.jump() && mountController.canFly()) || noGravity || onFly if (delegate is Mob) { delegate.isNoAi = fly } else { delegate.isNoGravity = fly } onFly = fly && !delegate.onGround() if (onFly) { delegate.resetFallDistance() } } private fun rideInput(player: ServerPlayer, travelVector: Vec3): Vector3f { return mountController.move( if (onFly) { MountController.MoveType.FLY } else { MountController.MoveType.DEFAULT }, player.connection.wrap(), (delegate as LivingEntity).wrap(), Vector3f( player.xMovement(), player.yMovement(), player.zMovement() ), Vector3f( travelVector.x.toFloat(), travelVector.y.toFloat(), travelVector.z.toFloat() ) ) .mul(movementSpeed()) .rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) } override fun tick() { delegate.removalReason?.let { removalReason -> if (!isRemoved) { remove(removalReason) } return } val controller = controllingPassenger if (jumpDelay > 0) { jumpDelay-- } interaction.isInvisible = delegate.isInvisible if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { if (delegate is Mob) { delegate.navigation.stop() } mountControl(controller) } else { initialSetup() } yRot = bone.rotation().y yHeadRot = yRot yBodyRot = yRot val pos = relativePosition() val minusHeight = source.minY * bone.hitBoxScale() setPos( pos.x.toDouble(), pos.y.toDouble() + minusHeight, pos.z.toDouble() ) BlockGetter.forEachBlockIntersectedBetween( oldPosition(), position(), boundingBox ) { pos, _ -> level().getBlockState(pos).entityInside( level(), pos, delegate, applier, true ) true } applier.applyAndClear(delegate) if (isInLava) { delegate.lavaHurt() } firstTick = false listener.sync(this) } override fun remove(reason: RemovalReason) { initialSetup() listener.handle(HitBoxRemoveEvent(this)) interaction.remove(reason) super.remove(reason) } override fun hasExactlyOnePlayerPassenger(): Boolean = false override fun isDeadOrDying(): Boolean = delegate is LivingEntity && delegate.isDeadOrDying override fun hide(player: PlatformPlayer) { TODO("with mixin") } override fun show(player: PlatformPlayer) { TODO("with mixin") } override fun interact(player: Player, hand: InteractionHand, vec: Vec3): InteractionResult { if (player === delegate) { return InteractionResult.FAIL } val serverPlayer = player as ServerPlayer val interact = HitBoxInteractAtEvent( serverPlayer.connection.wrap(), this, when (hand) { InteractionHand.MAIN_HAND -> ModelInteractionHand.RIGHT InteractionHand.OFF_HAND -> ModelInteractionHand.LEFT }, vec.toVector3f() ) if (!listener.handle(interact)) return InteractionResult.FAIL serverPlayer.connection.handleInteract( ServerboundInteractPacket( delegate.id, hand, vec, player.isShiftKeyDown ) ) return InteractionResult.SUCCESS } override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { return if (entity == delegate) { false } else { delegate is LivingEntity && delegate.addEffect(effectInstance, entity) } } override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { if (delegate == source.entity || delegate.isInvulnerable || source.entity == controllingPassenger && !mountController.canBeDamagedByRider() ) { return false } val sourceImpl = ModelDamageSourceImpl(source) val event = HitBoxDamagedEvent(this, sourceImpl, amount) if (!listener.handle(event)) return false return delegate is LivingEntity && delegate.hurtServer(world, source, event.damage) } override fun deflection(projectile: Projectile): ProjectileDeflection { if (projectile.owner?.uuid == delegate.uuid) { return ProjectileDeflection.NONE } return (delegate as? LivingEntity)?.deflection(projectile) ?: ProjectileDeflection.NONE } override fun getHealth(): Float { return (delegate as? LivingEntity)?.health ?: super.getHealth() } override fun makeBoundingBox(vec3: Vec3): AABB { if (!initialized) { return super.makeBoundingBox(vec3) } val scale = bone.hitBoxScale() val boundingBox = AABB( vec3.x + source.minX * scale, vec3.y, vec3.z + source.minZ * scale, vec3.x + source.maxX * scale, vec3.y + source.y() * scale, vec3.z + source.maxZ * scale ) if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { val level = level() as ServerLevel val particleOptions = DustParticleOptions(Color.RED.rgb, 1F) level.sendParticles( particleOptions, true, true, boundingBox.minX, boundingBox.minY, boundingBox.minZ, 1, 0.0, 0.0, 0.0, 1.0 ) level.sendParticles( particleOptions, true, true, boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ, 1, 0.0, 0.0, 0.0, 1.0 ) } return boundingBox } override fun getDefaultDimensions(pose: Pose): EntityDimensions { return if (initialized) { calculateDimensions() } else { super.getDefaultDimensions(pose) } } override fun removeHitBox() { source().task { dismountAll() remove((delegate as? LivingEntity)?.removalReason ?: RemovalReason.KILLED) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/InteractionEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 class InteractionEntityImpl(val delegate: HitBoxEntityImpl) : Interaction(EntityType.INTERACTION, delegate.level()) { override fun tick() { delegate.calculateDimensions().let { dimensions -> width = dimensions.width height = dimensions.height } yRot = delegate.yRot xRot = delegate.xRot setSharedFlagOnFire(delegate.remainingFireTicks > 0) } override fun skipAttackInteraction(entity: Entity): Boolean { if (entity !is Player) { return false } entity.attack(delegate) return true } override fun interact(player: Player, hand: InteractionHand, vec: Vec3): InteractionResult { delegate.interact(player, hand, vec) return InteractionResult.FAIL } override fun shouldBeSaved(): Boolean = false } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ModelDisplayEntityImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import it.unimi.dsi.fastutil.ints.IntOpenHashSet import kr.toxicity.model.api.entity.BaseEntity import kr.toxicity.model.api.nms.DisplayTransformer import kr.toxicity.model.api.nms.ModelDisplay import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformBillboard import kr.toxicity.model.api.platform.PlatformItemStack import kr.toxicity.model.api.platform.PlatformItemTransform import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.util.lock.SingleLock import kr.toxicity.model.impl.fabric.manager.markDirty import kr.toxicity.model.impl.fabric.network.pack import kr.toxicity.model.impl.fabric.network.plusAssign import kr.toxicity.model.impl.fabric.unwarp import kr.toxicity.model.mixin.DisplayAccessor import kr.toxicity.model.mixin.EntityAccessor import kr.toxicity.model.mixin.ItemDisplayAccessor import kr.toxicity.model.util.CONFIG import net.minecraft.network.protocol.game.* import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.util.Brightness import net.minecraft.world.entity.Display import net.minecraft.world.entity.Entity import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.ItemStack import net.minecraft.world.item.Items import org.joml.Vector3d import java.util.* import java.util.concurrent.atomic.AtomicBoolean class ModelDisplayEntityImpl( private val pos: Vector3d, val display: Display.ItemDisplay, val yOffset: Double ) : ModelDisplay { private val entityData: SynchedEntityData = display.entityData private val entityDataLock: SingleLock = SingleLock() private val forceGlow = AtomicBoolean() private val forceInvisibility = AtomicBoolean() private val oldPos = Vector3d(pos) override fun id(): Int = display.id override fun uuid(): UUID = display.uuid override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { display.xRot = rotation.x display.yRot = rotation.y bundler += ClientboundMoveEntityPacket.Rot( display.id, rotation.packedY(), rotation.packedX(), display.onGround() ) } override fun invisible(invisible: Boolean) { if (forceInvisibility.compareAndSet(!invisible, invisible)) { entityData.packDirty() entityDataLock.accessToLock { entityData.markDirty(ItemDisplayAccessor.`bettermodel$getDataItemStackId`()) } } } override fun syncPotionEffect(entity: BaseEntity) { val beforeInvisible = display.isInvisible val afterInvisible = entity.invisible() entityDataLock.accessToLock { display.setGlowingTag(entity.glow() || forceGlow.get()) if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { display.isInvisible = afterInvisible entityData.markDirty(ItemDisplayAccessor.`bettermodel$getDataItemStackId`()) } } } override fun syncPosition(location: PlatformLocation) { oldPos.set(pos) pos.set(location.x(), location.y(), location.z()) } override fun spawn(showItem: Boolean, bundler: PacketBundler) { bundler += createAddPacket() } override fun remove(bundler: PacketBundler) { bundler += removePacket } override fun teleport(location: PlatformLocation, bundler: PacketBundler) { display.snapTo( location.x(), location.y(), location.z(), location.yaw(), 0F ) bundler += ClientboundTeleportEntityPacket.teleport( display.id, PositionMoveRotation.of(display), emptySet(), display.onGround() ) } override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { val handle = adapter.handle() as Entity if (oldPos.distanceSquared(pos) < 1e-8) return bundler += ClientboundEntityPositionSyncPacket( display.id, PositionMoveRotation.of(handle), handle.onGround() ) } override fun display(transform: PlatformItemTransform) { entityDataLock.accessToLock { display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) } } override fun moveDuration(duration: Int) { entityDataLock.accessToLock { entityData[DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`()] = duration } } override fun item(itemStack: PlatformItemStack) { entityDataLock.accessToLock { display.itemStack = itemStack.clone().unwarp() } } override fun brightness(block: Int, sky: Int) { entityDataLock.accessToLock { display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( block, sky ) } } override fun viewRange(range: Float) { entityDataLock.accessToLock { display.viewRange = range } } override fun shadowRadius(radius: Float) { entityDataLock.accessToLock { display.shadowRadius = radius } } override fun glow(glow: Boolean) { if (!forceGlow.compareAndSet(!glow, glow)) return entityDataLock.accessToLock { display.setGlowingTag(display.isCurrentlyGlowing || glow) } } override fun glowColor(glowColor: Int) { entityDataLock.accessToLock { display.glowColorOverride = glowColor } } override fun billboard(billboard: PlatformBillboard) { entityDataLock.accessToLock { display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) } } override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) override fun invisible(): Boolean { return entityDataLock.accessToLock { display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) } } override fun sendDirtyEntityData(bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( clean = true, itemFilter = { it.isDirty }, valueFilter = { ACCESSOR_IDS.contains(it.id) } ) }?.markVisible(!invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { entityDataLock.accessToLock { entityData.pack( valueFilter = { ACCESSOR_IDS.contains(it.id) } ) }?.markVisible(showItem && !invisible())?.run { bundler += ClientboundSetEntityDataPacket(display.id, this) } } private fun List>.markVisible(showItem: Boolean) = map { if (it.id == ItemDisplayAccessor.`bettermodel$getDataItemStackId`().id) SynchedEntityData.DataValue( it.id, EntityDataSerializers.ITEM_STACK, if (showItem) display.itemStack else ItemStack.EMPTY ) else it } private fun createAddPacket() = ClientboundAddEntityPacket( display.id, display.uuid, pos.x, pos.y + yOffset, pos.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket = ClientboundRemoveEntitiesPacket(display.id) companion object { private val ACCESSOR_IDS by lazy { IntOpenHashSet().apply { setOf( EntityAccessor.`bettermodel$getDataSharedFlagsId`(), DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`(), // index: 7 ~ last DisplayAccessor.`bettermodel$getDataBillboardRenderConstraintsId`(), DisplayAccessor.`bettermodel$getDataBrightnessOverrideId`(), DisplayAccessor.`bettermodel$getDataViewRangeId`(), DisplayAccessor.`bettermodel$getDataShadowRadiusId`(), DisplayAccessor.`bettermodel$getDataShadowStrengthId`(), DisplayAccessor.`bettermodel$getDataWidthId`(), DisplayAccessor.`bettermodel$getDataHeightId`(), DisplayAccessor.`bettermodel$getDataGlowColorOverrideId`(), // all ItemDisplayAccessor.`bettermodel$getDataItemStackId`(), ItemDisplayAccessor.`bettermodel$getDataItemDisplayId`() ).mapTo(this) { it.id } } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ModelNametagImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import com.mojang.math.Transformation import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bone.BoneMovement import kr.toxicity.model.api.bone.BonePosition import kr.toxicity.model.api.bone.RenderedBone import kr.toxicity.model.api.mod.BetterModelMod import kr.toxicity.model.api.nms.ModelNametag import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.EntityUtil import kr.toxicity.model.impl.fabric.chat.asVanilla import kr.toxicity.model.impl.fabric.network.bundlerOf import kr.toxicity.model.impl.fabric.network.bundlerOfNotNull import kr.toxicity.model.impl.fabric.network.pack import kr.toxicity.model.impl.fabric.network.plusAssign import kr.toxicity.model.mixin.DisplayAccessor import net.minecraft.network.chat.Component import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.world.entity.Display import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f import java.util.* import java.util.concurrent.ConcurrentHashMap class ModelNametagImpl( private val bone: RenderedBone ) : ModelNametag { private companion object { private val emptyVector = Vector3f() private val emptyTransformation = Transformation( Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), null, null, null ) } private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( EntityType.TEXT_DISPLAY, BetterModelMod.platform().server().overworld() ).apply { entityData[DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`()] = 3 setTransformation(emptyTransformation) billboardConstraints = Display.BillboardConstraints.CENTER } private val posCache = BoneMovement() private var alwaysVisible = false private var location = BetterModel.platform().adapter().zero() override fun component(component: net.kyori.adventure.text.Component?) { display.text = component?.asVanilla() ?: Component.empty() } override fun teleport(location: PlatformLocation) { this.location = location } override fun alwaysVisible(alwaysVisible: Boolean) { this.alwaysVisible = alwaysVisible } override fun send(player: PlatformPlayer) { if (display.text == Component.empty()) return val hb = bone.group.hitBoxPoint val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) display.snapTo(Vec3( location.x() + pos.x, location.y() + pos.y, location.z() + pos.z )) val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) when { inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( addPacket, display.entityData.pack()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) inPoint -> bundlerOfNotNull( ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), display.entityData.packDirty()?.let { ClientboundSetEntityDataPacket(display.id, it) } ) viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) else -> null }?.send(player) } override fun remove(bundler: PacketBundler) { bundler += removePacket } private val addPacket get() = ClientboundAddEntityPacket( display.id, display.uuid, display.x, display.y, display.z, display.xRot, display.yRot, display.type, 0, display.deltaMovement, display.yHeadRot.toDouble() ) private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/PlayerChannelHandlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelPromise import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.entity.BasePlayer import kr.toxicity.model.api.mod.BetterModelMod import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.nms.PlayerChannelHandler import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.TrackerUpdateAction import kr.toxicity.model.impl.fabric.network.* import kr.toxicity.model.impl.fabric.wrap import kr.toxicity.model.mixin.DisplayAccessor import kr.toxicity.model.mixin.EntityAccessor import kr.toxicity.model.util.CONFIG import kr.toxicity.model.util.PLATFORM import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking import net.minecraft.network.Connection import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.* import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerPlayerConnection import net.minecraft.world.entity.Display import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemStack import java.util.stream.IntStream class PlayerChannelHandlerImpl( private val connection: ServerPlayerConnection ) : PlayerChannelHandler, ChannelDuplexHandler() { private val player get() = connection.player private val uuid = player.uuid private val basePlayer = PLATFORM.nms().adapt(connection.wrap()) init { val pipeline = connection.player.connection.connection.channel.pipeline() pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) } override fun close() { val channel = connection.player.connection.connection.channel channel.eventLoop().submit { channel.pipeline().remove(INJECT_NAME) } } override fun base(): BasePlayer = basePlayer override fun isModEnabled(): Boolean = ServerPlayNetworking.getReceived(player).contains(ModAnimationBundlerImpl.IDENTIFIER) private val playerModel get() = connection.player.id.toRegistry() private fun getEntity(id: Int, level: ServerLevel) = level.getEntity(id) private fun getPlayerEntity(id: Int) = getEntity(id, connection.player.level()) private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) private inline fun Int.toRegistry(ifHitBox: (Entity) -> Unit = {}) = (EntityTrackerRegistry.registry(this) ?: getPlayerEntity(this)?.let { if (it is HitBox) ifHitBox(it) it.toRegistry() })?.takeIf { it.isSpawned(player.uuid) } override fun sendEntityData(registry: EntityTrackerRegistry) { val handle = registry.entity().handle() as? Entity ?: return val list = bundlerOf( ClientboundSetPassengersPacket(handle) ) handle.entityData.pack( valueFilter = { it.id == EntityAccessor.`bettermodel$getDataSharedFlagsId`().id } )?.let { list.add(ClientboundSetEntityDataPacket(handle.id, it)) } if (handle is LivingEntity) handle.toEquipmentPacket()?.let { list.add(it) } list.send(connection.wrap()) } private fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( id, uuid, x, y, z, xRot, yRot, EntityType.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() ) private fun Packet.handle(): Packet? { when (this) { is ClientboundBundlePacket -> { return if ((this as BetterModelBundlePacket).`bettermodel$isBetterModelPacket`()) this else ClientboundBundlePacket(subPackets().mapNotNull { it.handle() }) } is ClientboundAddEntityPacket -> { val entity = getPlayerEntity(id) ?: return this if (entity is HitBox) return entity.toFakeAddPacket() val wrap = entity.wrap() BetterModel.registry(wrap).ifPresent { wrap.taskLater(1) { it.spawn(connection.wrap()) } } } is ClientboundRemoveEntitiesPacket -> { entityIds .asSequence() .mapNotNull map@{ it.toRegistry { return@map null } } .forEach { it.remove() } } is ClientboundSetPassengersPacket -> { vehicle.toRegistry()?.let { registry -> return registry.mountPacket( entity = registry.entity().handle() as? Entity ?: return this, passengerIds = IntStream.of(*passengers) ) } } is ClientboundUpdateAttributesPacket if getPlayerEntity(entityId) is HitBox -> return null is ClientboundSetEntityDataPacket -> id.toRegistry { return ClientboundSetEntityDataPacket(id, hitBoxData) }?.let { registry -> return toRegistryDataPacket(uuid, registry) } is ClientboundSetEquipmentPacket -> entity.toRegistry { return null }?.let { registry -> if (registry.hideOption(uuid).equipment()) { (registry.entity().handle() as? LivingEntity) ?.toEquipmentPacket { ItemStack.EMPTY } ?.let { packet -> return packet } } } is ClientboundRespawnPacket -> playerModel?.let { bundlerOf(it.mountPacket(connection.player)).send(connection.wrap()) } is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetSlotPacket(containerId, stateId, slot, ItemStack.EMPTY) } is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { return ClientboundContainerSetContentPacket( containerId, stateId, items.apply { eachEquipmentSlots { set(it, ItemStack.EMPTY) } set(connection.player.hotbarSlot, ItemStack.EMPTY) }, carriedItem ) } } return this } override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { if (isClosed) return@asyncTaskLater player.containerMenu.sendAllDataToRemote() trackers().forEach { tracker -> tracker.update(TrackerUpdateAction.itemMapping()) { bone -> !bone.itemMapper.fixed() } } } when (msg) { is ServerboundSetCarriedItemPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) { connection.send(ClientboundSetHeldSlotPacket(player.inventory.selectedSlot)) return } registry.updatePlayerLimb() } } is ServerboundPlayerActionPacket -> { playerModel?.let { registry -> if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) if (CONFIG.cancelPlayerModelInventory()) return registry.updatePlayerLimb() } } } super.channelRead(ctx, msg) } private fun EntityTrackerRegistry.remove() { remove(connection.wrap()) } companion object { private const val INJECT_NAME = "bettermodel_channel_handler" private val hitBoxData by lazy { Display.ItemDisplay( EntityType.ITEM_DISPLAY, (PLATFORM as BetterModelMod).server().overworld() ).run { entityData.set(DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`(), 3) entityData.nonDefaultValues!! } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ProfiledImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import kr.toxicity.model.api.armor.PlayerArmor import kr.toxicity.model.api.nms.Profiled import kr.toxicity.model.api.player.PlayerSkinParts import kr.toxicity.model.api.profile.ModelProfile internal class ProfiledImpl( private val playerArmor: PlayerArmor, private val modelProfile: () -> ModelProfile, private val playerSkinParts: () -> PlayerSkinParts ) : Profiled { override fun profile(): ModelProfile = modelProfile() override fun armors(): PlayerArmor = playerArmor override fun skinParts(): PlayerSkinParts = playerSkinParts() } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/TransformationData.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.entity import kr.toxicity.model.api.nms.AnimationBundler import kr.toxicity.model.api.util.MathUtil import kr.toxicity.model.impl.fabric.network.plusAssign import kr.toxicity.model.mixin.DisplayAccessor import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData import org.joml.Quaternionf import org.joml.Vector3f class TransformationData { private var duration = 0 private val durationDataValue get() = SynchedEntityData.DataValue( DisplayAccessor.`bettermodel$getDataTransformationInterpolationDurationId`().id, DisplayAccessor.`bettermodel$getDataTransformationInterpolationDurationId`().serializer, duration ) private val translation = Item( Vector3f(), DisplayAccessor.`bettermodel$getDataTranslationId`(), { a, b -> // unchecked cast MathUtil.isSimilar(a, b) }, { a, b -> // unchecked cast (a as Vector3f).set(b) } ) private val scale = Item( Vector3f(), DisplayAccessor.`bettermodel$getDataScaleId`(), { a, b -> // unchecked cast MathUtil.isSimilar(a, b) }, { a, b -> // unchecked cast (a as Vector3f).set(b) } ) private val rotation = Item( Quaternionf(), DisplayAccessor.`bettermodel$getDataLeftRotationId`(), { a, b -> // unchecked cast MathUtil.isSimilar(a as Quaternionf, b as Quaternionf) }, { a, b -> // unchecked cast (a as Quaternionf).set(b) } ) fun packDirty(entityId: Int, dest: AnimationBundler) { val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex if (i == 0) return dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { add(INTERPOLATION_DELAY_VALUE) translation.value?.let { add(it) } rotation.value?.let { add(it) } scale.value?.let { add(it) } add(durationDataValue) }) } fun transform( duration: Int, translation: Vector3f, scale: Vector3f, rotation: Quaternionf ) { this.duration = duration this.translation.set(translation) this.scale.set(scale) this.rotation.set(rotation) } fun pack(): List> { return listOf( INTERPOLATION_DELAY_VALUE, durationDataValue, translation.forceValue, scale.forceValue, rotation.forceValue ) } private class Item( initialValue: T, private val accessor: EntityDataAccessor, private val dirtyChecker: (T, T) -> Boolean, private val setter: (T, T) -> Unit ) { private val _value: T = initialValue private var _dirty = false val dirty get() = _dirty val cleanIndex get() = if (dirty) 1 else 0 val value get() = if (_dirty) { _dirty = false forceValue } else { null } val forceValue get() = SynchedEntityData.DataValue( accessor.id, accessor.serializer, _value ) fun set(other: T) { if (dirtyChecker(_value, other)) { return } _dirty = true setter(_value, other) } } companion object { private val INTERPOLATION_DELAY_VALUE = DisplayAccessor.`bettermodel$getDataTransformationInterpolationStartDeltaTicksId`() .let { accessor -> SynchedEntityData.DataValue(accessor.id, accessor.serializer, 0) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/events/ServerEntityDismountCallback.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.events import net.fabricmc.fabric.api.event.EventFactory import net.minecraft.world.entity.Entity fun interface ServerEntityDismountCallback { fun onDismount(passenger: Entity, vehicle: Entity): Boolean companion object { @JvmField val EVENT = EventFactory.createArrayBacked(ServerEntityDismountCallback::class.java) { callbacks -> { passenger, vehicle -> callbacks.all { it.onDismount(passenger, vehicle) } } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/events/ServerLivingEntityJumpCallback.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.events import net.fabricmc.fabric.api.event.EventFactory import net.minecraft.world.entity.LivingEntity fun interface ServerLivingEntityJumpCallback { fun onJump(entity: LivingEntity) companion object { @JvmField val EVENT = EventFactory.createArrayBacked(ServerLivingEntityJumpCallback::class.java) { callbacks -> { entity -> callbacks.forEach { it.onJump(entity) } } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/events/ServerMobEffectLoadCallback.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.events import net.fabricmc.fabric.api.event.EventFactory import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.LivingEntity fun interface ServerMobEffectLoadCallback { fun onLoad(entity: LivingEntity, effect: MobEffectInstance) companion object { @JvmField val EVENT = EventFactory.createArrayBacked(ServerMobEffectLoadCallback::class.java) { callbacks -> { entity, effect -> callbacks.forEach { it.onLoad(entity, effect) } } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/events/ServerMobEffectUnloadCallback.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.events import net.fabricmc.fabric.api.event.EventFactory import net.minecraft.world.effect.MobEffectInstance import net.minecraft.world.entity.LivingEntity fun interface ServerMobEffectUnloadCallback { fun onUnload(entity: LivingEntity, effect: MobEffectInstance) companion object { @JvmField val EVENT = EventFactory.createArrayBacked(ServerMobEffectUnloadCallback::class.java) { callbacks -> { entity, effect -> callbacks.forEach { it.onUnload(entity, effect) } } } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/manager/EntityManager.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.manager import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.mod.entity.BaseModEntity import kr.toxicity.model.api.nms.HitBox import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.tracker.EntityTracker import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.api.tracker.Tracker import kr.toxicity.model.api.tracker.TrackerExtraAnimation import kr.toxicity.model.impl.fabric.events.ServerEntityDismountCallback import kr.toxicity.model.impl.fabric.events.ServerLivingEntityJumpCallback import kr.toxicity.model.impl.fabric.events.ServerMobEffectLoadCallback import kr.toxicity.model.impl.fabric.events.ServerMobEffectUnloadCallback import kr.toxicity.model.impl.fabric.wrap import kr.toxicity.model.manager.GlobalManager import kr.toxicity.model.manager.ReloadPipeline import kr.toxicity.model.util.PLATFORM import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents import net.fabricmc.fabric.api.event.player.UseEntityCallback import net.minecraft.server.level.ServerPlayer import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.effect.MobEffects import net.minecraft.world.entity.Entity object EntityManager : GlobalManager { override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) { EntityTrackerRegistry.registries { registry -> registry.reload() } } override fun end() { EntityTrackerRegistry.registries { registry -> registry.save() registry.close(Tracker.CloseReason.PLUGIN_DISABLE) } } /* EntityAddToWorldEvent ✅ EntityRemoveFromWorldEvent ✅ EntityJumpEvent ✅ EntityDamageEvent ✅ EntityDamageByEntityEvent ✅ EntityDeathEvent ✅ EntityDismountEvent ✅ EntityPotionEffectEvent ✅ EntityRemoveEvent ✅ EntitySpawnEvent ✅ PlayerChangedWorldEvent ✅ PlayerDeathEvent ✅ PlayerInteractAtEntityEvent ✅ PlayerInteractEntityEvent ✅ PlayerQuitEvent ✅ EntitiesUnloadEvent ❌ probably because ENTITY_UNLOAD contains this */ override fun start() { registerStateEvents() registerLifecycleEvents() registerCombatEvents() registerInteractionEvents() } private fun registerStateEvents() { // same as EntityPotionEffectEvent (added) ServerMobEffectLoadCallback.EVENT.register { entity, instance -> if (instance.effect == MobEffects.GLOWING || instance.effect == MobEffects.INVISIBILITY ) { entity.eachTracker { tracker -> tracker.updateBaseEntity() } } } // same as EntityPotionEffectEvent (removed) ServerMobEffectUnloadCallback.EVENT.register { entity, instance -> if (instance.effect == MobEffects.GLOWING || instance.effect == MobEffects.INVISIBILITY ) { entity.eachTracker { tracker -> tracker.updateBaseEntity() } } } // same as EntityDismountEvent ServerEntityDismountCallback.EVENT.register { _, vehicle -> vehicle !is HitBox || !(vehicle.mountController().canFly() || !vehicle.mountController().canDismountBySelf()) || vehicle.forceDismount() } // same as EntityJumpEvent ServerLivingEntityJumpCallback.EVENT.register { entity -> entity.eachTracker { tracker -> tracker.animate(TrackerExtraAnimation.JUMP) } } } private fun registerLifecycleEvents() { ServerEntityLevelChangeEvents.AFTER_ENTITY_CHANGE_LEVEL.register { oldEntity, newEntity, _, _ -> BetterModel.registryOrNull(oldEntity.uuid)?.let { registry -> (registry.entity() as BaseModEntity).entity(newEntity) } } // same as EntityAddToWorldEvent, EntitySpawnEvent ServerEntityEvents.ENTITY_LOAD.register { entity, _ -> BetterModel.registryOrNull(entity.uuid)?.refresh() } // same as EntityRemoveFromWorldEvent, EntityRemoveEvent ServerEntityEvents.ENTITY_UNLOAD.register { entity, _ -> BetterModel.registryOrNull(entity.uuid)?.despawn() } // same as PlayerChangedWorldEvent ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register { player, _, _ -> BetterModel.registryOrNull(player.uuid)?.let { registry -> registry.despawn() registry.refresh() } } // same as PlayerQuitEvent ServerPlayerEvents.LEAVE.register { player -> val fabricPlayer = player.connection.wrap() BetterModel.registryOrNull(fabricPlayer.uuid())?.close() PLATFORM.scheduler().asyncTask { EntityTrackerRegistry.registries { registry -> registry.remove(fabricPlayer) } } (player.vehicle as? HitBox)?.dismount(fabricPlayer) } } private fun registerCombatEvents() { // same as EntityDamageByEntityEvent // // EntityDamageByEntityEvent are not expected to be called for non-living entities. // therefore, ServerLivingEntityEvents is used. // ServerLivingEntityEvents.ALLOW_DAMAGE.register { entity, source, _ -> val damager = source.entity if (damager != null) { val victim = if (entity is HitBox) entity.source().uuid() else entity.uuid val vehicle = damager.vehicle if (vehicle is HitBox && !vehicle.mountController().canBeDamagedByRider() && vehicle.source().uuid() == victim ) { return@register false } } return@register true } // same as EntityDamageEvent // // EntityDamageEvent and EntityDamageByEntityEvent are not expected to be called for non-living entities. // therefore, ServerLivingEntityEvents is used. // ServerLivingEntityEvents.AFTER_DAMAGE.register { entity, _, _, _, _ -> entity.eachTracker { tracker -> tracker.animate(TrackerExtraAnimation.DAMAGE) tracker.damageTint() } } // same as EntityDeathEvent, PlayerDeathEvent ServerLivingEntityEvents.AFTER_DEATH.register { entity, _ -> entity.eachTracker { tracker -> tracker.animate(TrackerExtraAnimation.DEATH) } if (entity is ServerPlayer) { BetterModel.registryOrNull(entity.uuid)?.despawn() } } } private fun registerInteractionEvents() { // same as PlayerInteractAtEntityEvent, PlayerInteractEntityEvent UseEntityCallback.EVENT.register { clicker, _, hand, clicked, _ -> if (clicker !is ServerPlayer) { return@register InteractionResult.PASS } // for PlayerInteractAtEntityEvent (clicked as? HitBox)?.let { hitBox -> if (hand == InteractionHand.MAIN_HAND && !clicker.triggerDismount(clicked)) { clicker.triggerMount(hitBox) } } return@register InteractionResult.PASS } } private fun ServerPlayer.triggerDismount(entity: Entity): Boolean { val oldVehicle = vehicle if (oldVehicle !is HitBox) { return false } val uuid = if (entity is HitBox) entity.source().uuid() else entity.uuid if (oldVehicle.source().uuid() != uuid || !oldVehicle.mountController().canDismountBySelf()) { return false } oldVehicle.dismount(connection.wrap()) return true } private fun ServerPlayer.triggerMount(hitBox: HitBox) { if (hitBox.mountController().canMount()) { hitBox.mount(connection.wrap()) } } private fun Entity.eachTracker(block: (EntityTracker) -> Unit) { BetterModel.registryOrNull(uuid)?.trackers()?.forEach { tracker -> block(tracker) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/manager/PlayerManagerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.manager import kr.toxicity.model.api.manager.PlayerManager import kr.toxicity.model.api.nms.PlayerChannelHandler import kr.toxicity.model.api.pack.PackZipper import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.impl.fabric.wrap import kr.toxicity.model.manager.GlobalManager import kr.toxicity.model.manager.ReloadPipeline import kr.toxicity.model.manager.SkinManagerImpl import kr.toxicity.model.util.PLATFORM import kr.toxicity.model.util.handleFailure import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents import net.minecraft.server.level.ServerPlayer import java.util.* import java.util.concurrent.ConcurrentHashMap object PlayerManagerImpl : PlayerManager, GlobalManager { private val playerMap = ConcurrentHashMap() override fun start() { ServerPlayerEvents.JOIN.register { handleJoin(it) } ServerPlayerEvents.LEAVE.register { handleLeave(it) } } private fun handleJoin(player: ServerPlayer) { runCatching { player.connection.wrap().register() }.handleFailure { "Unable to load ${player.name}'s data." } } private fun handleLeave(player: ServerPlayer) { playerMap.remove(player.uuid)?.use { SkinManagerImpl.removeCache(it.base().profile()) } } override fun end() { playerMap.values.forEach { handler -> handler.use { used -> SkinManagerImpl.removeCache(used.base().profile()) } } playerMap.clear() } override fun player(uuid: UUID): PlayerChannelHandler? = playerMap[uuid] override fun player(player: PlatformPlayer): PlayerChannelHandler = player.register() private fun PlatformPlayer.register(): PlayerChannelHandler { return playerMap.computeIfAbsent(uuid()) { PLATFORM.nms().inject(this) }.apply { SkinManagerImpl.complete(base().profile().asUncompleted()) } } override fun reload(pipeline: ReloadPipeline, zipper: PackZipper) = Unit } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/manager/Syncers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.manager import kr.toxicity.model.mixin.SynchedEntityDataAccessor import net.minecraft.network.syncher.EntityDataAccessor import net.minecraft.network.syncher.SynchedEntityData fun SynchedEntityData.markDirty(accessor: EntityDataAccessor) { (this as SynchedEntityDataAccessor).`bettermodel$getItem`(accessor).isDirty = true `bettermodel$setDirty`(true) } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/network/ModAnimationBundlerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.network import kr.toxicity.model.api.mod.BetterModelMod import kr.toxicity.model.api.nms.ModAnimationBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.api.util.MathUtil import kr.toxicity.model.impl.fabric.unwarp import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket import net.minecraft.resources.Identifier import org.joml.Quaternionf import org.joml.Vector3f internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { companion object { const val KEY = "modelengine:bulk_data" val IDENTIFIER = Identifier.parse(KEY) const val PACKET_TYPE_BULK_DATA = 0x00 const val FIELD_TRANSLATION = 1 shl 0 const val FIELD_LEFT_ROTATION = 1 shl 1 const val FIELD_SCALE = 1 shl 2 const val FIELD_TRANSFORM_DURATION = 1 shl 4 private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} } private val packet by lazy { useByteBuf { buffer -> ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( RegistryFriendlyByteBuf( buffer, BetterModelMod.platform().server().registryAccess() ).apply { writeUtf(KEY) useByteBuf { it.writeByte(PACKET_TYPE_BULK_DATA) it.writeVarInt(builderList.size) builderList.forEach { builder -> builder(it) } writeBytes(it) } } ) } } private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) override fun send(player: PlatformPlayer) { player.unwarp().send(packet) } fun append(id: Int, scope: Appender.() -> Unit) { val build = Appender(id).apply(scope).build() if (build !== EMPTY_BUILD_TASK) builderList += build } class Appender( val entityId: Int, ) { private var mask = 0 private var buildTask = EMPTY_BUILD_TASK private val isEmpty get() = buildTask === EMPTY_BUILD_TASK fun appendPosition(vector: Vector3f) { mask = mask or FIELD_TRANSLATION task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendScale(vector: Vector3f) { mask = mask or FIELD_SCALE task { writeFloat(it, vector.x) writeFloat(it, vector.y) writeFloat(it, vector.z) } } fun appendRotation(quaternion: Quaternionf) { mask = mask or FIELD_LEFT_ROTATION task { writeFloat(it, quaternion.x) writeFloat(it, quaternion.y) writeFloat(it, quaternion.z) writeFloat(it, quaternion.w) } } fun appendDuration(duration: Int) { mask = mask or FIELD_TRANSFORM_DURATION task { writeVarInt(it, duration) } } fun build(): (FriendlyByteBuf) -> Unit { if (isEmpty) return EMPTY_BUILD_TASK val m = mask val t = buildTask return { writeVarInt(it,entityId) writeByte(it, m) t(it) } } private fun task(task: (FriendlyByteBuf) -> Unit) { if (isEmpty) { buildTask = task return } val last = buildTask buildTask = { last(it) task(it) } } private fun writeFloat(buf: FriendlyByteBuf, float: Float) { buf.writeShort(MathUtil.floatToHalf(float).toInt()) } private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { buf.writeVarInt(duration) } private fun writeByte(buf: FriendlyByteBuf, duration: Int) { buf.writeByte(duration) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/network/PacketBundlers.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.network import kr.toxicity.model.api.nms.PacketBundler import kr.toxicity.model.api.platform.PlatformPlayer import kr.toxicity.model.impl.fabric.unwarp import net.minecraft.network.PacketSendListener import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientGamePacketListener import net.minecraft.network.protocol.game.ClientboundBundlePacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket internal typealias ClientPacket = Packet internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) internal operator fun PacketBundler.plusAssign(other: ClientPacket) { when (this) { is SimpleBundler -> add(other) is ParallelBundler -> add(other) else -> throw RuntimeException("unsupported bundler.") } } internal fun Packet<*>.assumeSize() = when (this) { is ClientboundSetEntityDataPacket -> packedItems.size is ClientboundSetEquipmentPacket -> slots.size else -> 1 } internal interface PluginBundlePacketImpl : Iterable { val bundlePacket: ClientboundBundlePacket fun size(): Int fun isEmpty(): Boolean fun add(other: ClientPacket) } internal class SimpleBundler( private val list: MutableList ) : PacketBundler, PluginBundlePacketImpl { override val bundlePacket by lazy { ClientboundBundlePacket(this).apply { (this as BetterModelBundlePacket).`bettermodel$setBetterModelPacket`(true) } } override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = player.unwarp().player.connection connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) } override fun isEmpty(): Boolean = list.isEmpty() override fun size(): Int = list.size override fun iterator(): MutableIterator = list.iterator() override fun add(other: ClientPacket) { list += other } } internal class ParallelBundler( private val threshold: Int ) : PacketBundler { private val subBundlers = mutableListOf() private var sizeAssume = 0 private val newBundler get() = bundlerOf().apply { sizeAssume = 0 subBundlers += this } private var selectedBundler = newBundler override fun send(player: PlatformPlayer, onSuccess: Runnable) { if (isEmpty) return val connection = player.unwarp() subBundlers.forEach { connection.send(it.bundlePacket) } } override fun isEmpty(): Boolean = selectedBundler.isEmpty() override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) fun add(other: ClientPacket) { (if (sizeAssume > threshold) newBundler else selectedBundler) .apply { selectedBundler = this } .add(other) sizeAssume += other.assumeSize() } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/network/Packets.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.network import com.mojang.datafixers.util.Pair.of import io.netty.buffer.Unpooled import it.unimi.dsi.fastutil.ints.IntSet import kr.toxicity.model.api.tracker.EntityTrackerRegistry import kr.toxicity.model.mixin.ConnectionAccessor import kr.toxicity.model.mixin.EntityAccessor import kr.toxicity.model.mixin.ServerCommonPacketListenerImplAccessor import kr.toxicity.model.mixin.SynchedEntityDataAccessor import net.minecraft.network.Connection import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket import net.minecraft.network.syncher.EntityDataSerializers import net.minecraft.network.syncher.SynchedEntityData import net.minecraft.network.syncher.SynchedEntityData.DataItem import net.minecraft.network.syncher.SynchedEntityData.DataValue import net.minecraft.server.network.ServerGamePacketListenerImpl import net.minecraft.world.entity.Entity import net.minecraft.world.entity.EquipmentSlot import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player import net.minecraft.world.item.ItemStack import java.util.* import java.util.function.IntConsumer import java.util.stream.IntStream val Connection.channel get() = (this as ConnectionAccessor).`bettermodel$getChannel`() val ServerGamePacketListenerImpl.connection get() = (this as ServerCommonPacketListenerImplAccessor).`bettermodel$getConnection`() val Player.hotbarSlot get() = inventory.selectedSlot + 36 fun EntityTrackerRegistry.mountPacket( entity: Entity, passengerIds: IntStream = entity.getUnregisteredPassengerIds() ): ClientboundSetPassengersPacket { return useByteBuf { buffer -> val displayIds = displays().mapToInt { display -> display.id() } val ids = IntStream.concat(displayIds, passengerIds) buffer.writeVarInt(entity.id) buffer.writeVarIntArray(ids.toArray()) ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) } } fun Entity.getUnregisteredPassengerIds(): IntStream { return passengers.stream() .filter { passenger -> EntityTrackerRegistry.registry(passenger.uuid) == null } .mapToInt { passenger -> passenger.id } } inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { val buffer = FriendlyByteBuf(Unpooled.buffer()) return try { block(buffer) } finally { buffer.release() } } inline fun SynchedEntityData.pack( clean: Boolean = false, itemFilter: (DataItem<*>) -> Boolean = { true }, crossinline valueFilter: (DataValue<*>) -> Boolean = { true }, crossinline required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } ): List>? { return (this as SynchedEntityDataAccessor) .`bettermodel$getItemsById`() .mapNotNull { val item = it.takeIf(itemFilter) ?: return@mapNotNull null val value = item.value().takeIf(valueFilter) ?: return@mapNotNull null item to value } .takeIf(required) ?.map { if (clean) { it.first.isDirty = false } it.second } } fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket( id, packedItems().map { if (it.id == EntityAccessor.`bettermodel$getDataSharedFlagsId`().id) DataValue( it.id, EntityDataSerializers.BYTE, registry.entityFlag(uuid, it.value() as Byte) ) else it }) fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { var b = byte.toInt() val hideOption = hideOption(uuid) if (hideOption.fire()) b = b and 1.inv() if (hideOption.visibility()) b = b or (1 shl 5) if (hideOption.glowing()) b = b and (1 shl 6).inv() return b.toByte() } inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { val equip = EquipmentSlot.entries.mapNotNull { mapper(it)?.let { item -> of(it, item) } } return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null } fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } fun ClientboundContainerSetSlotPacket.isEquipment(player: Player): Boolean { return containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) } fun eachEquipmentSlots(block: (Int) -> Unit) { PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { slot -> block(slot) }) } private val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/profile/ModelProfileImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.profile import com.mojang.authlib.GameProfile import kr.toxicity.model.api.profile.ModelProfile import kr.toxicity.model.api.profile.ModelProfileInfo import kr.toxicity.model.api.profile.ModelProfileSkin import kr.toxicity.model.util.PLATFORM class ModelProfileImpl(private val profile: GameProfile) : ModelProfile { private val info = ModelProfileInfo( profile.id, profile.name ) private val skin by lazy { val properties = profile.properties val property = properties["textures"].firstOrNull() if (property == null) { ModelProfileSkin.EMPTY } else { PLATFORM.profileManager().skin(property.value) } } override fun info(): ModelProfileInfo = info override fun skin(): ModelProfileSkin = skin } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/scheduler/FabricModelSchedulerImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.scheduler import kr.toxicity.model.api.mod.platform.ModRegionHolder import kr.toxicity.model.api.mod.scheduler.ModModelScheduler import kr.toxicity.model.api.scheduler.ModelTask import kr.toxicity.model.api.util.LogUtil import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents import java.util.concurrent.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong object FabricModelSchedulerImpl : ModModelScheduler, ModRegionHolder { private val scheduler = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), object : ThreadFactory { private val integer = AtomicInteger() override fun newThread(r: Runnable): Thread { val thread = Thread(r) thread.setDaemon(true) thread.setName("BetterModel-Async-Scheduler-" + integer.getAndIncrement()) thread.setUncaughtExceptionHandler { t: Thread, e: Throwable -> LogUtil.handleException("Exception has occurred in " + t.name, e) } return thread } }) private val enabled = AtomicBoolean(true) private val queue = ConcurrentLinkedQueue() override fun asyncTask(runnable: Runnable): ModelTask { return AsyncTask(runnable) { submit(it) } } override fun asyncTaskLater(delay: Long, runnable: Runnable): ModelTask { return AsyncTask(runnable) { schedule(it, delay * 50, TimeUnit.MILLISECONDS) } } override fun asyncTaskTimer(delay: Long, period: Long, runnable: Runnable): ModelTask { return AsyncTask(runnable) { scheduleAtFixedRate(it, delay * 50, period * 50, TimeUnit.MILLISECONDS) } } override fun task(runnable: Runnable): ModelTask? { if (!enabled.get()) return null return SyncTask(runnable).apply { queue += this } } override fun taskLater(delay: Long, runnable: Runnable): ModelTask? { if (!enabled.get()) return null return SyncTask(runnable, delay).apply { queue += this } } private class AsyncTask( private val runnable: Runnable, scheduleFunction: (ScheduledExecutorService).(Runnable) -> Future<*> ) : Runnable, ModelTask { private val future = scheduler.scheduleFunction(this) override fun run() { if (enabled.get()) { runnable.run() } else { future.cancel(true) } } override fun isCancelled(): Boolean = future.isCancelled override fun cancel() { future.cancel(true) } } private class SyncTask( @Volatile var task: Runnable, counter: Long = 0L ) : ModelTask { private val atomicCounter = AtomicLong(counter) fun run() = if (atomicCounter.getAndDecrement() <= 0) { synchronized(this) { if (enabled.get()) task.run() } true } else false override fun isCancelled(): Boolean { return task === CANCELLED_TASK } override fun cancel() { if (isCancelled) return synchronized(this) { if (isCancelled) return task = CANCELLED_TASK atomicCounter.set(0) } } companion object { val CANCELLED_TASK: Runnable = {} } } private fun tick() { queue.removeIf { it.run() } } fun init() { ServerTickEvents.START_LEVEL_TICK.register { tick() } ServerLifecycleEvents.SERVER_STARTING.register { enabled.set(true) } ServerLifecycleEvents.SERVER_STOPPED.register { enabled.set(false) } } } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/world/Chunks.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ @file:Suppress("UnstableApiUsage") package kr.toxicity.model.impl.fabric.world import it.unimi.dsi.fastutil.ints.Int2ObjectMap import net.fabricmc.fabric.mixin.networking.accessor.ChunkMapAccessor import net.fabricmc.fabric.mixin.networking.accessor.EntityTrackerAccessor import net.minecraft.server.level.ChunkMap val ChunkMap.entityMap: Int2ObjectMap get() { return (this as ChunkMapAccessor).entityMap } ================================================ FILE: platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/world/damagesource/ModelDamageSourceImpl.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.impl.fabric.world.damagesource import kr.toxicity.model.api.event.ModelDamageSource import kr.toxicity.model.api.mod.platform.ModLocation import kr.toxicity.model.api.platform.PlatformEntity import kr.toxicity.model.api.platform.PlatformLocation import kr.toxicity.model.impl.fabric.wrap import net.minecraft.world.damagesource.DamageSource class ModelDamageSourceImpl(private val source: DamageSource) : ModelDamageSource { override fun getCausingEntity(): PlatformEntity? = source.entity?.wrap() override fun getDirectEntity(): PlatformEntity? = source.directEntity?.wrap() override fun getDamageLocation(): PlatformLocation? { return source.sourcePositionRaw()?.let { pos -> ModLocation.of( source.entity?.level(), pos.x, pos.y, pos.z, 0f, 0f ) } } override fun getSourceLocation(): PlatformLocation? { return source.sourcePosition?.let { pos -> ModLocation.of( source.entity?.level(), pos.x, pos.y, pos.z, 0f, 0f ) } } override fun isIndirect(): Boolean = !source.isDirect override fun getFoodExhaustion(): Float = source.foodExhaustion override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() } ================================================ FILE: platform/fabric/src/main/resources/bettermodel.accesswidener ================================================ accessWidener v1 named ================================================ FILE: platform/fabric/src/main/resources/bettermodel.mixins.json ================================================ { "required": true, "package": "kr.toxicity.model.mixin", "compatibilityLevel": "JAVA_25", "mixins": [ "AvatarAccessor", "ClientboundBundlePacketMixin", "ConnectionAccessor", "DisplayAccessor", "EntityAccessor", "EntityMixin", "ItemDisplayAccessor", "LivingEntityMixin", "MobAccessor", "ServerCommonPacketListenerImplAccessor", "ServerLevelEntityCallbacksMixin", "SynchedEntityDataAccessor" ], "injectors": { "defaultRequire": 1 } } ================================================ FILE: platform/fabric/src/testmod/kotlin/kr/toxicity/model/test/RollTest.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.test import com.mojang.brigadier.builder.LiteralArgumentBuilder import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.animation.AnimationModifier import kr.toxicity.model.api.mod.platform.ModPlayer import kr.toxicity.model.api.tracker.ModelRotation import kr.toxicity.model.api.tracker.TrackerModifier import net.fabricmc.api.ModInitializer import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.minecraft.commands.CommandSourceStack import net.minecraft.commands.Commands import net.minecraft.network.chat.Component import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.player.Input import kotlin.jvm.optionals.getOrNull import kotlin.math.atan2 class RollTest : ModInitializer { override fun onInitialize() { CommandRegistrationCallback.EVENT.register { dispatcher, _, _ -> dispatcher.register(argumentRoll()) } } private fun argumentRoll(): LiteralArgumentBuilder? { return Commands.literal("roll") .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) .then(argumentInfo()) .then(argumentPlay()) } private fun argumentInfo(): LiteralArgumentBuilder { return Commands.literal("info") .executes { context -> executeInfo(context.source) } } private fun argumentPlay(): LiteralArgumentBuilder { return Commands.literal("play") .executes { context -> executePlay(context.source, context.source.playerOrException) } } private fun executeInfo(source: CommandSourceStack): Int { val renderer = BetterModel.limb("steve").getOrNull() ?: let { source.sendFailure(Component.literal("Renderer not found: steve")) return 0 } val animation = renderer.animation("roll").getOrNull() ?: let { source.sendFailure(Component.literal("Animation not found: roll")) return 0 } source.sendSuccess( { Component.empty() .append("Loop mode: " + animation.loop) .append("\n") .append("Length: " + animation.length + " second") }, true ) return 1 } private fun executePlay(source: CommandSourceStack, player: ServerPlayer): Int { val renderer = BetterModel.limb("steve").getOrNull() ?: let { source.sendFailure(Component.literal("Renderer not found: steve")) return 0 } val yaw = player.lastClientInput.toYaw() val tracker = renderer.getOrCreate( ModPlayer.of(player.connection), TrackerModifier.DEFAULT ) { tracker -> tracker.rotation { ModelRotation( player.xRot, (yaw + tracker.registry().entity().bodyYaw()).packDegrees() ) } } val isAnimated = tracker.animate( "roll", AnimationModifier.DEFAULT_WITH_PLAY_ONCE ) { tracker.close() } if (!isAnimated) { tracker.close() } return 1 } private fun Input.toYaw(): Float { val forward = (if (forward) 1 else 0) - (if (backward) 1 else 0) val right = (if (right) 1 else 0) - (if (left) 1 else 0) return if (forward == 0 && right == 0) { 0f } else { Math.toDegrees(atan2(right.toDouble(), forward.toDouble())).toFloat() } } private fun Float.packDegrees(): Float { return if (this > 180) { this - 360 } else { this } } } ================================================ FILE: platform/fabric/src/testmod/resources/knight.bbmodel ================================================ {"meta":{"format_version":"5.0","model_format":"free","box_uv":false},"name":"knight","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"degrees=180","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":64,"height":64},"elements":[{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,11.25,-1.875],"to":[3.75,15,1.875],"autouv":0,"color":1,"origin":[0,11.25,0],"uv_offset":[16,24],"faces":{"north":{"uv":[20,28,28,32],"texture":0},"east":{"uv":[16,28,20,32],"texture":0},"south":{"uv":[32,28,40,32],"texture":0},"west":{"uv":[28,28,32,32],"texture":0},"up":{"uv":[28,28,20,24],"texture":0},"down":{"uv":[36,16,28,20],"texture":0}},"type":"cube","uuid":"ea42f7f7-a6f1-4479-c43f-48211bab5ed2"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,15,-1.875],"to":[3.75,18.75,1.875],"autouv":0,"color":2,"origin":[0,15,0],"uv_offset":[16,20],"faces":{"north":{"uv":[20,24,28,28],"texture":0},"east":{"uv":[16,24,20,28],"texture":0},"south":{"uv":[32,24,40,28],"texture":0},"west":{"uv":[28,24,32,28],"texture":0},"up":{"uv":[28,24,20,20],"texture":0},"down":{"uv":[28,28,20,32],"texture":0}},"type":"cube","uuid":"5ea74bdb-ba28-b8e3-103b-9be6ff2262da"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,18.75,-1.875],"to":[3.75,22.5,1.875],"autouv":0,"color":3,"origin":[0,18.75,0],"uv_offset":[16,16],"faces":{"north":{"uv":[20,20,28,24],"texture":0},"east":{"uv":[16,20,20,24],"texture":0},"south":{"uv":[32,20,40,24],"texture":0},"west":{"uv":[28,20,32,24],"texture":0},"up":{"uv":[28,20,20,16],"texture":0},"down":{"uv":[28,24,20,28],"texture":0}},"type":"cube","uuid":"dc1510db-a719-17b4-e253-c992a92c5d25"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,18.76563,-1.85937],"to":[3.73438,22.48438,1.85938],"autouv":0,"color":3,"inflate":0.25,"origin":[0,18.75,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,28,40],"texture":0},"east":{"uv":[16,36,20,40],"texture":0},"south":{"uv":[32,36,40,40],"texture":0},"west":{"uv":[28,36,32,40],"texture":0},"up":{"uv":[28,36,20,32],"texture":0},"down":{"uv":[8,0,0,4],"texture":0}},"type":"cube","uuid":"d51a8665-a2bc-af6e-1230-5acba07248e7"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,15.01563,-1.85937],"to":[3.73438,18.73438,1.85938],"autouv":0,"color":2,"inflate":0.25,"origin":[0,15,0],"uv_offset":[16,36],"faces":{"north":{"uv":[20,40,28,44],"texture":0},"east":{"uv":[16,40,20,44],"texture":0},"south":{"uv":[32,40,40,44],"texture":0},"west":{"uv":[28,40,32,44],"texture":0},"up":{"uv":[8,4,0,0],"texture":0},"down":{"uv":[8,0,0,4],"texture":0}},"type":"cube","uuid":"f5b9f499-b26b-f912-1a54-151d702e13ed"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,11.26563,-1.85937],"to":[3.73438,14.98438,1.85938],"autouv":0,"color":1,"inflate":0.25,"origin":[0,11.25,0],"uv_offset":[16,40],"faces":{"north":{"uv":[20,44,28,48],"texture":0},"east":{"uv":[16,44,20,48],"texture":0},"south":{"uv":[32,44,40,48],"texture":0},"west":{"uv":[28,44,32,48],"texture":0},"up":{"uv":[8,4,0,0],"texture":0},"down":{"uv":[36,32,28,36],"texture":0}},"type":"cube","uuid":"357ebf82-23ba-edb1-081f-dca75d94b83c"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.75,16.875,-1.875],"to":[7.5,22.5,1.875],"autouv":0,"color":4,"origin":[5.15625,21.5625,0],"uv_offset":[40,16],"faces":{"north":{"uv":[44,20.1,48,26.1],"texture":0},"east":{"uv":[40,20,44,26],"texture":0},"south":{"uv":[52,20,56,26],"texture":0},"west":{"uv":[48,20,52,26],"texture":0},"up":{"uv":[48,20.1,44,16.1],"texture":0},"down":{"uv":[48,26.1,44,30.1],"texture":0}},"type":"cube","uuid":"53d40d2e-0941-29f9-00ed-1b19c941dcd8"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.76563,16.89063,-1.85937],"to":[7.48438,22.48438,1.85938],"autouv":0,"color":4,"inflate":0.25,"origin":[5.15625,21.5625,0],"uv_offset":[40,32],"faces":{"north":{"uv":[44,36,48,42],"texture":0},"east":{"uv":[40,36,44,42],"texture":0},"south":{"uv":[52,36,56,42],"texture":0},"west":{"uv":[48,36,52,42],"texture":0},"up":{"uv":[48,36,44,32],"texture":0},"down":{"uv":[56,32,52,36],"texture":0}},"type":"cube","uuid":"b17452ef-afbe-e010-f2c9-e0ba945faa1f"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.5,16.875,-1.875],"to":[-3.75,22.5,1.875],"autouv":0,"color":4,"origin":[-5.15625,21.5625,0],"uv_offset":[32,48],"faces":{"north":{"uv":[36,52,40,58],"texture":0},"east":{"uv":[32,52,36,58],"texture":0},"south":{"uv":[44,52,48,58],"texture":0},"west":{"uv":[40,52,44,58],"texture":0},"up":{"uv":[40,52,36,48],"texture":0},"down":{"uv":[40,58,36,62],"texture":0}},"type":"cube","uuid":"addb9f66-a2a4-46f7-b6af-f5a23c00fe70"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.48437,16.89063,-1.85937],"to":[-3.76562,22.48438,1.85938],"autouv":0,"color":4,"inflate":0.25,"origin":[-5.15625,21.5625,0],"uv_offset":[48,48],"faces":{"north":{"uv":[52,52,56,58],"texture":0},"east":{"uv":[48,52,52,58],"texture":0},"south":{"uv":[60,52,64,58],"texture":0},"west":{"uv":[56,52,60,58],"texture":0},"up":{"uv":[56,52,52,48],"texture":0},"down":{"uv":[64,48,60,52],"texture":0}},"type":"cube","uuid":"5e7dc31c-c64a-ff15-90d9-52a6f77cb14b"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.75,11.25,-1.875],"to":[7.5,16.875,1.875],"autouv":0,"color":8,"origin":[5.15625,15.9375,0],"uv_offset":[40,22],"faces":{"north":{"uv":[44,26,48,32],"texture":0},"east":{"uv":[40,26,44,32],"texture":0},"south":{"uv":[52,26,56,32],"texture":0},"west":{"uv":[48,26,52,32],"texture":0},"up":{"uv":[48,26,44,22],"texture":0},"down":{"uv":[52,16,48,20],"texture":0}},"type":"cube","uuid":"e02c395d-e1bc-1375-a8ed-729e19544ce9"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.76563,11.26563,-1.85937],"to":[7.48438,16.85938,1.85938],"autouv":0,"color":8,"inflate":0.25,"origin":[5.15625,15.9375,0],"uv_offset":[40,38],"faces":{"north":{"uv":[44,42,48,48],"texture":0},"east":{"uv":[40,42,44,48],"texture":0},"south":{"uv":[52,42,56,48],"texture":0},"west":{"uv":[48,42,52,48],"texture":0},"up":{"uv":[44,36,40,32],"texture":0},"down":{"uv":[52,32,48,36],"texture":0}},"type":"cube","uuid":"a509c2d7-53ef-2b11-1331-f716b9c210d6"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.5,11.25,-1.875],"to":[-3.75,16.875,1.875],"autouv":0,"color":8,"origin":[-5.15625,15.9375,0],"uv_offset":[32,54],"faces":{"north":{"uv":[36,58,40,64],"texture":0},"east":{"uv":[32,58,36,64],"texture":0},"south":{"uv":[44,58,48,64],"texture":0},"west":{"uv":[40,58,44,64],"texture":0},"up":{"uv":[40,58,36,54],"texture":0},"down":{"uv":[44,48,40,52],"texture":0}},"type":"cube","uuid":"1433ee62-21ac-d72c-0fd0-37000e5eb221"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.48437,11.26563,-1.85937],"to":[-3.76562,16.85938,1.85938],"autouv":0,"color":8,"inflate":0.25,"origin":[-5.15625,15.9375,0],"uv_offset":[48,54],"faces":{"north":{"uv":[52,58,56,64],"texture":0},"east":{"uv":[48,58,52,64],"texture":0},"south":{"uv":[60,58,64,64],"texture":0},"west":{"uv":[56,58,60,64],"texture":0},"up":{"uv":[52,52,48,48],"texture":0},"down":{"uv":[60,48,56,52],"texture":0}},"type":"cube","uuid":"24bacfd6-cde8-81a0-f620-998cf5393d48"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0,5.625,-1.875],"to":[3.75,11.25,1.875],"autouv":0,"color":7,"origin":[1.40625,10.3125,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,8,26],"texture":0},"east":{"uv":[0,20,4,26],"texture":0},"south":{"uv":[12,20,16,26],"texture":0},"west":{"uv":[8,20,12,26],"texture":0},"up":{"uv":[8,20,4,16],"texture":0},"down":{"uv":[8,26,4,30],"texture":0}},"type":"cube","uuid":"b17675fc-b79b-0ad9-8f81-aa2dd1fc8c97"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.01563,5.64063,-1.85937],"to":[3.73438,11.23438,1.85938],"autouv":0,"color":7,"inflate":0.25,"origin":[1.40625,10.3125,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,42],"texture":0},"east":{"uv":[0,36,4,42],"texture":0},"south":{"uv":[12,36,16,42],"texture":0},"west":{"uv":[8,36,12,42],"texture":0},"up":{"uv":[8,36,4,32],"texture":0},"down":{"uv":[16,32,12,36],"texture":0}},"type":"cube","uuid":"2e42e483-1557-d21e-aee0-94af7bedfd40"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0,0,-1.875],"to":[3.75,5.625,1.875],"autouv":0,"color":6,"origin":[1.40625,4.6875,0],"uv_offset":[0,22],"faces":{"north":{"uv":[4,26,8,32],"texture":0},"east":{"uv":[0,26,4,32],"texture":0},"south":{"uv":[12,26,16,32],"texture":0},"west":{"uv":[8,26,12,32],"texture":0},"up":{"uv":[8,26,4,22],"texture":0},"down":{"uv":[12,16,8,20],"texture":0}},"type":"cube","uuid":"9455d16e-4bbf-4b63-881d-7daf943e782b"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.01563,0.01563,-1.85937],"to":[3.73438,5.60938,1.85938],"autouv":0,"color":6,"inflate":0.25,"origin":[1.40625,4.6875,0],"uv_offset":[0,38],"faces":{"north":{"uv":[4,42,8,48],"texture":0},"east":{"uv":[0,42,4,48],"texture":0},"south":{"uv":[12,42,16,48],"texture":0},"west":{"uv":[8,42,12,48],"texture":0},"up":{"uv":[4,36,0,32],"texture":0},"down":{"uv":[12,32,8,36],"texture":0}},"type":"cube","uuid":"16aee685-0ead-a542-5b3c-a62467ca45e3"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,5.625,-1.875],"to":[0,11.25,1.875],"autouv":0,"color":7,"origin":[-1.40625,10.3125,0],"uv_offset":[16,48],"faces":{"north":{"uv":[20,52,24,58],"texture":0},"east":{"uv":[16,52,20,58],"texture":0},"south":{"uv":[28,52,32,58],"texture":0},"west":{"uv":[24,52,28,58],"texture":0},"up":{"uv":[24,52,20,48],"texture":0},"down":{"uv":[24,58,20,62],"texture":0}},"type":"cube","uuid":"0e370edc-7b05-dccf-dd2a-a92cfe9f3e22"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,5.64063,-1.85937],"to":[-0.01562,11.23438,1.85938],"autouv":0,"color":7,"inflate":0.25,"origin":[-1.40625,10.3125,0],"uv_offset":[0,48],"faces":{"north":{"uv":[4,52,8,58],"texture":0},"east":{"uv":[0,52,4,58],"texture":0},"south":{"uv":[12,52,16,58],"texture":0},"west":{"uv":[8,52,12,58],"texture":0},"up":{"uv":[8,52,4,48],"texture":0},"down":{"uv":[16,48,12,52],"texture":0}},"type":"cube","uuid":"dc189735-0a58-619b-07ef-feec37d65e7d"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,0,-1.875],"to":[0,5.625,1.875],"autouv":0,"color":6,"origin":[-1.40625,4.6875,0],"uv_offset":[16,54],"faces":{"north":{"uv":[20,58,24,64],"texture":0},"east":{"uv":[16,58,20,64],"texture":0},"south":{"uv":[28,58,32,64],"texture":0},"west":{"uv":[24,58,28,64],"texture":0},"up":{"uv":[24,58,20,54],"texture":0},"down":{"uv":[28,48,24,52],"texture":0}},"type":"cube","uuid":"6bcf768b-beab-4c39-6d96-91266036a4e1"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,0.01563,-1.85938],"to":[-0.01562,5.60938,1.85937],"autouv":0,"color":6,"inflate":0.25,"origin":[-1.40625,4.6875,-0.00001],"uv_offset":[0,54],"faces":{"north":{"uv":[4,58,8,64],"texture":0},"east":{"uv":[0,58,4,64],"texture":0},"south":{"uv":[12,58,16,64],"texture":0},"west":{"uv":[8,58,12,64],"texture":0},"up":{"uv":[4,52,0,48],"texture":0},"down":{"uv":[12,48,8,52],"texture":0}},"type":"cube","uuid":"274282a7-bc7b-02f8-4dce-84226e9dd9c1"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,22.5,-3.75],"to":[3.75,30,3.75],"autouv":0,"color":4,"origin":[0,22.5,-1.875],"faces":{"north":{"uv":[8,8,16,16],"texture":0},"east":{"uv":[0,8,8,16],"texture":0},"south":{"uv":[24,8,32,16],"texture":0},"west":{"uv":[16,8,24,16],"texture":0},"up":{"uv":[16,8,8,0],"texture":0},"down":{"uv":[24,0,16,8],"texture":0}},"type":"cube","uuid":"7f60fbaf-510d-2e5f-b7d2-9111e08443cd"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,22.51563,-3.73437],"to":[3.73438,29.98438,3.73438],"autouv":0,"color":4,"inflate":0.5,"origin":[0,22.5,-1.875],"uv_offset":[32,0],"faces":{"north":{"uv":[40,8,48,16],"texture":0},"east":{"uv":[32,8,40,16],"texture":0},"south":{"uv":[56,8,64,16],"texture":0},"west":{"uv":[48,8,56,16],"texture":0},"up":{"uv":[48,8,40,0],"texture":0},"down":{"uv":[56,0,48,8],"texture":0}},"type":"cube","uuid":"e0f94313-bf88-492d-0c68-bd6b466a3b68"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.15,15,-0.75],"to":[6.85,16.5,0.75],"autouv":0,"color":2,"origin":[5,10.5,-1],"faces":{"north":{"uv":[27,22,29,24],"texture":1},"east":{"uv":[27,24,29,26],"texture":1},"south":{"uv":[27,26,29,28],"texture":1},"west":{"uv":[0,28,2,30],"texture":1},"up":{"uv":[30,2,28,0],"texture":1},"down":{"uv":[30,9,28,11],"texture":1}},"type":"cube","uuid":"aa7d3639-52cb-087e-0f54-7e31afe0d4b3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,19,0.25],"to":[6.8,21,3.75],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[28,11,30,13],"texture":1},"east":{"uv":[17,21,21,23],"texture":1},"south":{"uv":[19,28,21,30],"texture":1},"west":{"uv":[21,21,25,23],"texture":1},"up":{"uv":[24,4,22,0],"texture":1},"down":{"uv":[24,4,22,8],"texture":1}},"type":"cube","uuid":"596ead89-97bf-b419-dc31-372fe3dee9b9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.75,18.5,-1.25],"to":[7.25,21,1.25],"autouv":0,"color":2,"rotation":[-45,0,0],"origin":[6,19.75,0],"faces":{"north":{"uv":[17,18,20,21],"texture":1},"east":{"uv":[19,0,22,3],"texture":1},"south":{"uv":[19,3,22,6],"texture":1},"west":{"uv":[19,6,22,9],"texture":1},"up":{"uv":[22,12,19,9],"texture":1},"down":{"uv":[22,12,19,15],"texture":1}},"type":"cube","uuid":"4853fc6c-23a8-6a4b-07e2-d89b3aaa8c1c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.225,19,0.375],"to":[6.775,20.5,2.5],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,19.5,2.625],"faces":{"north":{"uv":[21,28,23,30],"texture":1},"east":{"uv":[23,28,25,30],"texture":1},"south":{"uv":[25,28,27,30],"texture":1},"west":{"uv":[27,28,29,30],"texture":1},"up":{"uv":[31,4,29,2],"texture":1},"down":{"uv":[31,4,29,6],"texture":1}},"type":"cube","uuid":"a04ef22a-5322-93ad-5281-e6b33566d6af"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.225,19,-2.5],"to":[6.775,20.5,-0.375],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,19.5,-2.625],"faces":{"north":{"uv":[29,6,31,8],"texture":1},"east":{"uv":[29,13,31,15],"texture":1},"south":{"uv":[29,15,31,17],"texture":1},"west":{"uv":[29,17,31,19],"texture":1},"up":{"uv":[31,21,29,19],"texture":1},"down":{"uv":[31,21,29,23],"texture":1}},"type":"cube","uuid":"dbcf2b15-20a0-6f43-48de-9c73557825c3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.15,17.5,-0.75],"to":[6.85,19,0.75],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[29,23,31,25],"texture":1},"east":{"uv":[29,25,31,27],"texture":1},"south":{"uv":[29,27,31,29],"texture":1},"west":{"uv":[29,29,31,31],"texture":1},"up":{"uv":[2,32,0,30],"texture":1},"down":{"uv":[32,0,30,2],"texture":1}},"type":"cube","uuid":"656eb85b-2130-71d1-8400-036016caf911"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.4,10.05,-0.6],"to":[6.6,10.75,0.6],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,10.4,0],"faces":{"north":{"uv":[28,2,29,3],"texture":1},"east":{"uv":[29,8,30,9],"texture":1},"south":{"uv":[30,12,31,13],"texture":1},"west":{"uv":[27,35,28,36],"texture":1},"up":{"uv":[36,29,35,28],"texture":1},"down":{"uv":[30,35,29,36],"texture":1}},"type":"cube","uuid":"61014d9b-3a39-4bbb-24b1-c1796ff81448"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,19.925,2.375],"to":[6.825,21.675,3.6],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,20.925,2.3],"faces":{"north":{"uv":[5,30,7,32],"texture":1},"east":{"uv":[22,16,23,18],"texture":1},"south":{"uv":[30,8,32,10],"texture":1},"west":{"uv":[24,14,25,16],"texture":1},"up":{"uv":[25,21,23,20],"texture":1},"down":{"uv":[34,11,32,12],"texture":1}},"type":"cube","uuid":"6e13574b-8944-aaee-8ad1-92edda71d4a3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,19.925,-3.6],"to":[6.825,21.675,-2.375],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,20.925,-2.3],"faces":{"north":{"uv":[30,10,32,12],"texture":1},"east":{"uv":[33,9,34,11],"texture":1},"south":{"uv":[17,30,19,32],"texture":1},"west":{"uv":[33,12,34,14],"texture":1},"up":{"uv":[35,4,33,3],"texture":1},"down":{"uv":[35,14,33,15],"texture":1}},"type":"cube","uuid":"5e0081ae-2c0e-03b7-b533-f33b34b8b9c8"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.25,6.25,-0.5],"to":[6.75,7.5,0.75],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,7,0],"faces":{"north":{"uv":[33,31,35,32],"texture":1},"east":{"uv":[34,35,35,36],"texture":1},"south":{"uv":[33,32,35,33],"texture":1},"west":{"uv":[35,34,36,35],"texture":1},"up":{"uv":[35,34,33,33],"texture":1},"down":{"uv":[36,0,34,1],"texture":1}},"type":"cube","uuid":"926dfb9a-8a36-2d53-6026-df9e212d6187"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5,13.75,-0.9],"to":[7,15,0.9],"autouv":0,"color":2,"origin":[5,10.25,-1],"faces":{"north":{"uv":[34,1,36,2],"texture":1},"east":{"uv":[34,2,36,3],"texture":1},"south":{"uv":[34,4,36,5],"texture":1},"west":{"uv":[5,34,7,35],"texture":1},"up":{"uv":[23,32,21,30],"texture":1},"down":{"uv":[25,30,23,32],"texture":1}},"type":"cube","uuid":"34f7ace6-abb3-82f1-ffe6-aa94ccf29240"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.025,14.05,0.725],"to":[6.975,15.2,1.2],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,14.675,0.3],"faces":{"north":{"uv":[35,3,37,4],"texture":1},"east":{"uv":[18,38,19,39],"texture":1},"south":{"uv":[4,35,6,36],"texture":1},"west":{"uv":[38,18,39,19],"texture":1},"up":{"uv":[8,36,6,35],"texture":1},"down":{"uv":[15,35,13,36],"texture":1}},"type":"cube","uuid":"3616c0c5-2bd5-b180-4ed5-2708c59c41bc"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.025,14.05,-1.2],"to":[6.975,15.2,-0.725],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,14.675,-0.3],"faces":{"north":{"uv":[35,14,37,15],"texture":1},"east":{"uv":[19,38,20,39],"texture":1},"south":{"uv":[15,35,17,36],"texture":1},"west":{"uv":[38,19,39,20],"texture":1},"up":{"uv":[19,36,17,35],"texture":1},"down":{"uv":[21,35,19,36],"texture":1}},"type":"cube","uuid":"20fe1d98-3b1b-7a8f-7945-78be2423e61c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.05,13.025,-0.9],"to":[6.95,14.3,0.375],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,13.4,0],"faces":{"north":{"uv":[34,30,36,31],"texture":1},"east":{"uv":[17,38,18,39],"texture":1},"south":{"uv":[31,34,33,35],"texture":1},"west":{"uv":[38,17,39,18],"texture":1},"up":{"uv":[35,35,33,34],"texture":1},"down":{"uv":[4,35,2,36],"texture":1}},"type":"cube","uuid":"bec338ce-2cb6-bade-c2a0-bf4b0256b6b2"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.4,9.05,-0.6],"to":[6.6,9.75,0.6],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,9.4,0],"faces":{"north":{"uv":[22,38,23,39],"texture":1},"east":{"uv":[38,22,39,23],"texture":1},"south":{"uv":[23,38,24,39],"texture":1},"west":{"uv":[38,23,39,24],"texture":1},"up":{"uv":[25,39,24,38],"texture":1},"down":{"uv":[39,24,38,25],"texture":1}},"type":"cube","uuid":"cc8271b8-7122-2c3d-7a10-390515d38ea2"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.4,8.05,-0.6],"to":[6.6,8.75,0.6],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,8.4,0],"faces":{"north":{"uv":[35,35,36,36],"texture":1},"east":{"uv":[0,36,1,37],"texture":1},"south":{"uv":[36,0,37,1],"texture":1},"west":{"uv":[1,36,2,37],"texture":1},"up":{"uv":[37,2,36,1],"texture":1},"down":{"uv":[3,36,2,37],"texture":1}},"type":"cube","uuid":"2d28b727-641e-2dc9-8e5f-15c9ed002244"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,7,-0.5],"to":[6.5,13.75,0.5],"autouv":0,"color":2,"origin":[5,10.5,-1],"faces":{"north":{"uv":[2,24,3,31],"texture":1},"east":{"uv":[3,24,4,31],"texture":1},"south":{"uv":[4,24,5,31],"texture":1},"west":{"uv":[24,4,25,11],"texture":1},"up":{"uv":[37,3,36,2],"texture":1},"down":{"uv":[4,36,3,37],"texture":1}},"type":"cube","uuid":"68112592-c5eb-c6ba-7370-8df3fd15c5be"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[6.3,14.7,0.925],"to":[6.75,19.15,1.675],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6.55,17.075,1.425],"faces":{"north":{"uv":[25,30,26,34],"texture":1},"east":{"uv":[26,30,27,34],"texture":1},"south":{"uv":[27,30,28,34],"texture":1},"west":{"uv":[28,30,29,34],"texture":1},"up":{"uv":[5,37,4,36],"texture":1},"down":{"uv":[37,4,36,5],"texture":1}},"type":"cube","uuid":"6bfcb5a6-b98e-2362-4350-0cff8e7c8417"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.25,14.7,0.925],"to":[5.7,19.15,1.675],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[5.45,17.075,1.425],"faces":{"north":{"uv":[2,31,3,35],"texture":1},"east":{"uv":[31,2,32,6],"texture":1},"south":{"uv":[3,31,4,35],"texture":1},"west":{"uv":[4,31,5,35],"texture":1},"up":{"uv":[6,37,5,36],"texture":1},"down":{"uv":[37,5,36,6],"texture":1}},"type":"cube","uuid":"8ec387cf-bccc-0ef1-a478-2c3c0c430d41"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.95,18.575,-5.425],"to":[7.05,21.45,-3.75],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,20.925,-4.45],"faces":{"north":{"uv":[21,25,23,28],"texture":1},"east":{"uv":[23,25,25,28],"texture":1},"south":{"uv":[25,25,27,28],"texture":1},"west":{"uv":[26,0,28,3],"texture":1},"up":{"uv":[33,8,31,6],"texture":1},"down":{"uv":[33,12,31,14],"texture":1}},"type":"cube","uuid":"da16de9a-4659-1141-c033-4ab1a11199da"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,19,-3.75],"to":[6.8,21,-0.25],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[31,14,33,16],"texture":1},"east":{"uv":[19,23,23,25],"texture":1},"south":{"uv":[31,16,33,18],"texture":1},"west":{"uv":[23,23,27,25],"texture":1},"up":{"uv":[2,28,0,24],"texture":1},"down":{"uv":[26,0,24,4],"texture":1}},"type":"cube","uuid":"b55c73af-0b1d-14ae-5198-2782a6d7294a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,18.075,5.2],"to":[6.8,20.675,6.3],"autouv":0,"color":2,"origin":[5,12.75,1.75],"faces":{"north":{"uv":[26,10,28,13],"texture":1},"east":{"uv":[32,28,33,31],"texture":1},"south":{"uv":[27,3,29,6],"texture":1},"west":{"uv":[32,31,33,34],"texture":1},"up":{"uv":[36,14,34,13],"texture":1},"down":{"uv":[36,15,34,16],"texture":1}},"type":"cube","uuid":"a2bc1c86-9f7f-2d61-139f-584cbbdc9ca8"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.425,5.875],"to":[6.55,20.325,6.65],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,19.475,6.2],"faces":{"north":{"uv":[21,35,22,37],"texture":1},"east":{"uv":[35,21,36,23],"texture":1},"south":{"uv":[22,35,23,37],"texture":1},"west":{"uv":[23,35,24,37],"texture":1},"up":{"uv":[21,39,20,38],"texture":1},"down":{"uv":[39,20,38,21],"texture":1}},"type":"cube","uuid":"976bb6d9-d09f-36bb-8bb6-62fe808734f1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.425,-6.65],"to":[6.55,20.325,-5.875],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,19.475,-6.2],"faces":{"north":{"uv":[35,23,36,25],"texture":1},"east":{"uv":[24,35,25,37],"texture":1},"south":{"uv":[35,25,36,27],"texture":1},"west":{"uv":[26,35,27,37],"texture":1},"up":{"uv":[22,39,21,38],"texture":1},"down":{"uv":[39,21,38,22],"texture":1}},"type":"cube","uuid":"db87f23a-29dc-e8a1-0d0d-9a2f369185af"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.95,18.575,3.75],"to":[7.05,21.45,5.425],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,20.925,4.45],"faces":{"north":{"uv":[5,27,7,30],"texture":1},"east":{"uv":[27,6,29,9],"texture":1},"south":{"uv":[27,13,29,16],"texture":1},"west":{"uv":[27,16,29,19],"texture":1},"up":{"uv":[33,20,31,18],"texture":1},"down":{"uv":[33,20,31,22],"texture":1}},"type":"cube","uuid":"0714f281-db15-8143-c32f-e89576734cad"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.725,4.475],"to":[6.55,20.2,5.2],"autouv":0,"color":2,"origin":[5,12.75,1.75],"faces":{"north":{"uv":[8,36,9,37],"texture":1},"east":{"uv":[36,8,37,9],"texture":1},"south":{"uv":[9,36,10,37],"texture":1},"west":{"uv":[36,9,37,10],"texture":1},"up":{"uv":[11,37,10,36],"texture":1},"down":{"uv":[37,10,36,11],"texture":1}},"type":"cube","uuid":"a4916b0b-ece2-a56b-c694-cb963a9a80b5"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,18.075,-6.3],"to":[6.8,20.675,-5.2],"autouv":0,"color":2,"origin":[5,12.75,-1.75],"faces":{"north":{"uv":[17,27,19,30],"texture":1},"east":{"uv":[33,0,34,3],"texture":1},"south":{"uv":[27,19,29,22],"texture":1},"west":{"uv":[33,6,34,9],"texture":1},"up":{"uv":[36,17,34,16],"texture":1},"down":{"uv":[36,17,34,18],"texture":1}},"type":"cube","uuid":"9916eeef-31e1-9c75-ffda-14bd4d589032"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.725,-5.2],"to":[6.55,20.2,-4.475],"autouv":0,"color":2,"origin":[5,12.75,-1.75],"faces":{"north":{"uv":[11,36,12,37],"texture":1},"east":{"uv":[36,11,37,12],"texture":1},"south":{"uv":[12,36,13,37],"texture":1},"west":{"uv":[36,12,37,13],"texture":1},"up":{"uv":[14,37,13,36],"texture":1},"down":{"uv":[37,13,36,14],"texture":1}},"type":"cube","uuid":"cb63eadb-42fb-9f2c-0cb2-f6b85a7607f9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.25,14.7,-1.675],"to":[5.7,19.15,-0.925],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[5.45,17.075,-1.425],"faces":{"north":{"uv":[31,22,32,26],"texture":1},"east":{"uv":[31,26,32,30],"texture":1},"south":{"uv":[29,31,30,35],"texture":1},"west":{"uv":[30,31,31,35],"texture":1},"up":{"uv":[15,37,14,36],"texture":1},"down":{"uv":[16,36,15,37],"texture":1}},"type":"cube","uuid":"8ae9fcff-9cca-67c2-69e2-f50e85162de2"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[6.3,14.7,-1.675],"to":[6.75,19.15,-0.925],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6.55,17.075,-1.425],"faces":{"north":{"uv":[31,30,32,34],"texture":1},"east":{"uv":[0,32,1,36],"texture":1},"south":{"uv":[32,0,33,4],"texture":1},"west":{"uv":[1,32,2,36],"texture":1},"up":{"uv":[37,16,36,15],"texture":1},"down":{"uv":[17,36,16,37],"texture":1}},"type":"cube","uuid":"792785c8-f110-dcf7-d0e4-61f223963cfd"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5,16.5,-1.05],"to":[7,17.5,1.05],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[34,18,36,19],"texture":1},"east":{"uv":[19,34,21,35],"texture":1},"south":{"uv":[34,19,36,20],"texture":1},"west":{"uv":[34,20,36,21],"texture":1},"up":{"uv":[34,6,32,4],"texture":1},"down":{"uv":[7,32,5,34],"texture":1}},"type":"cube","uuid":"2ad8829a-0eaf-c32f-765a-76ad72a1b336"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.75,16.75,1],"to":[6.25,17.25,1.7],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[36,16,37,17],"texture":1},"east":{"uv":[17,36,18,37],"texture":1},"south":{"uv":[36,17,37,18],"texture":1},"west":{"uv":[18,36,19,37],"texture":1},"up":{"uv":[37,19,36,18],"texture":1},"down":{"uv":[20,36,19,37],"texture":1}},"type":"cube","uuid":"2181542e-699e-c408-2ec8-b291a6325c33"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,16.75,1.55],"to":[6.5,17.25,2.05],"autouv":0,"color":2,"rotation":[-45,0,0],"origin":[6,17,1.8],"faces":{"north":{"uv":[36,19,37,20],"texture":1},"east":{"uv":[20,36,21,37],"texture":1},"south":{"uv":[36,20,37,21],"texture":1},"west":{"uv":[36,21,37,22],"texture":1},"up":{"uv":[37,23,36,22],"texture":1},"down":{"uv":[37,23,36,24],"texture":1}},"type":"cube","uuid":"58cd5f7e-7a90-1ed7-b5c8-5d739d36bb28"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.75,16.75,-1.7],"to":[6.25,17.25,-1],"autouv":0,"color":2,"origin":[5,13,1],"faces":{"north":{"uv":[36,24,37,25],"texture":1},"east":{"uv":[25,36,26,37],"texture":1},"south":{"uv":[36,25,37,26],"texture":1},"west":{"uv":[36,26,37,27],"texture":1},"up":{"uv":[28,37,27,36],"texture":1},"down":{"uv":[37,27,36,28],"texture":1}},"type":"cube","uuid":"71ebf1c3-a5f4-6dbf-8533-0b7bdc6093f4"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,16.75,-2.05],"to":[6.5,17.25,-1.55],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,17,-1.8],"faces":{"north":{"uv":[28,36,29,37],"texture":1},"east":{"uv":[36,28,37,29],"texture":1},"south":{"uv":[29,36,30,37],"texture":1},"west":{"uv":[36,29,37,30],"texture":1},"up":{"uv":[31,37,30,36],"texture":1},"down":{"uv":[37,30,36,31],"texture":1}},"type":"cube","uuid":"3f2aa919-4264-20d2-4dc0-5b3963226082"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,21.3,-3.725],"to":[6.825,21.95,-3.5],"autouv":0,"color":2,"rotation":[-45,0,0],"origin":[6,21.2,-3.425],"faces":{"north":{"uv":[34,5,36,6],"texture":1},"east":{"uv":[6,36,7,37],"texture":1},"south":{"uv":[34,6,36,7],"texture":1},"west":{"uv":[36,6,37,7],"texture":1},"up":{"uv":[36,8,34,7],"texture":1},"down":{"uv":[36,8,34,9],"texture":1}},"type":"cube","uuid":"188d0aa7-7147-c70b-58ea-850b803f6a8f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,21.3,3.5],"to":[6.825,21.95,3.725],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,21.2,3.425],"faces":{"north":{"uv":[34,9,36,10],"texture":1},"east":{"uv":[7,36,8,37],"texture":1},"south":{"uv":[34,10,36,11],"texture":1},"west":{"uv":[36,7,37,8],"texture":1},"up":{"uv":[36,12,34,11],"texture":1},"down":{"uv":[36,12,34,13],"texture":1}},"type":"cube","uuid":"7de5534c-d30a-43f1-6eb9-25858b29fce0"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,41.05,-1.3],"to":[6.5,44.55,0.7],"autouv":0,"color":1,"rotation":[-20,0,0],"origin":[6,48.8,-0.55],"faces":{"north":{"uv":[19,30,20,34],"texture":1},"east":{"uv":[23,16,25,20],"texture":1},"south":{"uv":[20,30,21,34],"texture":1},"west":{"uv":[17,23,19,27],"texture":1},"up":{"uv":[34,21,33,19],"texture":1},"down":{"uv":[34,21,33,23],"texture":1}},"type":"cube","uuid":"e85c96f1-e8a9-dc1b-28ef-5270bd4fdeea"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24,1],"to":[6.5,41.75,2.25],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[8,0,9,18],"texture":1},"east":{"uv":[9,0,10,18],"texture":1},"south":{"uv":[10,0,11,18],"texture":1},"west":{"uv":[11,0,12,18],"texture":1},"up":{"uv":[26,11,25,10],"texture":1},"down":{"uv":[27,3,26,4],"texture":1}},"type":"cube","uuid":"e2626689-c30d-fc58-7143-b714a916e0e3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,21,2.45],"to":[6.5,24,3.15],"autouv":0,"color":1,"rotation":[0,45,0],"origin":[6,28.5,2.95],"faces":{"north":{"uv":[13,32,14,35],"texture":1},"east":{"uv":[14,32,15,35],"texture":1},"south":{"uv":[15,32,16,35],"texture":1},"west":{"uv":[16,32,17,35],"texture":1},"up":{"uv":[36,30,35,29],"texture":1},"down":{"uv":[31,35,30,36],"texture":1}},"type":"cube","uuid":"b4e24ddd-0288-312f-4d29-4ee1bcad5926"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,24,1.95],"to":[6.5,41.75,2.65],"autouv":0,"color":1,"rotation":[0,45,0],"origin":[6,31.5,2.45],"faces":{"north":{"uv":[9,18,10,36],"texture":1},"east":{"uv":[10,18,11,36],"texture":1},"south":{"uv":[11,18,12,36],"texture":1},"west":{"uv":[12,18,13,36],"texture":1},"up":{"uv":[34,36,33,35],"texture":1},"down":{"uv":[36,33,35,34],"texture":1}},"type":"cube","uuid":"c7057fe5-eff1-3481-3701-b964a84f1c58"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,24,-2.65],"to":[6.5,41.75,-1.95],"autouv":0,"color":1,"rotation":[0,-45,0],"origin":[6,31.5,-2.45],"faces":{"north":{"uv":[16,0,17,18],"texture":1},"east":{"uv":[17,0,18,18],"texture":1},"south":{"uv":[18,0,19,18],"texture":1},"west":{"uv":[8,18,9,36],"texture":1},"up":{"uv":[32,36,31,35],"texture":1},"down":{"uv":[36,31,35,32],"texture":1}},"type":"cube","uuid":"662518b3-6709-eb4e-6278-8c012d80c705"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,21,-3.15],"to":[6.5,24,-2.45],"autouv":0,"color":1,"rotation":[0,-45,0],"origin":[6,28.5,-2.95],"faces":{"north":{"uv":[32,22,33,25],"texture":1},"east":{"uv":[23,32,24,35],"texture":1},"south":{"uv":[24,32,25,35],"texture":1},"west":{"uv":[32,25,33,28],"texture":1},"up":{"uv":[33,36,32,35],"texture":1},"down":{"uv":[36,32,35,33],"texture":1}},"type":"cube","uuid":"5bf81b09-08ec-d7a1-c186-9a432aac7ca1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.75,21,-1.25],"to":[6.25,44.5,1.25],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[6,0,7,24],"texture":1},"east":{"uv":[0,0,3,24],"texture":1},"south":{"uv":[7,0,8,24],"texture":1},"west":{"uv":[3,0,6,24],"texture":1},"up":{"uv":[8,35,7,32],"texture":1},"down":{"uv":[33,8,32,11],"texture":1}},"type":"cube","uuid":"a9a094c2-0f3d-58d6-b21e-6ba6ba3b526b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,43.85,-2.15],"to":[6.5,46.9,0.9],"autouv":0,"color":1,"rotation":[-45,0,0],"origin":[6,46,0],"faces":{"north":{"uv":[5,24,7,27],"texture":1},"east":{"uv":[19,15,22,18],"texture":1},"south":{"uv":[24,11,26,14],"texture":1},"west":{"uv":[20,18,23,21],"texture":1},"up":{"uv":[27,7,25,4],"texture":1},"down":{"uv":[27,7,25,10],"texture":1}},"type":"cube","uuid":"64526f07-ded7-1117-c95e-5e72febd29f7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24,-2.25],"to":[6.5,41.75,-1],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[12,0,13,18],"texture":1},"east":{"uv":[13,0,14,18],"texture":1},"south":{"uv":[14,0,15,18],"texture":1},"west":{"uv":[15,0,16,18],"texture":1},"up":{"uv":[27,14,26,13],"texture":1},"down":{"uv":[28,9,27,10],"texture":1}},"type":"cube","uuid":"d6541026-10ee-fe6a-8fe9-72cf75d33923"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,21,1.25],"to":[6.5,24,2.75],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[21,32,22,35],"texture":1},"east":{"uv":[19,25,21,28],"texture":1},"south":{"uv":[22,32,23,35],"texture":1},"west":{"uv":[25,20,27,23],"texture":1},"up":{"uv":[34,29,33,27],"texture":1},"down":{"uv":[34,29,33,31],"texture":1}},"type":"cube","uuid":"791744bb-3a31-3779-4bdf-f486a6933875"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,21,-2.75],"to":[6.5,24,-1.25],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[17,32,18,35],"texture":1},"east":{"uv":[25,14,27,17],"texture":1},"south":{"uv":[18,32,19,35],"texture":1},"west":{"uv":[25,17,27,20],"texture":1},"up":{"uv":[34,25,33,23],"texture":1},"down":{"uv":[34,25,33,27],"texture":1}},"type":"cube","uuid":"d9dda869-2759-4199-c1d8-b42d195aa834"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,41.05,-0.7],"to":[6.5,44.55,1.3],"autouv":0,"color":1,"rotation":[20,0,0],"origin":[6,48.8,0.55],"faces":{"north":{"uv":[7,24,8,28],"texture":1},"east":{"uv":[22,8,24,12],"texture":1},"south":{"uv":[7,28,8,32],"texture":1},"west":{"uv":[22,12,24,16],"texture":1},"up":{"uv":[34,17,33,15],"texture":1},"down":{"uv":[34,17,33,19],"texture":1}},"type":"cube","uuid":"abcc99c6-960c-1045-82bd-0a39d53f7a49"},{"name":"rune_1","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,21.975,-0.125],"to":[6.5,22.475,0.125],"autouv":0,"color":0,"origin":[6,35.975,0.375],"faces":{"north":{"uv":[2,38,3,39],"texture":1},"east":{"uv":[38,2,39,3],"texture":1},"south":{"uv":[3,38,4,39],"texture":1},"west":{"uv":[38,3,39,4],"texture":1},"up":{"uv":[5,39,4,38],"texture":1},"down":{"uv":[39,4,38,5],"texture":1}},"type":"cube","uuid":"44592a16-3ff8-e5d6-5197-a3d5fdb91e94"},{"name":"rune_2","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,22.5625,-0.75],"to":[6.5,23.3125,-0.375],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,21.8125,0],"faces":{"north":{"uv":[37,29,38,30],"texture":1},"east":{"uv":[30,37,31,38],"texture":1},"south":{"uv":[37,30,38,31],"texture":1},"west":{"uv":[31,37,32,38],"texture":1},"up":{"uv":[38,32,37,31],"texture":1},"down":{"uv":[33,37,32,38],"texture":1}},"type":"cube","uuid":"49542da5-539a-7743-3bbc-afdee9d770c3"},{"name":"rune_2","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,22.1875,-1.5],"to":[6.5,22.5625,-0.375],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,21.8125,0],"faces":{"north":{"uv":[37,36,38,37],"texture":1},"east":{"uv":[37,37,38,38],"texture":1},"south":{"uv":[0,38,1,39],"texture":1},"west":{"uv":[38,0,39,1],"texture":1},"up":{"uv":[2,39,1,38],"texture":1},"down":{"uv":[39,1,38,2],"texture":1}},"type":"cube","uuid":"a79acc07-9567-e68d-0a57-109e40d9acbc"},{"name":"rune_3","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24.25,-1.875],"to":[6.5,24.625,-0.375],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,25,0],"faces":{"north":{"uv":[37,35,38,36],"texture":1},"east":{"uv":[26,34,28,35],"texture":1},"south":{"uv":[36,37,37,38],"texture":1},"west":{"uv":[34,27,36,28],"texture":1},"up":{"uv":[29,36,28,34],"texture":1},"down":{"uv":[35,28,34,30],"texture":1}},"type":"cube","uuid":"8cadf035-ee63-7e4e-858c-fc3f8115fa7e"},{"name":"rune_3","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,23.125,-0.75],"to":[6.5,24.25,-0.375],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,25,0],"faces":{"north":{"uv":[37,32,38,33],"texture":1},"east":{"uv":[33,37,34,38],"texture":1},"south":{"uv":[37,33,38,34],"texture":1},"west":{"uv":[34,37,35,38],"texture":1},"up":{"uv":[38,35,37,34],"texture":1},"down":{"uv":[36,37,35,38],"texture":1}},"type":"cube","uuid":"a9027c5e-52c6-19e9-e232-a35125734ea3"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24.35,-0.125],"to":[6.5,37.85,0.125],"autouv":0,"color":0,"origin":[6,36.1,0.375],"faces":{"north":{"uv":[13,18,14,32],"texture":1},"east":{"uv":[14,18,15,32],"texture":1},"south":{"uv":[15,18,16,32],"texture":1},"west":{"uv":[16,18,17,32],"texture":1},"up":{"uv":[38,11,37,10],"texture":1},"down":{"uv":[12,37,11,38],"texture":1}},"type":"cube","uuid":"2cec3ae3-79ee-9f96-74f5-0e90611c5e6d"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,-0.75],"to":[6.5,26.05,-0.3],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[5.875,25.725,-0.425],"faces":{"north":{"uv":[5,38,6,39],"texture":1},"east":{"uv":[38,5,39,6],"texture":1},"south":{"uv":[6,38,7,39],"texture":1},"west":{"uv":[38,6,39,7],"texture":1},"up":{"uv":[8,39,7,38],"texture":1},"down":{"uv":[39,7,38,8],"texture":1}},"type":"cube","uuid":"e204bb47-1379-ea43-8d02-0b48c88a6282"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,-0.375],"to":[6.5,25.85,-0.125],"autouv":0,"color":0,"origin":[6,30.1,0.375],"faces":{"north":{"uv":[8,38,9,39],"texture":1},"east":{"uv":[38,8,39,9],"texture":1},"south":{"uv":[9,38,10,39],"texture":1},"west":{"uv":[38,9,39,10],"texture":1},"up":{"uv":[11,39,10,38],"texture":1},"down":{"uv":[39,10,38,11],"texture":1}},"type":"cube","uuid":"4c44df55-108a-c9c8-3c9c-3c3bdd7c3a6a"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,0.3],"to":[6.5,26.05,0.75],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[5.875,25.725,0.425],"faces":{"north":{"uv":[11,38,12,39],"texture":1},"east":{"uv":[38,11,39,12],"texture":1},"south":{"uv":[12,38,13,39],"texture":1},"west":{"uv":[38,12,39,13],"texture":1},"up":{"uv":[14,39,13,38],"texture":1},"down":{"uv":[39,13,38,14],"texture":1}},"type":"cube","uuid":"a4083602-2242-1b62-262d-bfd38f8c77f4"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,0.125],"to":[6.5,25.85,0.375],"autouv":0,"color":0,"origin":[6,30.1,-0.375],"faces":{"north":{"uv":[14,38,15,39],"texture":1},"east":{"uv":[38,14,39,15],"texture":1},"south":{"uv":[15,38,16,39],"texture":1},"west":{"uv":[38,15,39,16],"texture":1},"up":{"uv":[17,39,16,38],"texture":1},"down":{"uv":[39,16,38,17],"texture":1}},"type":"cube","uuid":"138546ac-7f07-33e3-b36d-0e06f98d386c"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.875,-0.5],"to":[6.5,38.375,-0.25],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,37.375,0],"faces":{"north":{"uv":[37,4,38,5],"texture":1},"east":{"uv":[5,37,6,38],"texture":1},"south":{"uv":[37,5,38,6],"texture":1},"west":{"uv":[6,37,7,38],"texture":1},"up":{"uv":[38,7,37,6],"texture":1},"down":{"uv":[8,37,7,38],"texture":1}},"type":"cube","uuid":"34d77773-c97c-7edd-a72c-887e3f21afcf"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.625,-1],"to":[6.5,37.875,-0.25],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,37.375,0],"faces":{"north":{"uv":[37,7,38,8],"texture":1},"east":{"uv":[8,37,9,38],"texture":1},"south":{"uv":[37,8,38,9],"texture":1},"west":{"uv":[9,37,10,38],"texture":1},"up":{"uv":[38,10,37,9],"texture":1},"down":{"uv":[11,37,10,38],"texture":1}},"type":"cube","uuid":"952d8c6c-c534-3ded-ebbb-8560169862f5"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.425,-0.825],"to":[6.5,37.575,-0.075],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[5.875,37.55,-0.2],"faces":{"north":{"uv":[37,11,38,12],"texture":1},"east":{"uv":[12,37,13,38],"texture":1},"south":{"uv":[37,12,38,13],"texture":1},"west":{"uv":[13,37,14,38],"texture":1},"up":{"uv":[38,14,37,13],"texture":1},"down":{"uv":[15,37,14,38],"texture":1}},"type":"cube","uuid":"677371f7-759a-8b3b-225b-89324ccd9e5c"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.925,-0.575],"to":[6.5,37.075,-0.075],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[5.875,37.05,-0.2],"faces":{"north":{"uv":[37,14,38,15],"texture":1},"east":{"uv":[15,37,16,38],"texture":1},"south":{"uv":[37,15,38,16],"texture":1},"west":{"uv":[16,37,17,38],"texture":1},"up":{"uv":[38,17,37,16],"texture":1},"down":{"uv":[18,37,17,38],"texture":1}},"type":"cube","uuid":"09b6fa84-9268-c1f0-d593-f49988b0a6a8"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.425,-0.325],"to":[6.5,36.575,-0.075],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[5.875,36.55,-0.2],"faces":{"north":{"uv":[37,17,38,18],"texture":1},"east":{"uv":[18,37,19,38],"texture":1},"south":{"uv":[37,18,38,19],"texture":1},"west":{"uv":[19,37,20,38],"texture":1},"up":{"uv":[38,20,37,19],"texture":1},"down":{"uv":[21,37,20,38],"texture":1}},"type":"cube","uuid":"2f533cab-6d66-5ebb-be57-520ac9cf1da9"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.425,0.075],"to":[6.5,36.575,0.325],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[5.875,36.55,0.2],"faces":{"north":{"uv":[37,20,38,21],"texture":1},"east":{"uv":[21,37,22,38],"texture":1},"south":{"uv":[37,21,38,22],"texture":1},"west":{"uv":[22,37,23,38],"texture":1},"up":{"uv":[38,23,37,22],"texture":1},"down":{"uv":[24,37,23,38],"texture":1}},"type":"cube","uuid":"e5a7d019-ec5d-ef93-5ffd-c092c664d2cb"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.925,0.075],"to":[6.5,37.075,0.575],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[5.875,37.05,0.2],"faces":{"north":{"uv":[37,23,38,24],"texture":1},"east":{"uv":[24,37,25,38],"texture":1},"south":{"uv":[37,24,38,25],"texture":1},"west":{"uv":[25,37,26,38],"texture":1},"up":{"uv":[38,26,37,25],"texture":1},"down":{"uv":[27,37,26,38],"texture":1}},"type":"cube","uuid":"11316f78-2485-c065-fa4d-7b1225df2bf6"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.425,0.075],"to":[6.5,37.575,0.825],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[5.875,37.55,0.2],"faces":{"north":{"uv":[37,26,38,27],"texture":1},"east":{"uv":[27,37,28,38],"texture":1},"south":{"uv":[37,27,38,28],"texture":1},"west":{"uv":[28,37,29,38],"texture":1},"up":{"uv":[38,29,37,28],"texture":1},"down":{"uv":[30,37,29,38],"texture":1}},"type":"cube","uuid":"e0d85bd5-8a87-a483-22e2-833204176141"},{"name":"rune_6","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,38.25,-0.5],"to":[6.5,39,-0.25],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,39.5,0],"faces":{"north":{"uv":[37,1,38,2],"texture":1},"east":{"uv":[2,37,3,38],"texture":1},"south":{"uv":[37,2,38,3],"texture":1},"west":{"uv":[3,37,4,38],"texture":1},"up":{"uv":[38,4,37,3],"texture":1},"down":{"uv":[5,37,4,38],"texture":1}},"type":"cube","uuid":"101292a8-6c83-621a-11ff-e6b17d4a82e2"},{"name":"rune_6","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,39,-1.25],"to":[6.5,39.25,-0.25],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,39.5,0],"faces":{"north":{"uv":[35,36,36,37],"texture":1},"east":{"uv":[36,35,37,36],"texture":1},"south":{"uv":[36,36,37,37],"texture":1},"west":{"uv":[0,37,1,38],"texture":1},"up":{"uv":[38,1,37,0],"texture":1},"down":{"uv":[2,37,1,38],"texture":1}},"type":"cube","uuid":"a7c81b6d-2419-772e-27a1-0068365ebce7"},{"name":"rune_7","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,40.9,-0.125],"to":[6.5,42.15,1.125],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[5.875,41.025,0],"faces":{"north":{"uv":[31,36,32,37],"texture":1},"east":{"uv":[36,31,37,32],"texture":1},"south":{"uv":[32,36,33,37],"texture":1},"west":{"uv":[36,32,37,33],"texture":1},"up":{"uv":[34,37,33,36],"texture":1},"down":{"uv":[37,33,36,34],"texture":1}},"type":"cube","uuid":"0ac8e87a-c521-a8ee-e14d-e205560a7626"},{"name":"rune_7","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,39.025,-0.125],"to":[6.5,41.025,0.125],"autouv":0,"color":0,"origin":[6,39.525,0.375],"faces":{"north":{"uv":[34,21,35,23],"texture":1},"east":{"uv":[34,23,35,25],"texture":1},"south":{"uv":[25,34,26,36],"texture":1},"west":{"uv":[34,25,35,27],"texture":1},"up":{"uv":[35,37,34,36],"texture":1},"down":{"uv":[37,34,36,35],"texture":1}},"type":"cube","uuid":"fc987521-07e4-10f9-b6fc-1512cb2da68b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1,35.8,-1],"to":[1,37.8,1],"autouv":1,"color":1,"visibility":false,"origin":[-1,36.8,-1],"faces":{"north":{"uv":[0,0,2,2]},"east":{"uv":[0,0,2,2]},"south":{"uv":[0,0,2,2]},"west":{"uv":[0,0,2,2]},"up":{"uv":[0,0,2,2]},"down":{"uv":[0,0,2,2]}},"type":"cube","uuid":"c8c75bdd-53bc-eb81-a2cb-0ea0dca06f35"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-8,0,-8],"to":[8,0,8],"autouv":1,"color":9,"visibility":false,"origin":[0,0,0],"faces":{"north":{"uv":[0,0,16,0]},"east":{"uv":[0,0,16,0]},"south":{"uv":[0,0,16,0]},"west":{"uv":[0,0,16,0]},"up":{"uv":[0,0,16,16]},"down":{"uv":[0,0,16,16]}},"type":"cube","uuid":"164a0f3b-aa15-e098-8e5a-f1fdf3e78bdd"}],"groups":[{"uuid":"778fa89c-759a-8884-89d9-238c555d2dc1","export":true,"locked":false,"origin":[0,17,0],"rotation":[0,0,0],"color":9,"name":"player_root","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"7e09ae80-d41f-d669-2538-64dec33e0e24","export":true,"locked":false,"origin":[0,17,0],"rotation":[0,0,0],"color":0,"name":"shadow","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":false},{"uuid":"9dc65952-10a9-876f-bd47-d6a7e9ec6183","export":true,"locked":false,"origin":[0,11.25,0],"rotation":[0,0,0],"color":1,"name":"phip_hip","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"e297aef6-7dfd-f100-2e7c-ab113699b922","export":true,"locked":false,"origin":[0,15,0],"rotation":[0,0,0],"color":2,"name":"pw_waist","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"a0c01522-9040-7533-fa11-f6a45d3d96ac","export":true,"locked":false,"origin":[0,18.75,0],"rotation":[0,0,0],"color":3,"name":"pc_chest","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"34097e46-c233-c03c-d8b9-aee154c9946f","export":true,"locked":false,"origin":[0,22.5,0],"rotation":[0,0,0],"color":4,"name":"h_ph_head","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"bfc2f156-b48b-dd08-1b9e-777d8ada16b2","export":true,"locked":false,"origin":[5,22,0],"rotation":[0,0,0],"color":4,"name":"pra_right_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"cf1618da-24d8-aab8-eebc-128815c02d35","export":true,"locked":false,"origin":[5,16.5,0],"rotation":[0,0,0],"color":8,"name":"prfa_right_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"fcaf8da0-0146-2587-b578-3e1af888deaa","export":true,"locked":false,"origin":[5.625,10.875,0],"rotation":[-90,0,0],"color":0,"name":"pri_right_item","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"859e9460-0016-acb1-017b-25fe27244cac","export":true,"locked":false,"origin":[5.625,44.875,0],"rotation":[0,0,0],"color":0,"name":"sword_point","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"dcb3a2dc-0f46-4628-586d-44f4265cc61c","export":true,"locked":false,"origin":[6,32.15507,0],"rotation":[0,0,0],"color":0,"name":"sword_body","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"b3135254-0351-3462-2479-e6a3286c89ff","export":true,"locked":false,"origin":[-5,22,0],"rotation":[0,0,0],"color":4,"name":"pla_left_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"1a9070b5-b8b6-b955-9f31-54f9625f8f3d","export":true,"locked":false,"origin":[-5,16.5,0],"rotation":[0,0,0],"color":8,"name":"plfa_left_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"0e94ff40-15b6-0b24-d0a0-447f000d761b","export":true,"locked":false,"origin":[-5.625,10.875,0],"rotation":[-90,0,0],"color":0,"name":"pli_left_item","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"7e8426f1-08b2-81a2-7703-cb76ff5e7003","export":true,"locked":false,"origin":[1.875,11.25,0],"rotation":[0,0,0],"color":7,"name":"prl_right_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"c6d9e946-1d10-482d-14b1-0766027adba8","export":true,"locked":false,"origin":[1.875,5.625,0],"rotation":[0,0,0],"color":6,"name":"prfl_right_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"5ef5d225-d5ae-6787-8838-b75ccb7a7a81","export":true,"locked":false,"origin":[-1.875,11.25,0],"rotation":[0,0,0],"color":7,"name":"pll_left_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"1b5cc202-c09e-faa0-5057-eb4ae60bf336","export":true,"locked":false,"origin":[-1.875,5.625,0],"rotation":[0,0,0],"color":6,"name":"plfl_left_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":false},{"uuid":"529b75e5-967f-8e76-9d18-0fc49998293a","export":true,"locked":false,"origin":[0,36.8,0],"rotation":[0,0,0],"color":0,"name":"tag_name","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":false},{"uuid":"dcbc3011-d7b7-bf7c-0fae-067573db08f5","export":true,"locked":false,"origin":[0,22.75,2],"rotation":[0,0,0],"color":0,"name":"cape_cape","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true}],"outliner":[{"uuid":"778fa89c-759a-8884-89d9-238c555d2dc1","isOpen":true,"children":[{"uuid":"7e09ae80-d41f-d669-2538-64dec33e0e24","isOpen":false,"children":["164a0f3b-aa15-e098-8e5a-f1fdf3e78bdd"]},{"uuid":"9dc65952-10a9-876f-bd47-d6a7e9ec6183","isOpen":true,"children":["ea42f7f7-a6f1-4479-c43f-48211bab5ed2","357ebf82-23ba-edb1-081f-dca75d94b83c",{"uuid":"e297aef6-7dfd-f100-2e7c-ab113699b922","isOpen":true,"children":["5ea74bdb-ba28-b8e3-103b-9be6ff2262da","f5b9f499-b26b-f912-1a54-151d702e13ed",{"uuid":"a0c01522-9040-7533-fa11-f6a45d3d96ac","isOpen":true,"children":[{"uuid":"dcbc3011-d7b7-bf7c-0fae-067573db08f5","isOpen":true,"children":[]},"dc1510db-a719-17b4-e253-c992a92c5d25","d51a8665-a2bc-af6e-1230-5acba07248e7",{"uuid":"34097e46-c233-c03c-d8b9-aee154c9946f","isOpen":true,"children":["7f60fbaf-510d-2e5f-b7d2-9111e08443cd","e0f94313-bf88-492d-0c68-bd6b466a3b68"]},{"uuid":"bfc2f156-b48b-dd08-1b9e-777d8ada16b2","isOpen":true,"children":["53d40d2e-0941-29f9-00ed-1b19c941dcd8","b17452ef-afbe-e010-f2c9-e0ba945faa1f",{"uuid":"cf1618da-24d8-aab8-eebc-128815c02d35","isOpen":true,"children":["e02c395d-e1bc-1375-a8ed-729e19544ce9","a509c2d7-53ef-2b11-1331-f716b9c210d6",{"uuid":"fcaf8da0-0146-2587-b578-3e1af888deaa","isOpen":true,"children":[{"uuid":"859e9460-0016-acb1-017b-25fe27244cac","isOpen":true,"children":[]},"44592a16-3ff8-e5d6-5197-a3d5fdb91e94","49542da5-539a-7743-3bbc-afdee9d770c3","a79acc07-9567-e68d-0a57-109e40d9acbc","8cadf035-ee63-7e4e-858c-fc3f8115fa7e","a9027c5e-52c6-19e9-e232-a35125734ea3","2cec3ae3-79ee-9f96-74f5-0e90611c5e6d","e204bb47-1379-ea43-8d02-0b48c88a6282","4c44df55-108a-c9c8-3c9c-3c3bdd7c3a6a","a4083602-2242-1b62-262d-bfd38f8c77f4","138546ac-7f07-33e3-b36d-0e06f98d386c","34d77773-c97c-7edd-a72c-887e3f21afcf","952d8c6c-c534-3ded-ebbb-8560169862f5","677371f7-759a-8b3b-225b-89324ccd9e5c","09b6fa84-9268-c1f0-d593-f49988b0a6a8","2f533cab-6d66-5ebb-be57-520ac9cf1da9","e5a7d019-ec5d-ef93-5ffd-c092c664d2cb","11316f78-2485-c065-fa4d-7b1225df2bf6","e0d85bd5-8a87-a483-22e2-833204176141","101292a8-6c83-621a-11ff-e6b17d4a82e2","a7c81b6d-2419-772e-27a1-0068365ebce7","0ac8e87a-c521-a8ee-e14d-e205560a7626","fc987521-07e4-10f9-b6fc-1512cb2da68b","aa7d3639-52cb-087e-0f54-7e31afe0d4b3","596ead89-97bf-b419-dc31-372fe3dee9b9","4853fc6c-23a8-6a4b-07e2-d89b3aaa8c1c","a04ef22a-5322-93ad-5281-e6b33566d6af","dbcf2b15-20a0-6f43-48de-9c73557825c3","656eb85b-2130-71d1-8400-036016caf911","61014d9b-3a39-4bbb-24b1-c1796ff81448","6e13574b-8944-aaee-8ad1-92edda71d4a3","5e0081ae-2c0e-03b7-b533-f33b34b8b9c8","926dfb9a-8a36-2d53-6026-df9e212d6187","34f7ace6-abb3-82f1-ffe6-aa94ccf29240","3616c0c5-2bd5-b180-4ed5-2708c59c41bc","20fe1d98-3b1b-7a8f-7945-78be2423e61c","bec338ce-2cb6-bade-c2a0-bf4b0256b6b2","cc8271b8-7122-2c3d-7a10-390515d38ea2","2d28b727-641e-2dc9-8e5f-15c9ed002244","68112592-c5eb-c6ba-7370-8df3fd15c5be","6bfcb5a6-b98e-2362-4350-0cff8e7c8417","8ec387cf-bccc-0ef1-a478-2c3c0c430d41","da16de9a-4659-1141-c033-4ab1a11199da","b55c73af-0b1d-14ae-5198-2782a6d7294a","a2bc1c86-9f7f-2d61-139f-584cbbdc9ca8","976bb6d9-d09f-36bb-8bb6-62fe808734f1","db87f23a-29dc-e8a1-0d0d-9a2f369185af","0714f281-db15-8143-c32f-e89576734cad","a4916b0b-ece2-a56b-c694-cb963a9a80b5","9916eeef-31e1-9c75-ffda-14bd4d589032","cb63eadb-42fb-9f2c-0cb2-f6b85a7607f9","8ae9fcff-9cca-67c2-69e2-f50e85162de2","792785c8-f110-dcf7-d0e4-61f223963cfd","2ad8829a-0eaf-c32f-765a-76ad72a1b336","2181542e-699e-c408-2ec8-b291a6325c33","58cd5f7e-7a90-1ed7-b5c8-5d739d36bb28","71ebf1c3-a5f4-6dbf-8533-0b7bdc6093f4","3f2aa919-4264-20d2-4dc0-5b3963226082","188d0aa7-7147-c70b-58ea-850b803f6a8f","7de5534c-d30a-43f1-6eb9-25858b29fce0",{"uuid":"dcb3a2dc-0f46-4628-586d-44f4265cc61c","isOpen":true,"children":["e85c96f1-e8a9-dc1b-28ef-5270bd4fdeea","e2626689-c30d-fc58-7143-b714a916e0e3","b4e24ddd-0288-312f-4d29-4ee1bcad5926","c7057fe5-eff1-3481-3701-b964a84f1c58","662518b3-6709-eb4e-6278-8c012d80c705","5bf81b09-08ec-d7a1-c186-9a432aac7ca1","a9a094c2-0f3d-58d6-b21e-6ba6ba3b526b","64526f07-ded7-1117-c95e-5e72febd29f7","d6541026-10ee-fe6a-8fe9-72cf75d33923","791744bb-3a31-3779-4bdf-f486a6933875","d9dda869-2759-4199-c1d8-b42d195aa834","abcc99c6-960c-1045-82bd-0a39d53f7a49"]}]}]}]},{"uuid":"b3135254-0351-3462-2479-e6a3286c89ff","isOpen":true,"children":["addb9f66-a2a4-46f7-b6af-f5a23c00fe70","5e7dc31c-c64a-ff15-90d9-52a6f77cb14b",{"uuid":"1a9070b5-b8b6-b955-9f31-54f9625f8f3d","isOpen":true,"children":["1433ee62-21ac-d72c-0fd0-37000e5eb221","24bacfd6-cde8-81a0-f620-998cf5393d48",{"uuid":"0e94ff40-15b6-0b24-d0a0-447f000d761b","isOpen":true,"children":[]}]}]}]}]}]},{"uuid":"7e8426f1-08b2-81a2-7703-cb76ff5e7003","isOpen":true,"children":["b17675fc-b79b-0ad9-8f81-aa2dd1fc8c97","2e42e483-1557-d21e-aee0-94af7bedfd40",{"uuid":"c6d9e946-1d10-482d-14b1-0766027adba8","isOpen":true,"children":["9455d16e-4bbf-4b63-881d-7daf943e782b","16aee685-0ead-a542-5b3c-a62467ca45e3"]}]},{"uuid":"5ef5d225-d5ae-6787-8838-b75ccb7a7a81","isOpen":true,"children":["0e370edc-7b05-dccf-dd2a-a92cfe9f3e22","dc189735-0a58-619b-07ef-feec37d65e7d",{"uuid":"1b5cc202-c09e-faa0-5057-eb4ae60bf336","isOpen":false,"children":["6bcf768b-beab-4c39-6d96-91266036a4e1","274282a7-bc7b-02f8-4dce-84226e9dd9c1"]}]}]},{"uuid":"529b75e5-967f-8e76-9d18-0fc49998293a","isOpen":false,"children":["c8c75bdd-53bc-eb81-a2cb-0ea0dca06f35"]}],"textures":[{"name":"-steve_template.png","path":"","folder":"","namespace":"","id":"0","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"44166cb6-b1bf-f227-4398-96c94f36d7f7","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAApBJREFUeF7tWztLA0EQvoCgEGsTiPhAwcbOJr12WmlhYWVlr1b+gFRqoZVVKkutLLUXwc5GUHxgIBFLBa1OJrmDzbi3O+u6SHJf2rndmftudl7fphBZfmc3T7HpkfreoXGHk/puwabDJA+t32ocGVCdHtXaeHn3EhEAlVJZK2+0mtFfALA0O661k2wj/Vk6Vta3Y5t+AGBzT3gAjgBiAIJgX6fB+eaeMc+XbzaMcfLo6sAoH1h9M8oXixWjPLT+AgFQLY3o83zrNSIDJspFrfyx+RERAMXJMa384+E5IgAGKzNa+VfjNiIALspb2nRMtpH+44VhrXzt/D0m/cM7O1r5e60Wk/6hqX2t/PN+MwYAEg/YnOt4wGmj8yGXE6/dv86JB+QeAMSAfg+CpjwUOg39exrkL8/7b97v29rL0P27r338fX/kR7X74/2+pL8P3T362gcAGAJaD1Cf6fsjwHsBHvR4rc9rex7EXIOma6/gal9WGZx+5K5S+JLV/rzW57V9WsunvQRfz8+bbT/+vG1/2360HgBkNELwgAQBHAG1G0QMQBDsngBJoiylQWSBZKSGNOg4Q5TODLM8TOKhqAMkdYBagbmWmj1fCmMewBCgfjvl4zn/LuHb1fU6cH35fF/7RPOA3AOAeYCCQC7mASk3x7k4zr1xro24NZXbc+XyJNydaX+JfaI6AAAk7Cw8gNHREhfDEVD4fcQAxwsNCIKCGxzIAoYjJolRSIOSdjj3dQDmAQoCvvx7r63XssO+7XD6/wLf+wW/Xe9iPwAIPRLzvV8Qen2bGwwZBH3nCaHXtwHwTYM+c3sTsySZ+/tWigAAHoAjgBiAIIgs4DkURRrMuCAhzeO+APp4MOqAvJfC38z1ql4UO+HHAAAAAElFTkSuQmCC"},{"name":"-knight_sword.png","path":"","folder":"","namespace":"","id":"1","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"c2982d79-5e5d-fbcd-faa3-d19a766eaaa5","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAEMRJREFUeF7tmmdYVVfWx//3ntsvcCnSLqCiGDWmzUSNZUzMq0mc2BtSbGhQYyxEBEIsY9Qx1hiFGCIqoIKABUssCVhiS2KLGonRqEhXeru9nMlZR+7I67wf5n0ez8MH9pd99in37P3b/73W2utc0cmTqaxExsBoMEMiZSCR8Mc7xROwPXApliTXY9aMqTCZGtBcRCIxdqTsw6yZk+Dr0w3DG9JxVFeKUmM5qioLUFNdgpOpBsf93EFIXA+IREqUa7bS+ZQNCVRPGJiFyu/NYP8qoXZiIovgSWPpePnyBFGLH3kODdG3x75hDXoTDV6pkMNqtcJqtWOvejIBWL6jCVHz56GqutDxerncGdu270bk9DB4e3fBiMZMApBfdhkVFQXQG0Q4vqMRIjGgUTKw2lgMneWHqqo6OPc+/wyA/IMG9BilpPNLl9mEBZBz6CtWKpXAYDBBpZLDZrXBbLYiRzONAKxMM+KD6ZOg19dArfaCTlcBTgG7Mw5j2tQwSKUqREjPEIAzN7JhMIpgswGHdzRADBHaOTGws8BfRyphtwOrVtux/siPNNiTX/8FfXurcW6PDqELe8DGMqj3TcOl7PXCKSAzayPbubMW5Y9qwDAMpBIGZosVe2ThBGBNhgXhYWNgNuvh5OSFpqYKKBQapO3MxJTJoZDLnTBZnMcvAf0DrNlwGrELXsGSj25CxAJaN17awxZ+ibK0WBisdnjHXqJzVzN6ontXBWrOm1BvYnFzcgGdD/WpxuNLGZg+Lf75L4EjR5NYra8nyVMkFsHD3YVg7JGFEYD1WXaEhY6F0dgAtdoTOl0lVKp22LkrHRPDg6FSuSOcPU4AbpcdhN1uhFyhRepKfjBX75px/MZdMBIpVozvBp2FhejJsDSvinFqr5HuO34skurwlWL0CDDhnXf7YMDfwp8/gJyDX7Ht23ujuroBDY06BHb0RXl5NfbIeQVs2i/C2LHDYTbrIJM5wWxugkrlgfSMLISGjIVS4YqJou94G1CS6bATe9ab6Pjy72YcunKNlsWysDfoHAdAJhHhaQDar8rpmjF9GQEQzAgeOJjIduzAAWhEbV0jOnfyQ0lpJbKVkwjA5hwGwePHkA2QSBSwWo0QiUQ4dPgUxo8bDrXKA+HgAeReyYa3jz9cXTXYuvgKDeJivhH7L16AWCxCRWE89LoabLbm0bVFzl0hlXfA6Q33IXMW05LRqMXwGLUWud//JIwX4BQQGKhFZWUd6uob8UKXABQWPkLWUwDGjxsNg6G2xRLI3rsP48eNgUrlijD7MQKw7/ReeHsCWt/2SFnxOw3y7K9GvBk8gY7f6pkLmxXY63Wb2v/wDIFYLMHPX/wMTfRPdI5J74fdg+6g89l4YQAcOryFANy7VwK1kxJ+2nYoLa1yxAGcAsaNHQWjsQ4ymfrJUlDjQM5RjBr5HlxcfBHOniAA2XnZ8HBn4e4G+Hc7AFeNNwZ0747BYaE0uL6v5NJS4Fzd9bwQx5rXlu9Ame80avc0pcIQxN8vSBzwfd521svTHQ8elELtpIKf1uMZAMOGvv3nfLFgGDlsNhOkUiVyDh7HmNHDSAGTRE+8gOEPrFl/Fp8sCADjuhXubn7o27UrhkwMowFxv5OQeoOO0xfbHQCifDNRrLOTm7xVbILsFf5+QQCcPJ3Kurs5o7i4EnKF9BkFfLEPCAnmvEAdLFYJpBIrucGs7ByETBgLJyd3hFiPkgLuVZ2DyVjKmTm4+O2Ep2dH9O3aA8OmhENy5yAN6q5bBNWK8GVUc0ZvvNNujIriB704Kg3dRy9E4cMyYQCcOp3Guro6obikAgq5DH5+nigtrXQsgTUZVoSGjCTjJxYzsNtt4CLB3en7MGVyGDQaL4wz5jzjBTT+6fD3fxFWi4WeWxr8Eg2wPOrfEWUzAK6OiJ5L180pfXFdLWAozCnASa1CScljKFUKcC6xtOTfAFak6BEREUJrn3N/en01L+H0I5gxYzqkUjkmiXIdNoAuAhg78Wc06Spgs4kJ2prpg+l8zcIiqq9GvoC4GDEYBuhYIMKmx7yh5OzBuq+mYlWaRRgF5J1MYZ2d1aira0RTkx7dunUgBaSJnmyGvmlAZGQoKYCbeZOpkTqakpqDOR/NgFyuQjjLu8GME1mQSPhQeEz4WRiM9TCbbeBC7S82JuC9/t9xpgQntvEbpReHKGFnWQSUA8XFNqikIvxQaIR3v4l0XRAbwMUBnQK1ePy4Fjq9AV1faI/83wpwyHU6xQGxCVWYN3c6BUDNAJRKN2z8MgnRC+aQG5sqOU0Adh7NgkIugs0OhE27iqrqIhQkjIHHlK1QKFzw40ne8p9Mb4RcIkZ9LK8Gzg5wZfa4i5BKJMi78KpwAPbnJLJBnf3x6FE17QQDArzJJe53nkoAFmx4hAXRM2gJcNbfYjGAA7BubSJi4+YRgAjpDwTAro2BWMTAYjXCbrPCaGrCz6uGIGhmMhQKV5z/npd58OBgqqcXLKeai/2/W/gXOn5v/S8YrNwk3BLg8gHePu4ke4vVggB/7xaB0EefFyE+fg4B4AbOBURSqRqrVydiyZIYSCRSTGF4BcAvDg2N5RQp3towFG8suYya2iKUlhbCZrNBZktCfV0x+ncf1WwqqM4zzEfFut6QM2IcC/uDgqAOHbXCbIbyTqWy3l5uKCurgsVihb+/Fx4+LHfkA2atfIj4+A9hs5mhVLrCYOADopUrN+Ozz+IgkcgwWXyKAFi85qNJV4na2sc4uW4aIjZfQpOuBjqdjqAU3YlGbW0ZLmToaOArv5xCdYUsDOsi36XjZgM4dFA53nhr3/PfDB06soXt2EFLS8BitVIcUFxc4QiFTe3mY/ZlDzx66RucaBcBk9GAtWtXt5jBIL8cR3vMxB8hkyooZ2CzWVBZVQSLxQyjsR5Xzs6EWAx8t6sRZiuLZZuDIJUHwslrCVaGDqG8we0yHe0Mz9w4gCFjHj5/AAdyEtkOHXxx/34xFEo5mrfGu5gQsgF6t9mYe82HAOSogiFmxFi7piWAQJ8ccmdWGzBiwnGIRAxUSjcKiDgAJSX3KNdwcNVsAtV/5TWqu9pnQsxocJtdi2Nz34BYBLw5RYpb90eh32tHhAGwJ/ML1t/fG0VFjyCVMfDx8UBJSSWOtptBAAweczHniicByJKOhMHQgOStKS0UEOibA0YMWK0s+vxPMtzdfKBWe2Dzh68DMiD4032QSORInDOMnnt7NQ+g6lx/sCzg+eYF5EX1gYwRIWRZP2zQpcJtfQAST9Q+fwWkpK1mfX08KCFiMlmh9WuHmqp6HPOeRQB0bh9i3jXfJwBGQK+vx7bktBYAJHey8FqAFCq5CDWNdtQY7OiikcIMFpeLzRgatxXu7l74YhYPQC7lx/XKMD4POCrsItavW4eexgNgenvg55tvQf9LBtYfqX7+AHanr2PlShnMRgvMJgs0rk5oajLgO9/ZBKDe+QMsuNmeByAZAb3hWQDyu5mw2VlUNtgdgzNZWNjsdihlYoxb/BVYlsW26Hmw2Fh4e/H8Rs+RI+DFw7RUtiWnwnIzg5YHlxPM3rUfvxeUPX8A6RkbWIbhwlU7GUG5TEYZm8PukQSgVh2BhbcCCcAeZih5g6+3JLdQwAeR4XBx9kTesgEIUDLQW1icKzbTPf3ayyCSiFCht+HgBT79VdzA1/Er3PFyn/1gGAmSt6bitdr9sPZkcMjvNoXKggAI/ZuGmxw8ruK3p1x5feR4quNio2Gx2BCT34kApOM9yu0lbE5sAWDGzIlwdvLEiom9YDDb4eUihkLGQCxi0WS0o7LRDpVMjMoqESngYR0fCs+L94A2aBWCgl5D0tdbweRn4u6HBRQHfJ2QisraxuevgGmDXQnAgyIbp0SK1XuN5gEs+vQT6HU6xN7uQgBSrW9DpXLBpi83tQAwNWIcbX2rqspgMNTg1qVZdJ3zLozYCXV1d6mdvFxPtclqx58pQkyMcsaA945i7qc7KQ/46RQpXRdqI8S9SzR1kIblZuf6HV6yXOk3jg9ZY2NiKDyOyeeXQKp1IBQKZyRs5r/qNJew8GHw03ZDRWURfUG6fpEHoNEwUDt5oqz0EbWPbtVDyohQqTfDYmXx8kApohedaQHg7+8ng0uQpg92eu6zTwAWDvdguQMud9dcVm3mfDjQo9dxiMQMon99YgSlw2l/v2VLUgsA7vey4Ktg0H7GRkp+3vklmq7LuY2RjXePBCCZV0CzDXh7hBrjJ6XA3d3H4VnOZaViwISpguwECcCEnu6sk5zB1cImMn7ccoj9rB119Nqdd/Bx1DzE/hb0xA0Og8lkxDdJ21oAUP6WCReZGNNXhNP55k2O76YO1G4OeblsD1cu3uFtQP8hSvg+FmPc+uPIzc3FuhUJSExoj7OXegkHIKSXO6uQiHGlsIncEWcEYv/BA7h5/11EzZ+LuCc2IFs2gkLhpKSWXqD2Ujrdz8XxTwNQ/NMfNhZI2s4nPCMjtlNdU8e9hUXA6wwCG6W47DwZ7w/wwNChI/DHzXnIPfeicAC+vVVI+owZ3NUxq+/0UoKRisAEjUJcbAyirgeQAvbKR8Nk0uHrLfwX3uZizc8AFIDZbyTmzZ2NK2fHUFi7K5ff9nJfgDt1ag+TvgA535pw94IFDUYb/rlRg8yNRsoIN+cIR5Z2R6NttiA7QVoCzQD0V/8GPx87lOpOyFjzGz+27iMRFxuHqOt+BGCfYjSamuqeCYXNNzJg5bxo95H4OGo+LuTyEV/WGd6YcgCCuvRExaMruHzNiK5dFPD3BapqAfkPdjBDA3HvjwLk3gglbxAQ4IPIyEXCGMHLP/Rmd2feQ/8+aiiVLDzavYJFH56DWs5gVsYN9NNaHJshTgE6XR0++f0FApJmGwSxWExxgjR9Mtp5uiImeiHiPlmMpMUdMVTtR9vkw+f3OtRy+aoevV5Xwde3PUpLi6D80YbirvxYT9/iAXBFiHQYKeDiqYHs8RO38HIPJVlsrmxdU0cS/njfLbzhY0LUda0jErRYjA6jmGIZCIlEgpj8zg4A0QsWID5+6f8J4OSZJgwa6AQ39wDU1hTTp/FBEV0gldiw49uBmBHaHRfOXxcOQE6GP3vxJwPNCucBuOLs4gq1SoFG/33o5anDgiducA/zPmw2qyM0Xv5ASyF0knEkAQiouASj1YZKbd8WALjA5unyUmf+G0FzUb3O/2mi8Pt+OPVrKKXIh73UQZglkJ0WwDbPCtcJpVIDmUwJTy+/Px2YBH5Byx2boXTR32G3Wylw4Urz153bleWQyVRYmsDHD5xBm3qsGxpgJ9uQ7zIW7kXZSPqBoX9/cGHum6Nm0kB7ejbhSqUTjL/0w+Kl/N9jhEqH0RLoFqilea+ua3R0imvXJvWBh7MY6YPu0KaIy9vteeThOObuaf6fT83FbxyzuWjDS0hR5KJDYkeY7Xb6i8yp60a82FGC3x5a8eWuYbhz+zy4WW/+Jwj3cO/ghS3agtmApUvnPhF+C1VSI79Y7jBK/6n97BP//Zmn38HNPPdJjCuCAdi+43O2+aX/aSbuv/k5zfr/XsdPn3/3rQdoqs+HwWCH9NVzNICFw/s6aAixrf3v0fNPCGJo/r+dE+K5NgBCUG7N72hTQGueHSH61qYAISi35ne0KaA1z44QfWtTgBCUW/M72hTQmmdHiL61KUAIyq35HW0KaM2zI0Tf2hQgBOXW/I42BbTm2RGib20KEIJya35HmwJa8+wI0bd/AQszlaq8R8kcAAAAAElFTkSuQmCC"}],"animations":[{"uuid":"91f45b91-0388-39e0-52fd-e98e668d0d06","name":"roll","loop":"once","override":false,"length":0.66667,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10.3452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"4e015e1c-020c-c0cc-cf03-d0f365b91b4f","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"96a1cbef-dc1c-8cb3-893d-1844d13a8898","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55.3452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"cffaa113-7472-9a2d-879c-3b37b7918233","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.8452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"1859ff86-2a12-36c8-57b7-cf230e86e3e3","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-173.4065947693","y":"12.6083678689","z":"-8.1925230243"}],"uuid":"6c944778-0f30-a404-20be-7d985529dcc3","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-254.4306397576","y":"-5.2125464926","z":"-2.586642225"}],"uuid":"6fea6dfb-ac36-8bb7-7b6c-f1b31c3fb09c","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-299.316821318","y":"-7.7099874859","z":"-2.5994534331"}],"uuid":"19e7bcfa-34ef-ca1f-9316-34616fa523cb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-334.7684189709","y":"2.2798591799","z":"-2.5779800118"}],"uuid":"143b95c6-51d7-3d73-dab9-4afc76a18bee","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-359.7452658674","y":"0.977484586","z":"-0.7993651888"}],"uuid":"ca8b487a-5316-821c-6a0d-2b17dc09e2ce","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"739f6fd0-eeff-515d-bc2c-05459d38fb35","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"610dffa2-b064-c32e-49b0-95e662c9fbe0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f8040d86-2633-7d51-02f2-d276ac128c20","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-15","z":"-5"}],"uuid":"2613c6a2-076c-8a2d-7022-314ada456d6a","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-16","z":"3"}],"uuid":"65475ddb-0901-3649-c168-8f332b92d1c4","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-4","z":"5"}],"uuid":"d26510d5-a53d-5570-114b-1a49178fc6f7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"4"}],"uuid":"03b08547-82b6-c8ce-eb7e-8147925b4d3c","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3d14037c-74a9-abc2-b13d-3600ae6c5dd0","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3","z":"0.31"}],"uuid":"7a5c383e-b443-3773-3fc7-b41a95b66aa3","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"f218f334-8ebe-66d8-1769-41de9f9f7ad0","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"75314a09-9386-3e4d-3f96-a8e59911fe5e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e579328d-a99d-9a7f-0a05-746e42159080","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5169123047","y":"4.8873117739","z":"2.6276738824"}],"uuid":"57494920-9783-7099-8971-f8828309b07f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.2554184495","y":"1.9324031631","z":"0.6242872643"}],"uuid":"14faa28e-7b7c-4fca-04bd-fb15427d1d14","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.321532076","y":"-0.1985844587","z":"-0.7364643904"}],"uuid":"2960010b-2391-100b-9059-499f90d98595","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.7446837252","y":"-2.2894808314","z":"1.027565895"}],"uuid":"6436ac0d-b7c1-eb4c-741b-c6fec8760846","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"6cb82785-ecee-7220-9b21-7f748ca4ff73","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"554f1255-7efc-bebf-d907-e6b38e212768","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"918337ed-db4f-55f0-7a21-d10d25500ae6","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"66b19558-a367-598b-50c7-142c4211a07d","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"af84a634-f0ee-f31b-42da-a9789b3af450","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9b812e3b-ff6a-5bcf-9b14-d88149ef2a29","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.9335647827","y":"0.1910653446","z":"0.5860712492"}],"uuid":"d26f71b7-3eb6-c334-8e41-f3048cc39bfd","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.8337650092","y":"1.5719388815","z":"0.836263831"}],"uuid":"908fd1af-b92a-7c86-ec15-33aa208e85d9","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.5153158634","y":"1.5694142029","z":"1.1566234175"}],"uuid":"06e3b992-d5bc-6bd5-da5e-ec28155c8af4","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-5","y":"0","z":"0"}],"uuid":"af2c3045-852e-36da-7224-00ca89c0c360","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4e57e7e0-bff8-8f86-f5a9-14379ed8f0cc","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"7e8f0cb8-aacf-3a06-0990-319fcca439ca","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"951b375f-8b17-0bce-c1e5-b020c20b1378","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"0","z":"0"}],"uuid":"96e0a9d7-f9a1-ca27-c4c1-49b9b9ae9739","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"d691f682-5653-24b5-64cd-f593d802ae7a","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"64d27d7c-16f1-4e24-11de-1df5b650e5b7","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-44.7041455577","y":"-0.5463397482","z":"2.4016359177"}],"uuid":"cf7881c9-f9af-c342-d155-dda6ca40df0f","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-42.1987087714","y":"0.9199484607","z":"2.3855070851"}],"uuid":"1e94d2b6-12f7-f0b3-5513-e088c678cddc","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"4.7824447755","y":"-15.0272212866","z":"0.6748976469"}],"uuid":"d578b08c-e775-fb98-c9f8-42f4e180dc8f","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3a4776d8-2065-0aff-5901-b4d3d6ecf2f6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.9790874962","y":"-5.8032396565","z":"15.815264958"}],"uuid":"f9ce9b56-16a1-0ade-1b32-395dc6a6508f","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.1815443852","y":"7.1155834398","z":"18.1304160091"}],"uuid":"14d812e3-97c4-fb55-ae8c-7ec92768c5bd","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.9790874962","y":"-5.8032396565","z":"15.815264958"}],"uuid":"5afbcdab-4795-1ca8-59a9-285e3dfac932","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.9171104584","y":"-4.5000466504","z":"5.8600103802"}],"uuid":"6499750a-a4fb-5c57-4995-c4d47b630672","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"8.2765048988","y":"-2.9212329552","z":"-1.4866588636"}],"uuid":"7f018859-2372-9fd4-17e0-756d8fffc147","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"aba6447d-d798-c3c4-8579-72b987b8b41c","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.7013003421","y":"1.5426491326","z":"16.086881186"}],"uuid":"3896e9cb-f48c-75cc-888d-769cf83332f2","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"55","y":"0","z":"0"}],"uuid":"8e1409c4-941b-084c-02e5-df03cc1a4690","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"92ea27f8-0e0a-b629-1027-b86cf738d7ec","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"79.7622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"fc368334-2af0-e8a5-5d0a-5cbdb752e343","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"117.2622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"7f4efd9b-c5ba-5977-b5fa-b1b9799bbac1","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"94.7622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"7db6c762-536f-e3b9-482d-8337cffe5746","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95.0395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"bbc95573-feb7-ffe2-345c-af69f5f8f939","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"72.5395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"ab7c0856-f341-34ea-f57c-6904b0e30d92","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"9e4fcb79-0aa2-9377-3fc6-ab698d600e8a","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"63743b29-3a44-0bc8-f5a5-8903bb15d390","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"25.6848204073","y":"10.1778091184","z":"-20.173933666"}],"uuid":"7a3b3bdf-dd57-badd-a6dd-7580d30e23e0","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4323899f-7d3f-557a-beda-d05603cd2f96","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.4403124196","y":"16.3255534092","z":"-34.2402741209"}],"uuid":"744316fc-f3d8-67fd-9a68-2b2c568b639e","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.2988707081","y":"3.4553272208","z":"-6.6606725408"}],"uuid":"6cbe013b-e0b9-702c-e627-f2c374f53718","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fd38995a-2f81-eb5e-6c06-404eceec04f6","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"25","y":"0","z":"0"}],"uuid":"5f5fe94f-19c5-c019-7a47-ac924d74285c","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"339cdfba-9029-f75f-9d19-ac8567b07424","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"87d530dc-a3ca-5bc5-53fd-d0b58e67c115","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95","y":"0","z":"0"}],"uuid":"4056d3f0-9227-879d-0ccd-a715a6acefd8","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"60","y":"0","z":"0"}],"uuid":"5cb622b9-5c77-b222-ba79-a62ddbf1e801","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.9979365876","y":"19.3545961515","z":"-11.7009195082"}],"uuid":"d08bec63-419b-45f1-d3ac-4bf88c534563","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.9979365876","y":"19.3545961515","z":"-11.7009195082"}],"uuid":"aeaa276f-1181-1922-960d-7fb6845fd9f3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"13.2306976448","y":"4.1075261935","z":"1.5640491387"}],"uuid":"2416d5e6-91bb-6991-a145-7778a9ab717b","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ce1a67cc-be9b-632e-7d0f-1ea657ce8f06","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"3e549bb1-58c3-92b5-d13c-9c3c0772ea12","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c6b905c0-5196-e52b-59c7-8d3927a73397","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.5","y":"0","z":"0"}],"uuid":"e4592899-dc47-7f8c-acd9-92811b5fdd77","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.1404769782","y":"-17.3455144203","z":"2.3566635967"}],"uuid":"07d4219f-d8d7-811e-fba2-06fb667c4f62","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"31.427095159","y":"-13.5306627252","z":"-4.344153106"}],"uuid":"7a496aba-7df1-b5b5-4dbe-7e6420e13fef","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"11.427095159","y":"-13.5306627252","z":"-4.344153106"}],"uuid":"446c86e4-1c3b-eff5-a180-a62769af7bb8","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"95fe2e07-173b-a066-a66d-0be58ae890a5","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"48.3424105805","y":"1.1690538795","z":"-1.2575696869"}],"uuid":"06246a98-556c-f22c-45a0-44083c73dbe9","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.8934503775","y":"-3.8213109421","z":"17.9129813754"}],"uuid":"aead35ef-607e-93dc-f018-85e424e6a137","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"447233aa-66b2-dbd7-e45d-483f46dadbae","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"bd388449-ced9-61f6-e408-4f913fc0355d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.4009318272","y":"-4.2154088774","z":"2.6913572849"}],"uuid":"7b84f346-ab75-70f4-a8f4-67879637f9e5","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.4009318272","y":"-4.2154088774","z":"2.6913572849"}],"uuid":"724ba19e-1a1c-00e9-111a-d67ed5aa5c83","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"48.3698475508","y":"-13.3767559929","z":"0.8884687898"}],"uuid":"28e08960-9fa6-1789-1edf-5b1e443db7bd","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-0.7930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"3629a4e5-a2bf-6722-7a63-824e74693c95","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-23.2930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"fc5d0a27-b180-4a2f-5fb1-c25e9d8d0cbc","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.2930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"0471b600-cd96-fb88-b394-25b68b236191","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b3528148-886c-4981-aad9-a449912e4ff3","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"c6d9e946-1d10-482d-14b1-0766027adba8":{"name":"prfl_right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"4d65f4ff-4666-1d6e-e2a2-5754675f39d3","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"713091ea-3c5f-2107-8abf-ee97bc6176ee","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-35","y":"0","z":"0"}],"uuid":"020d854c-8f1d-eea6-0d44-f050772acad3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"747e19b0-5b0d-1bde-37c5-d61eae5ee649","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1a3a7fe0-9ac6-642d-1655-a02cbedc50c0","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-63.5104629601","y":"-0.0562419487","z":"-3.8034923955"}],"uuid":"cf494402-d95a-c9a9-0666-1105ac4ff1ba","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30.3667568215","y":"-5.6104628256","z":"0.822128921"}],"uuid":"338edd44-0be5-f9d0-495a-16b3c7a47468","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5627211181","y":"5.0414022802","z":"3.6575498102"}],"uuid":"601767a5-ac9a-bc96-4b0b-237160c36700","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50.8538632696","y":"5.4159878628","z":"2.605219904"}],"uuid":"b124849a-6e1c-463f-9163-98272fab4254","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-17.1096711992","y":"-3.7317133585","z":"-11.938445897"}],"uuid":"486ede20-d032-4d46-d9cd-95cf257c04d9","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1e3e41fd-1374-7b20-aada-7d42393cb399","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-93.8329224881","y":"-12.0025641778","z":"-18.1816740733"}],"uuid":"13a334d3-48cd-1136-e910-d86f10ecd6ff","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"25.8053212577","y":"7.0178718272","z":"-29.2161110452"}],"uuid":"7e87c3b5-9d38-b35b-df3b-aa05fc47517d","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.0037108296","y":"-5.9031625207","z":"-19.1431448415"}],"uuid":"6601f44d-7b3e-1933-edfb-aaf37ed8f707","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-14.2395920994","y":"3.5940186635","z":"-7.5169369411"}],"uuid":"284cc08b-c152-0ab3-3689-6d441335ef56","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"8085208f-1d05-3f74-9820-0173fe43fd6b","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.9519624501","y":"-7.2690960426","z":"-16.7194764292"}],"uuid":"487c4a04-c29a-1aa1-e26a-aa6dbafe7e0a","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"18.247962068","y":"7.8313214321","z":"-24.4844657083"}],"uuid":"73a7fea9-6029-f2f6-ba64-9449179096e1","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"a0002446-b908-df56-d45b-79e09a54036d","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"71dde296-8a96-6c52-d883-110fe28f6083","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18.550117808","y":"-0.4065298268","z":"4.9663131482"}],"uuid":"a9e10a80-2787-3737-8868-9c28c2cbafdb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5298058614","y":"-2.9158033357","z":"6.6597637739"}],"uuid":"3ec121c2-1295-28ed-2ca7-22374f35c684","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c43f7153-1df2-38ec-1edd-b3694bed6dc8","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.1236819605","y":"-0.9306502051","z":"1.542865843"}],"uuid":"fefea430-0ffa-21c0-738a-21ab44462a5e","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-33.1776348771","y":"-2.2590643664","z":"4.7558127555"}],"uuid":"3245ce43-4453-1563-6426-957785fa545f","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-53.9268912309","y":"-7.3104700825","z":"8.1053141589"}],"uuid":"8d32a863-19e6-66eb-dd63-bc4a4490440e","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.6010831098","y":"0.530086938","z":"2.60850085"}],"uuid":"1a5cc3fb-2736-9e73-929b-2b1f796b4824","time":0.58333,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"6f35223a-32fc-86de-273e-bf2a2dab29d9","name":"left_attack_1","loop":"once","override":false,"length":0.75,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"1.4426116839","y":"-29.9685 + 60 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"-2.8861405929"}],"uuid":"9cea2183-ab9a-e257-3abe-870ecefb3983","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"4.3169042909","y":"30 ","z":"2.8752087286"}],"uuid":"dbc63c99-e467-c1e1-657c-cb6af7445267","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-0.6830957091","y":"30","z":"2.8752087286"}],"uuid":"fd08cba1-1f23-bfc2-9441-476ca36f74cb","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"06b875b6-cb7b-6d84-3602-76e8d8392a4d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"22.5","z":"0"}],"uuid":"f0fba0f9-402e-360e-0a94-d64aeb5c5c6d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366","z":"0.3377321701"}],"uuid":"b50394e2-3528-65dd-22fb-512dbfc2f810","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"b375bdf1-a5b9-5f1e-8257-47941e069d20","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"22.5","z":"0"}],"uuid":"6671e902-6e55-2c82-dded-c5df61e041e5","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366","z":"0.3377321701"}],"uuid":"df42619a-31b1-392a-4568-0f18b8533893","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"fc915813-af51-faa4-ecdc-03c64965677b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"22.5","z":"0"}],"uuid":"e225cf07-6e84-70ca-6695-b29a2846ed8b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"15","z":"0"}],"uuid":"c25352f3-66c2-2b02-bd35-d880f2b176f3","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10","y":"-75 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"b43318a9-dcd4-607f-65e1-8b00cc43b3a0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"-75","z":"0"}],"uuid":"93681b83-7b39-874b-e811-a67b95855b3f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4079498817","y":"-69.6105803251","z":"7.2928402867"}],"uuid":"9a3a0d44-c650-ac83-4813-2a10370f9eb4","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"226.442612794","y":"-29.9685204144","z":"-92.8861411476"}],"uuid":"e93ee5dc-184d-1bc3-117e-457e5f8d35b0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"118.942612794","y":"-29.9685204144","z":"-92.8861411476"}],"uuid":"abee3b46-8686-08a6-5fc2-88f23a08686b","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"91.4812744911","y":"-32.4677070432","z":"-92.9607170541"}],"uuid":"f5310bc1-f902-b35d-17f6-5c492a08e390","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"104.2082921546","y":"-34.614390135","z":"-84.3352155418"}],"uuid":"546f17cc-3f95-55f6-ce39-34d45c4bc65f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"120.1958079089","y":"-58.4299921475","z":"-95.76303279"}],"uuid":"50a71a0f-23a3-58bf-a9ca-dfd506c8fc73","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"cfdee1e2-175a-bfa2-c031-d225ff9b5d21","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50","y":"0","z":"0"}],"uuid":"74aba8f3-7600-c98b-c42b-8ae7248974c0","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-48.85","y":"0","z":"0"}],"uuid":"982fbd70-9b5f-e211-2106-7bb13df19813","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-77.5","y":"0","z":"0"}],"uuid":"093ab868-a3e5-1727-de2d-9699650ad3b7","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"fcaf8da0-0146-2587-b578-3e1af888deaa":{"name":"pri_right_item","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"47ab1042-3dbe-9fbe-5b2f-2d4e55759eb9","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.5","y":"0","z":"0"}],"uuid":"68868a70-d07d-55bc-ea5e-d62b6d58ee36","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-70","y":"0","z":"0"}],"uuid":"26f156a2-f872-2e31-e28b-5cd012a57108","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-72.5","y":"0","z":"0"}],"uuid":"81cb1fac-09fb-254d-c7ef-605c48f3687c","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.75","y":"0","z":"0"}],"uuid":"7128c27e-62a5-eab2-1edc-b9fdd5a2d1db","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-87.5","y":"0","z":"0"}],"uuid":"09cbf278-b5ef-45bf-9f24-057b421dce72","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-102.5","y":"0","z":"0"}],"uuid":"1cae9cba-9706-843e-87c5-c87e76ba05a8","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-102.5","y":"0","z":"0"}],"uuid":"ea0dd9ba-ef9e-ae70-1647-8ded38c17181","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"630a55a7-df7d-e844-f270-7bc23e9091e0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"a7909b5c-2b7b-30d8-daa2-ce4e1eb0ae89","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"a2809a1d-00c4-c013-9496-dafe98018493","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"6360ce54-fddd-4efd-f7e0-bf14889ca2f5","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"5bafd1e1-dd4f-e647-0ae9-7a670f0d888d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"2b40d9f0-62fd-aaa3-6543-793a1dbb7a30","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"df09c19b-92e5-fda1-3fc0-88b5f94777c3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7ec93d93-fd31-0af1-95b5-7b2a760ebf0e","time":0.75,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"74458596-1e8e-e629-df60-95abd2e494a9","name":"left_attack_2","loop":"once","override":false,"length":0.75,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-0.6830957091","y":"30 - 45 * math.sin(query.anim_time * degrees)","z":"2.8752087286"}],"uuid":"10958ace-f54f-8523-893a-df0d2fa7ee2b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"1.992544964","y":"-14.8854093306","z":"-7.4750460859"}],"uuid":"6cd93535-eeb9-7d73-b0ac-d4d382bbbb01","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"-29.9409605825","z":"-5.1865092834"}],"uuid":"33442214-ce7c-a7f0-da2c-c32579010ed2","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"-29.9409605825","z":"-5.1865092834"}],"uuid":"893ec734-086c-2dc3-e43c-1881431cc207","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366 - 45 * math.sin(query.anim_time / 0.75 * degrees * 0.5)","z":"0.3377321701"}],"uuid":"feec51b1-b546-e183-06af-f2fd4e999baf","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-30","z":"0.3377321701"}],"uuid":"bbb6334b-d352-6cc7-7708-a92e8267d1a9","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-15","z":"0.3377321701"}],"uuid":"977401eb-1cd9-d20f-ba13-fcc04bb0bbe4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1118499707","y":"-19.9999907433","z":"0.3276399241"}],"uuid":"7c4918c9-c0da-f306-f08f-5065e20e6365","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366 - 45 * math.sin(query.anim_time * degrees)","z":"0.3377321701"}],"uuid":"039aa55f-3a2b-951f-687c-39c2028a075b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-30","z":"0.3377321701"}],"uuid":"743c1219-e220-c341-e567-62633a006f8e","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"-15.0000239764","z":"0.3668883205"}],"uuid":"d78bfc1e-149a-c305-1cab-d11998cd8fa4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1002819443","y":"-20.0000165354","z":"0.3578398578"}],"uuid":"c993c4bb-f2dd-0d3e-2a90-ccdd9a6e28bf","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"2.5","y":"15 - 45 * math.sin(query.anim_time * degrees)","z":"0"}],"uuid":"bc207a6b-8c2e-fae1-615a-58640bfd30a4","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"-30","z":"0"}],"uuid":"99c4936a-ccb2-0076-d4a6-3f602366dd4b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"-15.0126547749","z":"0.6697153441"}],"uuid":"dc834c65-8788-5a0c-5e8e-b17cbc524ed7","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.3040300919","y":"-20.0087274139","z":"0.4618653633"}],"uuid":"2b68f55d-7ce7-d9ac-6140-1e11f21bea83","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.4079498817","y":"110","z":"7.2928402867"}],"uuid":"63e11b48-1acd-31fb-9aec-038ef11318d1","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.2466262711","y":"74.742560924","z":"4.9451721289"}],"uuid":"272befd3-7dac-c524-f191-be00f2b480f7","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10.6822205364","y":"79.6837812733","z":"1.4255362599"}],"uuid":"665dcf82-4d8a-60f1-a147-3ea0fd090a3b","time":0.625,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4079498817","y":"-70 + 180 * math.sin(query.anim_time * degrees)","z":"7.2928402867"}],"uuid":"fc76d21e-d81d-fe56-8713-8c4511a33f4c","time":0,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"88.1929174825 + 135 * math.sin(query.anim_time * degrees)","y":"-6.5328079718","z":"-99.7693965866"}],"uuid":"f15b8a26-69eb-3236-f121-089f6c12adac","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"225","y":"-6.5328079718","z":"-99.7693965866"}],"uuid":"c4d1ddb3-58fe-1339-9935-9c017764aa14","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"181.7685110117","y":"-29.2024066267","z":"-94.1691034871"}],"uuid":"52ecc208-4ae8-93fa-fb45-2116be5c1bd7","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"200.7469404305","y":"-14.5514885328","z":"-96.9206545934"}],"uuid":"291f2fd2-9348-7e45-94ec-a65ccfe23fb2","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-87.5 + 90 * math.sin(query.anim_time * degrees)","y":"0","z":"0"}],"uuid":"cbc64268-9d72-314a-bb49-cd0bb865f92d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"0","z":"0"}],"uuid":"abe0830d-48f3-34ee-0d56-79a82a40d0fd","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"8475acbb-ec58-046f-cf73-1d5f1bd2f682","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"47670f80-2a64-0e92-40cf-cf7dba16da3a","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"fcaf8da0-0146-2587-b578-3e1af888deaa":{"name":"pri_right_item","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-122.5","y":"0","z":"0"}],"uuid":"7be4764d-8e4f-962f-b52e-22c0e27e24a6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-122.5","y":"0","z":"0"}],"uuid":"4cbdb4a2-378e-76db-2887-6c510059585f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-147.5","y":"0","z":"0"}],"uuid":"23006a51-cd73-e3ba-b213-c537823211c8","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-107.5","y":"0","z":"0"}],"uuid":"d388d056-819e-3f5c-244d-1a7c2779def9","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"ad988529-fbb5-7dee-e2be-6470903df197","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"cd2ae947-1b0d-2cb4-55b3-e73bfe412ae4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"65109525-d50a-8c57-a1a1-c1f845f31b89","time":0.625,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"81c482f3-e0f4-8689-5723-b978e50693d7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"852792fa-dba4-c696-8903-76d0cf6cd30a","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"e03c676a-178b-f03e-b40d-4404ee7b6eaa","time":0.33333,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"21117162-b28a-dbe7-5b2d-ba2da62704d0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"e9534f91-cc2b-e3cf-3dda-d16bcac0381b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"59988b0f-f288-1498-fd3f-e5e76fb31afd","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"2b7c6f42-5bc9-0d92-f071-1774547278c1","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"a4e124d9-47e1-c56a-d58f-03a3b368a9a0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"73269add-beb1-4df4-dd72-7882b3fc38fb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"12.5"}],"uuid":"3b636b10-cdb7-a40f-b218-8eb1b3aa0008","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"12.5"}],"uuid":"f3b94148-4f92-306c-6646-68077a96e7fc","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"43625c14-44b7-769e-d20c-b7ae66efc0c9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ad20692b-165d-cf72-0674-86e893570e2e","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"9001b4a2-3e10-4782-15de-b440b94a0265","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"b40d0e65-f781-9910-9f5d-6872fb4aab46","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"eae385ac-0193-0adf-dd33-7896c8c38519","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1906f341-ffb9-6bcf-a293-56f56d5a7a7d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"46b1e55d-2185-fe99-fc55-d1de75cdd0cd","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"929e78b1-3d73-f810-5d38-67582cb8f00e","time":0.625,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"3868a416-df35-aa7c-72ca-aa2f7000eb3e","name":"left_attack_3","loop":"once","override":false,"length":0.75,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.783096475","y":"-30 + 30 * math.sin(query.anim_time * degrees)","z":"-5.1865092834"}],"uuid":"d0abd38c-d0de-ed94-01d0-fe17313caa21","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"0","z":"-5.1865092834"}],"uuid":"5d322262-38d5-191f-9f2e-ba246377ea6b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"0","z":"-2.6865092834"}],"uuid":"f42022d3-fbfb-e7b9-fb75-d9607fdff0ec","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-15 + 45 * math.sin(query.anim_time * degrees)","z":"0.3377321701"}],"uuid":"a026bb6e-9f3c-f381-1672-4fdb60d5d173","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"30","z":"0.3377321701"}],"uuid":"f49eded7-50f0-328f-f9bc-832f8306614c","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15","z":"0.3377321701"}],"uuid":"132e27b6-de90-f3e8-0f91-a1a0326dd4bb","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"-15 + 45 * math.sin(query.anim_time * degrees)","z":"0.3668883205"}],"uuid":"83b142f3-3039-78b5-5027-82fc96140962","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"30","z":"0.3668883205"}],"uuid":"9b1408ae-bacb-3696-9353-f583fe01691d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"15","z":"0.3668883205"}],"uuid":"72b4cbd4-6cd8-ecf3-49e1-ba9bdca95ed5","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"-15 + 45 * math.sin(query.anim_time * degrees)","z":"0.6697153441"}],"uuid":"080664d7-7ba7-0e3b-eadf-4aeca7d6ab28","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"30","z":"0.6697153441"}],"uuid":"32c655bc-7cb1-4989-8d04-ba6601a20ffa","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"15","z":"0.6697153441"}],"uuid":"2394fbbe-c4be-f4a9-7147-6f775caa2b57","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.2466262711","y":"74.742560924","z":"4.9451721289"}],"uuid":"0ca4d269-75b1-053b-2e29-97ed95ee8a27","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.25","y":"-90.26","z":"4.95"}],"uuid":"9d554f75-1c6d-a52e-231c-5b3fccb718f0","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.25","y":"-45.26","z":"4.95"}],"uuid":"1dd76bcb-6748-32e6-f1c0-af82dbaaf6ef","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25.2303398612","y":"-66.3113221393","z":"25.0049672424"}],"uuid":"f6e86818-45b8-d124-5048-235d4438e1f2","time":0.25,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-67.7996254024","y":"-18.7498221312","z":"46.5234420106"}],"uuid":"0475513c-2096-eade-6e28-e9543b1326dc","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-64.1068768939","y":"-7.3820305827","z":"59.216259349"}],"uuid":"c9030e72-7c34-0050-fac4-653b7eaf67a5","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.6615555673","y":"10.04018335","z":"57.8138546454"}],"uuid":"8ec4a464-cce1-1660-9e7e-aed5dc55ef21","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.1615555673","y":"10.04018335","z":"65.3138546454"}],"uuid":"631fb268-9cc4-cad3-c10f-1b50204f5032","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.1615555673","y":"10.04018335","z":"87.8138546454"}],"uuid":"7bd8dd80-fa0e-a6db-44f9-3641c7a860a6","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.928317261","y":"43.8968318011","z":"33.8368278453"}],"uuid":"9ae0c3e6-af01-5eff-89d3-e344ae80d232","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"17.7412720053","y":"-29.1799804704","z":"-21.7474034651"}],"uuid":"6a40116a-b813-1b1d-0348-7c2e57651f0e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.3747088224","y":"-26.0561020798","z":"-53.9720884106"}],"uuid":"60015b32-254c-b385-5730-f1fb2dca341d","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"-42.5"}],"uuid":"4ad84a41-5d07-01a0-8466-4e63692e80ab","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"25"}],"uuid":"152728f0-c892-e72c-83ce-c307706a04aa","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"42.5"}],"uuid":"6a9ba9d9-8596-88fd-cbd3-56574d16d159","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5.379429148","y":"-0.4004441987","z":"30.0108045086"}],"uuid":"2443bfb5-32c5-7ceb-7748-f884f86bff3f","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"fcaf8da0-0146-2587-b578-3e1af888deaa":{"name":"pri_right_item","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-13.5768860212","y":"23.1473092203","z":"-27.013532475"}],"uuid":"c6821d4f-794f-c7c7-0f89-80033761c353","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.6994195496","y":"13.8964693362","z":"-30.1451170148"}],"uuid":"275ad9e7-67ec-4daf-b4c5-c49ff92c0c7c","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.697928082","y":"-29.8984523542","z":"-46.3767048046"}],"uuid":"2908fdad-6769-ad97-152b-f2e86f8fba88","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"81.4976285226","y":"-85.6361454504","z":"-145.5405300408"}],"uuid":"e38f1288-48fd-30ea-940e-a1713aa5e533","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"111.4976285226","y":"-85.6361454504","z":"-145.5405300408"}],"uuid":"0bb1777f-8437-9e14-420b-ad2af484cf2e","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"66.4976285226","y":"-85.6361454504","z":"-145.5405300408"}],"uuid":"f77bbe2b-eb20-5dca-9e04-984240155daa","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"218.1984733219","y":"-82.2634224785","z":"-252.6403714152"}],"uuid":"282590c6-2b2c-80b1-d27d-1cf6d8b8d539","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"241ef05d-3297-bd29-63f7-9ac98d2fab11","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"762de5c9-6726-ade1-3357-92c739fda0b1","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"ab041c20-61fb-26af-0653-35423dce4973","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"6f854563-d459-37eb-82ce-ce7b114058d3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"35","y":"0","z":"0"}],"uuid":"0751f55e-78d5-7ea0-ae92-a3f56f61570d","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"9e37eb37-bc4a-ce3e-c99e-d247cc12acf4","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"12.5"}],"uuid":"dfdbf96d-60fd-d00b-d057-7a0fddebd2bf","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.6809719992","y":"2.3896261399","z":"5.3762737066"}],"uuid":"d8c7c5f2-d9bb-8be8-4395-282a02893319","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.6809719992","y":"2.3896261399","z":"5.3762737066"}],"uuid":"1f138668-b13a-8c4c-6d27-ae7c168a168b","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"c6d9e946-1d10-482d-14b1-0766027adba8":{"name":"prfl_right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"522f3da4-0892-8397-935d-de98f65aa7ad","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.9759091971","y":"-0.569987466","z":"-0.8300310517"}],"uuid":"484dd03c-d5cd-3c01-f305-7b0a3e0b4524","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.9759091971","y":"-0.569987466","z":"-0.8300310517"}],"uuid":"7222f20e-0201-a3b1-7e99-6902147d0697","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"9a8681c9-5339-28f0-edfd-2b8d0c193484","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.2171371372","y":"-0.2383668654","z":"6.7491823315"}],"uuid":"1e3c6439-78ef-20ec-5850-64f6d2e9b77b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.2171371372","y":"-0.2383668654","z":"6.7491823315"}],"uuid":"55cf2e15-14ac-bb80-69de-3db8dff8ff47","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"b91601e4-1df3-23af-4d55-d2a8849a97b9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"e7b6f9bd-8abf-e22f-a9c9-9bc55e7aa644","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"bb218f38-7f01-5744-470c-7767bc629b9e","time":0.75,"color":-1,"interpolation":"catmullrom"}]}}}],"animation_variable_placeholders":"degrees=180"} ================================================ FILE: platform/fabric/src/testmod/resources/knight_line.json ================================================ { "format_version": "1.21.6", "credit": "Made with Blockbench", "textures": { "0": "bettermodel:item/knight_line", "particle": "bettermodel:item/knight_line" }, "elements": [ { "from": [3, 8.5, -8], "to": [8, 8.5, 8], "rotation": {"angle": 0, "axis": "y", "origin": [-0.5, 0, 0]}, "faces": { "north": {"uv": [0, 0, 2.5, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 2.5, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [2.5, 8, 0, 0], "texture": "#0"}, "down": {"uv": [2.5, 0, 0, 8], "texture": "#0"} } }, { "from": [4.725, 7.975, -8], "to": [6.725, 7.975, 8], "rotation": {"angle": 0, "axis": "y", "origin": [-1.775, 16.475, 0]}, "faces": { "north": {"uv": [0, 0, 1, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 1, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [2.5, 16, 1.5, 8], "texture": "#0"}, "down": {"uv": [3.5, 8, 2.5, 16], "texture": "#0"} } }, { "from": [5.5, 8.5, -8], "to": [8, 8.5, 8], "rotation": {"angle": -22.5, "axis": "z", "origin": [8, 8.5, 0]}, "faces": { "north": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [6.5, 8, 5, 0], "texture": "#0"}, "down": {"uv": [8, 0, 6.5, 8], "texture": "#0"} } }, { "from": [5.5, 8.5, -8], "to": [8, 8.5, 8], "rotation": {"angle": 22.5, "axis": "z", "origin": [8, 8.5, 0]}, "faces": { "north": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [8, 8, 6.5, 0], "texture": "#0"}, "down": {"uv": [6.5, 0, 5, 8], "texture": "#0"} } }, { "from": [7.75, 6.5, 0.5], "to": [7.75, 7.5, 2.5], "rotation": {"angle": 0, "axis": "y", "origin": [7.75, 6.5, -0.5]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } }, { "from": [7, 6.75, -5.5], "to": [7, 7.75, -3.5], "rotation": {"angle": 0, "axis": "y", "origin": [7, 6.75, -6.5]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } }, { "from": [4.5, 9.5, -3.5], "to": [4.5, 10.5, -1.5], "rotation": {"angle": 0, "axis": "y", "origin": [4.5, 9.5, -3.5]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } }, { "from": [4.5, 9, 2.75], "to": [4.5, 10, 4.75], "rotation": {"angle": 0, "axis": "y", "origin": [4.5, 9, 2.75]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } } ], "groups": [ { "name": "group", "origin": [0, 0, 0], "color": 0, "children": [0, 1, 2, 3, 4, 5, 6, 7] } ] } ================================================ FILE: platform/fabric/src/testmod/resources/knight_sword.json ================================================ { "textures": { "0": "bettermodel:item/knight_sword", "particle": "bettermodel:item/knight_sword" }, "elements": [ { "from": [7.15, -4, 7.25], "to": [8.85, -2.5, 8.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -8.5, 7]}, "faces": { "north": {"uv": [6.75, 5.5, 7.25, 6], "texture": "#0"}, "east": {"uv": [6.75, 6, 7.25, 6.5], "texture": "#0"}, "south": {"uv": [6.75, 6.5, 7.25, 7], "texture": "#0"}, "west": {"uv": [0, 7, 0.5, 7.5], "texture": "#0"}, "up": {"uv": [7.5, 0.5, 7, 0], "texture": "#0"}, "down": {"uv": [7.5, 2.25, 7, 2.75], "texture": "#0"} } }, { "from": [7.2, 0, 8.25], "to": [8.8, 2, 11.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [7, 2.75, 7.5, 3.25], "texture": "#0"}, "east": {"uv": [4.25, 5.25, 5.25, 5.75], "texture": "#0"}, "south": {"uv": [4.75, 7, 5.25, 7.5], "texture": "#0"}, "west": {"uv": [5.25, 5.25, 6.25, 5.75], "texture": "#0"}, "up": {"uv": [6, 1, 5.5, 0], "texture": "#0"}, "down": {"uv": [6, 1, 5.5, 2], "texture": "#0"} } }, { "from": [6.75, -0.5, 6.75], "to": [9.25, 2, 9.25], "rotation": {"angle": -45, "axis": "x", "origin": [8, 0.75, 8]}, "faces": { "north": {"uv": [4.25, 4.5, 5, 5.25], "texture": "#0"}, "east": {"uv": [4.75, 0, 5.5, 0.75], "texture": "#0"}, "south": {"uv": [4.75, 0.75, 5.5, 1.5], "texture": "#0"}, "west": {"uv": [4.75, 1.5, 5.5, 2.25], "texture": "#0"}, "up": {"uv": [5.5, 3, 4.75, 2.25], "texture": "#0"}, "down": {"uv": [5.5, 3, 4.75, 3.75], "texture": "#0"} } }, { "from": [7.225, 0, 8.375], "to": [8.775, 1.5, 10.5], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 0.5, 10.625]}, "faces": { "north": {"uv": [5.25, 7, 5.75, 7.5], "texture": "#0"}, "east": {"uv": [5.75, 7, 6.25, 7.5], "texture": "#0"}, "south": {"uv": [6.25, 7, 6.75, 7.5], "texture": "#0"}, "west": {"uv": [6.75, 7, 7.25, 7.5], "texture": "#0"}, "up": {"uv": [7.75, 1, 7.25, 0.5], "texture": "#0"}, "down": {"uv": [7.75, 1, 7.25, 1.5], "texture": "#0"} } }, { "from": [7.225, 0, 5.5], "to": [8.775, 1.5, 7.625], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 0.5, 5.375]}, "faces": { "north": {"uv": [7.25, 1.5, 7.75, 2], "texture": "#0"}, "east": {"uv": [7.25, 3.25, 7.75, 3.75], "texture": "#0"}, "south": {"uv": [7.25, 3.75, 7.75, 4.25], "texture": "#0"}, "west": {"uv": [7.25, 4.25, 7.75, 4.75], "texture": "#0"}, "up": {"uv": [7.75, 5.25, 7.25, 4.75], "texture": "#0"}, "down": {"uv": [7.75, 5.25, 7.25, 5.75], "texture": "#0"} } }, { "from": [7.15, -1.5, 7.25], "to": [8.85, 0, 8.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [7.25, 5.75, 7.75, 6.25], "texture": "#0"}, "east": {"uv": [7.25, 6.25, 7.75, 6.75], "texture": "#0"}, "south": {"uv": [7.25, 6.75, 7.75, 7.25], "texture": "#0"}, "west": {"uv": [7.25, 7.25, 7.75, 7.75], "texture": "#0"}, "up": {"uv": [0.5, 8, 0, 7.5], "texture": "#0"}, "down": {"uv": [8, 0, 7.5, 0.5], "texture": "#0"} } }, { "from": [7.4, -8.95, 7.4], "to": [8.6, -8.25, 8.6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -8.6, 8]}, "faces": { "north": {"uv": [7, 0.5, 7.25, 0.75], "texture": "#0"}, "east": {"uv": [7.25, 2, 7.5, 2.25], "texture": "#0"}, "south": {"uv": [7.5, 3, 7.75, 3.25], "texture": "#0"}, "west": {"uv": [6.75, 8.75, 7, 9], "texture": "#0"}, "up": {"uv": [9, 7.25, 8.75, 7], "texture": "#0"}, "down": {"uv": [7.5, 8.75, 7.25, 9], "texture": "#0"} } }, { "from": [7.175, 0.925, 10.375], "to": [8.825, 2.675, 11.6], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 1.925, 10.3]}, "faces": { "north": {"uv": [1.25, 7.5, 1.75, 8], "texture": "#0"}, "east": {"uv": [5.5, 4, 5.75, 4.5], "texture": "#0"}, "south": {"uv": [7.5, 2, 8, 2.5], "texture": "#0"}, "west": {"uv": [6, 3.5, 6.25, 4], "texture": "#0"}, "up": {"uv": [6.25, 5.25, 5.75, 5], "texture": "#0"}, "down": {"uv": [8.5, 2.75, 8, 3], "texture": "#0"} } }, { "from": [7.175, 0.925, 4.4], "to": [8.825, 2.675, 5.625], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 1.925, 5.7]}, "faces": { "north": {"uv": [7.5, 2.5, 8, 3], "texture": "#0"}, "east": {"uv": [8.25, 2.25, 8.5, 2.75], "texture": "#0"}, "south": {"uv": [4.25, 7.5, 4.75, 8], "texture": "#0"}, "west": {"uv": [8.25, 3, 8.5, 3.5], "texture": "#0"}, "up": {"uv": [8.75, 1, 8.25, 0.75], "texture": "#0"}, "down": {"uv": [8.75, 3.5, 8.25, 3.75], "texture": "#0"} } }, { "from": [7.25, -12.75, 7.5], "to": [8.75, -11.5, 8.75], "rotation": {"angle": 45, "axis": "x", "origin": [8, -12, 8]}, "faces": { "north": {"uv": [8.25, 7.75, 8.75, 8], "texture": "#0"}, "east": {"uv": [8.5, 8.75, 8.75, 9], "texture": "#0"}, "south": {"uv": [8.25, 8, 8.75, 8.25], "texture": "#0"}, "west": {"uv": [8.75, 8.5, 9, 8.75], "texture": "#0"}, "up": {"uv": [8.75, 8.5, 8.25, 8.25], "texture": "#0"}, "down": {"uv": [9, 0, 8.5, 0.25], "texture": "#0"} } }, { "from": [7, -5.25, 7.1], "to": [9, -4, 8.9], "rotation": {"angle": 0, "axis": "y", "origin": [7, -8.75, 7]}, "faces": { "north": {"uv": [8.5, 0.25, 9, 0.5], "texture": "#0"}, "east": {"uv": [8.5, 0.5, 9, 0.75], "texture": "#0"}, "south": {"uv": [8.5, 1, 9, 1.25], "texture": "#0"}, "west": {"uv": [1.25, 8.5, 1.75, 8.75], "texture": "#0"}, "up": {"uv": [5.75, 8, 5.25, 7.5], "texture": "#0"}, "down": {"uv": [6.25, 7.5, 5.75, 8], "texture": "#0"} } }, { "from": [7.025, -4.95, 8.725], "to": [8.975, -3.8, 9.2], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, -4.325, 8.3]}, "faces": { "north": {"uv": [8.75, 0.75, 9.25, 1], "texture": "#0"}, "east": {"uv": [4.5, 9.5, 4.75, 9.75], "texture": "#0"}, "south": {"uv": [1, 8.75, 1.5, 9], "texture": "#0"}, "west": {"uv": [9.5, 4.5, 9.75, 4.75], "texture": "#0"}, "up": {"uv": [2, 9, 1.5, 8.75], "texture": "#0"}, "down": {"uv": [3.75, 8.75, 3.25, 9], "texture": "#0"} } }, { "from": [7.025, -4.95, 6.8], "to": [8.975, -3.8, 7.275], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -4.325, 7.7]}, "faces": { "north": {"uv": [8.75, 3.5, 9.25, 3.75], "texture": "#0"}, "east": {"uv": [4.75, 9.5, 5, 9.75], "texture": "#0"}, "south": {"uv": [3.75, 8.75, 4.25, 9], "texture": "#0"}, "west": {"uv": [9.5, 4.75, 9.75, 5], "texture": "#0"}, "up": {"uv": [4.75, 9, 4.25, 8.75], "texture": "#0"}, "down": {"uv": [5.25, 8.75, 4.75, 9], "texture": "#0"} } }, { "from": [7.05, -5.975, 7.1], "to": [8.95, -4.7, 8.375], "rotation": {"angle": 45, "axis": "x", "origin": [8, -5.6, 8]}, "faces": { "north": {"uv": [8.5, 7.5, 9, 7.75], "texture": "#0"}, "east": {"uv": [4.25, 9.5, 4.5, 9.75], "texture": "#0"}, "south": {"uv": [7.75, 8.5, 8.25, 8.75], "texture": "#0"}, "west": {"uv": [9.5, 4.25, 9.75, 4.5], "texture": "#0"}, "up": {"uv": [8.75, 8.75, 8.25, 8.5], "texture": "#0"}, "down": {"uv": [1, 8.75, 0.5, 9], "texture": "#0"} } }, { "from": [7.4, -9.95, 7.4], "to": [8.6, -9.25, 8.6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -9.6, 8]}, "faces": { "north": {"uv": [5.5, 9.5, 5.75, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 5.5, 9.75, 5.75], "texture": "#0"}, "south": {"uv": [5.75, 9.5, 6, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 5.75, 9.75, 6], "texture": "#0"}, "up": {"uv": [6.25, 9.75, 6, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 6, 9.5, 6.25], "texture": "#0"} } }, { "from": [7.4, -10.95, 7.4], "to": [8.6, -10.25, 8.6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -10.6, 8]}, "faces": { "north": {"uv": [8.75, 8.75, 9, 9], "texture": "#0"}, "east": {"uv": [0, 9, 0.25, 9.25], "texture": "#0"}, "south": {"uv": [9, 0, 9.25, 0.25], "texture": "#0"}, "west": {"uv": [0.25, 9, 0.5, 9.25], "texture": "#0"}, "up": {"uv": [9.25, 0.5, 9, 0.25], "texture": "#0"}, "down": {"uv": [0.75, 9, 0.5, 9.25], "texture": "#0"} } }, { "from": [7.5, -12, 7.5], "to": [8.5, -5.25, 8.5], "rotation": {"angle": 0, "axis": "y", "origin": [7, -8.5, 7]}, "faces": { "north": {"uv": [0.5, 6, 0.75, 7.75], "texture": "#0"}, "east": {"uv": [0.75, 6, 1, 7.75], "texture": "#0"}, "south": {"uv": [1, 6, 1.25, 7.75], "texture": "#0"}, "west": {"uv": [6, 1, 6.25, 2.75], "texture": "#0"}, "up": {"uv": [9.25, 0.75, 9, 0.5], "texture": "#0"}, "down": {"uv": [1, 9, 0.75, 9.25], "texture": "#0"} } }, { "from": [8.3, -4.3, 8.925], "to": [8.75, 0.15, 9.675], "rotation": {"angle": 22.5, "axis": "x", "origin": [8.55, -1.925, 9.425]}, "faces": { "north": {"uv": [6.25, 7.5, 6.5, 8.5], "texture": "#0"}, "east": {"uv": [6.5, 7.5, 6.75, 8.5], "texture": "#0"}, "south": {"uv": [6.75, 7.5, 7, 8.5], "texture": "#0"}, "west": {"uv": [7, 7.5, 7.25, 8.5], "texture": "#0"}, "up": {"uv": [1.25, 9.25, 1, 9], "texture": "#0"}, "down": {"uv": [9.25, 1, 9, 1.25], "texture": "#0"} } }, { "from": [7.25, -4.3, 8.925], "to": [7.7, 0.15, 9.675], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.45, -1.925, 9.425]}, "faces": { "north": {"uv": [0.5, 7.75, 0.75, 8.75], "texture": "#0"}, "east": {"uv": [7.75, 0.5, 8, 1.5], "texture": "#0"}, "south": {"uv": [0.75, 7.75, 1, 8.75], "texture": "#0"}, "west": {"uv": [1, 7.75, 1.25, 8.75], "texture": "#0"}, "up": {"uv": [1.5, 9.25, 1.25, 9], "texture": "#0"}, "down": {"uv": [9.25, 1.25, 9, 1.5], "texture": "#0"} } }, { "from": [6.95, -0.425, 2.575], "to": [9.05, 2.45, 4.25], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 1.925, 3.55]}, "faces": { "north": {"uv": [5.25, 6.25, 5.75, 7], "texture": "#0"}, "east": {"uv": [5.75, 6.25, 6.25, 7], "texture": "#0"}, "south": {"uv": [6.25, 6.25, 6.75, 7], "texture": "#0"}, "west": {"uv": [6.5, 0, 7, 0.75], "texture": "#0"}, "up": {"uv": [8.25, 2, 7.75, 1.5], "texture": "#0"}, "down": {"uv": [8.25, 3, 7.75, 3.5], "texture": "#0"} } }, { "from": [7.2, 0, 4.25], "to": [8.8, 2, 7.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [7.75, 3.5, 8.25, 4], "texture": "#0"}, "east": {"uv": [4.75, 5.75, 5.75, 6.25], "texture": "#0"}, "south": {"uv": [7.75, 4, 8.25, 4.5], "texture": "#0"}, "west": {"uv": [5.75, 5.75, 6.75, 6.25], "texture": "#0"}, "up": {"uv": [0.5, 7, 0, 6], "texture": "#0"}, "down": {"uv": [6.5, 0, 6, 1], "texture": "#0"} } }, { "from": [7.2, -0.925, 13.2], "to": [8.8, 1.675, 14.3], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 9.75]}, "faces": { "north": {"uv": [6.5, 2.5, 7, 3.25], "texture": "#0"}, "east": {"uv": [8, 7, 8.25, 7.75], "texture": "#0"}, "south": {"uv": [6.75, 0.75, 7.25, 1.5], "texture": "#0"}, "west": {"uv": [8, 7.75, 8.25, 8.5], "texture": "#0"}, "up": {"uv": [9, 3.5, 8.5, 3.25], "texture": "#0"}, "down": {"uv": [9, 3.75, 8.5, 4], "texture": "#0"} } }, { "from": [7.45, -0.575, 13.875], "to": [8.55, 1.325, 14.65], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 0.475, 14.2]}, "faces": { "north": {"uv": [5.25, 8.75, 5.5, 9.25], "texture": "#0"}, "east": {"uv": [8.75, 5.25, 9, 5.75], "texture": "#0"}, "south": {"uv": [5.5, 8.75, 5.75, 9.25], "texture": "#0"}, "west": {"uv": [5.75, 8.75, 6, 9.25], "texture": "#0"}, "up": {"uv": [5.25, 9.75, 5, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 5, 9.5, 5.25], "texture": "#0"} } }, { "from": [7.45, -0.575, 1.35], "to": [8.55, 1.325, 2.125], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 0.475, 1.8]}, "faces": { "north": {"uv": [8.75, 5.75, 9, 6.25], "texture": "#0"}, "east": {"uv": [6, 8.75, 6.25, 9.25], "texture": "#0"}, "south": {"uv": [8.75, 6.25, 9, 6.75], "texture": "#0"}, "west": {"uv": [6.5, 8.75, 6.75, 9.25], "texture": "#0"}, "up": {"uv": [5.5, 9.75, 5.25, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 5.25, 9.5, 5.5], "texture": "#0"} } }, { "from": [6.95, -0.425, 11.75], "to": [9.05, 2.45, 13.425], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 1.925, 12.45]}, "faces": { "north": {"uv": [1.25, 6.75, 1.75, 7.5], "texture": "#0"}, "east": {"uv": [6.75, 1.5, 7.25, 2.25], "texture": "#0"}, "south": {"uv": [6.75, 3.25, 7.25, 4], "texture": "#0"}, "west": {"uv": [6.75, 4, 7.25, 4.75], "texture": "#0"}, "up": {"uv": [8.25, 5, 7.75, 4.5], "texture": "#0"}, "down": {"uv": [8.25, 5, 7.75, 5.5], "texture": "#0"} } }, { "from": [7.45, -0.275, 12.475], "to": [8.55, 1.2, 13.2], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 9.75]}, "faces": { "north": {"uv": [2, 9, 2.25, 9.25], "texture": "#0"}, "east": {"uv": [9, 2, 9.25, 2.25], "texture": "#0"}, "south": {"uv": [2.25, 9, 2.5, 9.25], "texture": "#0"}, "west": {"uv": [9, 2.25, 9.25, 2.5], "texture": "#0"}, "up": {"uv": [2.75, 9.25, 2.5, 9], "texture": "#0"}, "down": {"uv": [9.25, 2.5, 9, 2.75], "texture": "#0"} } }, { "from": [7.2, -0.925, 1.7], "to": [8.8, 1.675, 2.8], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 6.25]}, "faces": { "north": {"uv": [4.25, 6.75, 4.75, 7.5], "texture": "#0"}, "east": {"uv": [8.25, 0, 8.5, 0.75], "texture": "#0"}, "south": {"uv": [6.75, 4.75, 7.25, 5.5], "texture": "#0"}, "west": {"uv": [8.25, 1.5, 8.5, 2.25], "texture": "#0"}, "up": {"uv": [9, 4.25, 8.5, 4], "texture": "#0"}, "down": {"uv": [9, 4.25, 8.5, 4.5], "texture": "#0"} } }, { "from": [7.45, -0.275, 2.8], "to": [8.55, 1.2, 3.525], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 6.25]}, "faces": { "north": {"uv": [2.75, 9, 3, 9.25], "texture": "#0"}, "east": {"uv": [9, 2.75, 9.25, 3], "texture": "#0"}, "south": {"uv": [3, 9, 3.25, 9.25], "texture": "#0"}, "west": {"uv": [9, 3, 9.25, 3.25], "texture": "#0"}, "up": {"uv": [3.5, 9.25, 3.25, 9], "texture": "#0"}, "down": {"uv": [9.25, 3.25, 9, 3.5], "texture": "#0"} } }, { "from": [7.25, -4.3, 6.325], "to": [7.7, 0.15, 7.075], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.45, -1.925, 6.575]}, "faces": { "north": {"uv": [7.75, 5.5, 8, 6.5], "texture": "#0"}, "east": {"uv": [7.75, 6.5, 8, 7.5], "texture": "#0"}, "south": {"uv": [7.25, 7.75, 7.5, 8.75], "texture": "#0"}, "west": {"uv": [7.5, 7.75, 7.75, 8.75], "texture": "#0"}, "up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#0"}, "down": {"uv": [4, 9, 3.75, 9.25], "texture": "#0"} } }, { "from": [8.3, -4.3, 6.325], "to": [8.75, 0.15, 7.075], "rotation": {"angle": -22.5, "axis": "x", "origin": [8.55, -1.925, 6.575]}, "faces": { "north": {"uv": [7.75, 7.5, 8, 8.5], "texture": "#0"}, "east": {"uv": [0, 8, 0.25, 9], "texture": "#0"}, "south": {"uv": [8, 0, 8.25, 1], "texture": "#0"}, "west": {"uv": [0.25, 8, 0.5, 9], "texture": "#0"}, "up": {"uv": [9.25, 4, 9, 3.75], "texture": "#0"}, "down": {"uv": [4.25, 9, 4, 9.25], "texture": "#0"} } }, { "from": [7, -2.5, 6.95], "to": [9, -1.5, 9.05], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [8.5, 4.5, 9, 4.75], "texture": "#0"}, "east": {"uv": [4.75, 8.5, 5.25, 8.75], "texture": "#0"}, "south": {"uv": [8.5, 4.75, 9, 5], "texture": "#0"}, "west": {"uv": [8.5, 5, 9, 5.25], "texture": "#0"}, "up": {"uv": [8.5, 1.5, 8, 1], "texture": "#0"}, "down": {"uv": [1.75, 8, 1.25, 8.5], "texture": "#0"} } }, { "from": [7.75, -2.25, 9], "to": [8.25, -1.75, 9.7], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [9, 4, 9.25, 4.25], "texture": "#0"}, "east": {"uv": [4.25, 9, 4.5, 9.25], "texture": "#0"}, "south": {"uv": [9, 4.25, 9.25, 4.5], "texture": "#0"}, "west": {"uv": [4.5, 9, 4.75, 9.25], "texture": "#0"}, "up": {"uv": [9.25, 4.75, 9, 4.5], "texture": "#0"}, "down": {"uv": [5, 9, 4.75, 9.25], "texture": "#0"} } }, { "from": [7.5, -2.25, 9.55], "to": [8.5, -1.75, 10.05], "rotation": {"angle": -45, "axis": "x", "origin": [8, -2, 9.8]}, "faces": { "north": {"uv": [9, 4.75, 9.25, 5], "texture": "#0"}, "east": {"uv": [5, 9, 5.25, 9.25], "texture": "#0"}, "south": {"uv": [9, 5, 9.25, 5.25], "texture": "#0"}, "west": {"uv": [9, 5.25, 9.25, 5.5], "texture": "#0"}, "up": {"uv": [9.25, 5.75, 9, 5.5], "texture": "#0"}, "down": {"uv": [9.25, 5.75, 9, 6], "texture": "#0"} } }, { "from": [7.75, -2.25, 6.3], "to": [8.25, -1.75, 7], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 9]}, "faces": { "north": {"uv": [9, 6, 9.25, 6.25], "texture": "#0"}, "east": {"uv": [6.25, 9, 6.5, 9.25], "texture": "#0"}, "south": {"uv": [9, 6.25, 9.25, 6.5], "texture": "#0"}, "west": {"uv": [9, 6.5, 9.25, 6.75], "texture": "#0"}, "up": {"uv": [7, 9.25, 6.75, 9], "texture": "#0"}, "down": {"uv": [9.25, 6.75, 9, 7], "texture": "#0"} } }, { "from": [7.5, -2.25, 5.95], "to": [8.5, -1.75, 6.45], "rotation": {"angle": 45, "axis": "x", "origin": [8, -2, 6.2]}, "faces": { "north": {"uv": [7, 9, 7.25, 9.25], "texture": "#0"}, "east": {"uv": [9, 7, 9.25, 7.25], "texture": "#0"}, "south": {"uv": [7.25, 9, 7.5, 9.25], "texture": "#0"}, "west": {"uv": [9, 7.25, 9.25, 7.5], "texture": "#0"}, "up": {"uv": [7.75, 9.25, 7.5, 9], "texture": "#0"}, "down": {"uv": [9.25, 7.5, 9, 7.75], "texture": "#0"} } }, { "from": [7.175, 2.3, 4.275], "to": [8.825, 2.95, 4.5], "rotation": {"angle": -45, "axis": "x", "origin": [8, 2.2, 4.575]}, "faces": { "north": {"uv": [8.5, 1.25, 9, 1.5], "texture": "#0"}, "east": {"uv": [1.5, 9, 1.75, 9.25], "texture": "#0"}, "south": {"uv": [8.5, 1.5, 9, 1.75], "texture": "#0"}, "west": {"uv": [9, 1.5, 9.25, 1.75], "texture": "#0"}, "up": {"uv": [9, 2, 8.5, 1.75], "texture": "#0"}, "down": {"uv": [9, 2, 8.5, 2.25], "texture": "#0"} } }, { "from": [7.175, 2.3, 11.5], "to": [8.825, 2.95, 11.725], "rotation": {"angle": 45, "axis": "x", "origin": [8, 2.2, 11.425]}, "faces": { "north": {"uv": [8.5, 2.25, 9, 2.5], "texture": "#0"}, "east": {"uv": [1.75, 9, 2, 9.25], "texture": "#0"}, "south": {"uv": [8.5, 2.5, 9, 2.75], "texture": "#0"}, "west": {"uv": [9, 1.75, 9.25, 2], "texture": "#0"}, "up": {"uv": [9, 3, 8.5, 2.75], "texture": "#0"}, "down": {"uv": [9, 3, 8.5, 3.25], "texture": "#0"} } }, { "from": [7.5, 22.05, 6.7], "to": [8.5, 25.4, 8.75], "rotation": {"angle": -20, "axis": "x", "origin": [8, 29.8, 7.45]}, "faces": { "north": {"uv": [4.75, 7.5, 5, 8.5], "texture": "#0"}, "east": {"uv": [5.75, 4, 6.25, 5], "texture": "#0"}, "south": {"uv": [5, 7.5, 5.25, 8.5], "texture": "#0"}, "west": {"uv": [4.25, 5.75, 4.75, 6.75], "texture": "#0"}, "up": {"uv": [8.5, 5.25, 8.25, 4.75], "texture": "#0"}, "down": {"uv": [8.5, 5.25, 8.25, 5.75], "texture": "#0"} } }, { "from": [7.5, 5, 9], "to": [8.5, 22.75, 10.25], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [2, 0, 2.25, 4.5], "texture": "#0"}, "east": {"uv": [2.25, 0, 2.5, 4.5], "texture": "#0"}, "south": {"uv": [2.5, 0, 2.75, 4.5], "texture": "#0"}, "west": {"uv": [2.75, 0, 3, 4.5], "texture": "#0"}, "up": {"uv": [6.5, 2.75, 6.25, 2.5], "texture": "#0"}, "down": {"uv": [6.75, 0.75, 6.5, 1], "texture": "#0"} } }, { "from": [7.8, 2, 10.45], "to": [8.5, 5, 11.15], "rotation": {"angle": 45, "axis": "y", "origin": [8, 9.5, 10.95]}, "faces": { "north": {"uv": [3.25, 8, 3.5, 8.75], "texture": "#0"}, "east": {"uv": [3.5, 8, 3.75, 8.75], "texture": "#0"}, "south": {"uv": [3.75, 8, 4, 8.75], "texture": "#0"}, "west": {"uv": [4, 8, 4.25, 8.75], "texture": "#0"}, "up": {"uv": [9, 7.5, 8.75, 7.25], "texture": "#0"}, "down": {"uv": [7.75, 8.75, 7.5, 9], "texture": "#0"} } }, { "from": [7.8, 5, 9.95], "to": [8.5, 22.75, 10.65], "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 10.45]}, "faces": { "north": {"uv": [2.25, 4.5, 2.5, 9], "texture": "#0"}, "east": {"uv": [2.5, 4.5, 2.75, 9], "texture": "#0"}, "south": {"uv": [2.75, 4.5, 3, 9], "texture": "#0"}, "west": {"uv": [3, 4.5, 3.25, 9], "texture": "#0"}, "up": {"uv": [8.5, 9, 8.25, 8.75], "texture": "#0"}, "down": {"uv": [9, 8.25, 8.75, 8.5], "texture": "#0"} } }, { "from": [7.8, 5, 5.35], "to": [8.5, 22.75, 6.05], "rotation": {"angle": -45, "axis": "y", "origin": [8, 12.5, 5.55]}, "faces": { "north": {"uv": [4, 0, 4.25, 4.5], "texture": "#0"}, "east": {"uv": [4.25, 0, 4.5, 4.5], "texture": "#0"}, "south": {"uv": [4.5, 0, 4.75, 4.5], "texture": "#0"}, "west": {"uv": [2, 4.5, 2.25, 9], "texture": "#0"}, "up": {"uv": [8, 9, 7.75, 8.75], "texture": "#0"}, "down": {"uv": [9, 7.75, 8.75, 8], "texture": "#0"} } }, { "from": [7.8, 2, 4.85], "to": [8.5, 5, 5.55], "rotation": {"angle": -45, "axis": "y", "origin": [8, 9.5, 5.05]}, "faces": { "north": {"uv": [8, 5.5, 8.25, 6.25], "texture": "#0"}, "east": {"uv": [5.75, 8, 6, 8.75], "texture": "#0"}, "south": {"uv": [6, 8, 6.25, 8.75], "texture": "#0"}, "west": {"uv": [8, 6.25, 8.25, 7], "texture": "#0"}, "up": {"uv": [8.25, 9, 8, 8.75], "texture": "#0"}, "down": {"uv": [9, 8, 8.75, 8.25], "texture": "#0"} } }, { "from": [7.75, 2, 6.75], "to": [8.25, 25.5, 9.25], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [1.5, 0, 1.75, 6], "texture": "#0"}, "east": {"uv": [0, 0, 0.75, 6], "texture": "#0"}, "south": {"uv": [1.75, 0, 2, 6], "texture": "#0"}, "west": {"uv": [0.75, 0, 1.5, 6], "texture": "#0"}, "up": {"uv": [2, 8.75, 1.75, 8], "texture": "#0"}, "down": {"uv": [8.25, 2, 8, 2.75], "texture": "#0"} } }, { "from": [7.5, 24.825, 5.825], "to": [8.5, 27.9, 8.9], "rotation": {"angle": -45, "axis": "x", "origin": [8, 27, 8]}, "faces": { "north": {"uv": [1.25, 6, 1.75, 6.75], "texture": "#0"}, "east": {"uv": [4.75, 3.75, 5.5, 4.5], "texture": "#0"}, "south": {"uv": [6, 2.75, 6.5, 3.5], "texture": "#0"}, "west": {"uv": [5, 4.5, 5.75, 5.25], "texture": "#0"}, "up": {"uv": [6.75, 1.75, 6.25, 1], "texture": "#0"}, "down": {"uv": [6.75, 1.75, 6.25, 2.5], "texture": "#0"} } }, { "from": [7.5, 5, 5.75], "to": [8.5, 22.75, 7], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [3, 0, 3.25, 4.5], "texture": "#0"}, "east": {"uv": [3.25, 0, 3.5, 4.5], "texture": "#0"}, "south": {"uv": [3.5, 0, 3.75, 4.5], "texture": "#0"}, "west": {"uv": [3.75, 0, 4, 4.5], "texture": "#0"}, "up": {"uv": [6.75, 3.5, 6.5, 3.25], "texture": "#0"}, "down": {"uv": [7, 2.25, 6.75, 2.5], "texture": "#0"} } }, { "from": [7.5, 2, 9.25], "to": [8.5, 5, 10.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [5.25, 8, 5.5, 8.75], "texture": "#0"}, "east": {"uv": [4.75, 6.25, 5.25, 7], "texture": "#0"}, "south": {"uv": [5.5, 8, 5.75, 8.75], "texture": "#0"}, "west": {"uv": [6.25, 5, 6.75, 5.75], "texture": "#0"}, "up": {"uv": [8.5, 7.25, 8.25, 6.75], "texture": "#0"}, "down": {"uv": [8.5, 7.25, 8.25, 7.75], "texture": "#0"} } }, { "from": [7.5, 2, 5.25], "to": [8.5, 5, 6.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [4.25, 8, 4.5, 8.75], "texture": "#0"}, "east": {"uv": [6.25, 3.5, 6.75, 4.25], "texture": "#0"}, "south": {"uv": [4.5, 8, 4.75, 8.75], "texture": "#0"}, "west": {"uv": [6.25, 4.25, 6.75, 5], "texture": "#0"}, "up": {"uv": [8.5, 6.25, 8.25, 5.75], "texture": "#0"}, "down": {"uv": [8.5, 6.25, 8.25, 6.75], "texture": "#0"} } }, { "from": [7.5, 22.05, 7.25], "to": [8.5, 25.4, 9.3], "rotation": {"angle": 20, "axis": "x", "origin": [8, 29.8, 8.55]}, "faces": { "north": {"uv": [1.75, 6, 2, 7], "texture": "#0"}, "east": {"uv": [5.5, 2, 6, 3], "texture": "#0"}, "south": {"uv": [1.75, 7, 2, 8], "texture": "#0"}, "west": {"uv": [5.5, 3, 6, 4], "texture": "#0"}, "up": {"uv": [8.5, 4.25, 8.25, 3.75], "texture": "#0"}, "down": {"uv": [8.5, 4.25, 8.25, 4.75], "texture": "#0"} } }, { "name": "rune_1", "from": [7.5, 2.975, 7.875], "to": [8.5, 3.475, 8.125], "rotation": {"angle": 0, "axis": "y", "origin": [8, 16.975, 8.375]}, "faces": { "north": {"uv": [0.5, 9.5, 0.75, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 0.5, 9.75, 0.75], "texture": "#0"}, "south": {"uv": [0.75, 9.5, 1, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 0.75, 9.75, 1], "texture": "#0"}, "up": {"uv": [1.25, 9.75, 1, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 1, 9.5, 1.25], "texture": "#0"} } }, { "name": "rune_2", "from": [7.5, 3.5625, 7.25], "to": [8.5, 4.3125, 7.625], "rotation": {"angle": 45, "axis": "x", "origin": [8, 2.8125, 8]}, "faces": { "north": {"uv": [9.25, 7.25, 9.5, 7.5], "texture": "#0"}, "east": {"uv": [7.5, 9.25, 7.75, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 7.5, 9.5, 7.75], "texture": "#0"}, "west": {"uv": [7.75, 9.25, 8, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 8, 9.25, 7.75], "texture": "#0"}, "down": {"uv": [8.25, 9.25, 8, 9.5], "texture": "#0"} } }, { "name": "rune_2", "from": [7.5, 3.1875, 6.5], "to": [8.5, 3.5625, 7.625], "rotation": {"angle": 45, "axis": "x", "origin": [8, 2.8125, 8]}, "faces": { "north": {"uv": [9.25, 9, 9.5, 9.25], "texture": "#0"}, "east": {"uv": [9.25, 9.25, 9.5, 9.5], "texture": "#0"}, "south": {"uv": [0, 9.5, 0.25, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 0, 9.75, 0.25], "texture": "#0"}, "up": {"uv": [0.5, 9.75, 0.25, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 0.25, 9.5, 0.5], "texture": "#0"} } }, { "name": "rune_3", "from": [7.5, 5.25, 6.125], "to": [8.5, 5.625, 7.625], "rotation": {"angle": -45, "axis": "x", "origin": [8, 6, 8]}, "faces": { "north": {"uv": [9.25, 8.75, 9.5, 9], "texture": "#0"}, "east": {"uv": [6.5, 8.5, 7, 8.75], "texture": "#0"}, "south": {"uv": [9, 9.25, 9.25, 9.5], "texture": "#0"}, "west": {"uv": [8.5, 6.75, 9, 7], "texture": "#0"}, "up": {"uv": [7.25, 9, 7, 8.5], "texture": "#0"}, "down": {"uv": [8.75, 7, 8.5, 7.5], "texture": "#0"} } }, { "name": "rune_3", "from": [7.5, 4.125, 7.25], "to": [8.5, 5.25, 7.625], "rotation": {"angle": -45, "axis": "x", "origin": [8, 6, 8]}, "faces": { "north": {"uv": [9.25, 8, 9.5, 8.25], "texture": "#0"}, "east": {"uv": [8.25, 9.25, 8.5, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 8.25, 9.5, 8.5], "texture": "#0"}, "west": {"uv": [8.5, 9.25, 8.75, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 8.75, 9.25, 8.5], "texture": "#0"}, "down": {"uv": [9, 9.25, 8.75, 9.5], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 5.35, 7.875], "to": [8.5, 18.85, 8.125], "rotation": {"angle": 0, "axis": "y", "origin": [8, 17.1, 8.375]}, "faces": { "north": {"uv": [3.25, 4.5, 3.5, 8], "texture": "#0"}, "east": {"uv": [3.5, 4.5, 3.75, 8], "texture": "#0"}, "south": {"uv": [3.75, 4.5, 4, 8], "texture": "#0"}, "west": {"uv": [4, 4.5, 4.25, 8], "texture": "#0"}, "up": {"uv": [9.5, 2.75, 9.25, 2.5], "texture": "#0"}, "down": {"uv": [3, 9.25, 2.75, 9.5], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 7.25], "to": [8.5, 7.05, 7.7], "rotation": {"angle": -45, "axis": "x", "origin": [7.875, 6.725, 7.575]}, "faces": { "north": {"uv": [1.25, 9.5, 1.5, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 1.25, 9.75, 1.5], "texture": "#0"}, "south": {"uv": [1.5, 9.5, 1.75, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 1.5, 9.75, 1.75], "texture": "#0"}, "up": {"uv": [2, 9.75, 1.75, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 1.75, 9.5, 2], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 7.625], "to": [8.5, 6.85, 7.875], "rotation": {"angle": 0, "axis": "y", "origin": [8, 11.1, 8.375]}, "faces": { "north": {"uv": [2, 9.5, 2.25, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 2, 9.75, 2.25], "texture": "#0"}, "south": {"uv": [2.25, 9.5, 2.5, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 2.25, 9.75, 2.5], "texture": "#0"}, "up": {"uv": [2.75, 9.75, 2.5, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 2.5, 9.5, 2.75], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 8.3], "to": [8.5, 7.05, 8.75], "rotation": {"angle": 45, "axis": "x", "origin": [7.875, 6.725, 8.425]}, "faces": { "north": {"uv": [2.75, 9.5, 3, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 2.75, 9.75, 3], "texture": "#0"}, "south": {"uv": [3, 9.5, 3.25, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 3, 9.75, 3.25], "texture": "#0"}, "up": {"uv": [3.5, 9.75, 3.25, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 3.25, 9.5, 3.5], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 8.125], "to": [8.5, 6.85, 8.375], "rotation": {"angle": 0, "axis": "y", "origin": [8, 11.1, 7.625]}, "faces": { "north": {"uv": [3.5, 9.5, 3.75, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 3.5, 9.75, 3.75], "texture": "#0"}, "south": {"uv": [3.75, 9.5, 4, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 3.75, 9.75, 4], "texture": "#0"}, "up": {"uv": [4.25, 9.75, 4, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 4, 9.5, 4.25], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.875, 7.5], "to": [8.5, 19.375, 7.75], "rotation": {"angle": 45, "axis": "x", "origin": [8, 18.375, 8]}, "faces": { "north": {"uv": [9.25, 1, 9.5, 1.25], "texture": "#0"}, "east": {"uv": [1.25, 9.25, 1.5, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 1.25, 9.5, 1.5], "texture": "#0"}, "west": {"uv": [1.5, 9.25, 1.75, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 1.75, 9.25, 1.5], "texture": "#0"}, "down": {"uv": [2, 9.25, 1.75, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.625, 7], "to": [8.5, 18.875, 7.75], "rotation": {"angle": 45, "axis": "x", "origin": [8, 18.375, 8]}, "faces": { "north": {"uv": [9.25, 1.75, 9.5, 2], "texture": "#0"}, "east": {"uv": [2, 9.25, 2.25, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 2, 9.5, 2.25], "texture": "#0"}, "west": {"uv": [2.25, 9.25, 2.5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 2.5, 9.25, 2.25], "texture": "#0"}, "down": {"uv": [2.75, 9.25, 2.5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.425, 7.175], "to": [8.5, 18.575, 7.925], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.875, 18.55, 7.8]}, "faces": { "north": {"uv": [9.25, 2.75, 9.5, 3], "texture": "#0"}, "east": {"uv": [3, 9.25, 3.25, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 3, 9.5, 3.25], "texture": "#0"}, "west": {"uv": [3.25, 9.25, 3.5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 3.5, 9.25, 3.25], "texture": "#0"}, "down": {"uv": [3.75, 9.25, 3.5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.925, 7.425], "to": [8.5, 18.075, 7.925], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.875, 18.05, 7.8]}, "faces": { "north": {"uv": [9.25, 3.5, 9.5, 3.75], "texture": "#0"}, "east": {"uv": [3.75, 9.25, 4, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 3.75, 9.5, 4], "texture": "#0"}, "west": {"uv": [4, 9.25, 4.25, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 4.25, 9.25, 4], "texture": "#0"}, "down": {"uv": [4.5, 9.25, 4.25, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.425, 7.675], "to": [8.5, 17.575, 7.925], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.875, 17.55, 7.8]}, "faces": { "north": {"uv": [9.25, 4.25, 9.5, 4.5], "texture": "#0"}, "east": {"uv": [4.5, 9.25, 4.75, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 4.5, 9.5, 4.75], "texture": "#0"}, "west": {"uv": [4.75, 9.25, 5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 5, 9.25, 4.75], "texture": "#0"}, "down": {"uv": [5.25, 9.25, 5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.425, 8.075], "to": [8.5, 17.575, 8.325], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.875, 17.55, 8.2]}, "faces": { "north": {"uv": [9.25, 5, 9.5, 5.25], "texture": "#0"}, "east": {"uv": [5.25, 9.25, 5.5, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 5.25, 9.5, 5.5], "texture": "#0"}, "west": {"uv": [5.5, 9.25, 5.75, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 5.75, 9.25, 5.5], "texture": "#0"}, "down": {"uv": [6, 9.25, 5.75, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.925, 8.075], "to": [8.5, 18.075, 8.575], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.875, 18.05, 8.2]}, "faces": { "north": {"uv": [9.25, 5.75, 9.5, 6], "texture": "#0"}, "east": {"uv": [6, 9.25, 6.25, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 6, 9.5, 6.25], "texture": "#0"}, "west": {"uv": [6.25, 9.25, 6.5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 6.5, 9.25, 6.25], "texture": "#0"}, "down": {"uv": [6.75, 9.25, 6.5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.425, 8.075], "to": [8.5, 18.575, 8.825], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.875, 18.55, 8.2]}, "faces": { "north": {"uv": [9.25, 6.5, 9.5, 6.75], "texture": "#0"}, "east": {"uv": [6.75, 9.25, 7, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 6.75, 9.5, 7], "texture": "#0"}, "west": {"uv": [7, 9.25, 7.25, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 7.25, 9.25, 7], "texture": "#0"}, "down": {"uv": [7.5, 9.25, 7.25, 9.5], "texture": "#0"} } }, { "name": "rune_6", "from": [7.5, 19.25, 7.5], "to": [8.5, 20, 7.75], "rotation": {"angle": -45, "axis": "x", "origin": [8, 20.5, 8]}, "faces": { "north": {"uv": [9.25, 0.25, 9.5, 0.5], "texture": "#0"}, "east": {"uv": [0.5, 9.25, 0.75, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 0.5, 9.5, 0.75], "texture": "#0"}, "west": {"uv": [0.75, 9.25, 1, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 1, 9.25, 0.75], "texture": "#0"}, "down": {"uv": [1.25, 9.25, 1, 9.5], "texture": "#0"} } }, { "name": "rune_6", "from": [7.5, 20, 6.75], "to": [8.5, 20.25, 7.75], "rotation": {"angle": -45, "axis": "x", "origin": [8, 20.5, 8]}, "faces": { "north": {"uv": [8.75, 9, 9, 9.25], "texture": "#0"}, "east": {"uv": [9, 8.75, 9.25, 9], "texture": "#0"}, "south": {"uv": [9, 9, 9.25, 9.25], "texture": "#0"}, "west": {"uv": [0, 9.25, 0.25, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 0.25, 9.25, 0], "texture": "#0"}, "down": {"uv": [0.5, 9.25, 0.25, 9.5], "texture": "#0"} } }, { "name": "rune_7", "from": [7.5, 21.9, 7.875], "to": [8.5, 23.15, 9.125], "rotation": {"angle": -45, "axis": "x", "origin": [7.875, 22.025, 8]}, "faces": { "north": {"uv": [7.75, 9, 8, 9.25], "texture": "#0"}, "east": {"uv": [9, 7.75, 9.25, 8], "texture": "#0"}, "south": {"uv": [8, 9, 8.25, 9.25], "texture": "#0"}, "west": {"uv": [9, 8, 9.25, 8.25], "texture": "#0"}, "up": {"uv": [8.5, 9.25, 8.25, 9], "texture": "#0"}, "down": {"uv": [9.25, 8.25, 9, 8.5], "texture": "#0"} } }, { "name": "rune_7", "from": [7.5, 20.025, 7.875], "to": [8.5, 22.025, 8.125], "rotation": {"angle": 0, "axis": "y", "origin": [8, 20.525, 8.375]}, "faces": { "north": {"uv": [8.5, 5.25, 8.75, 5.75], "texture": "#0"}, "east": {"uv": [8.5, 5.75, 8.75, 6.25], "texture": "#0"}, "south": {"uv": [6.25, 8.5, 6.5, 9], "texture": "#0"}, "west": {"uv": [8.5, 6.25, 8.75, 6.75], "texture": "#0"}, "up": {"uv": [8.75, 9.25, 8.5, 9], "texture": "#0"}, "down": {"uv": [9.25, 8.5, 9, 8.75], "texture": "#0"} } } ], "display": { "thirdperson_righthand": { "translation": [0, 14.25, 0] }, "thirdperson_lefthand": { "translation": [0, 14.25, 0] }, "firstperson_righthand": { "rotation": [-5, 5, -5], "translation": [0, 9.25, 0] }, "firstperson_lefthand": { "rotation": [-5, 5, -5], "translation": [0, 9.25, 0] }, "ground": { "rotation": [0, 0, 90] }, "gui": { "rotation": [90, -135, 90], "scale": [1, 0.5, 0.5] }, "fixed": { "rotation": [90, -45, 90], "scale": [1, 0.5, 0.5] } }, "groups": [ { "name": "group", "origin": [7, -8.5, 7], "color": 0, "children": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36] }, { "name": "group", "origin": [8, 29.8, 7.45], "color": 0, "children": [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48] }, { "name": "group", "origin": [8, 11.1, 7.625], "color": 0, "children": [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70] } ] } ================================================ FILE: platform/paper/build.gradle.kts ================================================ import xyz.jpenilla.resourcefactory.bukkit.Permission import xyz.jpenilla.resourcefactory.paper.PaperPluginYaml plugins { alias(libs.plugins.convention.plugin) alias(libs.plugins.resourcefactory.paper) } val libraryDir: Provider = layout.buildDirectory.file("generated/paper-library") val dependenciesContent: String = libs.bundles.library.map { bundle -> bundle.joinToString("\n") { dep -> dep.toString() } }.get() dependencies { shade(project(":nms:v1_21_R3")) { isTransitive = false } shade(project(":nms:v1_21_R4")) { isTransitive = false } shade(project(":nms:v1_21_R5")) { isTransitive = false } shade(project(":nms:v1_21_R6")) { isTransitive = false } shade(project(":nms:v1_21_R7")) { isTransitive = false } shade(project(":nms:v26_R1")) { isTransitive = false } } modrinth { gameVersions = SUPPORTED_VERSIONS loaders = PAPER_LOADERS } tasks.modrinth { dependsOn(tasks.modrinthSyncBody) } val generatePaperLibrary by tasks.registering { val outputProvider = libraryDir val contentProvider = dependenciesContent outputs.file(outputProvider) doLast { val file = outputProvider.get().asFile file.parentFile.mkdirs() file.writeText(contentProvider) } } tasks.shadowJar { dependsOn(generatePaperLibrary) from(libraryDir) manifest { attributes["paperweight-mappings-namespace"] = "mojang" } } paperPluginYaml { main = "$group.paper.BetterModelPaper" loader = "$group.paper.BetterModelLoader" version = project.version.toString() name = "BetterModel" foliaSupported = true apiVersion = "1.21.4" author = "toxicity188" contributors = listOf("https://github.com/toxicity188/BetterModel/graphs/contributors") description = "Modern Bedrock model engine for Minecraft Java Edition" website = "https://modrinth.com/plugin/bettermodel" dependencies { server( name = "MythicMobs", required = false, load = PaperPluginYaml.Load.BEFORE ) server( name = "Citizens", required = false, load = PaperPluginYaml.Load.BEFORE ) server( name = "SkinsRestorer", required = false, load = PaperPluginYaml.Load.BEFORE ) server( name = "Nexo", required = false, load = PaperPluginYaml.Load.OMIT ) } permissions.create("bettermodel") { default = Permission.Default.OP description = "Accesses to command." children = mapOf( "reload" to true, "spawn" to true, "disguise" to true, "undisguise" to true, "test" to true, "play" to true, "version" to true, "hide" to true, "show" to true ) } } ================================================ FILE: platform/paper/src/main/java/kr/toxicity/model/paper/BetterModelLoader.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.paper; import io.papermc.paper.plugin.loader.PluginClasspathBuilder; import io.papermc.paper.plugin.loader.PluginLoader; import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.repository.RemoteRepository; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Objects; @SuppressWarnings({"UnstableApiUsage", "unused"}) public final class BetterModelLoader implements PluginLoader { @Override public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) { var lib = new MavenLibraryResolver(); lib.addRepository(new RemoteRepository.Builder( null, "default", "https://maven-central.storage-download.googleapis.com/maven2" ).build()); try ( var stream = Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("paper-library")); var streamReader = new InputStreamReader(stream, StandardCharsets.UTF_8); var reader = new BufferedReader(streamReader) ) { String next; while ((next = reader.readLine()) != null) { lib.addDependency(new Dependency(new DefaultArtifact(next), null)); } } catch (IOException e) { throw new RuntimeException(e); } classpathBuilder.addLibrary(lib); } } ================================================ FILE: platform/paper/src/main/kotlin/kr/toxicity/model/paper/BetterModelPaper.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.paper import kr.toxicity.model.api.BetterModelPlatform import kr.toxicity.model.bukkit.BetterModelPlugin @Suppress("UNUSED") class BetterModelPaper : BetterModelPlugin() { override fun jarType(): BetterModelPlatform.JarType { return BetterModelPlatform.JarType.PAPER } } ================================================ FILE: platform/spigot/build.gradle.kts ================================================ import xyz.jpenilla.resourcefactory.bukkit.Permission plugins { alias(libs.plugins.convention.plugin) alias(libs.plugins.resourcefactory.bukkit) } val dependenciesContent: List = libs.bundles.library.map { it.map(Any::toString) }.get() dependencies { shade(project(":nms:v1_21_R3", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v1_21_R4", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v1_21_R5", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v1_21_R6", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v1_21_R7", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v26_R1")) { isTransitive = false } } modrinth { gameVersions = SUPPORTED_VERSIONS loaders = BUKKIT_LOADERS } tasks.shadowJar { manifest { attributes["paperweight-mappings-namespace"] = "spigot" } } bukkitPluginYaml { main = "$group.spigot.BetterModelSpigot" version = project.version.toString() name = "BetterModel" foliaSupported = true apiVersion = "1.21.4" author = "toxicity188" description = "Modern Bedrock model engine for Minecraft Java Edition" website = "https://modrinth.com/plugin/bettermodel" softDepend = listOf( "MythicMobs", "Citizens", "SkinsRestorer" ) libraries = dependenciesContent permissions.create("bettermodel") { default = Permission.Default.OP description = "Accesses to command." children = mapOf( "reload" to true, "spawn" to true, "disguise" to true, "undisguise" to true, "test" to true, "play" to true, "version" to true, "hide" to true, "show" to true ) } } ================================================ FILE: platform/spigot/src/main/kotlin/kr/toxicity/model/spigot/BetterModelSpigot.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.spigot import kr.toxicity.model.api.BetterModelPlatform import kr.toxicity.model.bukkit.BetterModelPlugin import kr.toxicity.model.util.toComponent import kr.toxicity.model.util.warn import org.bukkit.Bukkit @Suppress("UNUSED") class BetterModelSpigot : BetterModelPlugin() { override fun onEnable() { if (IS_PAPER) { warn( "You're using Paper, so you have to use Paper jar!".toComponent(), "Please download Paper jar from Modrinth! (https://modrinth.com/plugin/bettermodel)".toComponent() ) return Bukkit.getPluginManager().disablePlugin(this) } super.onEnable() } override fun jarType(): BetterModelPlatform.JarType { return BetterModelPlatform.JarType.SPIGOT } } ================================================ FILE: purpur/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.standard) } dependencies { compileOnly(project(":bettermodel-api")) compileOnly(project(":bettermodel-api:bettermodel-bukkit-api")) compileOnly("org.purpurmc.purpur:purpur-api:${property("minecraft_version")}.build.+") } ================================================ FILE: purpur/src/main/kotlin/kr/toxicity/model/bukkit/purpur/PurpurHook.kt ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2026 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.bukkit.purpur import kr.toxicity.model.api.BetterModel import kr.toxicity.model.api.bukkit.platform.BukkitPlayer import kr.toxicity.model.api.event.CreateDummyTrackerEvent import kr.toxicity.model.api.event.CreateEntityTrackerEvent import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor object PurpurHook { fun start() { val platform = BetterModel.platform() val config = BetterModel.config() platform.logger().info( Component.text("BetterModel is currently running in Purpur.").color(NamedTextColor.LIGHT_PURPLE), Component.text("Some Purpur features will be enabled.").color(NamedTextColor.LIGHT_PURPLE) ) BetterModel.eventBus().subscribe(platform, CreateDummyTrackerEvent::class.java) { event -> event.tracker().pipeline.viewFilter { !config.usePurpurAfk() || !(it as BukkitPlayer).source().isAfk } } BetterModel.eventBus().subscribe(platform, CreateEntityTrackerEvent::class.java) { event -> event.tracker().pipeline.viewFilter { !config.usePurpurAfk() || !(it as BukkitPlayer).source().isAfk } } } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ], "ignoreDeps": [ "org.jetbrains:annotations", "org.incendo:cloud-paper", "org.incendo:cloud-fabric", "io.papermc.parchment.data:parchment", "net.fabricmc.fabric-api:fabric-api", "net.fabricmc.fabric-loom-repositories", "eu.pb4:polymer-resource-pack" ], "baseBranches": [ "v3-dev" ] } ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { gradlePluginPortal() mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") maven("https://maven.fabricmc.net/") maven("https://maven.neoforged.net/releases/") } } plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" id("net.fabricmc.fabric-loom-repositories") version "1.16-SNAPSHOT" id("net.neoforged.moddev.repositories") version "2.0.141" } dependencyResolutionManagement { repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") maven("https://maven.fabricmc.net/") maven("https://maven.neoforged.net/releases/") maven("https://repo.codemc.org/repository/maven-public/") maven("https://repo.alessiodp.com/releases/") maven("https://maven.blamejared.com/") maven("https://repo.purpurmc.org/snapshots") maven("https://maven.citizensnpcs.co/repo/") maven("https://mvn.lumine.io/repository/maven-public/") maven("https://maven.nucleoid.xyz/") maven("https://repo.nexomc.com/releases/") // for development builds maven(url = "https://central.sonatype.com/repository/maven-snapshots/") { name = "central-snapshots" mavenContent { snapshotsOnly() } } } } rootProject.name = "bettermodel" val published = setOf( "api", "api:bukkit-api", "api:mod-api", "core", "core:bukkit-core", "platform:spigot", "platform:paper", "platform:fabric", ) include(published) include( "purpur", //nms "nms:v1_21_R3", "nms:v1_21_R4", "nms:v1_21_R5", "nms:v1_21_R6", "nms:v1_21_R7", "nms:v26_R1", //test "test-plugin" ) published.forEach { target -> findProject(":$target")?.let { it.name = "${rootProject.name}-${it.name}" } } ================================================ FILE: test-plugin/build.gradle.kts ================================================ plugins { alias(libs.plugins.convention.bukkit) alias(libs.plugins.resourcefactory.bukkit) } dependencies { compileOnly(project(":bettermodel-api")) compileOnly(project(":bettermodel-api:bettermodel-bukkit-api")) compileOnly(libs.lombok) annotationProcessor(libs.lombok) testCompileOnly(libs.lombok) testAnnotationProcessor(libs.lombok) } val pluginName = "BetterModel-TestPlugin" tasks.jar { archiveBaseName = pluginName } bukkitPluginYaml { main = "$group.test.BetterModelTest" version = project.version.toString() name = pluginName foliaSupported = true apiVersion = "1.20" author = "toxicity" description = "BetterModel's test plugin" depend = listOf( "BetterModel" ) commands.register("rollinfo") { usage = "/" description = "Gets roll animation's info." permission = "bettermodel.rollinfo" } commands.register("knightsword") { usage = "/" description = "Gets knight sword" permission = "bettermodel.knightsword" } } ================================================ FILE: test-plugin/src/main/java/kr/toxicity/model/test/BetterModelTest.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.test; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.function.Supplier; @SuppressWarnings("unused") public final class BetterModelTest extends JavaPlugin { private final List testers = List.of( new RollTester(), new FightTester() ); @Override public void onEnable() { for (ModelTester tester : testers) { tester.start(this); } getLogger().info("Plugin enabled."); } @Override public void onDisable() { for (ModelTester tester : testers) { tester.end(this); } getLogger().info("Plugin disabled."); } public @NotNull Supplier asByte(@NotNull String path) { try ( var get = Objects.requireNonNull(getResource(path)) ) { var bytes = get.readAllBytes(); return () -> bytes; } catch (IOException e) { throw new RuntimeException(e); } } } ================================================ FILE: test-plugin/src/main/java/kr/toxicity/model/test/FightTester.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.test; import com.google.gson.JsonObject; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.bone.RenderedBone; import kr.toxicity.model.api.bukkit.BetterModelBukkit; import kr.toxicity.model.api.bukkit.platform.BukkitAdapter; import kr.toxicity.model.api.data.ModelAsset; import kr.toxicity.model.api.data.renderer.ModelRenderer; import kr.toxicity.model.api.event.ModelAssetsEvent; import kr.toxicity.model.api.event.PluginStartReloadEvent; import kr.toxicity.model.api.nms.AnimationBundler; import kr.toxicity.model.api.pack.PackNamespace; import kr.toxicity.model.api.platform.PlatformPlayer; import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; import org.joml.Quaternionf; import org.joml.Vector3f; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; import java.util.stream.Stream; import static java.lang.Math.*; public final class FightTester implements ModelTester, Listener { @NotNull private static final NamespacedKey KNIGHT_SWORD_KEY = Objects.requireNonNull(NamespacedKey.fromString("knight_sword")); private final Map playerCounterMap = new ConcurrentHashMap<>(); private ItemStack lineItem; private BetterModelTest test; @Override public void start(@NotNull BetterModelTest test) { this.test = test; this.lineItem = createLine(); Bukkit.getPluginManager().registerEvents(this, test); var command = test.getCommand("knightsword"); if (command != null) command.setExecutor((sender, command1, label, args) -> { if (sender instanceof Player player) giveKnightSword(player); return true; }); BetterModelBukkit.platform().eventBus().subscribe(test, PluginStartReloadEvent.class, event -> { var path = event.zipper() .modern() .bettermodel(); loadItem(path, "knight_sword"); loadItem(path, "knight_line"); }); BetterModelBukkit.platform().eventBus().subscribe(test, ModelAssetsEvent.class, event -> { if (event.type() == ModelRenderer.Type.PLAYER) event.addAsset(ModelAsset.of( "knight", () -> Objects.requireNonNull(test.getResource("knight.bbmodel")) )); }); } @Override public void end(@NotNull BetterModelTest test) { HandlerList.unregisterAll(this); } private void loadItem(@NotNull PackNamespace path, @NotNull String itemName) { path.models().resolve("class_item").add(itemName + ".json", test.asByte(itemName + ".json")); path.textures().add(itemName + ".png", test.asByte(itemName + ".png")); var model = new JsonObject(); model.addProperty("type", "minecraft:model"); model.addProperty("model", "bettermodel:class_item/" + itemName); var json = new JsonObject(); json.add("model", model); path.items().add(itemName + ".json", () -> json.toString().getBytes(StandardCharsets.UTF_8)); } @EventHandler public void rightClick(@NotNull PlayerInteractEvent event) { var player = event.getPlayer(); var uuid = player.getUniqueId(); switch (event.getAction()) { case LEFT_CLICK_AIR, LEFT_CLICK_BLOCK -> {} default -> { return; } } if (!player.getInventory().getItemInMainHand().getPersistentDataContainer().has(KNIGHT_SWORD_KEY)) return; playerCounterMap.computeIfAbsent(uuid, u -> new PlayerSkillCounter(player) .skill("left_attack_1") .skill("left_attack_2") .skill("left_attack_3")).execute(); } private void giveKnightSword(@NotNull Player player) { var sword = new ItemStack(Material.NETHERITE_SWORD); sword.editMeta(meta -> { meta.displayName(MiniMessage.miniMessage().deserialize("Knight Sword")); meta.setUnbreakable(true); meta.setItemModel(new NamespacedKey( (Plugin) BetterModel.platform(), "knight_sword" )); meta.addItemFlags(ItemFlag.values()); meta.getPersistentDataContainer().set(KNIGHT_SWORD_KEY, PersistentDataType.BOOLEAN, true); }); player.getInventory().addItem(sword); } private static @NotNull ItemStack createLine() { var line = new ItemStack(Material.PAPER); line.editMeta(meta -> meta.setItemModel(new NamespacedKey( (Plugin) BetterModel.platform(), "knight_line" ))); return line; } private static @NotNull Vector3f toDeltaVector(@NotNull Location before, @NotNull Location after, float yRot) { var rd = after.toVector().subtract(before.toVector()).rotateAroundY(yRot); return new Vector3f((float) rd.getX(), (float) rd.getY(), (float) rd.getZ()); } @RequiredArgsConstructor private class PlayerSkillCounter { private final Player player; private final Queue skillQueue = new LinkedList<>(); private LineDrawer lineDrawer; private long nextCooldown; @NotNull PlayerSkillCounter skill(@NotNull String name) { skillQueue.add(name); return this; } void execute() { if (nextCooldown > System.currentTimeMillis()) return; var dequeue = skillQueue.poll(); if (dequeue != null) execute(dequeue); } private void execute(@NotNull String target) { BetterModel.limb("knight") .map(limb -> limb.getOrCreate(BukkitAdapter.adapt(player))) .ifPresent(tracker -> { var drawer = tracker.bone("sword_point"); if (drawer == null) { tracker.close(); return; } lineDrawer = new LineDrawer(player, drawer, 30); Runnable cancel = () -> { tracker.close(); cancelDrawer(); playerCounterMap.remove(player.getUniqueId()); }; var animation = tracker.renderer().animation(target).orElse(null); if (animation == null) cancel.run(); else { tracker.animate(animation, AnimationModifier.DEFAULT, cancel); nextCooldown = (long) ((animation.length() - 0.25) * 1000) + System.currentTimeMillis(); playSound(); } }); } private void playSound() { var loc = player.getLocation(); player.playSound( loc, Sound.ENTITY_BREEZE_SHOOT, 0.75F, 0.5F ); player.playSound( loc, Sound.ENTITY_DROWNED_SHOOT, 2.0F, 0.75F ); } private void cancelDrawer() { if (lineDrawer != null) { lineDrawer.cancel(); lineDrawer = null; } } } private record DrawerFrame( float yaw, @NotNull Location location, Vector3f vector ) { } private class LineDrawer { private final List players; private DrawerFrame after; private final AtomicInteger counter = new AtomicInteger(); private final List queuedTask = new ArrayList<>(); private final ScheduledTask task; LineDrawer(@NotNull Player player, @NotNull RenderedBone bone, int count) { players = Stream.concat( Stream.of(BukkitAdapter.adapt(player)), player.getTrackedBy().stream().map(BukkitAdapter::adapt) ).toList(); task = Bukkit.getAsyncScheduler().runAtFixedRate((Plugin) BetterModel.platform(), task -> { queuedTask.removeIf(BooleanSupplier::getAsBoolean); var c = counter.incrementAndGet(); if (c >= count) return; var before = after; after = new DrawerFrame( bone.rotation().radianY(), player.getLocation(), bone.hitBoxPosition().rotateY(bone.rotation().radianY()) ); if (before == null) return; var delta = toDeltaVector( relativeLocation(before.location, before.vector, before.yaw), relativeLocation(after.location, after.vector, after.yaw), (float) toRadians(after.location.getYaw()) ); var yaw = atan2(delta.x, delta.z); var pitch = atan2(-delta.y, sqrt(fma(delta.x, delta.x, delta.z * delta.z))); createDisplay(relativeLocation(before.location, before.vector, before.yaw), delta.length(), new Quaternionf() .rotateLocalX((float) pitch) .rotateLocalY((float) yaw)); }, 50, 10, TimeUnit.MILLISECONDS); } void cancel() { task.cancel(); queuedTask.removeIf(BooleanSupplier::getAsBoolean); } @NotNull Location relativeLocation(@NotNull Location location, @NotNull Vector3f vector3f, float originYaw) { var loc = location.clone(); loc.setPitch(0); loc.add(new Vector(vector3f.x, vector3f.y, vector3f.z).rotateAroundY(-originYaw)); return loc; } void createDisplay(@NotNull Location start, float length, @NotNull Quaternionf quaternionf) { if (length <= 0.1) return; start.getWorld().spawnParticle( Particle.DUST, start, 3, 0.2, 0.2, 0.2, 0, new Particle.DustOptions(Color.YELLOW, 1) ); var display = BetterModel.nms().create(BukkitAdapter.adapt(start), 0, d -> { d.item(BukkitAdapter.adapt(lineItem)); d.brightness(15, 15); }); var transformer = display.createTransformer(); var bundler = BetterModel.nms().createBundler(2); display.spawn(bundler); display.sendEntityData(true, bundler); transformer.transform( 0, new Vector3f(), new Vector3f(1, 1, length), quaternionf, new AnimationBundler(bundler, BetterModel.nms().createModAnimationBuilder(2)) ); players.forEach(bundler::send); var displayCounter = new AtomicInteger(); queuedTask.add(() -> { if (displayCounter.incrementAndGet() >= 20 || task.isCancelled()) { var removeBundler = BetterModel.nms().createBundler(1); display.remove(removeBundler); players.forEach(removeBundler::send); return true; } return false; }); } } } ================================================ FILE: test-plugin/src/main/java/kr/toxicity/model/test/ModelTester.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.test; import org.jetbrains.annotations.NotNull; public interface ModelTester { void start(@NotNull BetterModelTest test); void end(@NotNull BetterModelTest test); } ================================================ FILE: test-plugin/src/main/java/kr/toxicity/model/test/RollTester.java ================================================ /* * This source file is part of BetterModel. * Copyright (c) 2025 toxicity188 * Licensed under the MIT License. * See LICENSE.md file for full license text. */ package kr.toxicity.model.test; import io.papermc.paper.event.entity.EntityPushedByEntityAttackEvent; import kr.toxicity.model.api.BetterModel; import kr.toxicity.model.api.animation.AnimationModifier; import kr.toxicity.model.api.bukkit.platform.BukkitAdapter; import kr.toxicity.model.api.tracker.ModelRotation; import kr.toxicity.model.api.tracker.TrackerModifier; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Particle; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.ProjectileHitEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerSwapHandItemsEvent; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public final class RollTester implements ModelTester, Listener { private final Set coolTimeSet = ConcurrentHashMap.newKeySet(); private final Set invulnerableSet = ConcurrentHashMap.newKeySet(); @Override public void start(@NotNull BetterModelTest test) { Bukkit.getPluginManager().registerEvents(this, test); var command = test.getCommand("rollinfo"); if (command != null) command.setExecutor((sender, command1, label, args) -> sendRollTime(sender)); } @Override public void end(@NotNull BetterModelTest test) { HandlerList.unregisterAll(this); } @EventHandler public void swap(@NotNull PlayerSwapHandItemsEvent event) { var player = event.getPlayer(); event.setCancelled(true); var block = underBlock(player); if (block.isEmpty()) return; if (coolTimeSet.contains(player.getUniqueId())) return; var loc = player.getLocation(); var data = block.getBlockData(); loc.getWorld().spawnParticle( Particle.BLOCK, loc, 20, 1, 0.25, 1, 0.2, data ); loc.getWorld().playSound(loc, data.getSoundGroup().getBreakSound(), 0.5F, 1.0F); playRoll(player); } @EventHandler public void hit(@NotNull ProjectileHitEvent event) { if (event.getHitEntity() instanceof Player player && invulnerableSet.contains(player.getUniqueId())) event.setCancelled(true); } @EventHandler public void quit(@NotNull PlayerQuitEvent event) { var get = event.getPlayer().getUniqueId(); invulnerableSet.remove(get); coolTimeSet.remove(get); } @EventHandler public void death(@NotNull PlayerDeathEvent event) { var get = event.getPlayer().getUniqueId(); invulnerableSet.remove(get); coolTimeSet.remove(get); } @EventHandler public void damage(@NotNull EntityDamageByEntityEvent event) { if (event.getEntity() instanceof Player player && invulnerableSet.contains(player.getUniqueId())) event.setCancelled(true); } @EventHandler public void push(@NotNull EntityPushedByEntityAttackEvent event) { if (event.getEntity() instanceof Player player && invulnerableSet.contains(player.getUniqueId())) event.setCancelled(true); } private static @NotNull Block underBlock(@NotNull Player player) { return player.getLocation().add(0, -1, 0).getBlock(); } private static boolean sendRollTime(@NotNull Audience audience) { return BetterModel.limb("steve") .flatMap(r -> r.animation("roll")) .map(animation -> { audience.sendMessage(Component.text() .append(Component.text("Loop mode: " + animation.loop())) .appendNewline() .append(Component.text("Length: " + animation.length() + " second"))); return audience; }) .isPresent(); } private void playRoll(@NotNull Player player) { var input = inputToYaw(player); BetterModel.limb("steve") .map(r -> r.getOrCreate(BukkitAdapter.adapt(player), TrackerModifier.DEFAULT, t -> { t.bodyRotator().lockRotation(true); t.rotation(() -> new ModelRotation(player.getPitch(), packDegree(input + t.registry().entity().yaw()))); })) .ifPresent(t -> { if (t.animate("roll", AnimationModifier.DEFAULT_WITH_PLAY_ONCE, () -> { BetterModel.platform().scheduler().asyncTaskLater(3, () -> coolTimeSet.remove(player.getUniqueId())); t.close(); })) { if (coolTimeSet.add(player.getUniqueId()) && invulnerableSet.add(player.getUniqueId())) { player.addPotionEffect(new PotionEffect( PotionEffectType.LUCK, 8, 5, true, false )); BetterModel.platform().scheduler().asyncTaskLater(8, () -> invulnerableSet.remove(player.getUniqueId())); player.setVelocity(player.getVelocity() .add(new Vector(0, 0, 0.75).rotateAroundY(-Math.toRadians(input + t.registry().entity().yaw()))) .setY(0.15)); } } else t.close(); }); } private static float inputToYaw(@NotNull Player player) { var input = player.getCurrentInput(); var leftRightDegree = switch (TriState.of(input.isLeft(), input.isRight())) { case LEFT -> 270F; case RIGHT -> 90F; case NOT_FOUND -> -1F; }; var forwardBackwardDegree = switch (TriState.of(input.isForward(), input.isBackward())) { case LEFT -> 0F; case RIGHT -> 180F; case NOT_FOUND -> -1F; }; if (forwardBackwardDegree < 0) return Math.max(leftRightDegree, 0); else if (leftRightDegree < 0) return forwardBackwardDegree; if (leftRightDegree - forwardBackwardDegree > 180) forwardBackwardDegree += 360; return (forwardBackwardDegree + leftRightDegree) / 2; } private static float packDegree(float degree) { return degree > 180 ? degree - 360 : degree; } private enum TriState { LEFT, RIGHT, NOT_FOUND ; static @NotNull TriState of(boolean left, boolean right) { return left ? LEFT : right ? RIGHT : NOT_FOUND; } } } ================================================ FILE: test-plugin/src/main/resources/knight.bbmodel ================================================ {"meta":{"format_version":"5.0","model_format":"free","box_uv":false},"name":"knight","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"degrees=180","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":64,"height":64},"elements":[{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,11.25,-1.875],"to":[3.75,15,1.875],"autouv":0,"color":1,"origin":[0,11.25,0],"uv_offset":[16,24],"faces":{"north":{"uv":[20,28,28,32],"texture":0},"east":{"uv":[16,28,20,32],"texture":0},"south":{"uv":[32,28,40,32],"texture":0},"west":{"uv":[28,28,32,32],"texture":0},"up":{"uv":[28,28,20,24],"texture":0},"down":{"uv":[36,16,28,20],"texture":0}},"type":"cube","uuid":"ea42f7f7-a6f1-4479-c43f-48211bab5ed2"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,15,-1.875],"to":[3.75,18.75,1.875],"autouv":0,"color":2,"origin":[0,15,0],"uv_offset":[16,20],"faces":{"north":{"uv":[20,24,28,28],"texture":0},"east":{"uv":[16,24,20,28],"texture":0},"south":{"uv":[32,24,40,28],"texture":0},"west":{"uv":[28,24,32,28],"texture":0},"up":{"uv":[28,24,20,20],"texture":0},"down":{"uv":[28,28,20,32],"texture":0}},"type":"cube","uuid":"5ea74bdb-ba28-b8e3-103b-9be6ff2262da"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,18.75,-1.875],"to":[3.75,22.5,1.875],"autouv":0,"color":3,"origin":[0,18.75,0],"uv_offset":[16,16],"faces":{"north":{"uv":[20,20,28,24],"texture":0},"east":{"uv":[16,20,20,24],"texture":0},"south":{"uv":[32,20,40,24],"texture":0},"west":{"uv":[28,20,32,24],"texture":0},"up":{"uv":[28,20,20,16],"texture":0},"down":{"uv":[28,24,20,28],"texture":0}},"type":"cube","uuid":"dc1510db-a719-17b4-e253-c992a92c5d25"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,18.76563,-1.85937],"to":[3.73438,22.48438,1.85938],"autouv":0,"color":3,"inflate":0.25,"origin":[0,18.75,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,28,40],"texture":0},"east":{"uv":[16,36,20,40],"texture":0},"south":{"uv":[32,36,40,40],"texture":0},"west":{"uv":[28,36,32,40],"texture":0},"up":{"uv":[28,36,20,32],"texture":0},"down":{"uv":[8,0,0,4],"texture":0}},"type":"cube","uuid":"d51a8665-a2bc-af6e-1230-5acba07248e7"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,15.01563,-1.85937],"to":[3.73438,18.73438,1.85938],"autouv":0,"color":2,"inflate":0.25,"origin":[0,15,0],"uv_offset":[16,36],"faces":{"north":{"uv":[20,40,28,44],"texture":0},"east":{"uv":[16,40,20,44],"texture":0},"south":{"uv":[32,40,40,44],"texture":0},"west":{"uv":[28,40,32,44],"texture":0},"up":{"uv":[8,4,0,0],"texture":0},"down":{"uv":[8,0,0,4],"texture":0}},"type":"cube","uuid":"f5b9f499-b26b-f912-1a54-151d702e13ed"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,11.26563,-1.85937],"to":[3.73438,14.98438,1.85938],"autouv":0,"color":1,"inflate":0.25,"origin":[0,11.25,0],"uv_offset":[16,40],"faces":{"north":{"uv":[20,44,28,48],"texture":0},"east":{"uv":[16,44,20,48],"texture":0},"south":{"uv":[32,44,40,48],"texture":0},"west":{"uv":[28,44,32,48],"texture":0},"up":{"uv":[8,4,0,0],"texture":0},"down":{"uv":[36,32,28,36],"texture":0}},"type":"cube","uuid":"357ebf82-23ba-edb1-081f-dca75d94b83c"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.75,16.875,-1.875],"to":[7.5,22.5,1.875],"autouv":0,"color":4,"origin":[5.15625,21.5625,0],"uv_offset":[40,16],"faces":{"north":{"uv":[44,20.1,48,26.1],"texture":0},"east":{"uv":[40,20,44,26],"texture":0},"south":{"uv":[52,20,56,26],"texture":0},"west":{"uv":[48,20,52,26],"texture":0},"up":{"uv":[48,20.1,44,16.1],"texture":0},"down":{"uv":[48,26.1,44,30.1],"texture":0}},"type":"cube","uuid":"53d40d2e-0941-29f9-00ed-1b19c941dcd8"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.76563,16.89063,-1.85937],"to":[7.48438,22.48438,1.85938],"autouv":0,"color":4,"inflate":0.25,"origin":[5.15625,21.5625,0],"uv_offset":[40,32],"faces":{"north":{"uv":[44,36,48,42],"texture":0},"east":{"uv":[40,36,44,42],"texture":0},"south":{"uv":[52,36,56,42],"texture":0},"west":{"uv":[48,36,52,42],"texture":0},"up":{"uv":[48,36,44,32],"texture":0},"down":{"uv":[56,32,52,36],"texture":0}},"type":"cube","uuid":"b17452ef-afbe-e010-f2c9-e0ba945faa1f"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.5,16.875,-1.875],"to":[-3.75,22.5,1.875],"autouv":0,"color":4,"origin":[-5.15625,21.5625,0],"uv_offset":[32,48],"faces":{"north":{"uv":[36,52,40,58],"texture":0},"east":{"uv":[32,52,36,58],"texture":0},"south":{"uv":[44,52,48,58],"texture":0},"west":{"uv":[40,52,44,58],"texture":0},"up":{"uv":[40,52,36,48],"texture":0},"down":{"uv":[40,58,36,62],"texture":0}},"type":"cube","uuid":"addb9f66-a2a4-46f7-b6af-f5a23c00fe70"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.48437,16.89063,-1.85937],"to":[-3.76562,22.48438,1.85938],"autouv":0,"color":4,"inflate":0.25,"origin":[-5.15625,21.5625,0],"uv_offset":[48,48],"faces":{"north":{"uv":[52,52,56,58],"texture":0},"east":{"uv":[48,52,52,58],"texture":0},"south":{"uv":[60,52,64,58],"texture":0},"west":{"uv":[56,52,60,58],"texture":0},"up":{"uv":[56,52,52,48],"texture":0},"down":{"uv":[64,48,60,52],"texture":0}},"type":"cube","uuid":"5e7dc31c-c64a-ff15-90d9-52a6f77cb14b"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.75,11.25,-1.875],"to":[7.5,16.875,1.875],"autouv":0,"color":8,"origin":[5.15625,15.9375,0],"uv_offset":[40,22],"faces":{"north":{"uv":[44,26,48,32],"texture":0},"east":{"uv":[40,26,44,32],"texture":0},"south":{"uv":[52,26,56,32],"texture":0},"west":{"uv":[48,26,52,32],"texture":0},"up":{"uv":[48,26,44,22],"texture":0},"down":{"uv":[52,16,48,20],"texture":0}},"type":"cube","uuid":"e02c395d-e1bc-1375-a8ed-729e19544ce9"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[3.76563,11.26563,-1.85937],"to":[7.48438,16.85938,1.85938],"autouv":0,"color":8,"inflate":0.25,"origin":[5.15625,15.9375,0],"uv_offset":[40,38],"faces":{"north":{"uv":[44,42,48,48],"texture":0},"east":{"uv":[40,42,44,48],"texture":0},"south":{"uv":[52,42,56,48],"texture":0},"west":{"uv":[48,42,52,48],"texture":0},"up":{"uv":[44,36,40,32],"texture":0},"down":{"uv":[52,32,48,36],"texture":0}},"type":"cube","uuid":"a509c2d7-53ef-2b11-1331-f716b9c210d6"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.5,11.25,-1.875],"to":[-3.75,16.875,1.875],"autouv":0,"color":8,"origin":[-5.15625,15.9375,0],"uv_offset":[32,54],"faces":{"north":{"uv":[36,58,40,64],"texture":0},"east":{"uv":[32,58,36,64],"texture":0},"south":{"uv":[44,58,48,64],"texture":0},"west":{"uv":[40,58,44,64],"texture":0},"up":{"uv":[40,58,36,54],"texture":0},"down":{"uv":[44,48,40,52],"texture":0}},"type":"cube","uuid":"1433ee62-21ac-d72c-0fd0-37000e5eb221"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-7.48437,11.26563,-1.85937],"to":[-3.76562,16.85938,1.85938],"autouv":0,"color":8,"inflate":0.25,"origin":[-5.15625,15.9375,0],"uv_offset":[48,54],"faces":{"north":{"uv":[52,58,56,64],"texture":0},"east":{"uv":[48,58,52,64],"texture":0},"south":{"uv":[60,58,64,64],"texture":0},"west":{"uv":[56,58,60,64],"texture":0},"up":{"uv":[52,52,48,48],"texture":0},"down":{"uv":[60,48,56,52],"texture":0}},"type":"cube","uuid":"24bacfd6-cde8-81a0-f620-998cf5393d48"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0,5.625,-1.875],"to":[3.75,11.25,1.875],"autouv":0,"color":7,"origin":[1.40625,10.3125,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,8,26],"texture":0},"east":{"uv":[0,20,4,26],"texture":0},"south":{"uv":[12,20,16,26],"texture":0},"west":{"uv":[8,20,12,26],"texture":0},"up":{"uv":[8,20,4,16],"texture":0},"down":{"uv":[8,26,4,30],"texture":0}},"type":"cube","uuid":"b17675fc-b79b-0ad9-8f81-aa2dd1fc8c97"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.01563,5.64063,-1.85937],"to":[3.73438,11.23438,1.85938],"autouv":0,"color":7,"inflate":0.25,"origin":[1.40625,10.3125,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,42],"texture":0},"east":{"uv":[0,36,4,42],"texture":0},"south":{"uv":[12,36,16,42],"texture":0},"west":{"uv":[8,36,12,42],"texture":0},"up":{"uv":[8,36,4,32],"texture":0},"down":{"uv":[16,32,12,36],"texture":0}},"type":"cube","uuid":"2e42e483-1557-d21e-aee0-94af7bedfd40"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0,0,-1.875],"to":[3.75,5.625,1.875],"autouv":0,"color":6,"origin":[1.40625,4.6875,0],"uv_offset":[0,22],"faces":{"north":{"uv":[4,26,8,32],"texture":0},"east":{"uv":[0,26,4,32],"texture":0},"south":{"uv":[12,26,16,32],"texture":0},"west":{"uv":[8,26,12,32],"texture":0},"up":{"uv":[8,26,4,22],"texture":0},"down":{"uv":[12,16,8,20],"texture":0}},"type":"cube","uuid":"9455d16e-4bbf-4b63-881d-7daf943e782b"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[0.01563,0.01563,-1.85937],"to":[3.73438,5.60938,1.85938],"autouv":0,"color":6,"inflate":0.25,"origin":[1.40625,4.6875,0],"uv_offset":[0,38],"faces":{"north":{"uv":[4,42,8,48],"texture":0},"east":{"uv":[0,42,4,48],"texture":0},"south":{"uv":[12,42,16,48],"texture":0},"west":{"uv":[8,42,12,48],"texture":0},"up":{"uv":[4,36,0,32],"texture":0},"down":{"uv":[12,32,8,36],"texture":0}},"type":"cube","uuid":"16aee685-0ead-a542-5b3c-a62467ca45e3"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,5.625,-1.875],"to":[0,11.25,1.875],"autouv":0,"color":7,"origin":[-1.40625,10.3125,0],"uv_offset":[16,48],"faces":{"north":{"uv":[20,52,24,58],"texture":0},"east":{"uv":[16,52,20,58],"texture":0},"south":{"uv":[28,52,32,58],"texture":0},"west":{"uv":[24,52,28,58],"texture":0},"up":{"uv":[24,52,20,48],"texture":0},"down":{"uv":[24,58,20,62],"texture":0}},"type":"cube","uuid":"0e370edc-7b05-dccf-dd2a-a92cfe9f3e22"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,5.64063,-1.85937],"to":[-0.01562,11.23438,1.85938],"autouv":0,"color":7,"inflate":0.25,"origin":[-1.40625,10.3125,0],"uv_offset":[0,48],"faces":{"north":{"uv":[4,52,8,58],"texture":0},"east":{"uv":[0,52,4,58],"texture":0},"south":{"uv":[12,52,16,58],"texture":0},"west":{"uv":[8,52,12,58],"texture":0},"up":{"uv":[8,52,4,48],"texture":0},"down":{"uv":[16,48,12,52],"texture":0}},"type":"cube","uuid":"dc189735-0a58-619b-07ef-feec37d65e7d"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,0,-1.875],"to":[0,5.625,1.875],"autouv":0,"color":6,"origin":[-1.40625,4.6875,0],"uv_offset":[16,54],"faces":{"north":{"uv":[20,58,24,64],"texture":0},"east":{"uv":[16,58,20,64],"texture":0},"south":{"uv":[28,58,32,64],"texture":0},"west":{"uv":[24,58,28,64],"texture":0},"up":{"uv":[24,58,20,54],"texture":0},"down":{"uv":[28,48,24,52],"texture":0}},"type":"cube","uuid":"6bcf768b-beab-4c39-6d96-91266036a4e1"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,0.01563,-1.85938],"to":[-0.01562,5.60938,1.85937],"autouv":0,"color":6,"inflate":0.25,"origin":[-1.40625,4.6875,-0.00001],"uv_offset":[0,54],"faces":{"north":{"uv":[4,58,8,64],"texture":0},"east":{"uv":[0,58,4,64],"texture":0},"south":{"uv":[12,58,16,64],"texture":0},"west":{"uv":[8,58,12,64],"texture":0},"up":{"uv":[4,52,0,48],"texture":0},"down":{"uv":[12,48,8,52],"texture":0}},"type":"cube","uuid":"274282a7-bc7b-02f8-4dce-84226e9dd9c1"},{"name":"skin","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.75,22.5,-3.75],"to":[3.75,30,3.75],"autouv":0,"color":4,"origin":[0,22.5,-1.875],"faces":{"north":{"uv":[8,8,16,16],"texture":0},"east":{"uv":[0,8,8,16],"texture":0},"south":{"uv":[24,8,32,16],"texture":0},"west":{"uv":[16,8,24,16],"texture":0},"up":{"uv":[16,8,8,0],"texture":0},"down":{"uv":[24,0,16,8],"texture":0}},"type":"cube","uuid":"7f60fbaf-510d-2e5f-b7d2-9111e08443cd"},{"name":"hat","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-3.73437,22.51563,-3.73437],"to":[3.73438,29.98438,3.73438],"autouv":0,"color":4,"inflate":0.5,"origin":[0,22.5,-1.875],"uv_offset":[32,0],"faces":{"north":{"uv":[40,8,48,16],"texture":0},"east":{"uv":[32,8,40,16],"texture":0},"south":{"uv":[56,8,64,16],"texture":0},"west":{"uv":[48,8,56,16],"texture":0},"up":{"uv":[48,8,40,0],"texture":0},"down":{"uv":[56,0,48,8],"texture":0}},"type":"cube","uuid":"e0f94313-bf88-492d-0c68-bd6b466a3b68"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.15,15,-0.75],"to":[6.85,16.5,0.75],"autouv":0,"color":2,"origin":[5,10.5,-1],"faces":{"north":{"uv":[27,22,29,24],"texture":1},"east":{"uv":[27,24,29,26],"texture":1},"south":{"uv":[27,26,29,28],"texture":1},"west":{"uv":[0,28,2,30],"texture":1},"up":{"uv":[30,2,28,0],"texture":1},"down":{"uv":[30,9,28,11],"texture":1}},"type":"cube","uuid":"aa7d3639-52cb-087e-0f54-7e31afe0d4b3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,19,0.25],"to":[6.8,21,3.75],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[28,11,30,13],"texture":1},"east":{"uv":[17,21,21,23],"texture":1},"south":{"uv":[19,28,21,30],"texture":1},"west":{"uv":[21,21,25,23],"texture":1},"up":{"uv":[24,4,22,0],"texture":1},"down":{"uv":[24,4,22,8],"texture":1}},"type":"cube","uuid":"596ead89-97bf-b419-dc31-372fe3dee9b9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.75,18.5,-1.25],"to":[7.25,21,1.25],"autouv":0,"color":2,"rotation":[-45,0,0],"origin":[6,19.75,0],"faces":{"north":{"uv":[17,18,20,21],"texture":1},"east":{"uv":[19,0,22,3],"texture":1},"south":{"uv":[19,3,22,6],"texture":1},"west":{"uv":[19,6,22,9],"texture":1},"up":{"uv":[22,12,19,9],"texture":1},"down":{"uv":[22,12,19,15],"texture":1}},"type":"cube","uuid":"4853fc6c-23a8-6a4b-07e2-d89b3aaa8c1c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.225,19,0.375],"to":[6.775,20.5,2.5],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,19.5,2.625],"faces":{"north":{"uv":[21,28,23,30],"texture":1},"east":{"uv":[23,28,25,30],"texture":1},"south":{"uv":[25,28,27,30],"texture":1},"west":{"uv":[27,28,29,30],"texture":1},"up":{"uv":[31,4,29,2],"texture":1},"down":{"uv":[31,4,29,6],"texture":1}},"type":"cube","uuid":"a04ef22a-5322-93ad-5281-e6b33566d6af"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.225,19,-2.5],"to":[6.775,20.5,-0.375],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,19.5,-2.625],"faces":{"north":{"uv":[29,6,31,8],"texture":1},"east":{"uv":[29,13,31,15],"texture":1},"south":{"uv":[29,15,31,17],"texture":1},"west":{"uv":[29,17,31,19],"texture":1},"up":{"uv":[31,21,29,19],"texture":1},"down":{"uv":[31,21,29,23],"texture":1}},"type":"cube","uuid":"dbcf2b15-20a0-6f43-48de-9c73557825c3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.15,17.5,-0.75],"to":[6.85,19,0.75],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[29,23,31,25],"texture":1},"east":{"uv":[29,25,31,27],"texture":1},"south":{"uv":[29,27,31,29],"texture":1},"west":{"uv":[29,29,31,31],"texture":1},"up":{"uv":[2,32,0,30],"texture":1},"down":{"uv":[32,0,30,2],"texture":1}},"type":"cube","uuid":"656eb85b-2130-71d1-8400-036016caf911"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.4,10.05,-0.6],"to":[6.6,10.75,0.6],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,10.4,0],"faces":{"north":{"uv":[28,2,29,3],"texture":1},"east":{"uv":[29,8,30,9],"texture":1},"south":{"uv":[30,12,31,13],"texture":1},"west":{"uv":[27,35,28,36],"texture":1},"up":{"uv":[36,29,35,28],"texture":1},"down":{"uv":[30,35,29,36],"texture":1}},"type":"cube","uuid":"61014d9b-3a39-4bbb-24b1-c1796ff81448"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,19.925,2.375],"to":[6.825,21.675,3.6],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,20.925,2.3],"faces":{"north":{"uv":[5,30,7,32],"texture":1},"east":{"uv":[22,16,23,18],"texture":1},"south":{"uv":[30,8,32,10],"texture":1},"west":{"uv":[24,14,25,16],"texture":1},"up":{"uv":[25,21,23,20],"texture":1},"down":{"uv":[34,11,32,12],"texture":1}},"type":"cube","uuid":"6e13574b-8944-aaee-8ad1-92edda71d4a3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,19.925,-3.6],"to":[6.825,21.675,-2.375],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,20.925,-2.3],"faces":{"north":{"uv":[30,10,32,12],"texture":1},"east":{"uv":[33,9,34,11],"texture":1},"south":{"uv":[17,30,19,32],"texture":1},"west":{"uv":[33,12,34,14],"texture":1},"up":{"uv":[35,4,33,3],"texture":1},"down":{"uv":[35,14,33,15],"texture":1}},"type":"cube","uuid":"5e0081ae-2c0e-03b7-b533-f33b34b8b9c8"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.25,6.25,-0.5],"to":[6.75,7.5,0.75],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,7,0],"faces":{"north":{"uv":[33,31,35,32],"texture":1},"east":{"uv":[34,35,35,36],"texture":1},"south":{"uv":[33,32,35,33],"texture":1},"west":{"uv":[35,34,36,35],"texture":1},"up":{"uv":[35,34,33,33],"texture":1},"down":{"uv":[36,0,34,1],"texture":1}},"type":"cube","uuid":"926dfb9a-8a36-2d53-6026-df9e212d6187"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5,13.75,-0.9],"to":[7,15,0.9],"autouv":0,"color":2,"origin":[5,10.25,-1],"faces":{"north":{"uv":[34,1,36,2],"texture":1},"east":{"uv":[34,2,36,3],"texture":1},"south":{"uv":[34,4,36,5],"texture":1},"west":{"uv":[5,34,7,35],"texture":1},"up":{"uv":[23,32,21,30],"texture":1},"down":{"uv":[25,30,23,32],"texture":1}},"type":"cube","uuid":"34f7ace6-abb3-82f1-ffe6-aa94ccf29240"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.025,14.05,0.725],"to":[6.975,15.2,1.2],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,14.675,0.3],"faces":{"north":{"uv":[35,3,37,4],"texture":1},"east":{"uv":[18,38,19,39],"texture":1},"south":{"uv":[4,35,6,36],"texture":1},"west":{"uv":[38,18,39,19],"texture":1},"up":{"uv":[8,36,6,35],"texture":1},"down":{"uv":[15,35,13,36],"texture":1}},"type":"cube","uuid":"3616c0c5-2bd5-b180-4ed5-2708c59c41bc"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.025,14.05,-1.2],"to":[6.975,15.2,-0.725],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,14.675,-0.3],"faces":{"north":{"uv":[35,14,37,15],"texture":1},"east":{"uv":[19,38,20,39],"texture":1},"south":{"uv":[15,35,17,36],"texture":1},"west":{"uv":[38,19,39,20],"texture":1},"up":{"uv":[19,36,17,35],"texture":1},"down":{"uv":[21,35,19,36],"texture":1}},"type":"cube","uuid":"20fe1d98-3b1b-7a8f-7945-78be2423e61c"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.05,13.025,-0.9],"to":[6.95,14.3,0.375],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,13.4,0],"faces":{"north":{"uv":[34,30,36,31],"texture":1},"east":{"uv":[17,38,18,39],"texture":1},"south":{"uv":[31,34,33,35],"texture":1},"west":{"uv":[38,17,39,18],"texture":1},"up":{"uv":[35,35,33,34],"texture":1},"down":{"uv":[4,35,2,36],"texture":1}},"type":"cube","uuid":"bec338ce-2cb6-bade-c2a0-bf4b0256b6b2"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.4,9.05,-0.6],"to":[6.6,9.75,0.6],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,9.4,0],"faces":{"north":{"uv":[22,38,23,39],"texture":1},"east":{"uv":[38,22,39,23],"texture":1},"south":{"uv":[23,38,24,39],"texture":1},"west":{"uv":[38,23,39,24],"texture":1},"up":{"uv":[25,39,24,38],"texture":1},"down":{"uv":[39,24,38,25],"texture":1}},"type":"cube","uuid":"cc8271b8-7122-2c3d-7a10-390515d38ea2"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.4,8.05,-0.6],"to":[6.6,8.75,0.6],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,8.4,0],"faces":{"north":{"uv":[35,35,36,36],"texture":1},"east":{"uv":[0,36,1,37],"texture":1},"south":{"uv":[36,0,37,1],"texture":1},"west":{"uv":[1,36,2,37],"texture":1},"up":{"uv":[37,2,36,1],"texture":1},"down":{"uv":[3,36,2,37],"texture":1}},"type":"cube","uuid":"2d28b727-641e-2dc9-8e5f-15c9ed002244"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,7,-0.5],"to":[6.5,13.75,0.5],"autouv":0,"color":2,"origin":[5,10.5,-1],"faces":{"north":{"uv":[2,24,3,31],"texture":1},"east":{"uv":[3,24,4,31],"texture":1},"south":{"uv":[4,24,5,31],"texture":1},"west":{"uv":[24,4,25,11],"texture":1},"up":{"uv":[37,3,36,2],"texture":1},"down":{"uv":[4,36,3,37],"texture":1}},"type":"cube","uuid":"68112592-c5eb-c6ba-7370-8df3fd15c5be"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[6.3,14.7,0.925],"to":[6.75,19.15,1.675],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6.55,17.075,1.425],"faces":{"north":{"uv":[25,30,26,34],"texture":1},"east":{"uv":[26,30,27,34],"texture":1},"south":{"uv":[27,30,28,34],"texture":1},"west":{"uv":[28,30,29,34],"texture":1},"up":{"uv":[5,37,4,36],"texture":1},"down":{"uv":[37,4,36,5],"texture":1}},"type":"cube","uuid":"6bfcb5a6-b98e-2362-4350-0cff8e7c8417"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.25,14.7,0.925],"to":[5.7,19.15,1.675],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[5.45,17.075,1.425],"faces":{"north":{"uv":[2,31,3,35],"texture":1},"east":{"uv":[31,2,32,6],"texture":1},"south":{"uv":[3,31,4,35],"texture":1},"west":{"uv":[4,31,5,35],"texture":1},"up":{"uv":[6,37,5,36],"texture":1},"down":{"uv":[37,5,36,6],"texture":1}},"type":"cube","uuid":"8ec387cf-bccc-0ef1-a478-2c3c0c430d41"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.95,18.575,-5.425],"to":[7.05,21.45,-3.75],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,20.925,-4.45],"faces":{"north":{"uv":[21,25,23,28],"texture":1},"east":{"uv":[23,25,25,28],"texture":1},"south":{"uv":[25,25,27,28],"texture":1},"west":{"uv":[26,0,28,3],"texture":1},"up":{"uv":[33,8,31,6],"texture":1},"down":{"uv":[33,12,31,14],"texture":1}},"type":"cube","uuid":"da16de9a-4659-1141-c033-4ab1a11199da"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,19,-3.75],"to":[6.8,21,-0.25],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[31,14,33,16],"texture":1},"east":{"uv":[19,23,23,25],"texture":1},"south":{"uv":[31,16,33,18],"texture":1},"west":{"uv":[23,23,27,25],"texture":1},"up":{"uv":[2,28,0,24],"texture":1},"down":{"uv":[26,0,24,4],"texture":1}},"type":"cube","uuid":"b55c73af-0b1d-14ae-5198-2782a6d7294a"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,18.075,5.2],"to":[6.8,20.675,6.3],"autouv":0,"color":2,"origin":[5,12.75,1.75],"faces":{"north":{"uv":[26,10,28,13],"texture":1},"east":{"uv":[32,28,33,31],"texture":1},"south":{"uv":[27,3,29,6],"texture":1},"west":{"uv":[32,31,33,34],"texture":1},"up":{"uv":[36,14,34,13],"texture":1},"down":{"uv":[36,15,34,16],"texture":1}},"type":"cube","uuid":"a2bc1c86-9f7f-2d61-139f-584cbbdc9ca8"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.425,5.875],"to":[6.55,20.325,6.65],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6,19.475,6.2],"faces":{"north":{"uv":[21,35,22,37],"texture":1},"east":{"uv":[35,21,36,23],"texture":1},"south":{"uv":[22,35,23,37],"texture":1},"west":{"uv":[23,35,24,37],"texture":1},"up":{"uv":[21,39,20,38],"texture":1},"down":{"uv":[39,20,38,21],"texture":1}},"type":"cube","uuid":"976bb6d9-d09f-36bb-8bb6-62fe808734f1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.425,-6.65],"to":[6.55,20.325,-5.875],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,19.475,-6.2],"faces":{"north":{"uv":[35,23,36,25],"texture":1},"east":{"uv":[24,35,25,37],"texture":1},"south":{"uv":[35,25,36,27],"texture":1},"west":{"uv":[26,35,27,37],"texture":1},"up":{"uv":[22,39,21,38],"texture":1},"down":{"uv":[39,21,38,22],"texture":1}},"type":"cube","uuid":"db87f23a-29dc-e8a1-0d0d-9a2f369185af"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[4.95,18.575,3.75],"to":[7.05,21.45,5.425],"autouv":0,"color":2,"rotation":[22.5,0,0],"origin":[6,20.925,4.45],"faces":{"north":{"uv":[5,27,7,30],"texture":1},"east":{"uv":[27,6,29,9],"texture":1},"south":{"uv":[27,13,29,16],"texture":1},"west":{"uv":[27,16,29,19],"texture":1},"up":{"uv":[33,20,31,18],"texture":1},"down":{"uv":[33,20,31,22],"texture":1}},"type":"cube","uuid":"0714f281-db15-8143-c32f-e89576734cad"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.725,4.475],"to":[6.55,20.2,5.2],"autouv":0,"color":2,"origin":[5,12.75,1.75],"faces":{"north":{"uv":[8,36,9,37],"texture":1},"east":{"uv":[36,8,37,9],"texture":1},"south":{"uv":[9,36,10,37],"texture":1},"west":{"uv":[36,9,37,10],"texture":1},"up":{"uv":[11,37,10,36],"texture":1},"down":{"uv":[37,10,36,11],"texture":1}},"type":"cube","uuid":"a4916b0b-ece2-a56b-c694-cb963a9a80b5"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.2,18.075,-6.3],"to":[6.8,20.675,-5.2],"autouv":0,"color":2,"origin":[5,12.75,-1.75],"faces":{"north":{"uv":[17,27,19,30],"texture":1},"east":{"uv":[33,0,34,3],"texture":1},"south":{"uv":[27,19,29,22],"texture":1},"west":{"uv":[33,6,34,9],"texture":1},"up":{"uv":[36,17,34,16],"texture":1},"down":{"uv":[36,17,34,18],"texture":1}},"type":"cube","uuid":"9916eeef-31e1-9c75-ffda-14bd4d589032"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.45,18.725,-5.2],"to":[6.55,20.2,-4.475],"autouv":0,"color":2,"origin":[5,12.75,-1.75],"faces":{"north":{"uv":[11,36,12,37],"texture":1},"east":{"uv":[36,11,37,12],"texture":1},"south":{"uv":[12,36,13,37],"texture":1},"west":{"uv":[36,12,37,13],"texture":1},"up":{"uv":[14,37,13,36],"texture":1},"down":{"uv":[37,13,36,14],"texture":1}},"type":"cube","uuid":"cb63eadb-42fb-9f2c-0cb2-f6b85a7607f9"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.25,14.7,-1.675],"to":[5.7,19.15,-0.925],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[5.45,17.075,-1.425],"faces":{"north":{"uv":[31,22,32,26],"texture":1},"east":{"uv":[31,26,32,30],"texture":1},"south":{"uv":[29,31,30,35],"texture":1},"west":{"uv":[30,31,31,35],"texture":1},"up":{"uv":[15,37,14,36],"texture":1},"down":{"uv":[16,36,15,37],"texture":1}},"type":"cube","uuid":"8ae9fcff-9cca-67c2-69e2-f50e85162de2"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[6.3,14.7,-1.675],"to":[6.75,19.15,-0.925],"autouv":0,"color":2,"rotation":[-22.5,0,0],"origin":[6.55,17.075,-1.425],"faces":{"north":{"uv":[31,30,32,34],"texture":1},"east":{"uv":[0,32,1,36],"texture":1},"south":{"uv":[32,0,33,4],"texture":1},"west":{"uv":[1,32,2,36],"texture":1},"up":{"uv":[37,16,36,15],"texture":1},"down":{"uv":[17,36,16,37],"texture":1}},"type":"cube","uuid":"792785c8-f110-dcf7-d0e4-61f223963cfd"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5,16.5,-1.05],"to":[7,17.5,1.05],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[34,18,36,19],"texture":1},"east":{"uv":[19,34,21,35],"texture":1},"south":{"uv":[34,19,36,20],"texture":1},"west":{"uv":[34,20,36,21],"texture":1},"up":{"uv":[34,6,32,4],"texture":1},"down":{"uv":[7,32,5,34],"texture":1}},"type":"cube","uuid":"2ad8829a-0eaf-c32f-765a-76ad72a1b336"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.75,16.75,1],"to":[6.25,17.25,1.7],"autouv":0,"color":2,"origin":[5,13,-1],"faces":{"north":{"uv":[36,16,37,17],"texture":1},"east":{"uv":[17,36,18,37],"texture":1},"south":{"uv":[36,17,37,18],"texture":1},"west":{"uv":[18,36,19,37],"texture":1},"up":{"uv":[37,19,36,18],"texture":1},"down":{"uv":[20,36,19,37],"texture":1}},"type":"cube","uuid":"2181542e-699e-c408-2ec8-b291a6325c33"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,16.75,1.55],"to":[6.5,17.25,2.05],"autouv":0,"color":2,"rotation":[-45,0,0],"origin":[6,17,1.8],"faces":{"north":{"uv":[36,19,37,20],"texture":1},"east":{"uv":[20,36,21,37],"texture":1},"south":{"uv":[36,20,37,21],"texture":1},"west":{"uv":[36,21,37,22],"texture":1},"up":{"uv":[37,23,36,22],"texture":1},"down":{"uv":[37,23,36,24],"texture":1}},"type":"cube","uuid":"58cd5f7e-7a90-1ed7-b5c8-5d739d36bb28"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.75,16.75,-1.7],"to":[6.25,17.25,-1],"autouv":0,"color":2,"origin":[5,13,1],"faces":{"north":{"uv":[36,24,37,25],"texture":1},"east":{"uv":[25,36,26,37],"texture":1},"south":{"uv":[36,25,37,26],"texture":1},"west":{"uv":[36,26,37,27],"texture":1},"up":{"uv":[28,37,27,36],"texture":1},"down":{"uv":[37,27,36,28],"texture":1}},"type":"cube","uuid":"71ebf1c3-a5f4-6dbf-8533-0b7bdc6093f4"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,16.75,-2.05],"to":[6.5,17.25,-1.55],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,17,-1.8],"faces":{"north":{"uv":[28,36,29,37],"texture":1},"east":{"uv":[36,28,37,29],"texture":1},"south":{"uv":[29,36,30,37],"texture":1},"west":{"uv":[36,29,37,30],"texture":1},"up":{"uv":[31,37,30,36],"texture":1},"down":{"uv":[37,30,36,31],"texture":1}},"type":"cube","uuid":"3f2aa919-4264-20d2-4dc0-5b3963226082"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,21.3,-3.725],"to":[6.825,21.95,-3.5],"autouv":0,"color":2,"rotation":[-45,0,0],"origin":[6,21.2,-3.425],"faces":{"north":{"uv":[34,5,36,6],"texture":1},"east":{"uv":[6,36,7,37],"texture":1},"south":{"uv":[34,6,36,7],"texture":1},"west":{"uv":[36,6,37,7],"texture":1},"up":{"uv":[36,8,34,7],"texture":1},"down":{"uv":[36,8,34,9],"texture":1}},"type":"cube","uuid":"188d0aa7-7147-c70b-58ea-850b803f6a8f"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.175,21.3,3.5],"to":[6.825,21.95,3.725],"autouv":0,"color":2,"rotation":[45,0,0],"origin":[6,21.2,3.425],"faces":{"north":{"uv":[34,9,36,10],"texture":1},"east":{"uv":[7,36,8,37],"texture":1},"south":{"uv":[34,10,36,11],"texture":1},"west":{"uv":[36,7,37,8],"texture":1},"up":{"uv":[36,12,34,11],"texture":1},"down":{"uv":[36,12,34,13],"texture":1}},"type":"cube","uuid":"7de5534c-d30a-43f1-6eb9-25858b29fce0"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,41.05,-1.3],"to":[6.5,44.55,0.7],"autouv":0,"color":1,"rotation":[-20,0,0],"origin":[6,48.8,-0.55],"faces":{"north":{"uv":[19,30,20,34],"texture":1},"east":{"uv":[23,16,25,20],"texture":1},"south":{"uv":[20,30,21,34],"texture":1},"west":{"uv":[17,23,19,27],"texture":1},"up":{"uv":[34,21,33,19],"texture":1},"down":{"uv":[34,21,33,23],"texture":1}},"type":"cube","uuid":"e85c96f1-e8a9-dc1b-28ef-5270bd4fdeea"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24,1],"to":[6.5,41.75,2.25],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[8,0,9,18],"texture":1},"east":{"uv":[9,0,10,18],"texture":1},"south":{"uv":[10,0,11,18],"texture":1},"west":{"uv":[11,0,12,18],"texture":1},"up":{"uv":[26,11,25,10],"texture":1},"down":{"uv":[27,3,26,4],"texture":1}},"type":"cube","uuid":"e2626689-c30d-fc58-7143-b714a916e0e3"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,21,2.45],"to":[6.5,24,3.15],"autouv":0,"color":1,"rotation":[0,45,0],"origin":[6,28.5,2.95],"faces":{"north":{"uv":[13,32,14,35],"texture":1},"east":{"uv":[14,32,15,35],"texture":1},"south":{"uv":[15,32,16,35],"texture":1},"west":{"uv":[16,32,17,35],"texture":1},"up":{"uv":[36,30,35,29],"texture":1},"down":{"uv":[31,35,30,36],"texture":1}},"type":"cube","uuid":"b4e24ddd-0288-312f-4d29-4ee1bcad5926"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,24,1.95],"to":[6.5,41.75,2.65],"autouv":0,"color":1,"rotation":[0,45,0],"origin":[6,31.5,2.45],"faces":{"north":{"uv":[9,18,10,36],"texture":1},"east":{"uv":[10,18,11,36],"texture":1},"south":{"uv":[11,18,12,36],"texture":1},"west":{"uv":[12,18,13,36],"texture":1},"up":{"uv":[34,36,33,35],"texture":1},"down":{"uv":[36,33,35,34],"texture":1}},"type":"cube","uuid":"c7057fe5-eff1-3481-3701-b964a84f1c58"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,24,-2.65],"to":[6.5,41.75,-1.95],"autouv":0,"color":1,"rotation":[0,-45,0],"origin":[6,31.5,-2.45],"faces":{"north":{"uv":[16,0,17,18],"texture":1},"east":{"uv":[17,0,18,18],"texture":1},"south":{"uv":[18,0,19,18],"texture":1},"west":{"uv":[8,18,9,36],"texture":1},"up":{"uv":[32,36,31,35],"texture":1},"down":{"uv":[36,31,35,32],"texture":1}},"type":"cube","uuid":"662518b3-6709-eb4e-6278-8c012d80c705"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.8,21,-3.15],"to":[6.5,24,-2.45],"autouv":0,"color":1,"rotation":[0,-45,0],"origin":[6,28.5,-2.95],"faces":{"north":{"uv":[32,22,33,25],"texture":1},"east":{"uv":[23,32,24,35],"texture":1},"south":{"uv":[24,32,25,35],"texture":1},"west":{"uv":[32,25,33,28],"texture":1},"up":{"uv":[33,36,32,35],"texture":1},"down":{"uv":[36,32,35,33],"texture":1}},"type":"cube","uuid":"5bf81b09-08ec-d7a1-c186-9a432aac7ca1"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.75,21,-1.25],"to":[6.25,44.5,1.25],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[6,0,7,24],"texture":1},"east":{"uv":[0,0,3,24],"texture":1},"south":{"uv":[7,0,8,24],"texture":1},"west":{"uv":[3,0,6,24],"texture":1},"up":{"uv":[8,35,7,32],"texture":1},"down":{"uv":[33,8,32,11],"texture":1}},"type":"cube","uuid":"a9a094c2-0f3d-58d6-b21e-6ba6ba3b526b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,43.85,-2.15],"to":[6.5,46.9,0.9],"autouv":0,"color":1,"rotation":[-45,0,0],"origin":[6,46,0],"faces":{"north":{"uv":[5,24,7,27],"texture":1},"east":{"uv":[19,15,22,18],"texture":1},"south":{"uv":[24,11,26,14],"texture":1},"west":{"uv":[20,18,23,21],"texture":1},"up":{"uv":[27,7,25,4],"texture":1},"down":{"uv":[27,7,25,10],"texture":1}},"type":"cube","uuid":"64526f07-ded7-1117-c95e-5e72febd29f7"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24,-2.25],"to":[6.5,41.75,-1],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[12,0,13,18],"texture":1},"east":{"uv":[13,0,14,18],"texture":1},"south":{"uv":[14,0,15,18],"texture":1},"west":{"uv":[15,0,16,18],"texture":1},"up":{"uv":[27,14,26,13],"texture":1},"down":{"uv":[28,9,27,10],"texture":1}},"type":"cube","uuid":"d6541026-10ee-fe6a-8fe9-72cf75d33923"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,21,1.25],"to":[6.5,24,2.75],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[21,32,22,35],"texture":1},"east":{"uv":[19,25,21,28],"texture":1},"south":{"uv":[22,32,23,35],"texture":1},"west":{"uv":[25,20,27,23],"texture":1},"up":{"uv":[34,29,33,27],"texture":1},"down":{"uv":[34,29,33,31],"texture":1}},"type":"cube","uuid":"791744bb-3a31-3779-4bdf-f486a6933875"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,21,-2.75],"to":[6.5,24,-1.25],"autouv":0,"color":1,"origin":[5,13,-1],"faces":{"north":{"uv":[17,32,18,35],"texture":1},"east":{"uv":[25,14,27,17],"texture":1},"south":{"uv":[18,32,19,35],"texture":1},"west":{"uv":[25,17,27,20],"texture":1},"up":{"uv":[34,25,33,23],"texture":1},"down":{"uv":[34,25,33,27],"texture":1}},"type":"cube","uuid":"d9dda869-2759-4199-c1d8-b42d195aa834"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,41.05,-0.7],"to":[6.5,44.55,1.3],"autouv":0,"color":1,"rotation":[20,0,0],"origin":[6,48.8,0.55],"faces":{"north":{"uv":[7,24,8,28],"texture":1},"east":{"uv":[22,8,24,12],"texture":1},"south":{"uv":[7,28,8,32],"texture":1},"west":{"uv":[22,12,24,16],"texture":1},"up":{"uv":[34,17,33,15],"texture":1},"down":{"uv":[34,17,33,19],"texture":1}},"type":"cube","uuid":"abcc99c6-960c-1045-82bd-0a39d53f7a49"},{"name":"rune_1","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,21.975,-0.125],"to":[6.5,22.475,0.125],"autouv":0,"color":0,"origin":[6,35.975,0.375],"faces":{"north":{"uv":[2,38,3,39],"texture":1},"east":{"uv":[38,2,39,3],"texture":1},"south":{"uv":[3,38,4,39],"texture":1},"west":{"uv":[38,3,39,4],"texture":1},"up":{"uv":[5,39,4,38],"texture":1},"down":{"uv":[39,4,38,5],"texture":1}},"type":"cube","uuid":"44592a16-3ff8-e5d6-5197-a3d5fdb91e94"},{"name":"rune_2","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,22.5625,-0.75],"to":[6.5,23.3125,-0.375],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,21.8125,0],"faces":{"north":{"uv":[37,29,38,30],"texture":1},"east":{"uv":[30,37,31,38],"texture":1},"south":{"uv":[37,30,38,31],"texture":1},"west":{"uv":[31,37,32,38],"texture":1},"up":{"uv":[38,32,37,31],"texture":1},"down":{"uv":[33,37,32,38],"texture":1}},"type":"cube","uuid":"49542da5-539a-7743-3bbc-afdee9d770c3"},{"name":"rune_2","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,22.1875,-1.5],"to":[6.5,22.5625,-0.375],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,21.8125,0],"faces":{"north":{"uv":[37,36,38,37],"texture":1},"east":{"uv":[37,37,38,38],"texture":1},"south":{"uv":[0,38,1,39],"texture":1},"west":{"uv":[38,0,39,1],"texture":1},"up":{"uv":[2,39,1,38],"texture":1},"down":{"uv":[39,1,38,2],"texture":1}},"type":"cube","uuid":"a79acc07-9567-e68d-0a57-109e40d9acbc"},{"name":"rune_3","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24.25,-1.875],"to":[6.5,24.625,-0.375],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,25,0],"faces":{"north":{"uv":[37,35,38,36],"texture":1},"east":{"uv":[26,34,28,35],"texture":1},"south":{"uv":[36,37,37,38],"texture":1},"west":{"uv":[34,27,36,28],"texture":1},"up":{"uv":[29,36,28,34],"texture":1},"down":{"uv":[35,28,34,30],"texture":1}},"type":"cube","uuid":"8cadf035-ee63-7e4e-858c-fc3f8115fa7e"},{"name":"rune_3","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,23.125,-0.75],"to":[6.5,24.25,-0.375],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,25,0],"faces":{"north":{"uv":[37,32,38,33],"texture":1},"east":{"uv":[33,37,34,38],"texture":1},"south":{"uv":[37,33,38,34],"texture":1},"west":{"uv":[34,37,35,38],"texture":1},"up":{"uv":[38,35,37,34],"texture":1},"down":{"uv":[36,37,35,38],"texture":1}},"type":"cube","uuid":"a9027c5e-52c6-19e9-e232-a35125734ea3"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,24.35,-0.125],"to":[6.5,37.85,0.125],"autouv":0,"color":0,"origin":[6,36.1,0.375],"faces":{"north":{"uv":[13,18,14,32],"texture":1},"east":{"uv":[14,18,15,32],"texture":1},"south":{"uv":[15,18,16,32],"texture":1},"west":{"uv":[16,18,17,32],"texture":1},"up":{"uv":[38,11,37,10],"texture":1},"down":{"uv":[12,37,11,38],"texture":1}},"type":"cube","uuid":"2cec3ae3-79ee-9f96-74f5-0e90611c5e6d"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,-0.75],"to":[6.5,26.05,-0.3],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[5.875,25.725,-0.425],"faces":{"north":{"uv":[5,38,6,39],"texture":1},"east":{"uv":[38,5,39,6],"texture":1},"south":{"uv":[6,38,7,39],"texture":1},"west":{"uv":[38,6,39,7],"texture":1},"up":{"uv":[8,39,7,38],"texture":1},"down":{"uv":[39,7,38,8],"texture":1}},"type":"cube","uuid":"e204bb47-1379-ea43-8d02-0b48c88a6282"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,-0.375],"to":[6.5,25.85,-0.125],"autouv":0,"color":0,"origin":[6,30.1,0.375],"faces":{"north":{"uv":[8,38,9,39],"texture":1},"east":{"uv":[38,8,39,9],"texture":1},"south":{"uv":[9,38,10,39],"texture":1},"west":{"uv":[38,9,39,10],"texture":1},"up":{"uv":[11,39,10,38],"texture":1},"down":{"uv":[39,10,38,11],"texture":1}},"type":"cube","uuid":"4c44df55-108a-c9c8-3c9c-3c3bdd7c3a6a"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,0.3],"to":[6.5,26.05,0.75],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[5.875,25.725,0.425],"faces":{"north":{"uv":[11,38,12,39],"texture":1},"east":{"uv":[38,11,39,12],"texture":1},"south":{"uv":[12,38,13,39],"texture":1},"west":{"uv":[38,12,39,13],"texture":1},"up":{"uv":[14,39,13,38],"texture":1},"down":{"uv":[39,13,38,14],"texture":1}},"type":"cube","uuid":"a4083602-2242-1b62-262d-bfd38f8c77f4"},{"name":"rune_4","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,25.6,0.125],"to":[6.5,25.85,0.375],"autouv":0,"color":0,"origin":[6,30.1,-0.375],"faces":{"north":{"uv":[14,38,15,39],"texture":1},"east":{"uv":[38,14,39,15],"texture":1},"south":{"uv":[15,38,16,39],"texture":1},"west":{"uv":[38,15,39,16],"texture":1},"up":{"uv":[17,39,16,38],"texture":1},"down":{"uv":[39,16,38,17],"texture":1}},"type":"cube","uuid":"138546ac-7f07-33e3-b36d-0e06f98d386c"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.875,-0.5],"to":[6.5,38.375,-0.25],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,37.375,0],"faces":{"north":{"uv":[37,4,38,5],"texture":1},"east":{"uv":[5,37,6,38],"texture":1},"south":{"uv":[37,5,38,6],"texture":1},"west":{"uv":[6,37,7,38],"texture":1},"up":{"uv":[38,7,37,6],"texture":1},"down":{"uv":[8,37,7,38],"texture":1}},"type":"cube","uuid":"34d77773-c97c-7edd-a72c-887e3f21afcf"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.625,-1],"to":[6.5,37.875,-0.25],"autouv":0,"color":0,"rotation":[45,0,0],"origin":[6,37.375,0],"faces":{"north":{"uv":[37,7,38,8],"texture":1},"east":{"uv":[8,37,9,38],"texture":1},"south":{"uv":[37,8,38,9],"texture":1},"west":{"uv":[9,37,10,38],"texture":1},"up":{"uv":[38,10,37,9],"texture":1},"down":{"uv":[11,37,10,38],"texture":1}},"type":"cube","uuid":"952d8c6c-c534-3ded-ebbb-8560169862f5"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.425,-0.825],"to":[6.5,37.575,-0.075],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[5.875,37.55,-0.2],"faces":{"north":{"uv":[37,11,38,12],"texture":1},"east":{"uv":[12,37,13,38],"texture":1},"south":{"uv":[37,12,38,13],"texture":1},"west":{"uv":[13,37,14,38],"texture":1},"up":{"uv":[38,14,37,13],"texture":1},"down":{"uv":[15,37,14,38],"texture":1}},"type":"cube","uuid":"677371f7-759a-8b3b-225b-89324ccd9e5c"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.925,-0.575],"to":[6.5,37.075,-0.075],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[5.875,37.05,-0.2],"faces":{"north":{"uv":[37,14,38,15],"texture":1},"east":{"uv":[15,37,16,38],"texture":1},"south":{"uv":[37,15,38,16],"texture":1},"west":{"uv":[16,37,17,38],"texture":1},"up":{"uv":[38,17,37,16],"texture":1},"down":{"uv":[18,37,17,38],"texture":1}},"type":"cube","uuid":"09b6fa84-9268-c1f0-d593-f49988b0a6a8"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.425,-0.325],"to":[6.5,36.575,-0.075],"autouv":0,"color":0,"rotation":[-22.5,0,0],"origin":[5.875,36.55,-0.2],"faces":{"north":{"uv":[37,17,38,18],"texture":1},"east":{"uv":[18,37,19,38],"texture":1},"south":{"uv":[37,18,38,19],"texture":1},"west":{"uv":[19,37,20,38],"texture":1},"up":{"uv":[38,20,37,19],"texture":1},"down":{"uv":[21,37,20,38],"texture":1}},"type":"cube","uuid":"2f533cab-6d66-5ebb-be57-520ac9cf1da9"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.425,0.075],"to":[6.5,36.575,0.325],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[5.875,36.55,0.2],"faces":{"north":{"uv":[37,20,38,21],"texture":1},"east":{"uv":[21,37,22,38],"texture":1},"south":{"uv":[37,21,38,22],"texture":1},"west":{"uv":[22,37,23,38],"texture":1},"up":{"uv":[38,23,37,22],"texture":1},"down":{"uv":[24,37,23,38],"texture":1}},"type":"cube","uuid":"e5a7d019-ec5d-ef93-5ffd-c092c664d2cb"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,36.925,0.075],"to":[6.5,37.075,0.575],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[5.875,37.05,0.2],"faces":{"north":{"uv":[37,23,38,24],"texture":1},"east":{"uv":[24,37,25,38],"texture":1},"south":{"uv":[37,24,38,25],"texture":1},"west":{"uv":[25,37,26,38],"texture":1},"up":{"uv":[38,26,37,25],"texture":1},"down":{"uv":[27,37,26,38],"texture":1}},"type":"cube","uuid":"11316f78-2485-c065-fa4d-7b1225df2bf6"},{"name":"rune_5","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,37.425,0.075],"to":[6.5,37.575,0.825],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[5.875,37.55,0.2],"faces":{"north":{"uv":[37,26,38,27],"texture":1},"east":{"uv":[27,37,28,38],"texture":1},"south":{"uv":[37,27,38,28],"texture":1},"west":{"uv":[28,37,29,38],"texture":1},"up":{"uv":[38,29,37,28],"texture":1},"down":{"uv":[30,37,29,38],"texture":1}},"type":"cube","uuid":"e0d85bd5-8a87-a483-22e2-833204176141"},{"name":"rune_6","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,38.25,-0.5],"to":[6.5,39,-0.25],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,39.5,0],"faces":{"north":{"uv":[37,1,38,2],"texture":1},"east":{"uv":[2,37,3,38],"texture":1},"south":{"uv":[37,2,38,3],"texture":1},"west":{"uv":[3,37,4,38],"texture":1},"up":{"uv":[38,4,37,3],"texture":1},"down":{"uv":[5,37,4,38],"texture":1}},"type":"cube","uuid":"101292a8-6c83-621a-11ff-e6b17d4a82e2"},{"name":"rune_6","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,39,-1.25],"to":[6.5,39.25,-0.25],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[6,39.5,0],"faces":{"north":{"uv":[35,36,36,37],"texture":1},"east":{"uv":[36,35,37,36],"texture":1},"south":{"uv":[36,36,37,37],"texture":1},"west":{"uv":[0,37,1,38],"texture":1},"up":{"uv":[38,1,37,0],"texture":1},"down":{"uv":[2,37,1,38],"texture":1}},"type":"cube","uuid":"a7c81b6d-2419-772e-27a1-0068365ebce7"},{"name":"rune_7","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,40.9,-0.125],"to":[6.5,42.15,1.125],"autouv":0,"color":0,"rotation":[-45,0,0],"origin":[5.875,41.025,0],"faces":{"north":{"uv":[31,36,32,37],"texture":1},"east":{"uv":[36,31,37,32],"texture":1},"south":{"uv":[32,36,33,37],"texture":1},"west":{"uv":[36,32,37,33],"texture":1},"up":{"uv":[34,37,33,36],"texture":1},"down":{"uv":[37,33,36,34],"texture":1}},"type":"cube","uuid":"0ac8e87a-c521-a8ee-e14d-e205560a7626"},{"name":"rune_7","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[5.5,39.025,-0.125],"to":[6.5,41.025,0.125],"autouv":0,"color":0,"origin":[6,39.525,0.375],"faces":{"north":{"uv":[34,21,35,23],"texture":1},"east":{"uv":[34,23,35,25],"texture":1},"south":{"uv":[25,34,26,36],"texture":1},"west":{"uv":[34,25,35,27],"texture":1},"up":{"uv":[35,37,34,36],"texture":1},"down":{"uv":[37,34,36,35],"texture":1}},"type":"cube","uuid":"fc987521-07e4-10f9-b6fc-1512cb2da68b"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-1,35.8,-1],"to":[1,37.8,1],"autouv":1,"color":1,"visibility":false,"origin":[-1,36.8,-1],"faces":{"north":{"uv":[0,0,2,2]},"east":{"uv":[0,0,2,2]},"south":{"uv":[0,0,2,2]},"west":{"uv":[0,0,2,2]},"up":{"uv":[0,0,2,2]},"down":{"uv":[0,0,2,2]}},"type":"cube","uuid":"c8c75bdd-53bc-eb81-a2cb-0ea0dca06f35"},{"name":"cube","box_uv":false,"render_order":"default","locked":false,"allow_mirror_modeling":true,"from":[-8,0,-8],"to":[8,0,8],"autouv":1,"color":9,"visibility":false,"origin":[0,0,0],"faces":{"north":{"uv":[0,0,16,0]},"east":{"uv":[0,0,16,0]},"south":{"uv":[0,0,16,0]},"west":{"uv":[0,0,16,0]},"up":{"uv":[0,0,16,16]},"down":{"uv":[0,0,16,16]}},"type":"cube","uuid":"164a0f3b-aa15-e098-8e5a-f1fdf3e78bdd"}],"groups":[{"uuid":"778fa89c-759a-8884-89d9-238c555d2dc1","export":true,"locked":false,"origin":[0,17,0],"rotation":[0,0,0],"color":9,"name":"player_root","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"7e09ae80-d41f-d669-2538-64dec33e0e24","export":true,"locked":false,"origin":[0,17,0],"rotation":[0,0,0],"color":0,"name":"shadow","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":false},{"uuid":"9dc65952-10a9-876f-bd47-d6a7e9ec6183","export":true,"locked":false,"origin":[0,11.25,0],"rotation":[0,0,0],"color":1,"name":"phip_hip","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"e297aef6-7dfd-f100-2e7c-ab113699b922","export":true,"locked":false,"origin":[0,15,0],"rotation":[0,0,0],"color":2,"name":"pw_waist","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"a0c01522-9040-7533-fa11-f6a45d3d96ac","export":true,"locked":false,"origin":[0,18.75,0],"rotation":[0,0,0],"color":3,"name":"pc_chest","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"34097e46-c233-c03c-d8b9-aee154c9946f","export":true,"locked":false,"origin":[0,22.5,0],"rotation":[0,0,0],"color":4,"name":"h_ph_head","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"bfc2f156-b48b-dd08-1b9e-777d8ada16b2","export":true,"locked":false,"origin":[5,22,0],"rotation":[0,0,0],"color":4,"name":"pra_right_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"cf1618da-24d8-aab8-eebc-128815c02d35","export":true,"locked":false,"origin":[5,16.5,0],"rotation":[0,0,0],"color":8,"name":"prfa_right_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"fcaf8da0-0146-2587-b578-3e1af888deaa","export":true,"locked":false,"origin":[5.625,10.875,0],"rotation":[-90,0,0],"color":0,"name":"pri_right_item","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"859e9460-0016-acb1-017b-25fe27244cac","export":true,"locked":false,"origin":[5.625,44.875,0],"rotation":[0,0,0],"color":0,"name":"sword_point","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"dcb3a2dc-0f46-4628-586d-44f4265cc61c","export":true,"locked":false,"origin":[6,32.15507,0],"rotation":[0,0,0],"color":0,"name":"sword_body","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"b3135254-0351-3462-2479-e6a3286c89ff","export":true,"locked":false,"origin":[-5,22,0],"rotation":[0,0,0],"color":4,"name":"pla_left_arm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"1a9070b5-b8b6-b955-9f31-54f9625f8f3d","export":true,"locked":false,"origin":[-5,16.5,0],"rotation":[0,0,0],"color":8,"name":"plfa_left_forearm","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"0e94ff40-15b6-0b24-d0a0-447f000d761b","export":true,"locked":false,"origin":[-5.625,10.875,0],"rotation":[-90,0,0],"color":0,"name":"pli_left_item","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"7e8426f1-08b2-81a2-7703-cb76ff5e7003","export":true,"locked":false,"origin":[1.875,11.25,0],"rotation":[0,0,0],"color":7,"name":"prl_right_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"c6d9e946-1d10-482d-14b1-0766027adba8","export":true,"locked":false,"origin":[1.875,5.625,0],"rotation":[0,0,0],"color":6,"name":"prfl_right_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"5ef5d225-d5ae-6787-8838-b75ccb7a7a81","export":true,"locked":false,"origin":[-1.875,11.25,0],"rotation":[0,0,0],"color":7,"name":"pll_left_leg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true},{"uuid":"1b5cc202-c09e-faa0-5057-eb4ae60bf336","export":true,"locked":false,"origin":[-1.875,5.625,0],"rotation":[0,0,0],"color":6,"name":"plfl_left_foreleg","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":false},{"uuid":"529b75e5-967f-8e76-9d18-0fc49998293a","export":true,"locked":false,"origin":[0,36.8,0],"rotation":[0,0,0],"color":0,"name":"tag_name","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":false},{"uuid":"dcbc3011-d7b7-bf7c-0fae-067573db08f5","export":true,"locked":false,"origin":[0,22.75,2],"rotation":[0,0,0],"color":0,"name":"cape_cape","children":[],"reset":false,"shade":true,"mirror_uv":false,"selected":false,"visibility":true,"autouv":0,"isOpen":true}],"outliner":[{"uuid":"778fa89c-759a-8884-89d9-238c555d2dc1","isOpen":true,"children":[{"uuid":"7e09ae80-d41f-d669-2538-64dec33e0e24","isOpen":false,"children":["164a0f3b-aa15-e098-8e5a-f1fdf3e78bdd"]},{"uuid":"9dc65952-10a9-876f-bd47-d6a7e9ec6183","isOpen":true,"children":["ea42f7f7-a6f1-4479-c43f-48211bab5ed2","357ebf82-23ba-edb1-081f-dca75d94b83c",{"uuid":"e297aef6-7dfd-f100-2e7c-ab113699b922","isOpen":true,"children":["5ea74bdb-ba28-b8e3-103b-9be6ff2262da","f5b9f499-b26b-f912-1a54-151d702e13ed",{"uuid":"a0c01522-9040-7533-fa11-f6a45d3d96ac","isOpen":true,"children":[{"uuid":"dcbc3011-d7b7-bf7c-0fae-067573db08f5","isOpen":true,"children":[]},"dc1510db-a719-17b4-e253-c992a92c5d25","d51a8665-a2bc-af6e-1230-5acba07248e7",{"uuid":"34097e46-c233-c03c-d8b9-aee154c9946f","isOpen":true,"children":["7f60fbaf-510d-2e5f-b7d2-9111e08443cd","e0f94313-bf88-492d-0c68-bd6b466a3b68"]},{"uuid":"bfc2f156-b48b-dd08-1b9e-777d8ada16b2","isOpen":true,"children":["53d40d2e-0941-29f9-00ed-1b19c941dcd8","b17452ef-afbe-e010-f2c9-e0ba945faa1f",{"uuid":"cf1618da-24d8-aab8-eebc-128815c02d35","isOpen":true,"children":["e02c395d-e1bc-1375-a8ed-729e19544ce9","a509c2d7-53ef-2b11-1331-f716b9c210d6",{"uuid":"fcaf8da0-0146-2587-b578-3e1af888deaa","isOpen":true,"children":[{"uuid":"859e9460-0016-acb1-017b-25fe27244cac","isOpen":true,"children":[]},"44592a16-3ff8-e5d6-5197-a3d5fdb91e94","49542da5-539a-7743-3bbc-afdee9d770c3","a79acc07-9567-e68d-0a57-109e40d9acbc","8cadf035-ee63-7e4e-858c-fc3f8115fa7e","a9027c5e-52c6-19e9-e232-a35125734ea3","2cec3ae3-79ee-9f96-74f5-0e90611c5e6d","e204bb47-1379-ea43-8d02-0b48c88a6282","4c44df55-108a-c9c8-3c9c-3c3bdd7c3a6a","a4083602-2242-1b62-262d-bfd38f8c77f4","138546ac-7f07-33e3-b36d-0e06f98d386c","34d77773-c97c-7edd-a72c-887e3f21afcf","952d8c6c-c534-3ded-ebbb-8560169862f5","677371f7-759a-8b3b-225b-89324ccd9e5c","09b6fa84-9268-c1f0-d593-f49988b0a6a8","2f533cab-6d66-5ebb-be57-520ac9cf1da9","e5a7d019-ec5d-ef93-5ffd-c092c664d2cb","11316f78-2485-c065-fa4d-7b1225df2bf6","e0d85bd5-8a87-a483-22e2-833204176141","101292a8-6c83-621a-11ff-e6b17d4a82e2","a7c81b6d-2419-772e-27a1-0068365ebce7","0ac8e87a-c521-a8ee-e14d-e205560a7626","fc987521-07e4-10f9-b6fc-1512cb2da68b","aa7d3639-52cb-087e-0f54-7e31afe0d4b3","596ead89-97bf-b419-dc31-372fe3dee9b9","4853fc6c-23a8-6a4b-07e2-d89b3aaa8c1c","a04ef22a-5322-93ad-5281-e6b33566d6af","dbcf2b15-20a0-6f43-48de-9c73557825c3","656eb85b-2130-71d1-8400-036016caf911","61014d9b-3a39-4bbb-24b1-c1796ff81448","6e13574b-8944-aaee-8ad1-92edda71d4a3","5e0081ae-2c0e-03b7-b533-f33b34b8b9c8","926dfb9a-8a36-2d53-6026-df9e212d6187","34f7ace6-abb3-82f1-ffe6-aa94ccf29240","3616c0c5-2bd5-b180-4ed5-2708c59c41bc","20fe1d98-3b1b-7a8f-7945-78be2423e61c","bec338ce-2cb6-bade-c2a0-bf4b0256b6b2","cc8271b8-7122-2c3d-7a10-390515d38ea2","2d28b727-641e-2dc9-8e5f-15c9ed002244","68112592-c5eb-c6ba-7370-8df3fd15c5be","6bfcb5a6-b98e-2362-4350-0cff8e7c8417","8ec387cf-bccc-0ef1-a478-2c3c0c430d41","da16de9a-4659-1141-c033-4ab1a11199da","b55c73af-0b1d-14ae-5198-2782a6d7294a","a2bc1c86-9f7f-2d61-139f-584cbbdc9ca8","976bb6d9-d09f-36bb-8bb6-62fe808734f1","db87f23a-29dc-e8a1-0d0d-9a2f369185af","0714f281-db15-8143-c32f-e89576734cad","a4916b0b-ece2-a56b-c694-cb963a9a80b5","9916eeef-31e1-9c75-ffda-14bd4d589032","cb63eadb-42fb-9f2c-0cb2-f6b85a7607f9","8ae9fcff-9cca-67c2-69e2-f50e85162de2","792785c8-f110-dcf7-d0e4-61f223963cfd","2ad8829a-0eaf-c32f-765a-76ad72a1b336","2181542e-699e-c408-2ec8-b291a6325c33","58cd5f7e-7a90-1ed7-b5c8-5d739d36bb28","71ebf1c3-a5f4-6dbf-8533-0b7bdc6093f4","3f2aa919-4264-20d2-4dc0-5b3963226082","188d0aa7-7147-c70b-58ea-850b803f6a8f","7de5534c-d30a-43f1-6eb9-25858b29fce0",{"uuid":"dcb3a2dc-0f46-4628-586d-44f4265cc61c","isOpen":true,"children":["e85c96f1-e8a9-dc1b-28ef-5270bd4fdeea","e2626689-c30d-fc58-7143-b714a916e0e3","b4e24ddd-0288-312f-4d29-4ee1bcad5926","c7057fe5-eff1-3481-3701-b964a84f1c58","662518b3-6709-eb4e-6278-8c012d80c705","5bf81b09-08ec-d7a1-c186-9a432aac7ca1","a9a094c2-0f3d-58d6-b21e-6ba6ba3b526b","64526f07-ded7-1117-c95e-5e72febd29f7","d6541026-10ee-fe6a-8fe9-72cf75d33923","791744bb-3a31-3779-4bdf-f486a6933875","d9dda869-2759-4199-c1d8-b42d195aa834","abcc99c6-960c-1045-82bd-0a39d53f7a49"]}]}]}]},{"uuid":"b3135254-0351-3462-2479-e6a3286c89ff","isOpen":true,"children":["addb9f66-a2a4-46f7-b6af-f5a23c00fe70","5e7dc31c-c64a-ff15-90d9-52a6f77cb14b",{"uuid":"1a9070b5-b8b6-b955-9f31-54f9625f8f3d","isOpen":true,"children":["1433ee62-21ac-d72c-0fd0-37000e5eb221","24bacfd6-cde8-81a0-f620-998cf5393d48",{"uuid":"0e94ff40-15b6-0b24-d0a0-447f000d761b","isOpen":true,"children":[]}]}]}]}]}]},{"uuid":"7e8426f1-08b2-81a2-7703-cb76ff5e7003","isOpen":true,"children":["b17675fc-b79b-0ad9-8f81-aa2dd1fc8c97","2e42e483-1557-d21e-aee0-94af7bedfd40",{"uuid":"c6d9e946-1d10-482d-14b1-0766027adba8","isOpen":true,"children":["9455d16e-4bbf-4b63-881d-7daf943e782b","16aee685-0ead-a542-5b3c-a62467ca45e3"]}]},{"uuid":"5ef5d225-d5ae-6787-8838-b75ccb7a7a81","isOpen":true,"children":["0e370edc-7b05-dccf-dd2a-a92cfe9f3e22","dc189735-0a58-619b-07ef-feec37d65e7d",{"uuid":"1b5cc202-c09e-faa0-5057-eb4ae60bf336","isOpen":false,"children":["6bcf768b-beab-4c39-6d96-91266036a4e1","274282a7-bc7b-02f8-4dce-84226e9dd9c1"]}]}]},{"uuid":"529b75e5-967f-8e76-9d18-0fc49998293a","isOpen":false,"children":["c8c75bdd-53bc-eb81-a2cb-0ea0dca06f35"]}],"textures":[{"name":"-steve_template.png","path":"","folder":"","namespace":"","id":"0","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"44166cb6-b1bf-f227-4398-96c94f36d7f7","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAApBJREFUeF7tWztLA0EQvoCgEGsTiPhAwcbOJr12WmlhYWVlr1b+gFRqoZVVKkutLLUXwc5GUHxgIBFLBa1OJrmDzbi3O+u6SHJf2rndmftudl7fphBZfmc3T7HpkfreoXGHk/puwabDJA+t32ocGVCdHtXaeHn3EhEAlVJZK2+0mtFfALA0O661k2wj/Vk6Vta3Y5t+AGBzT3gAjgBiAIJgX6fB+eaeMc+XbzaMcfLo6sAoH1h9M8oXixWjPLT+AgFQLY3o83zrNSIDJspFrfyx+RERAMXJMa384+E5IgAGKzNa+VfjNiIALspb2nRMtpH+44VhrXzt/D0m/cM7O1r5e60Wk/6hqX2t/PN+MwYAEg/YnOt4wGmj8yGXE6/dv86JB+QeAMSAfg+CpjwUOg39exrkL8/7b97v29rL0P27r338fX/kR7X74/2+pL8P3T362gcAGAJaD1Cf6fsjwHsBHvR4rc9rex7EXIOma6/gal9WGZx+5K5S+JLV/rzW57V9WsunvQRfz8+bbT/+vG1/2360HgBkNELwgAQBHAG1G0QMQBDsngBJoiylQWSBZKSGNOg4Q5TODLM8TOKhqAMkdYBagbmWmj1fCmMewBCgfjvl4zn/LuHb1fU6cH35fF/7RPOA3AOAeYCCQC7mASk3x7k4zr1xro24NZXbc+XyJNydaX+JfaI6AAAk7Cw8gNHREhfDEVD4fcQAxwsNCIKCGxzIAoYjJolRSIOSdjj3dQDmAQoCvvx7r63XssO+7XD6/wLf+wW/Xe9iPwAIPRLzvV8Qen2bGwwZBH3nCaHXtwHwTYM+c3sTsySZ+/tWigAAHoAjgBiAIIgs4DkURRrMuCAhzeO+APp4MOqAvJfC38z1ql4UO+HHAAAAAElFTkSuQmCC"},{"name":"-knight_sword.png","path":"","folder":"","namespace":"","id":"1","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":true,"saved":false,"uuid":"c2982d79-5e5d-fbcd-faa3-d19a766eaaa5","source":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAEMRJREFUeF7tmmdYVVfWx//3ntsvcCnSLqCiGDWmzUSNZUzMq0mc2BtSbGhQYyxEBEIsY9Qx1hiFGCIqoIKABUssCVhiS2KLGonRqEhXeru9nMlZR+7I67wf5n0ez8MH9pd99in37P3b/73W2utc0cmTqaxExsBoMEMiZSCR8Mc7xROwPXApliTXY9aMqTCZGtBcRCIxdqTsw6yZk+Dr0w3DG9JxVFeKUmM5qioLUFNdgpOpBsf93EFIXA+IREqUa7bS+ZQNCVRPGJiFyu/NYP8qoXZiIovgSWPpePnyBFGLH3kODdG3x75hDXoTDV6pkMNqtcJqtWOvejIBWL6jCVHz56GqutDxerncGdu270bk9DB4e3fBiMZMApBfdhkVFQXQG0Q4vqMRIjGgUTKw2lgMneWHqqo6OPc+/wyA/IMG9BilpPNLl9mEBZBz6CtWKpXAYDBBpZLDZrXBbLYiRzONAKxMM+KD6ZOg19dArfaCTlcBTgG7Mw5j2tQwSKUqREjPEIAzN7JhMIpgswGHdzRADBHaOTGws8BfRyphtwOrVtux/siPNNiTX/8FfXurcW6PDqELe8DGMqj3TcOl7PXCKSAzayPbubMW5Y9qwDAMpBIGZosVe2ThBGBNhgXhYWNgNuvh5OSFpqYKKBQapO3MxJTJoZDLnTBZnMcvAf0DrNlwGrELXsGSj25CxAJaN17awxZ+ibK0WBisdnjHXqJzVzN6ontXBWrOm1BvYnFzcgGdD/WpxuNLGZg+Lf75L4EjR5NYra8nyVMkFsHD3YVg7JGFEYD1WXaEhY6F0dgAtdoTOl0lVKp22LkrHRPDg6FSuSOcPU4AbpcdhN1uhFyhRepKfjBX75px/MZdMBIpVozvBp2FhejJsDSvinFqr5HuO34skurwlWL0CDDhnXf7YMDfwp8/gJyDX7Ht23ujuroBDY06BHb0RXl5NfbIeQVs2i/C2LHDYTbrIJM5wWxugkrlgfSMLISGjIVS4YqJou94G1CS6bATe9ab6Pjy72YcunKNlsWysDfoHAdAJhHhaQDar8rpmjF9GQEQzAgeOJjIduzAAWhEbV0jOnfyQ0lpJbKVkwjA5hwGwePHkA2QSBSwWo0QiUQ4dPgUxo8bDrXKA+HgAeReyYa3jz9cXTXYuvgKDeJivhH7L16AWCxCRWE89LoabLbm0bVFzl0hlXfA6Q33IXMW05LRqMXwGLUWud//JIwX4BQQGKhFZWUd6uob8UKXABQWPkLWUwDGjxsNg6G2xRLI3rsP48eNgUrlijD7MQKw7/ReeHsCWt/2SFnxOw3y7K9GvBk8gY7f6pkLmxXY63Wb2v/wDIFYLMHPX/wMTfRPdI5J74fdg+6g89l4YQAcOryFANy7VwK1kxJ+2nYoLa1yxAGcAsaNHQWjsQ4ymfrJUlDjQM5RjBr5HlxcfBHOniAA2XnZ8HBn4e4G+Hc7AFeNNwZ0747BYaE0uL6v5NJS4Fzd9bwQx5rXlu9Ame80avc0pcIQxN8vSBzwfd521svTHQ8elELtpIKf1uMZAMOGvv3nfLFgGDlsNhOkUiVyDh7HmNHDSAGTRE+8gOEPrFl/Fp8sCADjuhXubn7o27UrhkwMowFxv5OQeoOO0xfbHQCifDNRrLOTm7xVbILsFf5+QQCcPJ3Kurs5o7i4EnKF9BkFfLEPCAnmvEAdLFYJpBIrucGs7ByETBgLJyd3hFiPkgLuVZ2DyVjKmTm4+O2Ep2dH9O3aA8OmhENy5yAN6q5bBNWK8GVUc0ZvvNNujIriB704Kg3dRy9E4cMyYQCcOp3Guro6obikAgq5DH5+nigtrXQsgTUZVoSGjCTjJxYzsNtt4CLB3en7MGVyGDQaL4wz5jzjBTT+6fD3fxFWi4WeWxr8Eg2wPOrfEWUzAK6OiJ5L180pfXFdLWAozCnASa1CScljKFUKcC6xtOTfAFak6BEREUJrn3N/en01L+H0I5gxYzqkUjkmiXIdNoAuAhg78Wc06Spgs4kJ2prpg+l8zcIiqq9GvoC4GDEYBuhYIMKmx7yh5OzBuq+mYlWaRRgF5J1MYZ2d1aira0RTkx7dunUgBaSJnmyGvmlAZGQoKYCbeZOpkTqakpqDOR/NgFyuQjjLu8GME1mQSPhQeEz4WRiM9TCbbeBC7S82JuC9/t9xpgQntvEbpReHKGFnWQSUA8XFNqikIvxQaIR3v4l0XRAbwMUBnQK1ePy4Fjq9AV1faI/83wpwyHU6xQGxCVWYN3c6BUDNAJRKN2z8MgnRC+aQG5sqOU0Adh7NgkIugs0OhE27iqrqIhQkjIHHlK1QKFzw40ne8p9Mb4RcIkZ9LK8Gzg5wZfa4i5BKJMi78KpwAPbnJLJBnf3x6FE17QQDArzJJe53nkoAFmx4hAXRM2gJcNbfYjGAA7BubSJi4+YRgAjpDwTAro2BWMTAYjXCbrPCaGrCz6uGIGhmMhQKV5z/npd58OBgqqcXLKeai/2/W/gXOn5v/S8YrNwk3BLg8gHePu4ke4vVggB/7xaB0EefFyE+fg4B4AbOBURSqRqrVydiyZIYSCRSTGF4BcAvDg2N5RQp3towFG8suYya2iKUlhbCZrNBZktCfV0x+ncf1WwqqM4zzEfFut6QM2IcC/uDgqAOHbXCbIbyTqWy3l5uKCurgsVihb+/Fx4+LHfkA2atfIj4+A9hs5mhVLrCYOADopUrN+Ozz+IgkcgwWXyKAFi85qNJV4na2sc4uW4aIjZfQpOuBjqdjqAU3YlGbW0ZLmToaOArv5xCdYUsDOsi36XjZgM4dFA53nhr3/PfDB06soXt2EFLS8BitVIcUFxc4QiFTe3mY/ZlDzx66RucaBcBk9GAtWtXt5jBIL8cR3vMxB8hkyooZ2CzWVBZVQSLxQyjsR5Xzs6EWAx8t6sRZiuLZZuDIJUHwslrCVaGDqG8we0yHe0Mz9w4gCFjHj5/AAdyEtkOHXxx/34xFEo5mrfGu5gQsgF6t9mYe82HAOSogiFmxFi7piWAQJ8ccmdWGzBiwnGIRAxUSjcKiDgAJSX3KNdwcNVsAtV/5TWqu9pnQsxocJtdi2Nz34BYBLw5RYpb90eh32tHhAGwJ/ML1t/fG0VFjyCVMfDx8UBJSSWOtptBAAweczHniicByJKOhMHQgOStKS0UEOibA0YMWK0s+vxPMtzdfKBWe2Dzh68DMiD4032QSORInDOMnnt7NQ+g6lx/sCzg+eYF5EX1gYwRIWRZP2zQpcJtfQAST9Q+fwWkpK1mfX08KCFiMlmh9WuHmqp6HPOeRQB0bh9i3jXfJwBGQK+vx7bktBYAJHey8FqAFCq5CDWNdtQY7OiikcIMFpeLzRgatxXu7l74YhYPQC7lx/XKMD4POCrsItavW4eexgNgenvg55tvQf9LBtYfqX7+AHanr2PlShnMRgvMJgs0rk5oajLgO9/ZBKDe+QMsuNmeByAZAb3hWQDyu5mw2VlUNtgdgzNZWNjsdihlYoxb/BVYlsW26Hmw2Fh4e/H8Rs+RI+DFw7RUtiWnwnIzg5YHlxPM3rUfvxeUPX8A6RkbWIbhwlU7GUG5TEYZm8PukQSgVh2BhbcCCcAeZih5g6+3JLdQwAeR4XBx9kTesgEIUDLQW1icKzbTPf3ayyCSiFCht+HgBT79VdzA1/Er3PFyn/1gGAmSt6bitdr9sPZkcMjvNoXKggAI/ZuGmxw8ruK3p1x5feR4quNio2Gx2BCT34kApOM9yu0lbE5sAWDGzIlwdvLEiom9YDDb4eUihkLGQCxi0WS0o7LRDpVMjMoqESngYR0fCs+L94A2aBWCgl5D0tdbweRn4u6HBRQHfJ2QisraxuevgGmDXQnAgyIbp0SK1XuN5gEs+vQT6HU6xN7uQgBSrW9DpXLBpi83tQAwNWIcbX2rqspgMNTg1qVZdJ3zLozYCXV1d6mdvFxPtclqx58pQkyMcsaA945i7qc7KQ/46RQpXRdqI8S9SzR1kIblZuf6HV6yXOk3jg9ZY2NiKDyOyeeXQKp1IBQKZyRs5r/qNJew8GHw03ZDRWURfUG6fpEHoNEwUDt5oqz0EbWPbtVDyohQqTfDYmXx8kApohedaQHg7+8ng0uQpg92eu6zTwAWDvdguQMud9dcVm3mfDjQo9dxiMQMon99YgSlw2l/v2VLUgsA7vey4Ktg0H7GRkp+3vklmq7LuY2RjXePBCCZV0CzDXh7hBrjJ6XA3d3H4VnOZaViwISpguwECcCEnu6sk5zB1cImMn7ccoj9rB119Nqdd/Bx1DzE/hb0xA0Og8lkxDdJ21oAUP6WCReZGNNXhNP55k2O76YO1G4OeblsD1cu3uFtQP8hSvg+FmPc+uPIzc3FuhUJSExoj7OXegkHIKSXO6uQiHGlsIncEWcEYv/BA7h5/11EzZ+LuCc2IFs2gkLhpKSWXqD2Ujrdz8XxTwNQ/NMfNhZI2s4nPCMjtlNdU8e9hUXA6wwCG6W47DwZ7w/wwNChI/DHzXnIPfeicAC+vVVI+owZ3NUxq+/0UoKRisAEjUJcbAyirgeQAvbKR8Nk0uHrLfwX3uZizc8AFIDZbyTmzZ2NK2fHUFi7K5ff9nJfgDt1ag+TvgA535pw94IFDUYb/rlRg8yNRsoIN+cIR5Z2R6NttiA7QVoCzQD0V/8GPx87lOpOyFjzGz+27iMRFxuHqOt+BGCfYjSamuqeCYXNNzJg5bxo95H4OGo+LuTyEV/WGd6YcgCCuvRExaMruHzNiK5dFPD3BapqAfkPdjBDA3HvjwLk3gglbxAQ4IPIyEXCGMHLP/Rmd2feQ/8+aiiVLDzavYJFH56DWs5gVsYN9NNaHJshTgE6XR0++f0FApJmGwSxWExxgjR9Mtp5uiImeiHiPlmMpMUdMVTtR9vkw+f3OtRy+aoevV5Xwde3PUpLi6D80YbirvxYT9/iAXBFiHQYKeDiqYHs8RO38HIPJVlsrmxdU0cS/njfLbzhY0LUda0jErRYjA6jmGIZCIlEgpj8zg4A0QsWID5+6f8J4OSZJgwa6AQ39wDU1hTTp/FBEV0gldiw49uBmBHaHRfOXxcOQE6GP3vxJwPNCucBuOLs4gq1SoFG/33o5anDgiducA/zPmw2qyM0Xv5ASyF0knEkAQiouASj1YZKbd8WALjA5unyUmf+G0FzUb3O/2mi8Pt+OPVrKKXIh73UQZglkJ0WwDbPCtcJpVIDmUwJTy+/Px2YBH5Byx2boXTR32G3Wylw4Urz153bleWQyVRYmsDHD5xBm3qsGxpgJ9uQ7zIW7kXZSPqBoX9/cGHum6Nm0kB7ejbhSqUTjL/0w+Kl/N9jhEqH0RLoFqilea+ua3R0imvXJvWBh7MY6YPu0KaIy9vteeThOObuaf6fT83FbxyzuWjDS0hR5KJDYkeY7Xb6i8yp60a82FGC3x5a8eWuYbhz+zy4WW/+Jwj3cO/ghS3agtmApUvnPhF+C1VSI79Y7jBK/6n97BP//Zmn38HNPPdJjCuCAdi+43O2+aX/aSbuv/k5zfr/XsdPn3/3rQdoqs+HwWCH9NVzNICFw/s6aAixrf3v0fNPCGJo/r+dE+K5NgBCUG7N72hTQGueHSH61qYAISi35ne0KaA1z44QfWtTgBCUW/M72hTQmmdHiL61KUAIyq35HW0KaM2zI0Tf2hQgBOXW/I42BbTm2RGib20KEIJya35HmwJa8+wI0bd/AQszlaq8R8kcAAAAAElFTkSuQmCC"}],"animations":[{"uuid":"91f45b91-0388-39e0-52fd-e98e668d0d06","name":"roll","loop":"once","override":false,"length":0.66667,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10.3452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"4e015e1c-020c-c0cc-cf03-d0f365b91b4f","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"96a1cbef-dc1c-8cb3-893d-1844d13a8898","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-55.3452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"cffaa113-7472-9a2d-879c-3b37b7918233","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-67.8452700461","y":"14.7668896086","z":"-2.6639876167"}],"uuid":"1859ff86-2a12-36c8-57b7-cf230e86e3e3","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-173.4065947693","y":"12.6083678689","z":"-8.1925230243"}],"uuid":"6c944778-0f30-a404-20be-7d985529dcc3","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-254.4306397576","y":"-5.2125464926","z":"-2.586642225"}],"uuid":"6fea6dfb-ac36-8bb7-7b6c-f1b31c3fb09c","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-299.316821318","y":"-7.7099874859","z":"-2.5994534331"}],"uuid":"19e7bcfa-34ef-ca1f-9316-34616fa523cb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-334.7684189709","y":"2.2798591799","z":"-2.5779800118"}],"uuid":"143b95c6-51d7-3d73-dab9-4afc76a18bee","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-359.7452658674","y":"0.977484586","z":"-0.7993651888"}],"uuid":"ca8b487a-5316-821c-6a0d-2b17dc09e2ce","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"0"}],"uuid":"739f6fd0-eeff-515d-bc2c-05459d38fb35","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"610dffa2-b064-c32e-49b0-95e662c9fbe0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f8040d86-2633-7d51-02f2-d276ac128c20","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-15","z":"-5"}],"uuid":"2613c6a2-076c-8a2d-7022-314ada456d6a","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-16","z":"3"}],"uuid":"65475ddb-0901-3649-c168-8f332b92d1c4","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-4","z":"5"}],"uuid":"d26510d5-a53d-5570-114b-1a49178fc6f7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-1","z":"4"}],"uuid":"03b08547-82b6-c8ce-eb7e-8147925b4d3c","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3d14037c-74a9-abc2-b13d-3600ae6c5dd0","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"position","data_points":[{"x":"0","y":"-3","z":"0.31"}],"uuid":"7a5c383e-b443-3773-3fc7-b41a95b66aa3","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"f218f334-8ebe-66d8-1769-41de9f9f7ad0","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"75314a09-9386-3e4d-3f96-a8e59911fe5e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"e579328d-a99d-9a7f-0a05-746e42159080","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5169123047","y":"4.8873117739","z":"2.6276738824"}],"uuid":"57494920-9783-7099-8971-f8828309b07f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.2554184495","y":"1.9324031631","z":"0.6242872643"}],"uuid":"14faa28e-7b7c-4fca-04bd-fb15427d1d14","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.321532076","y":"-0.1985844587","z":"-0.7364643904"}],"uuid":"2960010b-2391-100b-9059-499f90d98595","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.7446837252","y":"-2.2894808314","z":"1.027565895"}],"uuid":"6436ac0d-b7c1-eb4c-741b-c6fec8760846","time":0.16667,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.5","y":"0","z":"0"}],"uuid":"6cb82785-ecee-7220-9b21-7f748ca4ff73","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"554f1255-7efc-bebf-d907-e6b38e212768","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"918337ed-db4f-55f0-7a21-d10d25500ae6","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5","y":"0","z":"0"}],"uuid":"66b19558-a367-598b-50c7-142c4211a07d","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-15","y":"0","z":"0"}],"uuid":"af84a634-f0ee-f31b-42da-a9789b3af450","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"9b812e3b-ff6a-5bcf-9b14-d88149ef2a29","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.9335647827","y":"0.1910653446","z":"0.5860712492"}],"uuid":"d26f71b7-3eb6-c334-8e41-f3048cc39bfd","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.8337650092","y":"1.5719388815","z":"0.836263831"}],"uuid":"908fd1af-b92a-7c86-ec15-33aa208e85d9","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-36.5153158634","y":"1.5694142029","z":"1.1566234175"}],"uuid":"06e3b992-d5bc-6bd5-da5e-ec28155c8af4","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-5","y":"0","z":"0"}],"uuid":"af2c3045-852e-36da-7224-00ca89c0c360","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4e57e7e0-bff8-8f86-f5a9-14379ed8f0cc","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25","y":"0","z":"0"}],"uuid":"7e8f0cb8-aacf-3a06-0990-319fcca439ca","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40","y":"0","z":"0"}],"uuid":"951b375f-8b17-0bce-c1e5-b020c20b1378","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-17.5","y":"0","z":"0"}],"uuid":"96e0a9d7-f9a1-ca27-c4c1-49b9b9ae9739","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"0","z":"0"}],"uuid":"d691f682-5653-24b5-64cd-f593d802ae7a","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"64d27d7c-16f1-4e24-11de-1df5b650e5b7","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-44.7041455577","y":"-0.5463397482","z":"2.4016359177"}],"uuid":"cf7881c9-f9af-c342-d155-dda6ca40df0f","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-42.1987087714","y":"0.9199484607","z":"2.3855070851"}],"uuid":"1e94d2b6-12f7-f0b3-5513-e088c678cddc","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"4.7824447755","y":"-15.0272212866","z":"0.6748976469"}],"uuid":"d578b08c-e775-fb98-c9f8-42f4e180dc8f","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"3a4776d8-2065-0aff-5901-b4d3d6ecf2f6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.9790874962","y":"-5.8032396565","z":"15.815264958"}],"uuid":"f9ce9b56-16a1-0ade-1b32-395dc6a6508f","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.1815443852","y":"7.1155834398","z":"18.1304160091"}],"uuid":"14d812e3-97c4-fb55-ae8c-7ec92768c5bd","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.9790874962","y":"-5.8032396565","z":"15.815264958"}],"uuid":"5afbcdab-4795-1ca8-59a9-285e3dfac932","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.9171104584","y":"-4.5000466504","z":"5.8600103802"}],"uuid":"6499750a-a4fb-5c57-4995-c4d47b630672","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"8.2765048988","y":"-2.9212329552","z":"-1.4866588636"}],"uuid":"7f018859-2372-9fd4-17e0-756d8fffc147","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"aba6447d-d798-c3c4-8579-72b987b8b41c","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.7013003421","y":"1.5426491326","z":"16.086881186"}],"uuid":"3896e9cb-f48c-75cc-888d-769cf83332f2","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"55","y":"0","z":"0"}],"uuid":"8e1409c4-941b-084c-02e5-df03cc1a4690","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"92ea27f8-0e0a-b629-1027-b86cf738d7ec","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"79.7622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"fc368334-2af0-e8a5-5d0a-5cbdb752e343","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"117.2622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"7f4efd9b-c5ba-5977-b5fa-b1b9799bbac1","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"94.7622809499","y":"12.3070971455","z":"-2.2046197553"}],"uuid":"7db6c762-536f-e3b9-482d-8337cffe5746","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95.0395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"bbc95573-feb7-ffe2-345c-af69f5f8f939","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"72.5395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"ab7c0856-f341-34ea-f57c-6904b0e30d92","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.5395494766","y":"-22.5728078416","z":"-5.1606746124"}],"uuid":"9e4fcb79-0aa2-9377-3fc6-ab698d600e8a","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"63743b29-3a44-0bc8-f5a5-8903bb15d390","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"25.6848204073","y":"10.1778091184","z":"-20.173933666"}],"uuid":"7a3b3bdf-dd57-badd-a6dd-7580d30e23e0","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"4323899f-7d3f-557a-beda-d05603cd2f96","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"22.4403124196","y":"16.3255534092","z":"-34.2402741209"}],"uuid":"744316fc-f3d8-67fd-9a68-2b2c568b639e","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"7.2988707081","y":"3.4553272208","z":"-6.6606725408"}],"uuid":"6cbe013b-e0b9-702c-e627-f2c374f53718","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"fd38995a-2f81-eb5e-6c06-404eceec04f6","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"25","y":"0","z":"0"}],"uuid":"5f5fe94f-19c5-c019-7a47-ac924d74285c","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"339cdfba-9029-f75f-9d19-ac8567b07424","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"87d530dc-a3ca-5bc5-53fd-d0b58e67c115","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"95","y":"0","z":"0"}],"uuid":"4056d3f0-9227-879d-0ccd-a715a6acefd8","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"60","y":"0","z":"0"}],"uuid":"5cb622b9-5c77-b222-ba79-a62ddbf1e801","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.9979365876","y":"19.3545961515","z":"-11.7009195082"}],"uuid":"d08bec63-419b-45f1-d3ac-4bf88c534563","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.9979365876","y":"19.3545961515","z":"-11.7009195082"}],"uuid":"aeaa276f-1181-1922-960d-7fb6845fd9f3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"13.2306976448","y":"4.1075261935","z":"1.5640491387"}],"uuid":"2416d5e6-91bb-6991-a145-7778a9ab717b","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ce1a67cc-be9b-632e-7d0f-1ea657ce8f06","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"3e549bb1-58c3-92b5-d13c-9c3c0772ea12","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c6b905c0-5196-e52b-59c7-8d3927a73397","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.5","y":"0","z":"0"}],"uuid":"e4592899-dc47-7f8c-acd9-92811b5fdd77","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"82.1404769782","y":"-17.3455144203","z":"2.3566635967"}],"uuid":"07d4219f-d8d7-811e-fba2-06fb667c4f62","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"31.427095159","y":"-13.5306627252","z":"-4.344153106"}],"uuid":"7a496aba-7df1-b5b5-4dbe-7e6420e13fef","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"11.427095159","y":"-13.5306627252","z":"-4.344153106"}],"uuid":"446c86e4-1c3b-eff5-a180-a62769af7bb8","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"95fe2e07-173b-a066-a66d-0be58ae890a5","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"48.3424105805","y":"1.1690538795","z":"-1.2575696869"}],"uuid":"06246a98-556c-f22c-45a0-44083c73dbe9","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"17.8934503775","y":"-3.8213109421","z":"17.9129813754"}],"uuid":"aead35ef-607e-93dc-f018-85e424e6a137","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"57.5","y":"0","z":"0"}],"uuid":"447233aa-66b2-dbd7-e45d-483f46dadbae","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"bd388449-ced9-61f6-e408-4f913fc0355d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"57.4009318272","y":"-4.2154088774","z":"2.6913572849"}],"uuid":"7b84f346-ab75-70f4-a8f4-67879637f9e5","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.4009318272","y":"-4.2154088774","z":"2.6913572849"}],"uuid":"724ba19e-1a1c-00e9-111a-d67ed5aa5c83","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"48.3698475508","y":"-13.3767559929","z":"0.8884687898"}],"uuid":"28e08960-9fa6-1789-1edf-5b1e443db7bd","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-0.7930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"3629a4e5-a2bf-6722-7a63-824e74693c95","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-23.2930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"fc5d0a27-b180-4a2f-5fb1-c25e9d8d0cbc","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-13.2930143118","y":"-7.6305598405","z":"9.4055935229"}],"uuid":"0471b600-cd96-fb88-b394-25b68b236191","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"b3528148-886c-4981-aad9-a449912e4ff3","time":0.66667,"color":-1,"interpolation":"catmullrom"}]},"c6d9e946-1d10-482d-14b1-0766027adba8":{"name":"prfl_right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"4d65f4ff-4666-1d6e-e2a2-5754675f39d3","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"713091ea-3c5f-2107-8abf-ee97bc6176ee","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-35","y":"0","z":"0"}],"uuid":"020d854c-8f1d-eea6-0d44-f050772acad3","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-12.5","y":"0","z":"0"}],"uuid":"747e19b0-5b0d-1bde-37c5-d61eae5ee649","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1a3a7fe0-9ac6-642d-1655-a02cbedc50c0","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-63.5104629601","y":"-0.0562419487","z":"-3.8034923955"}],"uuid":"cf494402-d95a-c9a9-0666-1105ac4ff1ba","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-30.3667568215","y":"-5.6104628256","z":"0.822128921"}],"uuid":"338edd44-0be5-f9d0-495a-16b3c7a47468","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5627211181","y":"5.0414022802","z":"3.6575498102"}],"uuid":"601767a5-ac9a-bc96-4b0b-237160c36700","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50.8538632696","y":"5.4159878628","z":"2.605219904"}],"uuid":"b124849a-6e1c-463f-9163-98272fab4254","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-17.1096711992","y":"-3.7317133585","z":"-11.938445897"}],"uuid":"486ede20-d032-4d46-d9cd-95cf257c04d9","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1e3e41fd-1374-7b20-aada-7d42393cb399","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-93.8329224881","y":"-12.0025641778","z":"-18.1816740733"}],"uuid":"13a334d3-48cd-1136-e910-d86f10ecd6ff","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"25.8053212577","y":"7.0178718272","z":"-29.2161110452"}],"uuid":"7e87c3b5-9d38-b35b-df3b-aa05fc47517d","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.0037108296","y":"-5.9031625207","z":"-19.1431448415"}],"uuid":"6601f44d-7b3e-1933-edfb-aaf37ed8f707","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-14.2395920994","y":"3.5940186635","z":"-7.5169369411"}],"uuid":"284cc08b-c152-0ab3-3689-6d441335ef56","time":0.58333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"8085208f-1d05-3f74-9820-0173fe43fd6b","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.9519624501","y":"-7.2690960426","z":"-16.7194764292"}],"uuid":"487c4a04-c29a-1aa1-e26a-aa6dbafe7e0a","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"18.247962068","y":"7.8313214321","z":"-24.4844657083"}],"uuid":"73a7fea9-6029-f2f6-ba64-9449179096e1","time":0.41667,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-30","y":"0","z":"0"}],"uuid":"a0002446-b908-df56-d45b-79e09a54036d","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"71dde296-8a96-6c52-d883-110fe28f6083","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-18.550117808","y":"-0.4065298268","z":"4.9663131482"}],"uuid":"a9e10a80-2787-3737-8868-9c28c2cbafdb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-37.5298058614","y":"-2.9158033357","z":"6.6597637739"}],"uuid":"3ec121c2-1295-28ed-2ca7-22374f35c684","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c43f7153-1df2-38ec-1edd-b3694bed6dc8","time":0.66667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-40.1236819605","y":"-0.9306502051","z":"1.542865843"}],"uuid":"fefea430-0ffa-21c0-738a-21ab44462a5e","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-33.1776348771","y":"-2.2590643664","z":"4.7558127555"}],"uuid":"3245ce43-4453-1563-6426-957785fa545f","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-53.9268912309","y":"-7.3104700825","z":"8.1053141589"}],"uuid":"8d32a863-19e6-66eb-dd63-bc4a4490440e","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.6010831098","y":"0.530086938","z":"2.60850085"}],"uuid":"1a5cc3fb-2736-9e73-929b-2b1f796b4824","time":0.58333,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"6f35223a-32fc-86de-273e-bf2a2dab29d9","name":"left_attack_1","loop":"once","override":false,"length":0.75,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"1.4426116839","y":"-29.9685 + 60 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"-2.8861405929"}],"uuid":"9cea2183-ab9a-e257-3abe-870ecefb3983","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"4.3169042909","y":"30 ","z":"2.8752087286"}],"uuid":"dbc63c99-e467-c1e1-657c-cb6af7445267","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-0.6830957091","y":"30","z":"2.8752087286"}],"uuid":"fd08cba1-1f23-bfc2-9441-476ca36f74cb","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"06b875b6-cb7b-6d84-3602-76e8d8392a4d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"22.5","z":"0"}],"uuid":"f0fba0f9-402e-360e-0a94-d64aeb5c5c6d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366","z":"0.3377321701"}],"uuid":"b50394e2-3528-65dd-22fb-512dbfc2f810","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"b375bdf1-a5b9-5f1e-8257-47941e069d20","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.5","y":"22.5","z":"0"}],"uuid":"6671e902-6e55-2c82-dded-c5df61e041e5","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366","z":"0.3377321701"}],"uuid":"df42619a-31b1-392a-4568-0f18b8533893","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"22.5 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"fc915813-af51-faa4-ecdc-03c64965677b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"22.5","z":"0"}],"uuid":"e225cf07-6e84-70ca-6695-b29a2846ed8b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"15","z":"0"}],"uuid":"c25352f3-66c2-2b02-bd35-d880f2b176f3","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-10","y":"-75 * math.sin(query.anim_time / 0.5 * degrees / 2)","z":"0"}],"uuid":"b43318a9-dcd4-607f-65e1-8b00cc43b3a0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10","y":"-75","z":"0"}],"uuid":"93681b83-7b39-874b-e811-a67b95855b3f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4079498817","y":"-69.6105803251","z":"7.2928402867"}],"uuid":"9a3a0d44-c650-ac83-4813-2a10370f9eb4","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"226.442612794","y":"-29.9685204144","z":"-92.8861411476"}],"uuid":"e93ee5dc-184d-1bc3-117e-457e5f8d35b0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"118.942612794","y":"-29.9685204144","z":"-92.8861411476"}],"uuid":"abee3b46-8686-08a6-5fc2-88f23a08686b","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"91.4812744911","y":"-32.4677070432","z":"-92.9607170541"}],"uuid":"f5310bc1-f902-b35d-17f6-5c492a08e390","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"104.2082921546","y":"-34.614390135","z":"-84.3352155418"}],"uuid":"546f17cc-3f95-55f6-ce39-34d45c4bc65f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"120.1958079089","y":"-58.4299921475","z":"-95.76303279"}],"uuid":"50a71a0f-23a3-58bf-a9ca-dfd506c8fc73","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"cfdee1e2-175a-bfa2-c031-d225ff9b5d21","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-50","y":"0","z":"0"}],"uuid":"74aba8f3-7600-c98b-c42b-8ae7248974c0","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-48.85","y":"0","z":"0"}],"uuid":"982fbd70-9b5f-e211-2106-7bb13df19813","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-77.5","y":"0","z":"0"}],"uuid":"093ab868-a3e5-1727-de2d-9699650ad3b7","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"fcaf8da0-0146-2587-b578-3e1af888deaa":{"name":"pri_right_item","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"47ab1042-3dbe-9fbe-5b2f-2d4e55759eb9","time":0.08333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-32.5","y":"0","z":"0"}],"uuid":"68868a70-d07d-55bc-ea5e-d62b6d58ee36","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-70","y":"0","z":"0"}],"uuid":"26f156a2-f872-2e31-e28b-5cd012a57108","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-72.5","y":"0","z":"0"}],"uuid":"81cb1fac-09fb-254d-c7ef-605c48f3687c","time":0.33333,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.75","y":"0","z":"0"}],"uuid":"7128c27e-62a5-eab2-1edc-b9fdd5a2d1db","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-87.5","y":"0","z":"0"}],"uuid":"09cbf278-b5ef-45bf-9f24-057b421dce72","time":0.41667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-102.5","y":"0","z":"0"}],"uuid":"1cae9cba-9706-843e-87c5-c87e76ba05a8","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-102.5","y":"0","z":"0"}],"uuid":"ea0dd9ba-ef9e-ae70-1647-8ded38c17181","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"630a55a7-df7d-e844-f270-7bc23e9091e0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"a7909b5c-2b7b-30d8-daa2-ce4e1eb0ae89","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"a2809a1d-00c4-c013-9496-dafe98018493","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"6360ce54-fddd-4efd-f7e0-bf14889ca2f5","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"5bafd1e1-dd4f-e647-0ae9-7a670f0d888d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"2b40d9f0-62fd-aaa3-6543-793a1dbb7a30","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"df09c19b-92e5-fda1-3fc0-88b5f94777c3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"7ec93d93-fd31-0af1-95b5-7b2a760ebf0e","time":0.75,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"74458596-1e8e-e629-df60-95abd2e494a9","name":"left_attack_2","loop":"once","override":false,"length":0.75,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-0.6830957091","y":"30 - 45 * math.sin(query.anim_time * degrees)","z":"2.8752087286"}],"uuid":"10958ace-f54f-8523-893a-df0d2fa7ee2b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"1.992544964","y":"-14.8854093306","z":"-7.4750460859"}],"uuid":"6cd93535-eeb9-7d73-b0ac-d4d382bbbb01","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"-29.9409605825","z":"-5.1865092834"}],"uuid":"33442214-ce7c-a7f0-da2c-c32579010ed2","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"-29.9409605825","z":"-5.1865092834"}],"uuid":"893ec734-086c-2dc3-e43c-1881431cc207","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366 - 45 * math.sin(query.anim_time / 0.75 * degrees * 0.5)","z":"0.3377321701"}],"uuid":"feec51b1-b546-e183-06af-f2fd4e999baf","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-30","z":"0.3377321701"}],"uuid":"bbb6334b-d352-6cc7-7708-a92e8267d1a9","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-15","z":"0.3377321701"}],"uuid":"977401eb-1cd9-d20f-ba13-fcc04bb0bbe4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1118499707","y":"-19.9999907433","z":"0.3276399241"}],"uuid":"7c4918c9-c0da-f306-f08f-5065e20e6365","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15.0068082366 - 45 * math.sin(query.anim_time * degrees)","z":"0.3377321701"}],"uuid":"039aa55f-3a2b-951f-687c-39c2028a075b","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-30","z":"0.3377321701"}],"uuid":"743c1219-e220-c341-e567-62633a006f8e","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"-15.0000239764","z":"0.3668883205"}],"uuid":"d78bfc1e-149a-c305-1cab-d11998cd8fa4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1002819443","y":"-20.0000165354","z":"0.3578398578"}],"uuid":"c993c4bb-f2dd-0d3e-2a90-ccdd9a6e28bf","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"2.5","y":"15 - 45 * math.sin(query.anim_time * degrees)","z":"0"}],"uuid":"bc207a6b-8c2e-fae1-615a-58640bfd30a4","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"-30","z":"0"}],"uuid":"99c4936a-ccb2-0076-d4a6-3f602366dd4b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"-15.0126547749","z":"0.6697153441"}],"uuid":"dc834c65-8788-5a0c-5e8e-b17cbc524ed7","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.3040300919","y":"-20.0087274139","z":"0.4618653633"}],"uuid":"2b68f55d-7ce7-d9ac-6140-1e11f21bea83","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.4079498817","y":"110","z":"7.2928402867"}],"uuid":"63e11b48-1acd-31fb-9aec-038ef11318d1","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.2466262711","y":"74.742560924","z":"4.9451721289"}],"uuid":"272befd3-7dac-c524-f191-be00f2b480f7","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-10.6822205364","y":"79.6837812733","z":"1.4255362599"}],"uuid":"665dcf82-4d8a-60f1-a147-3ea0fd090a3b","time":0.625,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.4079498817","y":"-70 + 180 * math.sin(query.anim_time * degrees)","z":"7.2928402867"}],"uuid":"fc76d21e-d81d-fe56-8713-8c4511a33f4c","time":0,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"88.1929174825 + 135 * math.sin(query.anim_time * degrees)","y":"-6.5328079718","z":"-99.7693965866"}],"uuid":"f15b8a26-69eb-3236-f121-089f6c12adac","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"225","y":"-6.5328079718","z":"-99.7693965866"}],"uuid":"c4d1ddb3-58fe-1339-9935-9c017764aa14","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"181.7685110117","y":"-29.2024066267","z":"-94.1691034871"}],"uuid":"52ecc208-4ae8-93fa-fb45-2116be5c1bd7","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"200.7469404305","y":"-14.5514885328","z":"-96.9206545934"}],"uuid":"291f2fd2-9348-7e45-94ec-a65ccfe23fb2","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-87.5 + 90 * math.sin(query.anim_time * degrees)","y":"0","z":"0"}],"uuid":"cbc64268-9d72-314a-bb49-cd0bb865f92d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.5","y":"0","z":"0"}],"uuid":"abe0830d-48f3-34ee-0d56-79a82a40d0fd","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"8475acbb-ec58-046f-cf73-1d5f1bd2f682","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-47.5","y":"0","z":"0"}],"uuid":"47670f80-2a64-0e92-40cf-cf7dba16da3a","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"fcaf8da0-0146-2587-b578-3e1af888deaa":{"name":"pri_right_item","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-122.5","y":"0","z":"0"}],"uuid":"7be4764d-8e4f-962f-b52e-22c0e27e24a6","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-122.5","y":"0","z":"0"}],"uuid":"4cbdb4a2-378e-76db-2887-6c510059585f","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-147.5","y":"0","z":"0"}],"uuid":"23006a51-cd73-e3ba-b213-c537823211c8","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-107.5","y":"0","z":"0"}],"uuid":"d388d056-819e-3f5c-244d-1a7c2779def9","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"ad988529-fbb5-7dee-e2be-6470903df197","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"cd2ae947-1b0d-2cb4-55b3-e73bfe412ae4","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"65109525-d50a-8c57-a1a1-c1f845f31b89","time":0.625,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"81c482f3-e0f4-8689-5723-b978e50693d7","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"852792fa-dba4-c696-8903-76d0cf6cd30a","time":0.16667,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-27.5","y":"0","z":"0"}],"uuid":"e03c676a-178b-f03e-b40d-4404ee7b6eaa","time":0.33333,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"21117162-b28a-dbe7-5b2d-ba2da62704d0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"47.5","y":"0","z":"0"}],"uuid":"e9534f91-cc2b-e3cf-3dda-d16bcac0381b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"59988b0f-f288-1498-fd3f-e5e76fb31afd","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"2b7c6f42-5bc9-0d92-f071-1774547278c1","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"a4e124d9-47e1-c56a-d58f-03a3b368a9a0","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"12.5"}],"uuid":"73269add-beb1-4df4-dd72-7882b3fc38fb","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"12.5"}],"uuid":"3b636b10-cdb7-a40f-b218-8eb1b3aa0008","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"12.5"}],"uuid":"f3b94148-4f92-306c-6646-68077a96e7fc","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"43625c14-44b7-769e-d20c-b7ae66efc0c9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"ad20692b-165d-cf72-0674-86e893570e2e","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"9001b4a2-3e10-4782-15de-b440b94a0265","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"b40d0e65-f781-9910-9f5d-6872fb4aab46","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"eae385ac-0193-0adf-dd33-7896c8c38519","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"1906f341-ffb9-6bcf-a293-56f56d5a7a7d","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"46b1e55d-2185-fe99-fc55-d1de75cdd0cd","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"929e78b1-3d73-f810-5d38-67582cb8f00e","time":0.625,"color":-1,"interpolation":"catmullrom"}]}}},{"uuid":"3868a416-df35-aa7c-72ca-aa2f7000eb3e","name":"left_attack_3","loop":"once","override":false,"length":0.75,"snapping":24,"selected":false,"group_name":"","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"778fa89c-759a-8884-89d9-238c555d2dc1":{"name":"player_root","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.783096475","y":"-30 + 30 * math.sin(query.anim_time * degrees)","z":"-5.1865092834"}],"uuid":"d0abd38c-d0de-ed94-01d0-fe17313caa21","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"0","z":"-5.1865092834"}],"uuid":"5d322262-38d5-191f-9f2e-ba246377ea6b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.783096475","y":"0","z":"-2.6865092834"}],"uuid":"f42022d3-fbfb-e7b9-fb75-d9607fdff0ec","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"9dc65952-10a9-876f-bd47-d6a7e9ec6183":{"name":"phip_hip","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"-15 + 45 * math.sin(query.anim_time * degrees)","z":"0.3377321701"}],"uuid":"a026bb6e-9f3c-f381-1672-4fdb60d5d173","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"30","z":"0.3377321701"}],"uuid":"f49eded7-50f0-328f-f9bc-832f8306614c","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.1088122834","y":"15","z":"0.3377321701"}],"uuid":"132e27b6-de90-f3e8-0f91-a1a0326dd4bb","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"e297aef6-7dfd-f100-2e7c-ab113699b922":{"name":"pw_waist","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"-15 + 45 * math.sin(query.anim_time * degrees)","z":"0.3668883205"}],"uuid":"83b142f3-3039-78b5-5027-82fc96140962","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"30","z":"0.3668883205"}],"uuid":"9b1408ae-bacb-3696-9353-f583fe01691d","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"0.0975584238","y":"15","z":"0.3668883205"}],"uuid":"72b4cbd4-6cd8-ecf3-49e1-ba9bdca95ed5","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"a0c01522-9040-7533-fa11-f6a45d3d96ac":{"name":"pc_chest","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"-15 + 45 * math.sin(query.anim_time * degrees)","z":"0.6697153441"}],"uuid":"080664d7-7ba7-0e3b-eadf-4aeca7d6ab28","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"30","z":"0.6697153441"}],"uuid":"32c655bc-7cb1-4989-8d04-ba6601a20ffa","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"2.2414318612","y":"15","z":"0.6697153441"}],"uuid":"2394fbbe-c4be-f4a9-7147-6f775caa2b57","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"34097e46-c233-c03c-d8b9-aee154c9946f":{"name":"h_ph_head","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-7.2466262711","y":"74.742560924","z":"4.9451721289"}],"uuid":"0ca4d269-75b1-053b-2e29-97ed95ee8a27","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.25","y":"-90.26","z":"4.95"}],"uuid":"9d554f75-1c6d-a52e-231c-5b3fccb718f0","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.25","y":"-45.26","z":"4.95"}],"uuid":"1dd76bcb-6748-32e6-f1c0-af82dbaaf6ef","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-25.2303398612","y":"-66.3113221393","z":"25.0049672424"}],"uuid":"f6e86818-45b8-d124-5048-235d4438e1f2","time":0.25,"color":-1,"interpolation":"catmullrom"}]},"bfc2f156-b48b-dd08-1b9e-777d8ada16b2":{"name":"pra_right_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-67.7996254024","y":"-18.7498221312","z":"46.5234420106"}],"uuid":"0475513c-2096-eade-6e28-e9543b1326dc","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-64.1068768939","y":"-7.3820305827","z":"59.216259349"}],"uuid":"c9030e72-7c34-0050-fac4-653b7eaf67a5","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-39.6615555673","y":"10.04018335","z":"57.8138546454"}],"uuid":"8ec4a464-cce1-1660-9e7e-aed5dc55ef21","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.1615555673","y":"10.04018335","z":"65.3138546454"}],"uuid":"631fb268-9cc4-cad3-c10f-1b50204f5032","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-7.1615555673","y":"10.04018335","z":"87.8138546454"}],"uuid":"7bd8dd80-fa0e-a6db-44f9-3641c7a860a6","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"20.928317261","y":"43.8968318011","z":"33.8368278453"}],"uuid":"9ae0c3e6-af01-5eff-89d3-e344ae80d232","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"cf1618da-24d8-aab8-eebc-128815c02d35":{"name":"prfa_right_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"17.7412720053","y":"-29.1799804704","z":"-21.7474034651"}],"uuid":"6a40116a-b813-1b1d-0348-7c2e57651f0e","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-2.3747088224","y":"-26.0561020798","z":"-53.9720884106"}],"uuid":"60015b32-254c-b385-5730-f1fb2dca341d","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"-42.5"}],"uuid":"4ad84a41-5d07-01a0-8466-4e63692e80ab","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"25"}],"uuid":"152728f0-c892-e72c-83ce-c307706a04aa","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5","y":"0","z":"42.5"}],"uuid":"6a9ba9d9-8596-88fd-cbd3-56574d16d159","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"5.379429148","y":"-0.4004441987","z":"30.0108045086"}],"uuid":"2443bfb5-32c5-7ceb-7748-f884f86bff3f","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"fcaf8da0-0146-2587-b578-3e1af888deaa":{"name":"pri_right_item","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-13.5768860212","y":"23.1473092203","z":"-27.013532475"}],"uuid":"c6821d4f-794f-c7c7-0f89-80033761c353","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-4.6994195496","y":"13.8964693362","z":"-30.1451170148"}],"uuid":"275ad9e7-67ec-4daf-b4c5-c49ff92c0c7c","time":0.125,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"27.697928082","y":"-29.8984523542","z":"-46.3767048046"}],"uuid":"2908fdad-6769-ad97-152b-f2e86f8fba88","time":0.25,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"81.4976285226","y":"-85.6361454504","z":"-145.5405300408"}],"uuid":"e38f1288-48fd-30ea-940e-a1713aa5e533","time":0.375,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"111.4976285226","y":"-85.6361454504","z":"-145.5405300408"}],"uuid":"0bb1777f-8437-9e14-420b-ad2af484cf2e","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"66.4976285226","y":"-85.6361454504","z":"-145.5405300408"}],"uuid":"f77bbe2b-eb20-5dca-9e04-984240155daa","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"218.1984733219","y":"-82.2634224785","z":"-252.6403714152"}],"uuid":"282590c6-2b2c-80b1-d27d-1cf6d8b8d539","time":0.625,"color":-1,"interpolation":"catmullrom"}]},"b3135254-0351-3462-2479-e6a3286c89ff":{"name":"pla_left_arm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"241ef05d-3297-bd29-63f7-9ac98d2fab11","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"762de5c9-6726-ade1-3357-92c739fda0b1","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-57.5","y":"0","z":"0"}],"uuid":"ab041c20-61fb-26af-0653-35423dce4973","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"1a9070b5-b8b6-b955-9f31-54f9625f8f3d":{"name":"plfa_left_forearm","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"6f854563-d459-37eb-82ce-ce7b114058d3","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"35","y":"0","z":"0"}],"uuid":"0751f55e-78d5-7ea0-ae92-a3f56f61570d","time":0.75,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"67.5","y":"0","z":"0"}],"uuid":"9e37eb37-bc4a-ce3e-c99e-d247cc12acf4","time":0.5,"color":-1,"interpolation":"catmullrom"}]},"7e8426f1-08b2-81a2-7703-cb76ff5e7003":{"name":"prl_right_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"12.5"}],"uuid":"dfdbf96d-60fd-d00b-d057-7a0fddebd2bf","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.6809719992","y":"2.3896261399","z":"5.3762737066"}],"uuid":"d8c7c5f2-d9bb-8be8-4395-282a02893319","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"26.6809719992","y":"2.3896261399","z":"5.3762737066"}],"uuid":"1f138668-b13a-8c4c-6d27-ae7c168a168b","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"c6d9e946-1d10-482d-14b1-0766027adba8":{"name":"prfl_right_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"522f3da4-0892-8397-935d-de98f65aa7ad","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.9759091971","y":"-0.569987466","z":"-0.8300310517"}],"uuid":"484dd03c-d5cd-3c01-f305-7b0a3e0b4524","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-24.9759091971","y":"-0.569987466","z":"-0.8300310517"}],"uuid":"7222f20e-0201-a3b1-7e99-6902147d0697","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"5ef5d225-d5ae-6787-8838-b75ccb7a7a81":{"name":"pll_left_leg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"20","y":"0","z":"0"}],"uuid":"9a8681c9-5339-28f0-edfd-2b8d0c193484","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.2171371372","y":"-0.2383668654","z":"6.7491823315"}],"uuid":"1e3c6439-78ef-20ec-5850-64f6d2e9b77b","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-28.2171371372","y":"-0.2383668654","z":"6.7491823315"}],"uuid":"55cf2e15-14ac-bb80-69de-3db8dff8ff47","time":0.75,"color":-1,"interpolation":"catmullrom"}]},"1b5cc202-c09e-faa0-5057-eb4ae60bf336":{"name":"plfl_left_foreleg","type":"bone","rotation_global":false,"quaternion_interpolation":false,"keyframes":[{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"b91601e4-1df3-23af-4d55-d2a8849a97b9","time":0,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"e7b6f9bd-8abf-e22f-a9c9-9bc55e7aa644","time":0.5,"color":-1,"interpolation":"catmullrom"},{"channel":"rotation","data_points":[{"x":"-20","y":"0","z":"0"}],"uuid":"bb218f38-7f01-5744-470c-7767bc629b9e","time":0.75,"color":-1,"interpolation":"catmullrom"}]}}}],"animation_variable_placeholders":"degrees=180"} ================================================ FILE: test-plugin/src/main/resources/knight_line.json ================================================ { "format_version": "1.21.6", "credit": "Made with Blockbench", "textures": { "0": "bettermodel:item/knight_line", "particle": "bettermodel:item/knight_line" }, "elements": [ { "from": [3, 8.5, -8], "to": [8, 8.5, 8], "rotation": {"angle": 0, "axis": "y", "origin": [-0.5, 0, 0]}, "faces": { "north": {"uv": [0, 0, 2.5, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 2.5, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [2.5, 8, 0, 0], "texture": "#0"}, "down": {"uv": [2.5, 0, 0, 8], "texture": "#0"} } }, { "from": [4.725, 7.975, -8], "to": [6.725, 7.975, 8], "rotation": {"angle": 0, "axis": "y", "origin": [-1.775, 16.475, 0]}, "faces": { "north": {"uv": [0, 0, 1, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 1, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [2.5, 16, 1.5, 8], "texture": "#0"}, "down": {"uv": [3.5, 8, 2.5, 16], "texture": "#0"} } }, { "from": [5.5, 8.5, -8], "to": [8, 8.5, 8], "rotation": {"angle": -22.5, "axis": "z", "origin": [8, 8.5, 0]}, "faces": { "north": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [6.5, 8, 5, 0], "texture": "#0"}, "down": {"uv": [8, 0, 6.5, 8], "texture": "#0"} } }, { "from": [5.5, 8.5, -8], "to": [8, 8.5, 8], "rotation": {"angle": 22.5, "axis": "z", "origin": [8, 8.5, 0]}, "faces": { "north": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "east": {"uv": [0, 0, 8, 0], "texture": "#0"}, "south": {"uv": [0, 0, 1.5, 0], "texture": "#0"}, "west": {"uv": [0, 0, 8, 0], "texture": "#0"}, "up": {"uv": [8, 8, 6.5, 0], "texture": "#0"}, "down": {"uv": [6.5, 0, 5, 8], "texture": "#0"} } }, { "from": [7.75, 6.5, 0.5], "to": [7.75, 7.5, 2.5], "rotation": {"angle": 0, "axis": "y", "origin": [7.75, 6.5, -0.5]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } }, { "from": [7, 6.75, -5.5], "to": [7, 7.75, -3.5], "rotation": {"angle": 0, "axis": "y", "origin": [7, 6.75, -6.5]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } }, { "from": [4.5, 9.5, -3.5], "to": [4.5, 10.5, -1.5], "rotation": {"angle": 0, "axis": "y", "origin": [4.5, 9.5, -3.5]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } }, { "from": [4.5, 9, 2.75], "to": [4.5, 10, 4.75], "rotation": {"angle": 0, "axis": "y", "origin": [4.5, 9, 2.75]}, "faces": { "east": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"}, "west": {"uv": [5.5, 8, 6.5, 9], "texture": "#0"} } } ], "groups": [ { "name": "group", "origin": [0, 0, 0], "color": 0, "children": [0, 1, 2, 3, 4, 5, 6, 7] } ] } ================================================ FILE: test-plugin/src/main/resources/knight_sword.json ================================================ { "textures": { "0": "bettermodel:item/knight_sword", "particle": "bettermodel:item/knight_sword" }, "elements": [ { "from": [7.15, -4, 7.25], "to": [8.85, -2.5, 8.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -8.5, 7]}, "faces": { "north": {"uv": [6.75, 5.5, 7.25, 6], "texture": "#0"}, "east": {"uv": [6.75, 6, 7.25, 6.5], "texture": "#0"}, "south": {"uv": [6.75, 6.5, 7.25, 7], "texture": "#0"}, "west": {"uv": [0, 7, 0.5, 7.5], "texture": "#0"}, "up": {"uv": [7.5, 0.5, 7, 0], "texture": "#0"}, "down": {"uv": [7.5, 2.25, 7, 2.75], "texture": "#0"} } }, { "from": [7.2, 0, 8.25], "to": [8.8, 2, 11.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [7, 2.75, 7.5, 3.25], "texture": "#0"}, "east": {"uv": [4.25, 5.25, 5.25, 5.75], "texture": "#0"}, "south": {"uv": [4.75, 7, 5.25, 7.5], "texture": "#0"}, "west": {"uv": [5.25, 5.25, 6.25, 5.75], "texture": "#0"}, "up": {"uv": [6, 1, 5.5, 0], "texture": "#0"}, "down": {"uv": [6, 1, 5.5, 2], "texture": "#0"} } }, { "from": [6.75, -0.5, 6.75], "to": [9.25, 2, 9.25], "rotation": {"angle": -45, "axis": "x", "origin": [8, 0.75, 8]}, "faces": { "north": {"uv": [4.25, 4.5, 5, 5.25], "texture": "#0"}, "east": {"uv": [4.75, 0, 5.5, 0.75], "texture": "#0"}, "south": {"uv": [4.75, 0.75, 5.5, 1.5], "texture": "#0"}, "west": {"uv": [4.75, 1.5, 5.5, 2.25], "texture": "#0"}, "up": {"uv": [5.5, 3, 4.75, 2.25], "texture": "#0"}, "down": {"uv": [5.5, 3, 4.75, 3.75], "texture": "#0"} } }, { "from": [7.225, 0, 8.375], "to": [8.775, 1.5, 10.5], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 0.5, 10.625]}, "faces": { "north": {"uv": [5.25, 7, 5.75, 7.5], "texture": "#0"}, "east": {"uv": [5.75, 7, 6.25, 7.5], "texture": "#0"}, "south": {"uv": [6.25, 7, 6.75, 7.5], "texture": "#0"}, "west": {"uv": [6.75, 7, 7.25, 7.5], "texture": "#0"}, "up": {"uv": [7.75, 1, 7.25, 0.5], "texture": "#0"}, "down": {"uv": [7.75, 1, 7.25, 1.5], "texture": "#0"} } }, { "from": [7.225, 0, 5.5], "to": [8.775, 1.5, 7.625], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 0.5, 5.375]}, "faces": { "north": {"uv": [7.25, 1.5, 7.75, 2], "texture": "#0"}, "east": {"uv": [7.25, 3.25, 7.75, 3.75], "texture": "#0"}, "south": {"uv": [7.25, 3.75, 7.75, 4.25], "texture": "#0"}, "west": {"uv": [7.25, 4.25, 7.75, 4.75], "texture": "#0"}, "up": {"uv": [7.75, 5.25, 7.25, 4.75], "texture": "#0"}, "down": {"uv": [7.75, 5.25, 7.25, 5.75], "texture": "#0"} } }, { "from": [7.15, -1.5, 7.25], "to": [8.85, 0, 8.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [7.25, 5.75, 7.75, 6.25], "texture": "#0"}, "east": {"uv": [7.25, 6.25, 7.75, 6.75], "texture": "#0"}, "south": {"uv": [7.25, 6.75, 7.75, 7.25], "texture": "#0"}, "west": {"uv": [7.25, 7.25, 7.75, 7.75], "texture": "#0"}, "up": {"uv": [0.5, 8, 0, 7.5], "texture": "#0"}, "down": {"uv": [8, 0, 7.5, 0.5], "texture": "#0"} } }, { "from": [7.4, -8.95, 7.4], "to": [8.6, -8.25, 8.6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -8.6, 8]}, "faces": { "north": {"uv": [7, 0.5, 7.25, 0.75], "texture": "#0"}, "east": {"uv": [7.25, 2, 7.5, 2.25], "texture": "#0"}, "south": {"uv": [7.5, 3, 7.75, 3.25], "texture": "#0"}, "west": {"uv": [6.75, 8.75, 7, 9], "texture": "#0"}, "up": {"uv": [9, 7.25, 8.75, 7], "texture": "#0"}, "down": {"uv": [7.5, 8.75, 7.25, 9], "texture": "#0"} } }, { "from": [7.175, 0.925, 10.375], "to": [8.825, 2.675, 11.6], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 1.925, 10.3]}, "faces": { "north": {"uv": [1.25, 7.5, 1.75, 8], "texture": "#0"}, "east": {"uv": [5.5, 4, 5.75, 4.5], "texture": "#0"}, "south": {"uv": [7.5, 2, 8, 2.5], "texture": "#0"}, "west": {"uv": [6, 3.5, 6.25, 4], "texture": "#0"}, "up": {"uv": [6.25, 5.25, 5.75, 5], "texture": "#0"}, "down": {"uv": [8.5, 2.75, 8, 3], "texture": "#0"} } }, { "from": [7.175, 0.925, 4.4], "to": [8.825, 2.675, 5.625], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 1.925, 5.7]}, "faces": { "north": {"uv": [7.5, 2.5, 8, 3], "texture": "#0"}, "east": {"uv": [8.25, 2.25, 8.5, 2.75], "texture": "#0"}, "south": {"uv": [4.25, 7.5, 4.75, 8], "texture": "#0"}, "west": {"uv": [8.25, 3, 8.5, 3.5], "texture": "#0"}, "up": {"uv": [8.75, 1, 8.25, 0.75], "texture": "#0"}, "down": {"uv": [8.75, 3.5, 8.25, 3.75], "texture": "#0"} } }, { "from": [7.25, -12.75, 7.5], "to": [8.75, -11.5, 8.75], "rotation": {"angle": 45, "axis": "x", "origin": [8, -12, 8]}, "faces": { "north": {"uv": [8.25, 7.75, 8.75, 8], "texture": "#0"}, "east": {"uv": [8.5, 8.75, 8.75, 9], "texture": "#0"}, "south": {"uv": [8.25, 8, 8.75, 8.25], "texture": "#0"}, "west": {"uv": [8.75, 8.5, 9, 8.75], "texture": "#0"}, "up": {"uv": [8.75, 8.5, 8.25, 8.25], "texture": "#0"}, "down": {"uv": [9, 0, 8.5, 0.25], "texture": "#0"} } }, { "from": [7, -5.25, 7.1], "to": [9, -4, 8.9], "rotation": {"angle": 0, "axis": "y", "origin": [7, -8.75, 7]}, "faces": { "north": {"uv": [8.5, 0.25, 9, 0.5], "texture": "#0"}, "east": {"uv": [8.5, 0.5, 9, 0.75], "texture": "#0"}, "south": {"uv": [8.5, 1, 9, 1.25], "texture": "#0"}, "west": {"uv": [1.25, 8.5, 1.75, 8.75], "texture": "#0"}, "up": {"uv": [5.75, 8, 5.25, 7.5], "texture": "#0"}, "down": {"uv": [6.25, 7.5, 5.75, 8], "texture": "#0"} } }, { "from": [7.025, -4.95, 8.725], "to": [8.975, -3.8, 9.2], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, -4.325, 8.3]}, "faces": { "north": {"uv": [8.75, 0.75, 9.25, 1], "texture": "#0"}, "east": {"uv": [4.5, 9.5, 4.75, 9.75], "texture": "#0"}, "south": {"uv": [1, 8.75, 1.5, 9], "texture": "#0"}, "west": {"uv": [9.5, 4.5, 9.75, 4.75], "texture": "#0"}, "up": {"uv": [2, 9, 1.5, 8.75], "texture": "#0"}, "down": {"uv": [3.75, 8.75, 3.25, 9], "texture": "#0"} } }, { "from": [7.025, -4.95, 6.8], "to": [8.975, -3.8, 7.275], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -4.325, 7.7]}, "faces": { "north": {"uv": [8.75, 3.5, 9.25, 3.75], "texture": "#0"}, "east": {"uv": [4.75, 9.5, 5, 9.75], "texture": "#0"}, "south": {"uv": [3.75, 8.75, 4.25, 9], "texture": "#0"}, "west": {"uv": [9.5, 4.75, 9.75, 5], "texture": "#0"}, "up": {"uv": [4.75, 9, 4.25, 8.75], "texture": "#0"}, "down": {"uv": [5.25, 8.75, 4.75, 9], "texture": "#0"} } }, { "from": [7.05, -5.975, 7.1], "to": [8.95, -4.7, 8.375], "rotation": {"angle": 45, "axis": "x", "origin": [8, -5.6, 8]}, "faces": { "north": {"uv": [8.5, 7.5, 9, 7.75], "texture": "#0"}, "east": {"uv": [4.25, 9.5, 4.5, 9.75], "texture": "#0"}, "south": {"uv": [7.75, 8.5, 8.25, 8.75], "texture": "#0"}, "west": {"uv": [9.5, 4.25, 9.75, 4.5], "texture": "#0"}, "up": {"uv": [8.75, 8.75, 8.25, 8.5], "texture": "#0"}, "down": {"uv": [1, 8.75, 0.5, 9], "texture": "#0"} } }, { "from": [7.4, -9.95, 7.4], "to": [8.6, -9.25, 8.6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -9.6, 8]}, "faces": { "north": {"uv": [5.5, 9.5, 5.75, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 5.5, 9.75, 5.75], "texture": "#0"}, "south": {"uv": [5.75, 9.5, 6, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 5.75, 9.75, 6], "texture": "#0"}, "up": {"uv": [6.25, 9.75, 6, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 6, 9.5, 6.25], "texture": "#0"} } }, { "from": [7.4, -10.95, 7.4], "to": [8.6, -10.25, 8.6], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, -10.6, 8]}, "faces": { "north": {"uv": [8.75, 8.75, 9, 9], "texture": "#0"}, "east": {"uv": [0, 9, 0.25, 9.25], "texture": "#0"}, "south": {"uv": [9, 0, 9.25, 0.25], "texture": "#0"}, "west": {"uv": [0.25, 9, 0.5, 9.25], "texture": "#0"}, "up": {"uv": [9.25, 0.5, 9, 0.25], "texture": "#0"}, "down": {"uv": [0.75, 9, 0.5, 9.25], "texture": "#0"} } }, { "from": [7.5, -12, 7.5], "to": [8.5, -5.25, 8.5], "rotation": {"angle": 0, "axis": "y", "origin": [7, -8.5, 7]}, "faces": { "north": {"uv": [0.5, 6, 0.75, 7.75], "texture": "#0"}, "east": {"uv": [0.75, 6, 1, 7.75], "texture": "#0"}, "south": {"uv": [1, 6, 1.25, 7.75], "texture": "#0"}, "west": {"uv": [6, 1, 6.25, 2.75], "texture": "#0"}, "up": {"uv": [9.25, 0.75, 9, 0.5], "texture": "#0"}, "down": {"uv": [1, 9, 0.75, 9.25], "texture": "#0"} } }, { "from": [8.3, -4.3, 8.925], "to": [8.75, 0.15, 9.675], "rotation": {"angle": 22.5, "axis": "x", "origin": [8.55, -1.925, 9.425]}, "faces": { "north": {"uv": [6.25, 7.5, 6.5, 8.5], "texture": "#0"}, "east": {"uv": [6.5, 7.5, 6.75, 8.5], "texture": "#0"}, "south": {"uv": [6.75, 7.5, 7, 8.5], "texture": "#0"}, "west": {"uv": [7, 7.5, 7.25, 8.5], "texture": "#0"}, "up": {"uv": [1.25, 9.25, 1, 9], "texture": "#0"}, "down": {"uv": [9.25, 1, 9, 1.25], "texture": "#0"} } }, { "from": [7.25, -4.3, 8.925], "to": [7.7, 0.15, 9.675], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.45, -1.925, 9.425]}, "faces": { "north": {"uv": [0.5, 7.75, 0.75, 8.75], "texture": "#0"}, "east": {"uv": [7.75, 0.5, 8, 1.5], "texture": "#0"}, "south": {"uv": [0.75, 7.75, 1, 8.75], "texture": "#0"}, "west": {"uv": [1, 7.75, 1.25, 8.75], "texture": "#0"}, "up": {"uv": [1.5, 9.25, 1.25, 9], "texture": "#0"}, "down": {"uv": [9.25, 1.25, 9, 1.5], "texture": "#0"} } }, { "from": [6.95, -0.425, 2.575], "to": [9.05, 2.45, 4.25], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 1.925, 3.55]}, "faces": { "north": {"uv": [5.25, 6.25, 5.75, 7], "texture": "#0"}, "east": {"uv": [5.75, 6.25, 6.25, 7], "texture": "#0"}, "south": {"uv": [6.25, 6.25, 6.75, 7], "texture": "#0"}, "west": {"uv": [6.5, 0, 7, 0.75], "texture": "#0"}, "up": {"uv": [8.25, 2, 7.75, 1.5], "texture": "#0"}, "down": {"uv": [8.25, 3, 7.75, 3.5], "texture": "#0"} } }, { "from": [7.2, 0, 4.25], "to": [8.8, 2, 7.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [7.75, 3.5, 8.25, 4], "texture": "#0"}, "east": {"uv": [4.75, 5.75, 5.75, 6.25], "texture": "#0"}, "south": {"uv": [7.75, 4, 8.25, 4.5], "texture": "#0"}, "west": {"uv": [5.75, 5.75, 6.75, 6.25], "texture": "#0"}, "up": {"uv": [0.5, 7, 0, 6], "texture": "#0"}, "down": {"uv": [6.5, 0, 6, 1], "texture": "#0"} } }, { "from": [7.2, -0.925, 13.2], "to": [8.8, 1.675, 14.3], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 9.75]}, "faces": { "north": {"uv": [6.5, 2.5, 7, 3.25], "texture": "#0"}, "east": {"uv": [8, 7, 8.25, 7.75], "texture": "#0"}, "south": {"uv": [6.75, 0.75, 7.25, 1.5], "texture": "#0"}, "west": {"uv": [8, 7.75, 8.25, 8.5], "texture": "#0"}, "up": {"uv": [9, 3.5, 8.5, 3.25], "texture": "#0"}, "down": {"uv": [9, 3.75, 8.5, 4], "texture": "#0"} } }, { "from": [7.45, -0.575, 13.875], "to": [8.55, 1.325, 14.65], "rotation": {"angle": -22.5, "axis": "x", "origin": [8, 0.475, 14.2]}, "faces": { "north": {"uv": [5.25, 8.75, 5.5, 9.25], "texture": "#0"}, "east": {"uv": [8.75, 5.25, 9, 5.75], "texture": "#0"}, "south": {"uv": [5.5, 8.75, 5.75, 9.25], "texture": "#0"}, "west": {"uv": [5.75, 8.75, 6, 9.25], "texture": "#0"}, "up": {"uv": [5.25, 9.75, 5, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 5, 9.5, 5.25], "texture": "#0"} } }, { "from": [7.45, -0.575, 1.35], "to": [8.55, 1.325, 2.125], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 0.475, 1.8]}, "faces": { "north": {"uv": [8.75, 5.75, 9, 6.25], "texture": "#0"}, "east": {"uv": [6, 8.75, 6.25, 9.25], "texture": "#0"}, "south": {"uv": [8.75, 6.25, 9, 6.75], "texture": "#0"}, "west": {"uv": [6.5, 8.75, 6.75, 9.25], "texture": "#0"}, "up": {"uv": [5.5, 9.75, 5.25, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 5.25, 9.5, 5.5], "texture": "#0"} } }, { "from": [6.95, -0.425, 11.75], "to": [9.05, 2.45, 13.425], "rotation": {"angle": 22.5, "axis": "x", "origin": [8, 1.925, 12.45]}, "faces": { "north": {"uv": [1.25, 6.75, 1.75, 7.5], "texture": "#0"}, "east": {"uv": [6.75, 1.5, 7.25, 2.25], "texture": "#0"}, "south": {"uv": [6.75, 3.25, 7.25, 4], "texture": "#0"}, "west": {"uv": [6.75, 4, 7.25, 4.75], "texture": "#0"}, "up": {"uv": [8.25, 5, 7.75, 4.5], "texture": "#0"}, "down": {"uv": [8.25, 5, 7.75, 5.5], "texture": "#0"} } }, { "from": [7.45, -0.275, 12.475], "to": [8.55, 1.2, 13.2], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 9.75]}, "faces": { "north": {"uv": [2, 9, 2.25, 9.25], "texture": "#0"}, "east": {"uv": [9, 2, 9.25, 2.25], "texture": "#0"}, "south": {"uv": [2.25, 9, 2.5, 9.25], "texture": "#0"}, "west": {"uv": [9, 2.25, 9.25, 2.5], "texture": "#0"}, "up": {"uv": [2.75, 9.25, 2.5, 9], "texture": "#0"}, "down": {"uv": [9.25, 2.5, 9, 2.75], "texture": "#0"} } }, { "from": [7.2, -0.925, 1.7], "to": [8.8, 1.675, 2.8], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 6.25]}, "faces": { "north": {"uv": [4.25, 6.75, 4.75, 7.5], "texture": "#0"}, "east": {"uv": [8.25, 0, 8.5, 0.75], "texture": "#0"}, "south": {"uv": [6.75, 4.75, 7.25, 5.5], "texture": "#0"}, "west": {"uv": [8.25, 1.5, 8.5, 2.25], "texture": "#0"}, "up": {"uv": [9, 4.25, 8.5, 4], "texture": "#0"}, "down": {"uv": [9, 4.25, 8.5, 4.5], "texture": "#0"} } }, { "from": [7.45, -0.275, 2.8], "to": [8.55, 1.2, 3.525], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6.25, 6.25]}, "faces": { "north": {"uv": [2.75, 9, 3, 9.25], "texture": "#0"}, "east": {"uv": [9, 2.75, 9.25, 3], "texture": "#0"}, "south": {"uv": [3, 9, 3.25, 9.25], "texture": "#0"}, "west": {"uv": [9, 3, 9.25, 3.25], "texture": "#0"}, "up": {"uv": [3.5, 9.25, 3.25, 9], "texture": "#0"}, "down": {"uv": [9.25, 3.25, 9, 3.5], "texture": "#0"} } }, { "from": [7.25, -4.3, 6.325], "to": [7.7, 0.15, 7.075], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.45, -1.925, 6.575]}, "faces": { "north": {"uv": [7.75, 5.5, 8, 6.5], "texture": "#0"}, "east": {"uv": [7.75, 6.5, 8, 7.5], "texture": "#0"}, "south": {"uv": [7.25, 7.75, 7.5, 8.75], "texture": "#0"}, "west": {"uv": [7.5, 7.75, 7.75, 8.75], "texture": "#0"}, "up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#0"}, "down": {"uv": [4, 9, 3.75, 9.25], "texture": "#0"} } }, { "from": [8.3, -4.3, 6.325], "to": [8.75, 0.15, 7.075], "rotation": {"angle": -22.5, "axis": "x", "origin": [8.55, -1.925, 6.575]}, "faces": { "north": {"uv": [7.75, 7.5, 8, 8.5], "texture": "#0"}, "east": {"uv": [0, 8, 0.25, 9], "texture": "#0"}, "south": {"uv": [8, 0, 8.25, 1], "texture": "#0"}, "west": {"uv": [0.25, 8, 0.5, 9], "texture": "#0"}, "up": {"uv": [9.25, 4, 9, 3.75], "texture": "#0"}, "down": {"uv": [4.25, 9, 4, 9.25], "texture": "#0"} } }, { "from": [7, -2.5, 6.95], "to": [9, -1.5, 9.05], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [8.5, 4.5, 9, 4.75], "texture": "#0"}, "east": {"uv": [4.75, 8.5, 5.25, 8.75], "texture": "#0"}, "south": {"uv": [8.5, 4.75, 9, 5], "texture": "#0"}, "west": {"uv": [8.5, 5, 9, 5.25], "texture": "#0"}, "up": {"uv": [8.5, 1.5, 8, 1], "texture": "#0"}, "down": {"uv": [1.75, 8, 1.25, 8.5], "texture": "#0"} } }, { "from": [7.75, -2.25, 9], "to": [8.25, -1.75, 9.7], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [9, 4, 9.25, 4.25], "texture": "#0"}, "east": {"uv": [4.25, 9, 4.5, 9.25], "texture": "#0"}, "south": {"uv": [9, 4.25, 9.25, 4.5], "texture": "#0"}, "west": {"uv": [4.5, 9, 4.75, 9.25], "texture": "#0"}, "up": {"uv": [9.25, 4.75, 9, 4.5], "texture": "#0"}, "down": {"uv": [5, 9, 4.75, 9.25], "texture": "#0"} } }, { "from": [7.5, -2.25, 9.55], "to": [8.5, -1.75, 10.05], "rotation": {"angle": -45, "axis": "x", "origin": [8, -2, 9.8]}, "faces": { "north": {"uv": [9, 4.75, 9.25, 5], "texture": "#0"}, "east": {"uv": [5, 9, 5.25, 9.25], "texture": "#0"}, "south": {"uv": [9, 5, 9.25, 5.25], "texture": "#0"}, "west": {"uv": [9, 5.25, 9.25, 5.5], "texture": "#0"}, "up": {"uv": [9.25, 5.75, 9, 5.5], "texture": "#0"}, "down": {"uv": [9.25, 5.75, 9, 6], "texture": "#0"} } }, { "from": [7.75, -2.25, 6.3], "to": [8.25, -1.75, 7], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 9]}, "faces": { "north": {"uv": [9, 6, 9.25, 6.25], "texture": "#0"}, "east": {"uv": [6.25, 9, 6.5, 9.25], "texture": "#0"}, "south": {"uv": [9, 6.25, 9.25, 6.5], "texture": "#0"}, "west": {"uv": [9, 6.5, 9.25, 6.75], "texture": "#0"}, "up": {"uv": [7, 9.25, 6.75, 9], "texture": "#0"}, "down": {"uv": [9.25, 6.75, 9, 7], "texture": "#0"} } }, { "from": [7.5, -2.25, 5.95], "to": [8.5, -1.75, 6.45], "rotation": {"angle": 45, "axis": "x", "origin": [8, -2, 6.2]}, "faces": { "north": {"uv": [7, 9, 7.25, 9.25], "texture": "#0"}, "east": {"uv": [9, 7, 9.25, 7.25], "texture": "#0"}, "south": {"uv": [7.25, 9, 7.5, 9.25], "texture": "#0"}, "west": {"uv": [9, 7.25, 9.25, 7.5], "texture": "#0"}, "up": {"uv": [7.75, 9.25, 7.5, 9], "texture": "#0"}, "down": {"uv": [9.25, 7.5, 9, 7.75], "texture": "#0"} } }, { "from": [7.175, 2.3, 4.275], "to": [8.825, 2.95, 4.5], "rotation": {"angle": -45, "axis": "x", "origin": [8, 2.2, 4.575]}, "faces": { "north": {"uv": [8.5, 1.25, 9, 1.5], "texture": "#0"}, "east": {"uv": [1.5, 9, 1.75, 9.25], "texture": "#0"}, "south": {"uv": [8.5, 1.5, 9, 1.75], "texture": "#0"}, "west": {"uv": [9, 1.5, 9.25, 1.75], "texture": "#0"}, "up": {"uv": [9, 2, 8.5, 1.75], "texture": "#0"}, "down": {"uv": [9, 2, 8.5, 2.25], "texture": "#0"} } }, { "from": [7.175, 2.3, 11.5], "to": [8.825, 2.95, 11.725], "rotation": {"angle": 45, "axis": "x", "origin": [8, 2.2, 11.425]}, "faces": { "north": {"uv": [8.5, 2.25, 9, 2.5], "texture": "#0"}, "east": {"uv": [1.75, 9, 2, 9.25], "texture": "#0"}, "south": {"uv": [8.5, 2.5, 9, 2.75], "texture": "#0"}, "west": {"uv": [9, 1.75, 9.25, 2], "texture": "#0"}, "up": {"uv": [9, 3, 8.5, 2.75], "texture": "#0"}, "down": {"uv": [9, 3, 8.5, 3.25], "texture": "#0"} } }, { "from": [7.5, 22.05, 6.7], "to": [8.5, 25.4, 8.75], "rotation": {"angle": -20, "axis": "x", "origin": [8, 29.8, 7.45]}, "faces": { "north": {"uv": [4.75, 7.5, 5, 8.5], "texture": "#0"}, "east": {"uv": [5.75, 4, 6.25, 5], "texture": "#0"}, "south": {"uv": [5, 7.5, 5.25, 8.5], "texture": "#0"}, "west": {"uv": [4.25, 5.75, 4.75, 6.75], "texture": "#0"}, "up": {"uv": [8.5, 5.25, 8.25, 4.75], "texture": "#0"}, "down": {"uv": [8.5, 5.25, 8.25, 5.75], "texture": "#0"} } }, { "from": [7.5, 5, 9], "to": [8.5, 22.75, 10.25], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [2, 0, 2.25, 4.5], "texture": "#0"}, "east": {"uv": [2.25, 0, 2.5, 4.5], "texture": "#0"}, "south": {"uv": [2.5, 0, 2.75, 4.5], "texture": "#0"}, "west": {"uv": [2.75, 0, 3, 4.5], "texture": "#0"}, "up": {"uv": [6.5, 2.75, 6.25, 2.5], "texture": "#0"}, "down": {"uv": [6.75, 0.75, 6.5, 1], "texture": "#0"} } }, { "from": [7.8, 2, 10.45], "to": [8.5, 5, 11.15], "rotation": {"angle": 45, "axis": "y", "origin": [8, 9.5, 10.95]}, "faces": { "north": {"uv": [3.25, 8, 3.5, 8.75], "texture": "#0"}, "east": {"uv": [3.5, 8, 3.75, 8.75], "texture": "#0"}, "south": {"uv": [3.75, 8, 4, 8.75], "texture": "#0"}, "west": {"uv": [4, 8, 4.25, 8.75], "texture": "#0"}, "up": {"uv": [9, 7.5, 8.75, 7.25], "texture": "#0"}, "down": {"uv": [7.75, 8.75, 7.5, 9], "texture": "#0"} } }, { "from": [7.8, 5, 9.95], "to": [8.5, 22.75, 10.65], "rotation": {"angle": 45, "axis": "y", "origin": [8, 12.5, 10.45]}, "faces": { "north": {"uv": [2.25, 4.5, 2.5, 9], "texture": "#0"}, "east": {"uv": [2.5, 4.5, 2.75, 9], "texture": "#0"}, "south": {"uv": [2.75, 4.5, 3, 9], "texture": "#0"}, "west": {"uv": [3, 4.5, 3.25, 9], "texture": "#0"}, "up": {"uv": [8.5, 9, 8.25, 8.75], "texture": "#0"}, "down": {"uv": [9, 8.25, 8.75, 8.5], "texture": "#0"} } }, { "from": [7.8, 5, 5.35], "to": [8.5, 22.75, 6.05], "rotation": {"angle": -45, "axis": "y", "origin": [8, 12.5, 5.55]}, "faces": { "north": {"uv": [4, 0, 4.25, 4.5], "texture": "#0"}, "east": {"uv": [4.25, 0, 4.5, 4.5], "texture": "#0"}, "south": {"uv": [4.5, 0, 4.75, 4.5], "texture": "#0"}, "west": {"uv": [2, 4.5, 2.25, 9], "texture": "#0"}, "up": {"uv": [8, 9, 7.75, 8.75], "texture": "#0"}, "down": {"uv": [9, 7.75, 8.75, 8], "texture": "#0"} } }, { "from": [7.8, 2, 4.85], "to": [8.5, 5, 5.55], "rotation": {"angle": -45, "axis": "y", "origin": [8, 9.5, 5.05]}, "faces": { "north": {"uv": [8, 5.5, 8.25, 6.25], "texture": "#0"}, "east": {"uv": [5.75, 8, 6, 8.75], "texture": "#0"}, "south": {"uv": [6, 8, 6.25, 8.75], "texture": "#0"}, "west": {"uv": [8, 6.25, 8.25, 7], "texture": "#0"}, "up": {"uv": [8.25, 9, 8, 8.75], "texture": "#0"}, "down": {"uv": [9, 8, 8.75, 8.25], "texture": "#0"} } }, { "from": [7.75, 2, 6.75], "to": [8.25, 25.5, 9.25], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [1.5, 0, 1.75, 6], "texture": "#0"}, "east": {"uv": [0, 0, 0.75, 6], "texture": "#0"}, "south": {"uv": [1.75, 0, 2, 6], "texture": "#0"}, "west": {"uv": [0.75, 0, 1.5, 6], "texture": "#0"}, "up": {"uv": [2, 8.75, 1.75, 8], "texture": "#0"}, "down": {"uv": [8.25, 2, 8, 2.75], "texture": "#0"} } }, { "from": [7.5, 24.825, 5.825], "to": [8.5, 27.9, 8.9], "rotation": {"angle": -45, "axis": "x", "origin": [8, 27, 8]}, "faces": { "north": {"uv": [1.25, 6, 1.75, 6.75], "texture": "#0"}, "east": {"uv": [4.75, 3.75, 5.5, 4.5], "texture": "#0"}, "south": {"uv": [6, 2.75, 6.5, 3.5], "texture": "#0"}, "west": {"uv": [5, 4.5, 5.75, 5.25], "texture": "#0"}, "up": {"uv": [6.75, 1.75, 6.25, 1], "texture": "#0"}, "down": {"uv": [6.75, 1.75, 6.25, 2.5], "texture": "#0"} } }, { "from": [7.5, 5, 5.75], "to": [8.5, 22.75, 7], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [3, 0, 3.25, 4.5], "texture": "#0"}, "east": {"uv": [3.25, 0, 3.5, 4.5], "texture": "#0"}, "south": {"uv": [3.5, 0, 3.75, 4.5], "texture": "#0"}, "west": {"uv": [3.75, 0, 4, 4.5], "texture": "#0"}, "up": {"uv": [6.75, 3.5, 6.5, 3.25], "texture": "#0"}, "down": {"uv": [7, 2.25, 6.75, 2.5], "texture": "#0"} } }, { "from": [7.5, 2, 9.25], "to": [8.5, 5, 10.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [5.25, 8, 5.5, 8.75], "texture": "#0"}, "east": {"uv": [4.75, 6.25, 5.25, 7], "texture": "#0"}, "south": {"uv": [5.5, 8, 5.75, 8.75], "texture": "#0"}, "west": {"uv": [6.25, 5, 6.75, 5.75], "texture": "#0"}, "up": {"uv": [8.5, 7.25, 8.25, 6.75], "texture": "#0"}, "down": {"uv": [8.5, 7.25, 8.25, 7.75], "texture": "#0"} } }, { "from": [7.5, 2, 5.25], "to": [8.5, 5, 6.75], "rotation": {"angle": 0, "axis": "y", "origin": [7, -6, 7]}, "faces": { "north": {"uv": [4.25, 8, 4.5, 8.75], "texture": "#0"}, "east": {"uv": [6.25, 3.5, 6.75, 4.25], "texture": "#0"}, "south": {"uv": [4.5, 8, 4.75, 8.75], "texture": "#0"}, "west": {"uv": [6.25, 4.25, 6.75, 5], "texture": "#0"}, "up": {"uv": [8.5, 6.25, 8.25, 5.75], "texture": "#0"}, "down": {"uv": [8.5, 6.25, 8.25, 6.75], "texture": "#0"} } }, { "from": [7.5, 22.05, 7.25], "to": [8.5, 25.4, 9.3], "rotation": {"angle": 20, "axis": "x", "origin": [8, 29.8, 8.55]}, "faces": { "north": {"uv": [1.75, 6, 2, 7], "texture": "#0"}, "east": {"uv": [5.5, 2, 6, 3], "texture": "#0"}, "south": {"uv": [1.75, 7, 2, 8], "texture": "#0"}, "west": {"uv": [5.5, 3, 6, 4], "texture": "#0"}, "up": {"uv": [8.5, 4.25, 8.25, 3.75], "texture": "#0"}, "down": {"uv": [8.5, 4.25, 8.25, 4.75], "texture": "#0"} } }, { "name": "rune_1", "from": [7.5, 2.975, 7.875], "to": [8.5, 3.475, 8.125], "rotation": {"angle": 0, "axis": "y", "origin": [8, 16.975, 8.375]}, "faces": { "north": {"uv": [0.5, 9.5, 0.75, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 0.5, 9.75, 0.75], "texture": "#0"}, "south": {"uv": [0.75, 9.5, 1, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 0.75, 9.75, 1], "texture": "#0"}, "up": {"uv": [1.25, 9.75, 1, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 1, 9.5, 1.25], "texture": "#0"} } }, { "name": "rune_2", "from": [7.5, 3.5625, 7.25], "to": [8.5, 4.3125, 7.625], "rotation": {"angle": 45, "axis": "x", "origin": [8, 2.8125, 8]}, "faces": { "north": {"uv": [9.25, 7.25, 9.5, 7.5], "texture": "#0"}, "east": {"uv": [7.5, 9.25, 7.75, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 7.5, 9.5, 7.75], "texture": "#0"}, "west": {"uv": [7.75, 9.25, 8, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 8, 9.25, 7.75], "texture": "#0"}, "down": {"uv": [8.25, 9.25, 8, 9.5], "texture": "#0"} } }, { "name": "rune_2", "from": [7.5, 3.1875, 6.5], "to": [8.5, 3.5625, 7.625], "rotation": {"angle": 45, "axis": "x", "origin": [8, 2.8125, 8]}, "faces": { "north": {"uv": [9.25, 9, 9.5, 9.25], "texture": "#0"}, "east": {"uv": [9.25, 9.25, 9.5, 9.5], "texture": "#0"}, "south": {"uv": [0, 9.5, 0.25, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 0, 9.75, 0.25], "texture": "#0"}, "up": {"uv": [0.5, 9.75, 0.25, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 0.25, 9.5, 0.5], "texture": "#0"} } }, { "name": "rune_3", "from": [7.5, 5.25, 6.125], "to": [8.5, 5.625, 7.625], "rotation": {"angle": -45, "axis": "x", "origin": [8, 6, 8]}, "faces": { "north": {"uv": [9.25, 8.75, 9.5, 9], "texture": "#0"}, "east": {"uv": [6.5, 8.5, 7, 8.75], "texture": "#0"}, "south": {"uv": [9, 9.25, 9.25, 9.5], "texture": "#0"}, "west": {"uv": [8.5, 6.75, 9, 7], "texture": "#0"}, "up": {"uv": [7.25, 9, 7, 8.5], "texture": "#0"}, "down": {"uv": [8.75, 7, 8.5, 7.5], "texture": "#0"} } }, { "name": "rune_3", "from": [7.5, 4.125, 7.25], "to": [8.5, 5.25, 7.625], "rotation": {"angle": -45, "axis": "x", "origin": [8, 6, 8]}, "faces": { "north": {"uv": [9.25, 8, 9.5, 8.25], "texture": "#0"}, "east": {"uv": [8.25, 9.25, 8.5, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 8.25, 9.5, 8.5], "texture": "#0"}, "west": {"uv": [8.5, 9.25, 8.75, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 8.75, 9.25, 8.5], "texture": "#0"}, "down": {"uv": [9, 9.25, 8.75, 9.5], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 5.35, 7.875], "to": [8.5, 18.85, 8.125], "rotation": {"angle": 0, "axis": "y", "origin": [8, 17.1, 8.375]}, "faces": { "north": {"uv": [3.25, 4.5, 3.5, 8], "texture": "#0"}, "east": {"uv": [3.5, 4.5, 3.75, 8], "texture": "#0"}, "south": {"uv": [3.75, 4.5, 4, 8], "texture": "#0"}, "west": {"uv": [4, 4.5, 4.25, 8], "texture": "#0"}, "up": {"uv": [9.5, 2.75, 9.25, 2.5], "texture": "#0"}, "down": {"uv": [3, 9.25, 2.75, 9.5], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 7.25], "to": [8.5, 7.05, 7.7], "rotation": {"angle": -45, "axis": "x", "origin": [7.875, 6.725, 7.575]}, "faces": { "north": {"uv": [1.25, 9.5, 1.5, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 1.25, 9.75, 1.5], "texture": "#0"}, "south": {"uv": [1.5, 9.5, 1.75, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 1.5, 9.75, 1.75], "texture": "#0"}, "up": {"uv": [2, 9.75, 1.75, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 1.75, 9.5, 2], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 7.625], "to": [8.5, 6.85, 7.875], "rotation": {"angle": 0, "axis": "y", "origin": [8, 11.1, 8.375]}, "faces": { "north": {"uv": [2, 9.5, 2.25, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 2, 9.75, 2.25], "texture": "#0"}, "south": {"uv": [2.25, 9.5, 2.5, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 2.25, 9.75, 2.5], "texture": "#0"}, "up": {"uv": [2.75, 9.75, 2.5, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 2.5, 9.5, 2.75], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 8.3], "to": [8.5, 7.05, 8.75], "rotation": {"angle": 45, "axis": "x", "origin": [7.875, 6.725, 8.425]}, "faces": { "north": {"uv": [2.75, 9.5, 3, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 2.75, 9.75, 3], "texture": "#0"}, "south": {"uv": [3, 9.5, 3.25, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 3, 9.75, 3.25], "texture": "#0"}, "up": {"uv": [3.5, 9.75, 3.25, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 3.25, 9.5, 3.5], "texture": "#0"} } }, { "name": "rune_4", "from": [7.5, 6.6, 8.125], "to": [8.5, 6.85, 8.375], "rotation": {"angle": 0, "axis": "y", "origin": [8, 11.1, 7.625]}, "faces": { "north": {"uv": [3.5, 9.5, 3.75, 9.75], "texture": "#0"}, "east": {"uv": [9.5, 3.5, 9.75, 3.75], "texture": "#0"}, "south": {"uv": [3.75, 9.5, 4, 9.75], "texture": "#0"}, "west": {"uv": [9.5, 3.75, 9.75, 4], "texture": "#0"}, "up": {"uv": [4.25, 9.75, 4, 9.5], "texture": "#0"}, "down": {"uv": [9.75, 4, 9.5, 4.25], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.875, 7.5], "to": [8.5, 19.375, 7.75], "rotation": {"angle": 45, "axis": "x", "origin": [8, 18.375, 8]}, "faces": { "north": {"uv": [9.25, 1, 9.5, 1.25], "texture": "#0"}, "east": {"uv": [1.25, 9.25, 1.5, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 1.25, 9.5, 1.5], "texture": "#0"}, "west": {"uv": [1.5, 9.25, 1.75, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 1.75, 9.25, 1.5], "texture": "#0"}, "down": {"uv": [2, 9.25, 1.75, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.625, 7], "to": [8.5, 18.875, 7.75], "rotation": {"angle": 45, "axis": "x", "origin": [8, 18.375, 8]}, "faces": { "north": {"uv": [9.25, 1.75, 9.5, 2], "texture": "#0"}, "east": {"uv": [2, 9.25, 2.25, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 2, 9.5, 2.25], "texture": "#0"}, "west": {"uv": [2.25, 9.25, 2.5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 2.5, 9.25, 2.25], "texture": "#0"}, "down": {"uv": [2.75, 9.25, 2.5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.425, 7.175], "to": [8.5, 18.575, 7.925], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.875, 18.55, 7.8]}, "faces": { "north": {"uv": [9.25, 2.75, 9.5, 3], "texture": "#0"}, "east": {"uv": [3, 9.25, 3.25, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 3, 9.5, 3.25], "texture": "#0"}, "west": {"uv": [3.25, 9.25, 3.5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 3.5, 9.25, 3.25], "texture": "#0"}, "down": {"uv": [3.75, 9.25, 3.5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.925, 7.425], "to": [8.5, 18.075, 7.925], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.875, 18.05, 7.8]}, "faces": { "north": {"uv": [9.25, 3.5, 9.5, 3.75], "texture": "#0"}, "east": {"uv": [3.75, 9.25, 4, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 3.75, 9.5, 4], "texture": "#0"}, "west": {"uv": [4, 9.25, 4.25, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 4.25, 9.25, 4], "texture": "#0"}, "down": {"uv": [4.5, 9.25, 4.25, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.425, 7.675], "to": [8.5, 17.575, 7.925], "rotation": {"angle": -22.5, "axis": "x", "origin": [7.875, 17.55, 7.8]}, "faces": { "north": {"uv": [9.25, 4.25, 9.5, 4.5], "texture": "#0"}, "east": {"uv": [4.5, 9.25, 4.75, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 4.5, 9.5, 4.75], "texture": "#0"}, "west": {"uv": [4.75, 9.25, 5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 5, 9.25, 4.75], "texture": "#0"}, "down": {"uv": [5.25, 9.25, 5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.425, 8.075], "to": [8.5, 17.575, 8.325], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.875, 17.55, 8.2]}, "faces": { "north": {"uv": [9.25, 5, 9.5, 5.25], "texture": "#0"}, "east": {"uv": [5.25, 9.25, 5.5, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 5.25, 9.5, 5.5], "texture": "#0"}, "west": {"uv": [5.5, 9.25, 5.75, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 5.75, 9.25, 5.5], "texture": "#0"}, "down": {"uv": [6, 9.25, 5.75, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 17.925, 8.075], "to": [8.5, 18.075, 8.575], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.875, 18.05, 8.2]}, "faces": { "north": {"uv": [9.25, 5.75, 9.5, 6], "texture": "#0"}, "east": {"uv": [6, 9.25, 6.25, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 6, 9.5, 6.25], "texture": "#0"}, "west": {"uv": [6.25, 9.25, 6.5, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 6.5, 9.25, 6.25], "texture": "#0"}, "down": {"uv": [6.75, 9.25, 6.5, 9.5], "texture": "#0"} } }, { "name": "rune_5", "from": [7.5, 18.425, 8.075], "to": [8.5, 18.575, 8.825], "rotation": {"angle": 22.5, "axis": "x", "origin": [7.875, 18.55, 8.2]}, "faces": { "north": {"uv": [9.25, 6.5, 9.5, 6.75], "texture": "#0"}, "east": {"uv": [6.75, 9.25, 7, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 6.75, 9.5, 7], "texture": "#0"}, "west": {"uv": [7, 9.25, 7.25, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 7.25, 9.25, 7], "texture": "#0"}, "down": {"uv": [7.5, 9.25, 7.25, 9.5], "texture": "#0"} } }, { "name": "rune_6", "from": [7.5, 19.25, 7.5], "to": [8.5, 20, 7.75], "rotation": {"angle": -45, "axis": "x", "origin": [8, 20.5, 8]}, "faces": { "north": {"uv": [9.25, 0.25, 9.5, 0.5], "texture": "#0"}, "east": {"uv": [0.5, 9.25, 0.75, 9.5], "texture": "#0"}, "south": {"uv": [9.25, 0.5, 9.5, 0.75], "texture": "#0"}, "west": {"uv": [0.75, 9.25, 1, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 1, 9.25, 0.75], "texture": "#0"}, "down": {"uv": [1.25, 9.25, 1, 9.5], "texture": "#0"} } }, { "name": "rune_6", "from": [7.5, 20, 6.75], "to": [8.5, 20.25, 7.75], "rotation": {"angle": -45, "axis": "x", "origin": [8, 20.5, 8]}, "faces": { "north": {"uv": [8.75, 9, 9, 9.25], "texture": "#0"}, "east": {"uv": [9, 8.75, 9.25, 9], "texture": "#0"}, "south": {"uv": [9, 9, 9.25, 9.25], "texture": "#0"}, "west": {"uv": [0, 9.25, 0.25, 9.5], "texture": "#0"}, "up": {"uv": [9.5, 0.25, 9.25, 0], "texture": "#0"}, "down": {"uv": [0.5, 9.25, 0.25, 9.5], "texture": "#0"} } }, { "name": "rune_7", "from": [7.5, 21.9, 7.875], "to": [8.5, 23.15, 9.125], "rotation": {"angle": -45, "axis": "x", "origin": [7.875, 22.025, 8]}, "faces": { "north": {"uv": [7.75, 9, 8, 9.25], "texture": "#0"}, "east": {"uv": [9, 7.75, 9.25, 8], "texture": "#0"}, "south": {"uv": [8, 9, 8.25, 9.25], "texture": "#0"}, "west": {"uv": [9, 8, 9.25, 8.25], "texture": "#0"}, "up": {"uv": [8.5, 9.25, 8.25, 9], "texture": "#0"}, "down": {"uv": [9.25, 8.25, 9, 8.5], "texture": "#0"} } }, { "name": "rune_7", "from": [7.5, 20.025, 7.875], "to": [8.5, 22.025, 8.125], "rotation": {"angle": 0, "axis": "y", "origin": [8, 20.525, 8.375]}, "faces": { "north": {"uv": [8.5, 5.25, 8.75, 5.75], "texture": "#0"}, "east": {"uv": [8.5, 5.75, 8.75, 6.25], "texture": "#0"}, "south": {"uv": [6.25, 8.5, 6.5, 9], "texture": "#0"}, "west": {"uv": [8.5, 6.25, 8.75, 6.75], "texture": "#0"}, "up": {"uv": [8.75, 9.25, 8.5, 9], "texture": "#0"}, "down": {"uv": [9.25, 8.5, 9, 8.75], "texture": "#0"} } } ], "display": { "thirdperson_righthand": { "translation": [0, 14.25, 0] }, "thirdperson_lefthand": { "translation": [0, 14.25, 0] }, "firstperson_righthand": { "rotation": [-5, 5, -5], "translation": [0, 9.25, 0] }, "firstperson_lefthand": { "rotation": [-5, 5, -5], "translation": [0, 9.25, 0] }, "ground": { "rotation": [0, 0, 90] }, "gui": { "rotation": [90, -135, 90], "scale": [1, 0.5, 0.5] }, "fixed": { "rotation": [90, -45, 90], "scale": [1, 0.5, 0.5] } }, "groups": [ { "name": "group", "origin": [7, -8.5, 7], "color": 0, "children": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36] }, { "name": "group", "origin": [8, 29.8, 7.45], "color": 0, "children": [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48] }, { "name": "group", "origin": [8, 11.1, 7.625], "color": 0, "children": [49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70] } ] }