├── gradlew ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ ├── assets │ │ └── quadz │ │ │ ├── icon.png │ │ │ ├── misc │ │ │ ├── images │ │ │ │ ├── banner.png │ │ │ │ └── old_logo.png │ │ │ └── texts │ │ │ │ └── splashes.txt │ │ │ ├── textures │ │ │ ├── gui │ │ │ │ └── quadz.png │ │ │ └── item │ │ │ │ ├── goggles.png │ │ │ │ ├── remote.png │ │ │ │ ├── quadcopter.png │ │ │ │ └── armor │ │ │ │ └── goggles.png │ │ │ ├── models │ │ │ └── item │ │ │ │ ├── goggles.json │ │ │ │ ├── remote.json │ │ │ │ └── quadcopter.json │ │ │ ├── templates │ │ │ ├── pixel │ │ │ │ ├── texture │ │ │ │ │ └── pixel.png │ │ │ │ ├── pixel.settings.json │ │ │ │ ├── animations │ │ │ │ │ └── pixel.animation.json │ │ │ │ └── geo │ │ │ │ │ └── pixel.geo.json │ │ │ ├── voyager │ │ │ │ ├── texture │ │ │ │ │ └── voyager.png │ │ │ │ ├── voyager.settings.json │ │ │ │ ├── animations │ │ │ │ │ └── voyager.animation.json │ │ │ │ └── geo │ │ │ │ │ └── voyager.geo.json │ │ │ └── voxel_racer_one │ │ │ │ ├── texture │ │ │ │ └── voxel_racer_one.png │ │ │ │ ├── voxel_racer_one.settings.json │ │ │ │ ├── animations │ │ │ │ └── voxel_racer_one.animation.json │ │ │ │ └── geo │ │ │ │ └── voxel_racer_one.geo.json │ │ │ ├── shaders │ │ │ ├── post │ │ │ │ ├── fisheye.json │ │ │ │ └── static.json │ │ │ └── program │ │ │ │ ├── fisheye.json │ │ │ │ ├── static.json │ │ │ │ ├── fisheye.fsh │ │ │ │ └── static.fsh │ │ │ ├── animations │ │ │ └── item │ │ │ │ ├── armor │ │ │ │ └── goggles.animation.json │ │ │ │ └── quadcopter.animation.json │ │ │ ├── lang │ │ │ └── en_us.json │ │ │ └── geo │ │ │ └── item │ │ │ ├── armor │ │ │ └── goggles.geo.json │ │ │ └── quadcopter.geo.json │ ├── quadz.common.mixins.json │ ├── quadz.client.mixins.json │ ├── quadz.accessWidener │ └── fabric.mod.json │ └── java │ └── dev │ └── lazurite │ └── quadz │ ├── client │ ├── extension │ │ └── CameraTypeExtension.java │ ├── mixin │ │ ├── TitleScreenMixin.java │ │ ├── PauseScreenMixin.java │ │ ├── CameraTypeMixin.java │ │ ├── GuiMixin.java │ │ ├── MinecraftMixin.java │ │ └── GameRendererMixin.java │ ├── event │ │ ├── ClientNetworkEventHooks.java │ │ └── ClientEventHooks.java │ ├── render │ │ ├── screen │ │ │ ├── osd │ │ │ │ ├── VelocityUnit.java │ │ │ │ └── OnScreenDisplay.java │ │ │ ├── ScreenHooks.java │ │ │ ├── ControllerConnectedToast.java │ │ │ ├── MainConfigScreen.java │ │ │ └── ControllerSetupScreen.java │ │ ├── entity │ │ │ └── QuadcopterEntityRenderer.java │ │ ├── camera │ │ │ └── CameraHooks.java │ │ ├── RenderHooks.java │ │ └── QuadcopterView.java │ ├── resource │ │ └── SplashResourceLoader.java │ ├── QuadzClient.java │ └── Config.java │ ├── common │ ├── util │ │ ├── event │ │ │ ├── ClickEvents.java │ │ │ ├── CameraEvents.java │ │ │ └── JoystickEvents.java │ │ ├── FloatBufferUtil.java │ │ ├── BetaflightHelper.java │ │ ├── Matrix4fHelper.java │ │ ├── BindableItemWrapper.java │ │ ├── Bindable.java │ │ ├── JoystickOutput.java │ │ └── Search.java │ ├── item │ │ ├── RemoteItem.java │ │ ├── GogglesItem.java │ │ └── QuadcopterItem.java │ ├── extension │ │ └── PlayerExtension.java │ ├── mixin │ │ ├── ServerPlayerMixin.java │ │ └── PlayerMixin.java │ ├── hooks │ │ ├── ServerEventHooks.java │ │ ├── PlayerHooks.java │ │ └── ServerNetworkEventHooks.java │ └── entity │ │ └── Quadcopter.java │ └── Quadz.java ├── settings.gradle ├── .gitignore ├── gradle.properties ├── LICENSE ├── gradlew.bat ├── changelog.md └── README.md /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/gradlew -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/misc/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/misc/images/banner.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/textures/gui/quadz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/textures/gui/quadz.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/misc/images/old_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/misc/images/old_logo.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/textures/item/goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/textures/item/goggles.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/textures/item/remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/textures/item/remote.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/textures/item/quadcopter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/textures/item/quadcopter.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/models/item/goggles.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "item/generated", 3 | "textures": { 4 | "layer0": "quadz:item/goggles" 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/textures/item/armor/goggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/textures/item/armor/goggles.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/pixel/texture/pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/templates/pixel/texture/pixel.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voyager/texture/voyager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/templates/voyager/texture/voyager.png -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/misc/texts/splashes.txt: -------------------------------------------------------------------------------- 1 | Goggles down thumbs up! 2 | Never say last pack! 3 | Last pack! 4 | Arm 'em up! 5 | In less than five! 6 | How high can it go? 7 | Loose prop! 8 | Mid-air! 9 | Quadz! -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voxel_racer_one/texture/voxel_racer_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LazuriteMC/Quadz/HEAD/src/main/resources/assets/quadz/templates/voxel_racer_one/texture/voxel_racer_one.png -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/extension/CameraTypeExtension.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.extension; 2 | 3 | public interface CameraTypeExtension { 4 | 5 | default void quadz$reset() { 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | 11 | rootProject.name = 'quadz' -------------------------------------------------------------------------------- /src/main/resources/quadz.common.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "dev.lazurite.quadz.common.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | "PlayerMixin", 8 | "ServerPlayerMixin" 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/shaders/post/fisheye.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | "swap" 4 | ], 5 | "passes": [ 6 | { 7 | "name": "quadz:fisheye", 8 | "intarget": "minecraft:main", 9 | "outtarget": "swap" 10 | }, 11 | { 12 | "name": "blit", 13 | "intarget": "swap", 14 | "outtarget": "minecraft:main" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/shaders/post/static.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | "swap" 4 | ], 5 | "passes": [ 6 | { 7 | "name": "quadz:static", 8 | "intarget": "minecraft:main", 9 | "outtarget": "swap" 10 | }, 11 | { 12 | "name": "blit", 13 | "intarget": "swap", 14 | "outtarget": "minecraft:main" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # fabric 28 | 29 | run/ 30 | logs/ 31 | lib/ 32 | 33 | local.settings.gradle -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/models/item/remote.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "item/generated", 3 | "textures": { 4 | "layer0": "quadz:item/remote" 5 | }, 6 | "display": { 7 | "firstperson_righthand": { 8 | "rotation": [-45, 0, 0], 9 | "translation": [-9, 3, 0], 10 | "scale": [1, 1, 1] 11 | }, 12 | "firstperson_lefthand": { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/pixel/pixel.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "pixel", 3 | "name" : "Pixel", 4 | "author" : "BlueVista", 5 | "modid" : "quadz", 6 | "width" : 0.225, 7 | "height" : 0.125, 8 | "cameraX" : 0.025, 9 | "cameraY" : 0.1, 10 | "mass" : 0.01, 11 | "dragCoefficient" : 0.05, 12 | "thrust" : 0.4, 13 | "thrustCurve" : 1.0, 14 | "cameraAngle" : 5 15 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voyager/voyager.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "voyager", 3 | "name" : "Voyager", 4 | "author" : "BlueVista", 5 | "modid" : "quadz", 6 | "width" : 0.9, 7 | "height" : 0.2, 8 | "cameraX" : 0.43, 9 | "cameraY" : 0.0, 10 | "mass" : 1.2, 11 | "dragCoefficient" : 0.2, 12 | "thrust" : 55, 13 | "thrustCurve" : 0.8, 14 | "cameraAngle" : 20 15 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voxel_racer_one/voxel_racer_one.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "voxel_racer_one", 3 | "name" : "Voxel Racer 1", 4 | "author" : "BlueVista", 5 | "modid" : "quadz", 6 | "width" : 0.55, 7 | "height" : 0.3, 8 | "cameraX" : 0.175, 9 | "cameraY" : 0.08, 10 | "mass" : 0.5, 11 | "dragCoefficient" : 0.135, 12 | "thrust" : 65, 13 | "thrustCurve" : 1.0, 14 | "cameraAngle" : 45 15 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/event/ClickEvents.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util.event; 2 | 3 | import dev.lazurite.toolbox.api.event.Event; 4 | 5 | public class ClickEvents { 6 | public static Event RIGHT_CLICK_EVENT = Event.create(); 7 | public static Event LEFT_CLICK_EVENT = Event.create(); 8 | 9 | public interface Click { 10 | void onClick(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/quadz.client.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "dev.lazurite.quadz.client.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "client": [ 7 | "CameraTypeMixin", 8 | "GameRendererMixin", 9 | "GuiMixin", 10 | "MinecraftMixin", 11 | "PauseScreenMixin", 12 | "TitleScreenMixin" 13 | ], 14 | "injectors": { 15 | "defaultRequire": 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/item/RemoteItem.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.item; 2 | 3 | import net.minecraft.world.item.Item; 4 | 5 | /** 6 | * Represents a held "Transmitter" or remote for controlling a quadcopter. 7 | * However, this class is not likely to contain much of the logic. 8 | */ 9 | public class RemoteItem extends Item { 10 | 11 | public RemoteItem() { 12 | super(new Properties().stacksTo(1)); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/event/CameraEvents.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util.event; 2 | 3 | import dev.lazurite.toolbox.api.event.Event; 4 | import net.minecraft.world.entity.Entity; 5 | 6 | public class CameraEvents { 7 | public static Event SWITCH_CAMERA_EVENT = Event.create(); 8 | 9 | public interface SwitchCameraEvent { 10 | void onSwitchCamera(Entity previousCameraEntity, Entity currentCameraEntity); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/FloatBufferUtil.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | import java.nio.FloatBuffer; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public interface FloatBufferUtil { 8 | 9 | static List toArray(FloatBuffer floatBuffer) { 10 | var out = new ArrayList(); 11 | floatBuffer.rewind(); 12 | 13 | while (floatBuffer.hasRemaining()) { 14 | out.add(floatBuffer.get()); 15 | } 16 | 17 | return out; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | 3 | # Gradle 4 | loom_version = 0.12-SNAPSHOT 5 | 6 | # Minecraft/Fabric 7 | minecraft_version = 1.19.3 8 | fabric_loader_version = 0.14.12 9 | fabric_api_version = 0.71.0+1.19.3 10 | 11 | # Mod Properties 12 | quadz_version = 2.0.0 13 | 14 | # Lazurite Dependencies 15 | lattice_version = 3.0.0 16 | rayon_version = 1.6.6 17 | corduroy_version = 1.0.8 18 | form_version = 1.0.6 19 | 20 | # Other Dependencies 21 | satin_version = 1.10.0 22 | cloth_version = 9.0.94 23 | parchment_version = 2022.12.18 -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/animations/item/armor/goggles.animation.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.8.0", 3 | "animations": { 4 | "up": { 5 | "loop": true, 6 | "animation_length": 0.5, 7 | "bones": { 8 | "goggles": { 9 | "rotation": { 10 | "0.0": { 11 | "vector": [0, 0, 0] 12 | }, 13 | "0.5": { 14 | "vector": [-12.5, 0, 0] 15 | } 16 | }, 17 | "position": { 18 | "0.0": { 19 | "vector": [0, 0, 0] 20 | }, 21 | "0.5": { 22 | "vector": [0, 2, -1] 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/extension/PlayerExtension.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.extension; 2 | 3 | import net.minecraft.resources.ResourceLocation; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public interface PlayerExtension { 9 | 10 | default float quadz$getJoystickValue(ResourceLocation axis) { 11 | return 0.0f; 12 | } 13 | 14 | default void quadz$setJoystickValue(ResourceLocation axis, float value) { 15 | } 16 | 17 | default Map quadz$getAllAxes() { 18 | return new HashMap<>(); 19 | } 20 | 21 | default void quadz$syncJoystick() { 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/event/JoystickEvents.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util.event; 2 | 3 | import dev.lazurite.toolbox.api.event.Event; 4 | 5 | public class JoystickEvents { 6 | public static final Event JOYSTICK_CONNECT = Event.create(); 7 | public static final Event JOYSTICK_DISCONNECT = Event.create(); 8 | 9 | private JoystickEvents() {} 10 | 11 | @FunctionalInterface 12 | public interface JoystickConnectEvent { 13 | void onConnect(int id, String name); 14 | } 15 | 16 | @FunctionalInterface 17 | public interface JoystickDisconnectEvent { 18 | void onDisconnect(int id, String name); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/mixin/TitleScreenMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.mixin; 2 | 3 | import dev.lazurite.quadz.client.render.screen.ScreenHooks; 4 | import net.minecraft.client.gui.screens.TitleScreen; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | @Mixin(TitleScreen.class) 11 | public class TitleScreenMixin { 12 | 13 | @Inject(method = "init", at = @At("TAIL")) 14 | public void init(CallbackInfo ci) { 15 | ScreenHooks.addQuadzButtonToTitleScreen((TitleScreen) (Object) this); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/mixin/PauseScreenMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.mixin; 2 | 3 | import dev.lazurite.quadz.client.render.screen.ScreenHooks; 4 | import net.minecraft.client.gui.screens.PauseScreen; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | @Mixin(PauseScreen.class) 11 | public class PauseScreenMixin { 12 | 13 | @Inject(method = "createPauseMenu", at = @At("TAIL")) 14 | public void createPauseScreen$TAIL(CallbackInfo ci) { 15 | ScreenHooks.addQuadzButtonToPauseScreen((PauseScreen) (Object) this); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voyager/animations/voyager.animation.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.8.0", 3 | "animations": { 4 | "armed": { 5 | "loop": true, 6 | "animation_length": 0.12, 7 | "bones": { 8 | "front_left_prop": { 9 | "rotation": { 10 | "0.0": [0, 0, 0], 11 | "0.12": [0, 180, 0] 12 | } 13 | }, 14 | "front_right_prop": { 15 | "rotation": { 16 | "0.0": [0, 0, 0], 17 | "0.12": [0, -180, 0] 18 | } 19 | }, 20 | "back_right_prop": { 21 | "rotation": { 22 | "0.0": [0, 0, 0], 23 | "0.12": [0, 180, 0] 24 | } 25 | }, 26 | "back_left_prop": { 27 | "rotation": { 28 | "0.0": [0, 0, 0], 29 | "0.12": [0, -180, 0] 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "geckolib_format_version": 2 36 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/mixin/ServerPlayerMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.mixin; 2 | 3 | import dev.lazurite.quadz.common.hooks.PlayerHooks; 4 | import net.minecraft.server.level.ServerPlayer; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Redirect; 8 | 9 | @Mixin(ServerPlayer.class) 10 | public class ServerPlayerMixin { 11 | 12 | @Redirect( 13 | method = "tick", 14 | at = @At( 15 | value = "INVOKE", 16 | target = "Lnet/minecraft/server/level/ServerPlayer;wantsToStopRiding()Z" 17 | ) 18 | ) 19 | public boolean tick$wantsToStopRiding(ServerPlayer player) { 20 | return PlayerHooks.onWantsToStopRiding(player); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/shaders/program/fisheye.json: -------------------------------------------------------------------------------- 1 | { 2 | "blend": { 3 | "func": "add", 4 | "srcrgb": "srcalpha", 5 | "dstrgb": "1-srcalpha" 6 | }, 7 | "vertex": "blit", 8 | "fragment": "quadz:fisheye", 9 | "attributes": [ "Position" ], 10 | "samplers": [ 11 | { 12 | "name": "DiffuseSampler" 13 | } 14 | ], 15 | "uniforms": [ 16 | { 17 | "name": "ProjMat", 18 | "type": "matrix4x4", 19 | "count": 16, 20 | "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] 21 | }, 22 | { 23 | "name": "OutSize", 24 | "type": "float", 25 | "count": 2, 26 | "values": [ 1.0, 1.0 ] 27 | }, 28 | { 29 | "name": "Amount", 30 | "type": "float", 31 | "count": 1, 32 | "values": [ 0.8 ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/event/ClientNetworkEventHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.event; 2 | 3 | import dev.lazurite.toolbox.api.network.PacketRegistry; 4 | import net.minecraft.client.Minecraft; 5 | 6 | public class ClientNetworkEventHooks { 7 | 8 | public static void onJoystickInput(PacketRegistry.ClientboundContext context) { 9 | var buf = context.byteBuf(); 10 | var id = buf.readUUID(); 11 | var axisCount = buf.readInt(); 12 | var player = Minecraft.getInstance().level.getPlayerByUUID(id); 13 | 14 | if (player != null) { 15 | for (int i = 0; i < axisCount; i++) { 16 | var axis = buf.readResourceLocation(); 17 | var value = buf.readFloat(); 18 | player.quadz$setJoystickValue(axis, value); 19 | } 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/mixin/CameraTypeMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.mixin; 2 | 3 | import dev.lazurite.quadz.client.extension.CameraTypeExtension; 4 | import dev.lazurite.quadz.client.render.camera.CameraHooks; 5 | import net.minecraft.client.CameraType; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 10 | 11 | @Mixin(CameraType.class) 12 | public class CameraTypeMixin implements CameraTypeExtension { 13 | 14 | @Inject(method = "cycle", at = @At("HEAD"), cancellable = true) 15 | public void cycle$HEAD(CallbackInfoReturnable cir) { 16 | CameraHooks.onCycle().ifPresent(cir::setReturnValue); 17 | } 18 | 19 | @Override 20 | public void quadz$reset() { 21 | CameraHooks.onCameraReset(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/shaders/program/static.json: -------------------------------------------------------------------------------- 1 | { 2 | "blend": { 3 | "func": "add", 4 | "srcrgb": "srcalpha", 5 | "dstrgb": "1-srcalpha" 6 | }, 7 | "vertex": "blit", 8 | "fragment": "quadz:static", 9 | "attributes": [ "Position" ], 10 | "samplers": [ 11 | { 12 | "name": "DiffuseSampler" 13 | } 14 | ], 15 | "uniforms": [ 16 | { 17 | "name": "ProjMat", 18 | "type": "matrix4x4", 19 | "count": 16, 20 | "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] 21 | }, 22 | { 23 | "name": "OutSize", 24 | "type": "float", 25 | "count": 2, 26 | "values": [ 1.0, 1.0 ] 27 | }, 28 | { 29 | "name": "Time", 30 | "type": "float", 31 | "count": 1, 32 | "values": [ 0.0 ] 33 | }, 34 | { 35 | "name": "Amount", 36 | "type": "float", 37 | "count": 1, 38 | "values": [ 0.0 ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/BetaflightHelper.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | public class BetaflightHelper { 4 | 5 | public static double calculateRates(double rcCommand, double rcRate, double expo, double superRate, double delta) { 6 | var absRcCommand = Math.abs(rcCommand); 7 | 8 | if (rcRate > 2.0) { 9 | rcRate = rcRate + (14.54 * (rcRate - 2.0)); 10 | } 11 | 12 | if (expo != 0) { 13 | rcCommand = rcCommand * Math.pow(Math.abs(rcCommand), 3) * expo + rcCommand * (1.0 - expo); 14 | } 15 | 16 | var angleRate = 200.0 * rcRate * rcCommand; 17 | 18 | if (superRate != 0) { 19 | var rcSuperFactor = 1.0 / (clamp(1.0 - absRcCommand * (superRate), 0.01, 1.00)); 20 | angleRate *= rcSuperFactor; 21 | } 22 | 23 | return angleRate * delta; 24 | } 25 | 26 | private static double clamp(double n, double minn, double maxn) { 27 | return Math.max(Math.min(maxn, n), minn); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/animations/item/quadcopter.animation.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.8.0", 3 | "animations": { 4 | "armed": { 5 | "loop": true, 6 | "animation_length": 0.2, 7 | "bones": { 8 | "prop": { 9 | "rotation": { 10 | "0.0": { 11 | "vector": [0, 0, 0] 12 | }, 13 | "0.2": { 14 | "vector": [0, 180, 0] 15 | } 16 | } 17 | }, 18 | "prop2": { 19 | "rotation": { 20 | "0.0": { 21 | "vector": [0, 0, 0] 22 | }, 23 | "0.2": { 24 | "vector": [0, -180, 0] 25 | } 26 | } 27 | }, 28 | "prop3": { 29 | "rotation": { 30 | "0.0": { 31 | "vector": [0, 0, 0] 32 | }, 33 | "0.2": { 34 | "vector": [0, 180, 0] 35 | } 36 | } 37 | }, 38 | "prop4": { 39 | "rotation": { 40 | "0.0": { 41 | "vector": [0, 0, 0] 42 | }, 43 | "0.2": { 44 | "vector": [0, -180, 0] 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/pixel/animations/pixel.animation.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.8.0", 3 | "animations": { 4 | "armed": { 5 | "loop": true, 6 | "animation_length": 0.12, 7 | "bones": { 8 | "prop1": { 9 | "rotation": { 10 | "0.0": { 11 | "vector": [0, 0, 0] 12 | }, 13 | "0.12": { 14 | "vector": [0, 180, 0] 15 | } 16 | } 17 | }, 18 | "prop2": { 19 | "rotation": { 20 | "0.0": { 21 | "vector": [0, 0, 0] 22 | }, 23 | "0.12": { 24 | "vector": [0, 180, 0] 25 | } 26 | } 27 | }, 28 | "prop3": { 29 | "rotation": { 30 | "0.0": { 31 | "vector": [0, 0, 0] 32 | }, 33 | "0.12": { 34 | "vector": [0, -180, 0], 35 | "easing": "linear" 36 | } 37 | } 38 | }, 39 | "prop4": { 40 | "rotation": { 41 | "0.0": { 42 | "vector": [0, 0, 0] 43 | }, 44 | "0.12": { 45 | "vector": [0, -180, 0] 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voxel_racer_one/animations/voxel_racer_one.animation.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.8.0", 3 | "animations": { 4 | "armed": { 5 | "loop": true, 6 | "animation_length": 0.24, 7 | "bones": { 8 | "front_right": { 9 | "rotation": { 10 | "0.0": { 11 | "vector": [0, 0, 0] 12 | }, 13 | "0.24": { 14 | "vector": [0, 270, 0] 15 | } 16 | } 17 | }, 18 | "back_right": { 19 | "rotation": { 20 | "0.0": { 21 | "vector": [0, 0, 0] 22 | }, 23 | "0.24": { 24 | "vector": [0, -270, 0] 25 | } 26 | } 27 | }, 28 | "back_left": { 29 | "rotation": { 30 | "0.0": { 31 | "vector": [0, 0, 0] 32 | }, 33 | "0.24": { 34 | "vector": [0, 270, 0] 35 | } 36 | } 37 | }, 38 | "front_left": { 39 | "rotation": { 40 | "0.0": { 41 | "vector": [0, 0, 0] 42 | }, 43 | "0.24": { 44 | "vector": [0, -270, 0] 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/Matrix4fHelper.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | import org.joml.Matrix4f; 4 | import org.joml.Quaternionf; 5 | import org.joml.Vector3f; 6 | 7 | // jank 8 | public interface Matrix4fHelper { 9 | 10 | static Vector3f matrixToVector(Matrix4f mat) { 11 | return new Vector3f(mat.m02(), mat.m12(), mat.m22()); 12 | } 13 | 14 | static void fromQuaternion(Matrix4f mat, Quaternionf q) { 15 | double tmp1; 16 | double tmp2; 17 | 18 | double sqw = q.w() * q.w(); 19 | double sqx = q.x() * q.x(); 20 | double sqy = q.y() * q.y(); 21 | double sqz = q.z() * q.z(); 22 | 23 | double invs = 1 / (sqx + sqy + sqz + sqw); 24 | mat.m22((float) ((-sqx - sqy + sqz + sqw)*invs)); 25 | 26 | tmp1 = q.x() * q.z(); 27 | tmp2 = q.y() * q.w(); 28 | mat.m02((float) (2.0 * (tmp1 + tmp2)*invs)); 29 | 30 | tmp1 = q.y() * q.z(); 31 | tmp2 = q.x() * q.w(); 32 | mat.m12((float) (2.0 * (tmp1 - tmp2)*invs)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/mixin/PlayerMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.mixin; 2 | 3 | import dev.lazurite.quadz.common.extension.PlayerExtension; 4 | import dev.lazurite.quadz.common.hooks.PlayerHooks; 5 | import net.minecraft.resources.ResourceLocation; 6 | import net.minecraft.world.entity.player.Player; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | 9 | import java.util.Map; 10 | 11 | @Mixin(Player.class) 12 | public class PlayerMixin implements PlayerExtension { 13 | 14 | @Override 15 | public float quadz$getJoystickValue(ResourceLocation axis) { 16 | return PlayerHooks.onGetJoystickValues(axis); 17 | } 18 | 19 | @Override 20 | public void quadz$setJoystickValue(ResourceLocation axis, float value) { 21 | PlayerHooks.onSetJoystickValue(axis, value); 22 | } 23 | 24 | @Override 25 | public Map quadz$getAllAxes() { 26 | return PlayerHooks.onGetAllAxes(); 27 | } 28 | 29 | @Override 30 | public void quadz$syncJoystick() { 31 | PlayerHooks.onSyncJoystick((Player) (Object) this); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/shaders/program/fisheye.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | #define EPSILON 0.000011 3 | 4 | uniform sampler2D DiffuseSampler; 5 | uniform vec2 OutSize; 6 | uniform float Amount; 7 | 8 | /* 9 | https://www.shadertoy.com/view/4s2GRR 10 | */ 11 | 12 | out vec4 fragColor; 13 | in vec2 texCoord; 14 | 15 | void main() 16 | { 17 | vec2 p = gl_FragCoord.xy / OutSize.x; 18 | float prop = OutSize.x / OutSize.y; 19 | vec2 m = vec2(0.5, 0.5 / prop); 20 | vec2 d = p - m; 21 | float r = sqrt(dot(d, d)); 22 | float power = ( 2.0 * 3.141592 / (2.0 * sqrt(dot(m, m))) ) * 23 | (Amount - 0.5); 24 | float bind; 25 | if (power > 0.0) bind = sqrt(dot(m, m)); 26 | else {if (prop < 1.0) bind = m.x; else bind = m.y;} 27 | vec2 uv; 28 | if (power > 0.0) 29 | uv = m + normalize(d) * tan(r * power) * bind / tan( bind * power); 30 | else if (power < 0.0) 31 | uv = m + normalize(d) * atan(r * -power * 10.0) * bind / atan(-power * bind * 10.0); 32 | else 33 | uv = p; 34 | vec3 col = texture2D(DiffuseSampler, vec2(uv.x, uv.y * prop)).rgb; 35 | fragColor = vec4(col, 1.0); 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ethan Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/screen/osd/VelocityUnit.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.screen.osd; 2 | 3 | import net.minecraft.network.chat.Component; 4 | 5 | public enum VelocityUnit { 6 | METERS_PER_SECOND(Component.translatable("quadz.velocity.meters_per_second"), "m/s", 1.0f), 7 | KILOMETERS_PER_HOUR(Component.translatable("quadz.velocity.kilometers_per_hour"), "kph", 3.6f), 8 | MILES_PER_HOUR(Component.translatable("quadz.velocity.miles_per_hour"), "mph", 2.237f); 9 | 10 | private final Component translation; 11 | private final String abbreviation; 12 | private final float factor; 13 | 14 | VelocityUnit(Component translation, String abbreviation, float factor) { 15 | this.translation = translation; 16 | this.abbreviation = abbreviation; 17 | this.factor = factor; 18 | } 19 | 20 | public Component getTranslation() { 21 | return this.translation; 22 | } 23 | 24 | public String getAbbreviation() { 25 | return this.abbreviation; 26 | } 27 | 28 | public float getFactor() { 29 | return this.factor; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/quadz.accessWidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | extendable method net/minecraft/world/entity/Entity getPosition (F)Lnet/minecraft/world/phys/Vec3; 4 | accessible field net/minecraft/client/resources/SplashManager splashes Ljava/util/List; 5 | accessible field net/minecraft/client/Camera xRot F 6 | accessible field net/minecraft/client/Camera yRot F 7 | accessible field net/minecraft/client/Camera forwards Lorg/joml/Vector3f; 8 | accessible field net/minecraft/client/Camera up Lorg/joml/Vector3f; 9 | accessible field net/minecraft/client/Camera left Lorg/joml/Vector3f; 10 | accessible field net/minecraft/client/Camera rotation Lorg/joml/Quaternionf; 11 | accessible method net/minecraft/client/Camera move (DDD)V 12 | accessible method net/minecraft/client/Camera setPosition (Lnet/minecraft/world/phys/Vec3;)V 13 | accessible method net/minecraft/client/gui/screens/Screen addRenderableWidget (Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener; 14 | accessible field net/minecraft/client/gui/components/GridWidget children Ljava/util/List; 15 | accessible field net/minecraft/client/gui/screens/PauseScreen disconnectButton Lnet/minecraft/client/gui/components/Button; 16 | accessible method net/minecraft/client/Camera getMaxZoom (D)D -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/BindableItemWrapper.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | import net.minecraft.nbt.CompoundTag; 4 | import net.minecraft.world.item.ItemStack; 5 | 6 | /** 7 | * Stores the bind id in the transmitter through the {@link Bindable} interface. 8 | * @see Bindable 9 | */ 10 | public class BindableItemWrapper implements Bindable { 11 | 12 | private final ItemStack stack; 13 | private final CompoundTag tag; 14 | 15 | public BindableItemWrapper(ItemStack stack) { 16 | this.stack = stack; 17 | this.tag = this.stack.getOrCreateTagElement("bindable"); 18 | } 19 | 20 | @Override 21 | public void setBindId(int bindId) { 22 | tag.putInt("bind_id", bindId); 23 | } 24 | 25 | @Override 26 | public int getBindId() { 27 | if (!tag.contains("bind_id")) { 28 | this.setBindId(-1); 29 | } 30 | 31 | return tag.getInt("bind_id"); 32 | } 33 | 34 | public ItemStack getStack() { 35 | return this.stack; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object obj) { 40 | if (obj instanceof BindableItemWrapper) { 41 | return getBindId() == ((BindableItemWrapper) obj).getBindId(); 42 | } 43 | 44 | return false; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/models/item/quadcopter.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit": "Made with Blockbench", 3 | "parent": "builtin/entity", 4 | "texture_size": [ 5 | 32, 6 | 32 7 | ], 8 | "display": { 9 | "thirdperson_righthand": { 10 | "translation": [ 11 | 0, 12 | 0, 13 | 1 14 | ], 15 | "scale": [ 16 | 0.9, 17 | 0.9, 18 | 0.9 19 | ] 20 | }, 21 | "thirdperson_lefthand": { 22 | "translation": [ 23 | 0, 24 | 0, 25 | 1 26 | ], 27 | "scale": [ 28 | 0.9, 29 | 0.9, 30 | 0.9 31 | ] 32 | }, 33 | "firstperson_righthand": { 34 | "translation": [ 35 | 0, 36 | 0.5, 37 | 1.5 38 | ] 39 | }, 40 | "firstperson_lefthand": { 41 | "translation": [ 42 | 0, 43 | 0.5, 44 | 1.5 45 | ] 46 | }, 47 | "ground": { 48 | "translation": [ 49 | 0, 50 | -2, 51 | 0 52 | ] 53 | }, 54 | "gui": { 55 | "rotation": [ 56 | 33.25, 57 | -25.5, 58 | 0 59 | ], 60 | "translation": [ 61 | 0.25, 62 | -1, 63 | 0 64 | ] 65 | }, 66 | "fixed": { 67 | "rotation": [ 68 | -90, 69 | 0, 70 | 0 71 | ] 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/mixin/GuiMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.mixin; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import dev.lazurite.quadz.client.QuadzClient; 5 | import net.minecraft.client.gui.Gui; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 10 | 11 | @Mixin(Gui.class) 12 | public class GuiMixin { 13 | 14 | @Inject(method = "render", at = @At("TAIL")) 15 | public void render$TAIL(PoseStack poseStack, float tickDelta, CallbackInfo ci) { 16 | QuadzClient.getQuadcopterFromCamera().ifPresent(quadcopter -> 17 | quadcopter.getView().onGuiRender(poseStack, tickDelta) 18 | ); 19 | } 20 | 21 | @Inject(method = "renderCrosshair", at = @At("HEAD"), cancellable = true) 22 | private void renderCrosshair$HEAD(PoseStack poseStack, CallbackInfo ci) { 23 | QuadzClient.getQuadcopterFromCamera().ifPresent(quadcopter -> ci.cancel()); 24 | } 25 | 26 | @Inject(method = "renderExperienceBar", at = @At("HEAD"), cancellable = true) 27 | public void renderExperienceBar$HEAD(PoseStack poseStack, int i, CallbackInfo ci) { 28 | QuadzClient.getQuadcopterFromCamera().ifPresent(quadcopter -> ci.cancel()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/entity/QuadcopterEntityRenderer.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.entity; 2 | 3 | import com.jme3.math.Quaternion; 4 | import com.mojang.blaze3d.vertex.PoseStack; 5 | import dev.lazurite.form.impl.client.render.TemplatedEntityRenderer; 6 | import dev.lazurite.quadz.common.entity.Quadcopter; 7 | import dev.lazurite.rayon.impl.bullet.math.Convert; 8 | import net.minecraft.client.renderer.MultiBufferSource; 9 | import net.minecraft.client.renderer.entity.EntityRendererProvider; 10 | 11 | public class QuadcopterEntityRenderer extends TemplatedEntityRenderer { 12 | 13 | public QuadcopterEntityRenderer(EntityRendererProvider.Context ctx) { 14 | super(ctx); 15 | } 16 | 17 | @Override 18 | public void render(Quadcopter quadcopterEntity, float entityYaw, float tickDelta, PoseStack stack, MultiBufferSource bufferIn, int packedLightIn) { 19 | float temp = quadcopterEntity.yBodyRot; 20 | quadcopterEntity.yBodyRot = 0; 21 | quadcopterEntity.yBodyRotO = 0; 22 | 23 | stack.pushPose(); 24 | stack.mulPose(Convert.toMinecraft(quadcopterEntity.getPhysicsRotation(new Quaternion(), tickDelta))); 25 | stack.translate(0, -quadcopterEntity.getBoundingBox().getYsize() / 2, 0); 26 | super.render(quadcopterEntity, 0, tickDelta, stack, bufferIn, packedLightIn); 27 | stack.popPose(); 28 | 29 | quadcopterEntity.yBodyRot = temp; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/resource/SplashResourceLoader.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.resource; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.resources.ResourceLocation; 7 | import net.minecraft.server.packs.resources.ResourceManager; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.nio.charset.StandardCharsets; 13 | 14 | public class SplashResourceLoader implements SimpleSynchronousResourceReloadListener { 15 | 16 | public static final ResourceLocation location = new ResourceLocation(Quadz.MODID, "misc/texts/splashes.txt"); 17 | 18 | @Override 19 | public ResourceLocation getFabricId() { 20 | return new ResourceLocation(Quadz.MODID, "splash"); 21 | } 22 | 23 | @Override 24 | public void onResourceManagerReload(ResourceManager manager) { 25 | manager.getResource(location).ifPresent(resource -> { 26 | try { 27 | final var bufferedReader = new BufferedReader(new InputStreamReader(resource.open(), StandardCharsets.UTF_8)); 28 | final var strings = bufferedReader.lines().map(String::trim).filter((string) -> string.hashCode() != 125780783).toList(); 29 | Minecraft.getInstance().getSplashManager().splashes.addAll(strings); 30 | } catch (IOException e) { 31 | throw new RuntimeException(e); 32 | } 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/Bindable.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | import dev.lazurite.quadz.common.item.QuadcopterItem; 4 | import dev.lazurite.quadz.common.item.RemoteItem; 5 | import net.minecraft.world.item.ItemStack; 6 | 7 | import java.util.Optional; 8 | import java.util.Random; 9 | 10 | /** 11 | * @since 2.0.0 12 | */ 13 | public interface Bindable { 14 | 15 | static void bind(Bindable b1, Bindable b2) { 16 | var random = new Random(); 17 | var bindId = random.nextInt(10000); 18 | b1.setBindId(bindId); 19 | b2.setBindId(bindId); 20 | } 21 | 22 | static Optional get(ItemStack stack) { 23 | return Optional.ofNullable(stack.getItem() instanceof RemoteItem || stack.getItem() instanceof QuadcopterItem ? new BindableItemWrapper(stack) : null); 24 | } 25 | 26 | static Optional get(ItemStack stack, int bindId) { 27 | return get(stack).filter(bindable -> bindable.isBoundTo(bindId)); 28 | } 29 | 30 | static Optional get(ItemStack stack, Bindable bindable) { 31 | return get(stack, bindable.getBindId()); 32 | } 33 | 34 | default boolean isBoundTo(Bindable bindable) { 35 | return isBoundTo(bindable.getBindId()); 36 | } 37 | 38 | default boolean isBoundTo(int bindId) { 39 | return getBindId() != -1 && getBindId() == bindId; 40 | } 41 | 42 | default void copyFrom(Bindable bindable) { 43 | this.setBindId(bindable.getBindId()); 44 | } 45 | 46 | void setBindId(int bindId); 47 | 48 | int getBindId(); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/screen/ScreenHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.screen; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import net.minecraft.client.Minecraft; 5 | import net.minecraft.client.gui.components.ImageButton; 6 | import net.minecraft.client.gui.screens.PauseScreen; 7 | import net.minecraft.client.gui.screens.Screen; 8 | import net.minecraft.client.gui.screens.TitleScreen; 9 | import net.minecraft.network.chat.Component; 10 | import net.minecraft.resources.ResourceLocation; 11 | 12 | public class ScreenHooks { 13 | 14 | public static void addQuadzButtonToPauseScreen(PauseScreen screen) { 15 | screen.addRenderableWidget(getButton( 16 | screen, 17 | screen.disconnectButton.getX() + screen.disconnectButton.getWidth() + 5, 18 | screen.disconnectButton.getY() 19 | )); 20 | } 21 | 22 | public static void addQuadzButtonToTitleScreen(TitleScreen screen) { 23 | screen.addRenderableWidget(getButton( 24 | screen, 25 | screen.width / 2 + 128, 26 | screen.height / 4 + 132) 27 | ); 28 | } 29 | 30 | private static ImageButton getButton(Screen parent, int x, int y) { 31 | return new ImageButton( 32 | x, y, 20, 20, 0, 0, 20, 33 | new ResourceLocation(Quadz.MODID, "textures/gui/quadz.png"), 34 | 32, 64, 35 | button -> Minecraft.getInstance().setScreen(new ControllerSetupScreen(parent)), 36 | Component.translatable("quadz.config.title") 37 | ); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "quadz", 4 | "version": "$version", 5 | "environment": "*", 6 | "entrypoints": { 7 | "main": [ "dev.lazurite.quadz.Quadz" ], 8 | "client": [ "dev.lazurite.quadz.client.QuadzClient" ] 9 | }, 10 | "mixins": [ 11 | { 12 | "config": "quadz.common.mixins.json", 13 | "environment": "*" 14 | }, 15 | { 16 | "config": "quadz.client.mixins.json", 17 | "environment": "client" 18 | } 19 | ], 20 | "accessWidener": "quadz.accessWidener", 21 | "depends": { 22 | "minecraft": ">=1.19.3", 23 | "fabricloader": ">=0.14", 24 | "fabric": "*", 25 | "geckolib": ">=4", 26 | "rayon": ">=1.6.2" 27 | }, 28 | "breaks": { 29 | "immersive_portals": "*", 30 | "canvas": "*" 31 | }, 32 | "name": "Quadz", 33 | "description": "An FPV drone simulator for Fabric.", 34 | "authors": [ "The Lazurite Team" ], 35 | "contact": { 36 | "homepage": "https://lazurite.dev", 37 | "sources": "https://github.com/LazuriteMC/Quadz", 38 | "issues": "https://github.com/LazuriteMC/Quadz/issues" 39 | }, 40 | "license": "MIT", 41 | "icon": "assets/quadz/icon.png", 42 | "custom": { 43 | "modmenu": { 44 | "links": { 45 | "modmenu.discord": "https://discord.gg/NNPPHN7b3P" 46 | } 47 | }, 48 | "loom:injected_interfaces": { 49 | "net/minecraft/class_5498": [ 50 | "dev/lazurite/quadz/client/extension/CameraTypeExtension" 51 | ], 52 | "net/minecraft/class_1657": [ 53 | "dev/lazurite/quadz/common/extension/PlayerExtension" 54 | ] 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/mixin/MinecraftMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.mixin; 2 | 3 | import dev.lazurite.corduroy.api.ViewStack; 4 | import dev.lazurite.quadz.client.QuadzClient; 5 | import dev.lazurite.quadz.client.render.RenderHooks; 6 | import dev.lazurite.quadz.common.entity.Quadcopter; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.util.profiling.ProfilerFiller; 9 | import net.minecraft.world.entity.Entity; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | 16 | @Mixin(Minecraft.class) 17 | public class MinecraftMixin { 18 | 19 | @Shadow private ProfilerFiller profiler; 20 | 21 | @Inject( 22 | method = "runTick", 23 | at = @At( 24 | value = "INVOKE", 25 | target = "Lnet/minecraft/client/gui/components/toasts/ToastComponent;render(Lcom/mojang/blaze3d/vertex/PoseStack;)V", 26 | shift = At.Shift.AFTER 27 | ) 28 | ) 29 | public void runTick$render(boolean bl, CallbackInfo ci) { 30 | RenderHooks.onRenderMinecraft(this.profiler); 31 | } 32 | 33 | @Inject(method = "setCameraEntity", at = @At("HEAD")) 34 | public void setCameraEntity(Entity entity, CallbackInfo ci) { 35 | if (QuadzClient.getQuadcopterFromCamera().isEmpty() && entity instanceof Quadcopter quadcopter) { 36 | ViewStack.getInstance().push(quadcopter.getView()); 37 | } else { 38 | ViewStack.getInstance().pop(); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/hooks/ServerEventHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.hooks; 2 | 3 | import dev.lazurite.form.api.loader.TemplateLoader; 4 | import dev.lazurite.form.impl.common.template.model.Template; 5 | import dev.lazurite.quadz.Quadz; 6 | import dev.lazurite.quadz.common.entity.Quadcopter; 7 | import dev.lazurite.rayon.impl.bullet.collision.body.shape.MinecraftShape; 8 | import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroupEntries; 9 | import net.minecraft.world.entity.Entity; 10 | import net.minecraft.world.item.ItemStack; 11 | 12 | public class ServerEventHooks { 13 | 14 | public static void onEntityTemplateChanged(Entity entity) { 15 | if (entity instanceof Quadcopter quadcopter) { 16 | quadcopter.getRigidBody().setCollisionShape(MinecraftShape.box(quadcopter.getBoundingBox())); 17 | 18 | TemplateLoader.getTemplateById(quadcopter.getTemplate()).ifPresent(template -> { 19 | quadcopter.getEntityData().set(Quadcopter.CAMERA_ANGLE, template.metadata().get("cameraAngle").getAsInt()); 20 | quadcopter.getRigidBody().setMass(template.metadata().get("mass").getAsFloat()); 21 | quadcopter.getRigidBody().setDragCoefficient(template.metadata().get("dragCoefficient").getAsFloat()); 22 | }); 23 | } 24 | } 25 | 26 | public static void onTemplateLoaded(Template template) { 27 | Quadz.CREATIVE_MODE_TAB.rebuildSearchTree(); 28 | } 29 | 30 | public static void onRebuildCreativeTab(FabricItemGroupEntries content) { 31 | content.accept(new ItemStack(Quadz.GOGGLES_ITEM)); 32 | content.accept(new ItemStack(Quadz.REMOTE_ITEM)); 33 | 34 | TemplateLoader.getTemplateByModId(Quadz.MODID).forEach( 35 | template -> content.accept(TemplateLoader.getItemStackFor(template, Quadz.QUADCOPTER_ITEM)) 36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity.quadz.quadcopter" : "Quadcopter", 3 | 4 | "item.quadz.quadcopter" : "Quadcopter", 5 | "item.quadz.goggles" : "Goggles", 6 | "item.quadz.remote" : "Remote", 7 | 8 | "quadz.message.bound" : "Quadcopter Bound", 9 | 10 | "quadz.config.title" : "Quadz Config", 11 | 12 | "quadz.config.controller.title" : "Controller", 13 | "quadz.config.controller.rate" : "Rate", 14 | "quadz.config.controller.superRate" : "Super Rate", 15 | "quadz.config.controller.expo" : "Expo", 16 | "quadz.config.controller.deadzone" : "Deadzone", 17 | 18 | "quadz.config.visuals.title" : "Visuals", 19 | "quadz.config.visuals.static_toggle" : "Video Interference", 20 | "quadz.config.visuals.fisheye_toggle" : "Fisheye Lens", 21 | "quadz.config.visuals.fisheye_amount" : "Fisheye Amount", 22 | "quadz.config.visuals.osd_toggle" : "On Screen Display", 23 | "quadz.config.visuals.follow_los_toggle" : "Line of Sight Following", 24 | "quadz.config.visuals.velocity_unit" : "Velocity Unit", 25 | "quadz.config.visuals.camera_in_center_toggle" : "Camera in Center", 26 | "quadz.config.visuals.first_person_render_toggle" : "Render in First Person", 27 | 28 | "quadz.config.controller_setup.title" : "Controller Setup", 29 | "quadz.config.controller_setup.config" : "Config", 30 | "quadz.config.controller_setup.save" : "Save", 31 | "quadz.config.controller_setup.cancel" : "Cancel", 32 | "quadz.config.controller_setup.pitch": "Pitch", 33 | "quadz.config.controller_setup.yaw": "Yaw", 34 | "quadz.config.controller_setup.roll": "Roll", 35 | "quadz.config.controller_setup.throttle": "Throttle", 36 | "quadz.config.controller_setup.invert": "Invert", 37 | "quadz.config.controller_setup.prompt": "Move the correct stick to assign the axis.", 38 | 39 | "quadz.toast.connect" : "Connected", 40 | "quadz.toast.disconnect" : "Disconnected", 41 | 42 | "quadz.velocity.meters_per_second" : "Meters per Second", 43 | "quadz.velocity.miles_per_hour" : "Miles per Hour", 44 | "quadz.velocity.kilometers_per_hour" : "Kilometers per Hour" 45 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/screen/ControllerConnectedToast.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.screen; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import com.mojang.blaze3d.vertex.PoseStack; 5 | import dev.lazurite.quadz.Quadz; 6 | import net.minecraft.client.Minecraft; 7 | import net.minecraft.client.gui.components.toasts.Toast; 8 | import net.minecraft.client.gui.components.toasts.ToastComponent; 9 | import net.minecraft.client.renderer.GameRenderer; 10 | import net.minecraft.network.chat.Component; 11 | import net.minecraft.world.item.ItemStack; 12 | 13 | public class ControllerConnectedToast implements Toast { 14 | 15 | private final Component message; 16 | private final String controllerName; 17 | 18 | public ControllerConnectedToast(Component message, String controllerName) { 19 | this.message = message; 20 | 21 | if (controllerName.length() > 25) { 22 | this.controllerName = controllerName.substring(0, 25) + "..."; 23 | } else { 24 | this.controllerName = controllerName; 25 | } 26 | } 27 | 28 | @Override 29 | public Visibility render(PoseStack poseStack, ToastComponent toastComponent, long startTime) { 30 | RenderSystem.setShader(GameRenderer::getPositionTexShader); 31 | RenderSystem.setShaderTexture(0, TEXTURE); 32 | RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); 33 | 34 | toastComponent.blit(poseStack, 0, 0, 0, 0, width(), height()); 35 | toastComponent.getMinecraft().font.draw(poseStack, message, 30, 7, -1); 36 | toastComponent.getMinecraft().font.draw(poseStack, Component.literal(controllerName), 30, 18, -1); 37 | toastComponent.getMinecraft().getItemRenderer().renderAndDecorateFakeItem(new ItemStack(Quadz.REMOTE_ITEM), 8, 8); 38 | 39 | return startTime >= 5000L ? Visibility.HIDE : Visibility.SHOW; 40 | } 41 | 42 | public static void add(Component message, String name) { 43 | var manager = Minecraft.getInstance().getToasts(); 44 | manager.addToast(new ControllerConnectedToast(message, name)); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/mixin/GameRendererMixin.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.mixin; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import dev.lazurite.quadz.client.QuadzClient; 5 | import dev.lazurite.quadz.client.render.RenderHooks; 6 | import net.minecraft.client.Camera; 7 | import net.minecraft.client.renderer.GameRenderer; 8 | import org.joml.Quaternionf; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.ModifyArg; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(GameRenderer.class) 16 | public class GameRendererMixin { 17 | 18 | @Inject(method = "renderLevel", at = @At("HEAD")) 19 | public void renderLevel$HEAD(float f, long l, PoseStack poseStack, CallbackInfo ci) { 20 | RenderHooks.onRenderLevel(f); 21 | } 22 | 23 | @ModifyArg( 24 | method = "renderLevel", 25 | at = @At( 26 | value = "INVOKE", 27 | target = "Lcom/mojang/blaze3d/vertex/PoseStack;mulPose(Lorg/joml/Quaternionf;)V", 28 | ordinal = 2 29 | ) 30 | ) 31 | public Quaternionf renderLevel$multiplyYaw(Quaternionf quaternion) { 32 | return RenderHooks.onMultiplyYaw(quaternion); 33 | } 34 | 35 | @ModifyArg( 36 | method = "renderLevel", 37 | at = @At( 38 | value = "INVOKE", 39 | target = "Lcom/mojang/blaze3d/vertex/PoseStack;mulPose(Lorg/joml/Quaternionf;)V", 40 | ordinal = 3 41 | ) 42 | ) 43 | public Quaternionf renderLevel$multiplyPitch(Quaternionf quaternion) { 44 | return RenderHooks.onMultiplyPitch(quaternion); 45 | } 46 | 47 | @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) 48 | private void renderItemInHand$HEAD(PoseStack poseStack, Camera camera, float f, CallbackInfo ci) { 49 | if (QuadzClient.getQuadcopterFromCamera().isPresent()) { 50 | ci.cancel(); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/JoystickOutput.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | import com.google.common.collect.Maps; 4 | import dev.lazurite.quadz.client.Config; 5 | import net.minecraft.resources.ResourceLocation; 6 | import net.minecraft.world.entity.player.Player; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.nio.FloatBuffer; 10 | import java.util.Map; 11 | 12 | import static org.lwjgl.glfw.GLFW.*; 13 | 14 | public interface JoystickOutput { 15 | 16 | Map JOYSTICKS = Maps.newConcurrentMap(); 17 | 18 | static boolean controllerExists() { 19 | return Config.controllerId != -1 && glfwJoystickPresent(Config.controllerId); 20 | } 21 | 22 | static FloatBuffer getAllAxisValues() { 23 | if (glfwJoystickPresent(Config.controllerId)) { 24 | return glfwGetJoystickAxes(Config.controllerId).duplicate(); 25 | } 26 | 27 | return null; 28 | } 29 | 30 | static float getAxisValue(@Nullable Player player, int axis, ResourceLocation resourceLocation, boolean inverted, boolean halved) { 31 | var deadzone = Config.deadzone; 32 | var value = 0.0f; 33 | 34 | if (glfwJoystickPresent(Config.controllerId)) { 35 | value = glfwGetJoystickAxes(Config.controllerId).duplicate().get(axis); 36 | 37 | value = inverted ? -value : value; 38 | 39 | value = halved ? value >= 0.0f ? value * 2.0f - 1.0f : -1.0f : value; 40 | 41 | value = deadzone != 0 && value < deadzone * 0.5f && value > -deadzone * 0.5f ? 0.0f : value; 42 | } 43 | 44 | if (player != null && player.level.isClientSide()) { 45 | /* Queue for transmission to the server */ 46 | player.quadz$setJoystickValue(resourceLocation, value); 47 | } else if (player != null && !player.level.isClientSide()) { 48 | /* Server-side retrieval */ 49 | return player.quadz$getJoystickValue(resourceLocation); 50 | } 51 | 52 | return value; 53 | } 54 | 55 | static String getJoystickName(int id) { 56 | if (glfwJoystickPresent(id)) { 57 | return glfwGetJoystickName(id); 58 | } 59 | 60 | return null; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/geo/item/armor/goggles.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:geometry": [ 4 | { 5 | "description": { 6 | "identifier": "geometry.goggles", 7 | "texture_width": 32, 8 | "texture_height": 32, 9 | "visible_bounds_width": 2, 10 | "visible_bounds_height": 3.5, 11 | "visible_bounds_offset": [0, 1.25, 0] 12 | }, 13 | "bones": [ 14 | { 15 | "name": "bipedHead", 16 | "pivot": [0, 24, 0] 17 | }, 18 | { 19 | "name": "armorHead", 20 | "parent": "bipedHead", 21 | "pivot": [0, 23.75, 0], 22 | "cubes": [ 23 | { 24 | "origin": [-4.75, 26.25, -7], 25 | "size": [9.5, 4, 2.9], 26 | "uv": { 27 | "north": {"uv": [0, 0], "uv_size": [9, 4]}, 28 | "east": {"uv": [0, 13], "uv_size": [3, 4]}, 29 | "south": {"uv": [0, 4], "uv_size": [9, 4]}, 30 | "west": {"uv": [3, 13], "uv_size": [3, 4]}, 31 | "up": {"uv": [0, 8], "uv_size": [9, 3]}, 32 | "down": {"uv": [9, 3], "uv_size": [9, -3]} 33 | } 34 | }, 35 | { 36 | "origin": [4.1, 27.5, -4.1], 37 | "size": [0.5, 2, 8.2], 38 | "uv": { 39 | "north": {"uv": [10, 15], "uv_size": [1, 2]}, 40 | "east": {"uv": [9, 7], "uv_size": [8, 2]}, 41 | "south": {"uv": [11, 15], "uv_size": [1, 2]}, 42 | "west": {"uv": [9, 9], "uv_size": [8, 2]}, 43 | "up": {"uv": [6, 15], "uv_size": [1, 8]}, 44 | "down": {"uv": [7, 23], "uv_size": [1, -8]} 45 | } 46 | }, 47 | { 48 | "origin": [-4.6, 27.5, -4.15], 49 | "size": [0.5, 2, 8.25], 50 | "uv": { 51 | "north": {"uv": [12, 15], "uv_size": [1, 2]}, 52 | "east": {"uv": [0, 11], "uv_size": [8, 2]}, 53 | "south": {"uv": [13, 15], "uv_size": [1, 2]}, 54 | "west": {"uv": [8, 11], "uv_size": [8, 2]}, 55 | "up": {"uv": [8, 15], "uv_size": [1, 8]}, 56 | "down": {"uv": [9, 23], "uv_size": [1, -8]} 57 | } 58 | }, 59 | { 60 | "origin": [-4.6, 27.5, 4.1], 61 | "size": [9.2, 2, 0.5], 62 | "uv": { 63 | "north": {"uv": [9, 3], "uv_size": [9, 2]}, 64 | "east": {"uv": [15, 13], "uv_size": [1, 2]}, 65 | "south": {"uv": [9, 5], "uv_size": [9, 2]}, 66 | "west": {"uv": [14, 15], "uv_size": [1, 2]}, 67 | "up": {"uv": [6, 13], "uv_size": [9, 1]}, 68 | "down": {"uv": [6, 15], "uv_size": [9, -1]} 69 | } 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/hooks/PlayerHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.hooks; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import dev.lazurite.quadz.common.entity.Quadcopter; 5 | import dev.lazurite.toolbox.api.network.ClientNetworking; 6 | import dev.lazurite.toolbox.api.network.ServerNetworking; 7 | import net.minecraft.resources.ResourceLocation; 8 | import net.minecraft.server.level.ServerPlayer; 9 | import net.minecraft.world.entity.player.Player; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | public class PlayerHooks { 16 | 17 | private static final Map joystickValues = new ConcurrentHashMap<>(); 18 | 19 | public static float onGetJoystickValues(ResourceLocation axis) { 20 | return joystickValues.get(axis) == null ? 0.0f : joystickValues.get(axis); 21 | } 22 | 23 | public static Map onGetAllAxes() { 24 | return new HashMap<>(joystickValues); 25 | } 26 | 27 | public static void onSetJoystickValue(ResourceLocation axis, float value) { 28 | joystickValues.put(axis, value); 29 | } 30 | 31 | public static void onSyncJoystick(Player player) { 32 | var level = player.level; 33 | var joysticks = new HashMap<>(joystickValues); 34 | 35 | if (level.isClientSide()) { 36 | ClientNetworking.send(Quadz.Networking.JOYSTICK_INPUT, buf -> { 37 | buf.writeInt(joysticks.size()); 38 | 39 | joysticks.forEach((axis, value) -> { 40 | buf.writeResourceLocation(axis); 41 | buf.writeFloat(value); 42 | }); 43 | }); 44 | } else { 45 | level.players().forEach(p -> { 46 | if (!p.equals(player) && p instanceof ServerPlayer serverPlayer) { 47 | ServerNetworking.send(serverPlayer, Quadz.Networking.JOYSTICK_INPUT, buf -> { 48 | buf.writeUUID(serverPlayer.getUUID()); 49 | buf.writeInt(joysticks.size()); 50 | 51 | joysticks.forEach((axis, value) -> { 52 | buf.writeResourceLocation(axis); 53 | buf.writeFloat(value); 54 | }); 55 | }); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | /** 62 | * Prevents the camera from going back to the {@link ServerPlayer} 63 | * when they crouch at this particular instance of the game loop. 64 | */ 65 | public static boolean onWantsToStopRiding(ServerPlayer player) { 66 | return !(player.getCamera() instanceof Quadcopter) && player.isCrouching(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/camera/CameraHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.camera; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import dev.lazurite.quadz.client.QuadzClient; 5 | import dev.lazurite.quadz.common.entity.Quadcopter; 6 | import dev.lazurite.toolbox.api.network.ClientNetworking; 7 | import net.minecraft.client.CameraType; 8 | import net.minecraft.client.Minecraft; 9 | 10 | import java.util.Optional; 11 | import java.util.function.Consumer; 12 | 13 | public class CameraHooks { 14 | 15 | private static int index = 0; 16 | 17 | public static void onCameraReset() { 18 | index = 0; 19 | Minecraft.getInstance().options.setCameraType(CameraType.FIRST_PERSON); 20 | ClientNetworking.send(Quadz.Networking.REQUEST_PLAYER_VIEW, buf -> {}); 21 | } 22 | 23 | public static Optional onCycle() { 24 | var player = Minecraft.getInstance().player; 25 | 26 | /* 27 | Check to make sure the conditions for player viewing haven't changed. 28 | If they have changed, switch the player's view back to themself. 29 | */ 30 | Consumer changeIndex = quadcopter -> { 31 | var continueViewing = quadcopter.shouldPlayerBeViewing(player); 32 | index = (index + 1) % (continueViewing ? 5 : 3); 33 | 34 | if (index >= 0 && index <= 2) { 35 | if (QuadzClient.getQuadcopterFromCamera().isPresent()) { 36 | ClientNetworking.send(Quadz.Networking.REQUEST_PLAYER_VIEW, buf -> {}); 37 | } 38 | } 39 | }; 40 | 41 | QuadzClient.getQuadcopterFromRemote().ifPresentOrElse(changeIndex, // get quadcopter from remote or... 42 | () -> QuadzClient.getQuadcopterFromCamera().ifPresentOrElse(changeIndex, // get quadcopter from camera or... 43 | () -> index = (index + 1) % 3) // just do default behavior 44 | ); 45 | 46 | return switch (index) { 47 | case 0 -> Optional.of(CameraType.FIRST_PERSON); 48 | case 1 -> Optional.of(CameraType.THIRD_PERSON_BACK); 49 | case 2 -> Optional.of(CameraType.THIRD_PERSON_FRONT); 50 | case 3 -> { 51 | ClientNetworking.send(Quadz.Networking.REQUEST_QUADCOPTER_VIEW, buf -> buf.writeInt(0)); 52 | yield Optional.of(CameraType.FIRST_PERSON); 53 | } 54 | case 4 -> { 55 | if (QuadzClient.getQuadcopterFromCamera().isEmpty()) { 56 | index = 1; 57 | yield Optional.of(CameraType.THIRD_PERSON_BACK); 58 | } 59 | yield Optional.empty(); 60 | } 61 | default -> Optional.empty(); 62 | }; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/item/GogglesItem.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.item; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import net.minecraft.client.model.HumanoidModel; 5 | import net.minecraft.resources.ResourceLocation; 6 | import net.minecraft.world.entity.EquipmentSlot; 7 | import net.minecraft.world.entity.LivingEntity; 8 | import net.minecraft.world.item.ArmorItem; 9 | import net.minecraft.world.item.ArmorMaterials; 10 | import net.minecraft.world.item.ItemStack; 11 | import org.jetbrains.annotations.NotNull; 12 | import software.bernie.geckolib.animatable.GeoItem; 13 | import software.bernie.geckolib.animatable.client.RenderProvider; 14 | import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; 15 | import software.bernie.geckolib.core.animation.AnimatableManager; 16 | import software.bernie.geckolib.core.animation.AnimationController; 17 | import software.bernie.geckolib.core.object.PlayState; 18 | import software.bernie.geckolib.model.DefaultedItemGeoModel; 19 | import software.bernie.geckolib.renderer.GeoArmorRenderer; 20 | import software.bernie.geckolib.util.GeckoLibUtil; 21 | 22 | import java.util.function.Consumer; 23 | import java.util.function.Supplier; 24 | 25 | /** 26 | * Represents the goggles a player wears in order to see their quadcopter's POV. 27 | */ 28 | public class GogglesItem extends ArmorItem implements GeoItem { 29 | 30 | private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); 31 | private final Supplier renderProvider = GeoItem.makeRenderer(this); 32 | 33 | public GogglesItem() { 34 | super( 35 | ArmorMaterials.LEATHER, 36 | EquipmentSlot.HEAD, 37 | new Properties().stacksTo(1) 38 | ); 39 | } 40 | 41 | @Override 42 | public void createRenderer(Consumer consumer) { 43 | consumer.accept(new RenderProvider() { 44 | private GeoArmorRenderer renderer; 45 | 46 | @Override 47 | public @NotNull HumanoidModel getHumanoidArmorModel(LivingEntity livingEntity, ItemStack itemStack, EquipmentSlot equipmentSlot, HumanoidModel original) { 48 | if (this.renderer == null) { 49 | this.renderer = new GeoArmorRenderer<>(new DefaultedItemGeoModel<>(new ResourceLocation(Quadz.MODID, "armor/goggles"))); 50 | } 51 | 52 | this.renderer.prepForRender(livingEntity, itemStack, equipmentSlot, original); 53 | return this.renderer; 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | public Supplier getRenderProvider() { 60 | return this.renderProvider; 61 | } 62 | 63 | @Override 64 | public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { 65 | controllers.add(new AnimationController<>(this, 20, state -> PlayState.CONTINUE)); 66 | } 67 | 68 | @Override 69 | public AnimatableInstanceCache getAnimatableInstanceCache() { 70 | return this.cache; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/event/ClientEventHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.event; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import dev.lazurite.quadz.client.QuadzClient; 5 | import dev.lazurite.quadz.common.util.JoystickOutput; 6 | import dev.lazurite.quadz.client.Config; 7 | import dev.lazurite.quadz.client.render.screen.ControllerConnectedToast; 8 | import dev.lazurite.toolbox.api.network.ClientNetworking; 9 | import net.minecraft.client.Minecraft; 10 | import net.minecraft.client.multiplayer.ClientLevel; 11 | import net.minecraft.client.player.LocalPlayer; 12 | import net.minecraft.network.chat.Component; 13 | import net.minecraft.resources.ResourceLocation; 14 | 15 | public class ClientEventHooks { 16 | 17 | public static void onPostLogin(Minecraft minecraft, ClientLevel level, LocalPlayer player) { 18 | if (player != null) { 19 | player.quadz$setJoystickValue(new ResourceLocation(Quadz.MODID, "rate"), Config.rate); 20 | player.quadz$setJoystickValue(new ResourceLocation(Quadz.MODID, "super_rate"), Config.superRate); 21 | player.quadz$setJoystickValue(new ResourceLocation(Quadz.MODID, "expo"), Config.expo); 22 | } 23 | } 24 | 25 | public static void onClientTick(Minecraft minecraft) { 26 | if (!minecraft.isPaused() && minecraft.player != null && JoystickOutput.controllerExists()) { 27 | JoystickOutput.getAxisValue(minecraft.player, Config.pitch, new ResourceLocation(Quadz.MODID, "pitch"), Config.pitchInverted, false); 28 | JoystickOutput.getAxisValue(minecraft.player, Config.yaw, new ResourceLocation(Quadz.MODID, "yaw"), Config.yawInverted, false); 29 | JoystickOutput.getAxisValue(minecraft.player, Config.roll, new ResourceLocation(Quadz.MODID, "roll"), Config.rollInverted, false); 30 | JoystickOutput.getAxisValue(minecraft.player, Config.throttle, new ResourceLocation(Quadz.MODID, "throttle"), Config.throttleInverted, Config.throttleInCenter); 31 | } 32 | } 33 | 34 | public static void onJoystickConnect(int id, String name) { 35 | ControllerConnectedToast.add(Component.translatable("quadz.toast.connect"), name); 36 | } 37 | 38 | public static void onJoystickDisconnect(int id, String name) { 39 | ControllerConnectedToast.add(Component.translatable("quadz.toast.disconnect"), name); 40 | } 41 | 42 | public static void onLeftClick() { 43 | ClientNetworking.send(Quadz.Networking.REQUEST_QUADCOPTER_VIEW, buf -> buf.writeInt(-1)); 44 | } 45 | 46 | public static void onRightClick() { 47 | ClientNetworking.send(Quadz.Networking.REQUEST_QUADCOPTER_VIEW, buf -> buf.writeInt(1)); 48 | } 49 | 50 | public static void onClientLevelTick(ClientLevel level) { 51 | final var client = Minecraft.getInstance(); 52 | 53 | if (!client.isPaused()) { 54 | client.player.quadz$syncJoystick(); 55 | 56 | if (client.options.keyShift.isDown() && QuadzClient.getQuadcopterFromCamera().isPresent()) { 57 | client.options.getCameraType().quadz$reset(); 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Quadz 1.0.0 - At long last 2 | 3 | ### Overview 4 | The coveted new release of Quadz (formerly FPV Racing Mod) is here! 5 | 6 | Internally, the mod has been almost completely rewritten due to its constant revision over 7 | the past six months. A few new features of note are: 8 | * FPV Racing was renamed to Quadz 9 | * Physics was broken out into a separate lib called [Rayon](https://github.com/LazuriteMC/Rayon) 10 | * Chunk loading was broken out into a separate lib called [Lattice](https://github.com/LazuriteMC/Lattice) 11 | * A config screen was added (using [Cloth](https://www.curseforge.com/minecraft/mc-mods/cloth-config) and [Fiber](https://github.com/FabLabsMC/fiber)) 12 | * Quadcopters have actual models now and are animated by [Gecko Lib](https://www.curseforge.com/minecraft/mc-mods/geckolib-fabric) 13 | * Additional quadcopters can be loaded by users 14 | * Configurable quadcopter settings (on right-click) 15 | * A new goggles model when worn 16 | * Keyboard support 17 | * Angle mode 18 | * LOS (line of sight) camera following 19 | * An OSD (on screen display) 20 | * Toast popups showing controller connections/disconnections 21 | 22 | ### Rayon 23 | For those who don't know, Quadz uses the bullet physics engine to simulate realistic flight characteristics as well as 24 | collision handling. Until this release, Quadz included bullet integration into Minecraft as a part of its code. Now, 25 | that part has been broken out into a separate library called [Rayon](https://github.com/LazuriteMC/Rayon) which can be 26 | applied to other mods besides Quadz. A couple examples would be [Dropz](https://github.com/LazuriteMC/Dropz) and 27 | [Thinking With Portatos](https://github.com/Fusion-Flux/Thinking-With-Portatos). More information on how to use the API 28 | for other mods can be found [here](https://docs.lazurite.dev/rayon/getting-started). 29 | 30 | ### Lattice 31 | Quadz features another new subsystem called [Lattice](https://github.com/LazuriteMC/Lattice) which allows chunks to load 32 | not only around each player but also around each player's camera. The effect isn't noticeable unless the player and it's 33 | camera are in two separate locations. For Quadz, this means that quadcopters flown in first-person-view are capable of 34 | generating the world independently of the player. This was a feature of Quadz in the past (FPV Racing back then), but it 35 | was achieved by moving the player along with the quadcopter which was :concern:. 36 | 37 | ### Quadcopter Templates 38 | Data driven quadcopters are now possible in a similar way to how Minecraft's resource or data packs work. It allows 39 | users to pack quadcopter information into a zip file or a folder and load from their `.minecraft/quadz` directory. 40 | Additionally, any quadcopter template loaded into the game is transferable to other clients and servers. When a player 41 | joins a server, the server will send all of its known templates to that client and also receive all of the client's 42 | templates as well. This basically allows players to see templates created by other users without having to download it 43 | themselves! There are instructions, and an example on how to create a quadcopter template 44 | [here](https://github.com/LazuriteMC/Quadz-Template). -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/hooks/ServerNetworkEventHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.hooks; 2 | 3 | import dev.lazurite.quadz.common.util.Bindable; 4 | import dev.lazurite.quadz.common.util.Search; 5 | import dev.lazurite.quadz.common.util.event.CameraEvents; 6 | import dev.lazurite.quadz.common.entity.Quadcopter; 7 | import dev.lazurite.toolbox.api.network.PacketRegistry; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Optional; 11 | 12 | public class ServerNetworkEventHooks { 13 | 14 | public static void onJoystickInput(PacketRegistry.ServerboundContext context) { 15 | var player = context.player(); 16 | var buf = context.byteBuf(); 17 | var axisCount = buf.readInt(); 18 | 19 | for (int i = 0; i < axisCount; i++) { 20 | var axis = buf.readResourceLocation(); 21 | var value = buf.readFloat(); 22 | player.quadz$setJoystickValue(axis, value); 23 | } 24 | } 25 | 26 | public static void onQuadcopterViewRequested(PacketRegistry.ServerboundContext context) { 27 | var player = context.player(); 28 | var buf = context.byteBuf(); 29 | var spectateDirection = buf.readInt(); 30 | 31 | Optional.ofNullable(player.getServer()).ifPresent(server -> { 32 | server.execute(() -> { 33 | if (player.getCamera() instanceof Quadcopter quadcopter) { 34 | var allQuadcopters = new ArrayList<>(Search.forAllViewed(server)); 35 | var index = Math.max(allQuadcopters.lastIndexOf(quadcopter) + spectateDirection, 0); 36 | var entity = allQuadcopters.get(index % allQuadcopters.size()); 37 | player.setCamera(entity); 38 | CameraEvents.SWITCH_CAMERA_EVENT.invoke(player.getCamera(), entity); 39 | } else { 40 | Bindable.get(player.getMainHandItem()).ifPresent(bindable -> { 41 | Search.forQuadWithBindId( 42 | player.getLevel(), 43 | player.getCamera().position(), 44 | bindable.getBindId(), 45 | server.getPlayerList().getViewDistance() * 16) 46 | .ifPresentOrElse(entity -> { 47 | player.setCamera(entity); 48 | CameraEvents.SWITCH_CAMERA_EVENT.invoke(player.getCamera(), entity); 49 | }, () -> Search.forAllViewed(server).stream().findFirst().ifPresent(entity -> { 50 | player.setCamera(entity); 51 | CameraEvents.SWITCH_CAMERA_EVENT.invoke(player.getCamera(), entity); 52 | })); 53 | }); 54 | 55 | } 56 | }); 57 | }); 58 | } 59 | 60 | public static void onPlayerViewRequestReceived(PacketRegistry.ServerboundContext context) { 61 | var player = context.player(); 62 | Optional.ofNullable(player.getServer()).ifPresent(server -> server.execute(() -> player.setCamera(player))); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![icon](https://raw.githubusercontent.com/lazuritemc/Quadz/master/src/main/resources/assets/quadz/icon.png) 2 | 3 | # Quadz 4 | 5 | [![GitHub](https://img.shields.io/github/license/LazuriteMC/Quadz?color=A31F34&label=License&labelColor=8A8B8C)](https://github.com/LazuriteMC/Quadz/blob/development/LICENSE) 6 | [![Discord](https://img.shields.io/discord/719662192601071747?color=7289DA&label=Discord&labelColor=2C2F33&logo=Discord)](https://discord.gg/NNPPHN7b3P) 7 | [![Trello](https://img.shields.io/static/v1?label=Trello&message=Board&color=FFFFFF&logo=Trello&labelColor=0052CC)](https://trello.com/b/naSFhSWz) 8 | 9 | Quadz adds flyable FPV drones to Minecraft using the [Fabric Modloader and API](https://fabricmc.net/). It also makes 10 | use of several libraries including [Rayon](https://github.com/lazuritemc/rayon), a rigid body simulation library for minecraft, which was 11 | developed with this mod in mind. Another library it uses is called [Lattice](https://github.com/lazuritemc/lattice) which allows chunks 12 | to be loaded around the player's camera instead of just the player itself. 13 | 14 | A full list of changes can be found [here](https://github.com/LazuriteMC/Quadz/blob/development/changelog.md). For instructions on how 15 | to use quadcopter templates, look [here](https://github.com/LazuriteMC/Quadz-Template). 16 | 17 | ![banner](https://raw.githubusercontent.com/lazuritemc/Quadz/master/src/main/resources/assets/quadz/misc/images/banner.png) 18 | 19 | ### Background 20 | This mod began in late 2018 as a fun side project. Originally, it was just [BlueVista](https://github.com/ethanejohnsons) developing it 21 | on his own whenever he had time. The original codebase was written to work with Forge but was later changed to Fabric due to its ease 22 | of use and other benefits. It wasn't until May 2020 during COVID-19 lockdown that this mod began to take shape, and the first release came 23 | the following September. 24 | 25 | The old forge repository can be found [here](https://github.com/ethanejohnsons/FPV-Racing-Mod). 26 | 27 | ### Dependencies 28 | In order to use this mod, you'll have to download a few other things as well: 29 | * [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api) 30 | * [Geckolib](https://www.curseforge.com/minecraft/mc-mods/geckolib-fabric) 31 | * [Rayon](https://www.curseforge.com/minecraft/mc-mods/rayon) 32 | 33 | ### Optional Dependencies (Highly Recommended) 34 | The following dependencies allow you to configure Quadz using its config screen. If you don't install these, you won't 35 | be able to change your controller or camera settings. 36 | * [Cloth Config API](https://www.curseforge.com/minecraft/mc-mods/cloth-config) 37 | * [Mod Menu](https://www.curseforge.com/minecraft/mc-mods/modmenu) 38 | 39 | ### Conflicts 40 | The following mods are incompatible with Quadz at the moment but will hopefully work later on in development. 41 | * [Immersive Portals](https://www.curseforge.com/minecraft/mc-mods/immersive-portals-mod) 42 | * [Canvas](https://www.curseforge.com/minecraft/mc-mods/canvas-renderer) 43 | * [Lithium](https://www.curseforge.com/minecraft/mc-mods/lithium) 44 | 45 | ### Wiki 46 | We have a wiki now! It contains a list of known controller mappings. It can be found [here](https://docs.lazurite.dev/quadz/). 47 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/QuadzClient.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client; 2 | 3 | import dev.lazurite.corduroy.api.ViewStack; 4 | import dev.lazurite.form.api.Templated; 5 | import dev.lazurite.form.api.render.FormRegistry; 6 | import dev.lazurite.quadz.Quadz; 7 | import dev.lazurite.quadz.client.render.QuadcopterView; 8 | import dev.lazurite.quadz.client.render.entity.QuadcopterEntityRenderer; 9 | import dev.lazurite.quadz.common.util.Bindable; 10 | import dev.lazurite.quadz.common.util.Search; 11 | import dev.lazurite.quadz.common.util.event.ClickEvents; 12 | import dev.lazurite.quadz.common.util.event.JoystickEvents; 13 | import dev.lazurite.quadz.client.event.ClientEventHooks; 14 | import dev.lazurite.quadz.client.event.ClientNetworkEventHooks; 15 | import dev.lazurite.quadz.client.resource.SplashResourceLoader; 16 | import dev.lazurite.quadz.common.entity.Quadcopter; 17 | import dev.lazurite.toolbox.api.event.ClientEvents; 18 | import dev.lazurite.toolbox.api.network.PacketRegistry; 19 | import net.fabricmc.api.ClientModInitializer; 20 | import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; 21 | import net.fabricmc.fabric.api.resource.ResourceManagerHelper; 22 | import net.minecraft.client.Minecraft; 23 | import net.minecraft.server.packs.PackType; 24 | 25 | import java.util.Optional; 26 | 27 | public class QuadzClient implements ClientModInitializer { 28 | 29 | /** 30 | * Finds the player's quadcopter based on its camera view. 31 | */ 32 | public static Optional getQuadcopterFromCamera() { 33 | return ViewStack.getInstance() 34 | .peek() 35 | .filter(view -> view instanceof QuadcopterView) 36 | .map(view -> ((QuadcopterView) view).getQuadcopter()); 37 | } 38 | 39 | /** 40 | * Finds the player's quadcopter based on its held remote. 41 | *

42 | * Only works if it's within a 256 block radius. 43 | */ 44 | public static Optional getQuadcopterFromRemote() { 45 | var player = Minecraft.getInstance().player; 46 | 47 | if (player != null) { 48 | return Bindable.get(player.getMainHandItem()).flatMap(remote -> Search.forQuadWithBindId(player.level, player.position(), remote.getBindId(), 256)); 49 | } 50 | 51 | return Optional.empty(); 52 | } 53 | 54 | @Override 55 | public void onInitializeClient() { 56 | Config.load(); 57 | 58 | // Renderer 59 | EntityRendererRegistry.register(Quadz.QUADCOPTER, QuadcopterEntityRenderer::new); 60 | FormRegistry.register((Templated.Item) Quadz.QUADCOPTER_ITEM); 61 | 62 | // Splash screen text injection 63 | ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new SplashResourceLoader()); 64 | 65 | // Events 66 | ClientEvents.Lifecycle.POST_LOGIN.register(ClientEventHooks::onPostLogin); 67 | ClientEvents.Tick.START_CLIENT_TICK.register(ClientEventHooks::onClientTick); 68 | ClientEvents.Tick.START_LEVEL_TICK.register(ClientEventHooks::onClientLevelTick); 69 | JoystickEvents.JOYSTICK_CONNECT.register(ClientEventHooks::onJoystickConnect); 70 | JoystickEvents.JOYSTICK_DISCONNECT.register(ClientEventHooks::onJoystickDisconnect); 71 | ClickEvents.LEFT_CLICK_EVENT.register(ClientEventHooks::onLeftClick); 72 | ClickEvents.RIGHT_CLICK_EVENT.register(ClientEventHooks::onRightClick); 73 | 74 | // Network events 75 | PacketRegistry.registerClientbound(Quadz.Networking.JOYSTICK_INPUT, ClientNetworkEventHooks::onJoystickInput); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/RenderHooks.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.jme3.math.Vector3f; 5 | import dev.lazurite.quadz.common.util.event.JoystickEvents; 6 | import dev.lazurite.quadz.common.util.JoystickOutput; 7 | import dev.lazurite.quadz.client.Config; 8 | import dev.lazurite.quadz.client.QuadzClient; 9 | import dev.lazurite.rayon.impl.bullet.math.Convert; 10 | import dev.lazurite.toolbox.api.math.QuaternionHelper; 11 | import dev.lazurite.toolbox.api.math.VectorHelper; 12 | import net.minecraft.client.Minecraft; 13 | import net.minecraft.client.OptionInstance; 14 | import net.minecraft.util.profiling.ProfilerFiller; 15 | import org.joml.Quaternionf; 16 | 17 | import java.util.Map; 18 | 19 | import static org.lwjgl.glfw.GLFW.glfwJoystickPresent; 20 | 21 | public class RenderHooks { 22 | 23 | public static void onRenderLevel(float tickDelta) { 24 | /* Rotate the player's yaw and pitch to follow the quadcopter */ 25 | var player = Minecraft.getInstance().player; 26 | 27 | if (Config.followLOS && player != null) { 28 | QuadzClient.getQuadcopterFromRemote().ifPresent(quadcopter -> { 29 | if (player.hasLineOfSight(quadcopter)) { 30 | /* Get the difference in position between the player and the quadcopter */ 31 | var delta = Minecraft.getInstance().player.getEyePosition(tickDelta) 32 | .subtract(VectorHelper.toVec3(Convert.toMinecraft(quadcopter.getPhysicsLocation(new Vector3f(), tickDelta)))); 33 | 34 | /* Set new pitch and yaw */ 35 | player.setYRot((float) Math.toDegrees(Math.atan2(delta.z, delta.x)) + 90); 36 | player.setXRot(20 + (float) Math.toDegrees(Math.atan2(delta.y, Math.sqrt(Math.pow(delta.x, 2) + Math.pow(delta.z, 2))))); 37 | } 38 | }); 39 | } 40 | } 41 | 42 | private static boolean loaded; 43 | private static long next; 44 | 45 | public static void onRenderMinecraft(ProfilerFiller profiler) { 46 | profiler.popPush("gamepadInput"); 47 | 48 | if (System.currentTimeMillis() > next) { 49 | Map lastJoysticks = Maps.newHashMap(JoystickOutput.JOYSTICKS); 50 | JoystickOutput.JOYSTICKS.clear(); 51 | 52 | for (int i = 0; i < 16; i++) { 53 | if (glfwJoystickPresent(i)) { 54 | JoystickOutput.JOYSTICKS.put(i, JoystickOutput.getJoystickName(i)); 55 | 56 | if (!lastJoysticks.containsKey(i) && loaded) { 57 | JoystickEvents.JOYSTICK_CONNECT.invoke(i, JoystickOutput.getJoystickName(i)); 58 | } 59 | } else if (lastJoysticks.containsKey(i) && loaded) { 60 | JoystickEvents.JOYSTICK_DISCONNECT.invoke(i, lastJoysticks.get(i)); 61 | } 62 | } 63 | 64 | next = System.currentTimeMillis() + 500; 65 | loaded = true; 66 | } 67 | } 68 | 69 | public static Quaternionf onMultiplyYaw(Quaternionf quaternion) { 70 | if (QuadzClient.getQuadcopterFromCamera().isPresent()) { 71 | return QuaternionHelper.rotateY(new Quaternionf(0, 0, 0, 1), 180); 72 | } 73 | 74 | return quaternion; 75 | } 76 | 77 | public static Quaternionf onMultiplyPitch(Quaternionf quaternion) { 78 | if (QuadzClient.getQuadcopterFromCamera().isPresent()) { 79 | return new Quaternionf(0, 0, 0, 1); 80 | } 81 | 82 | return quaternion; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/screen/osd/OnScreenDisplay.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.screen.osd; 2 | 3 | import com.jme3.math.Vector3f; 4 | import com.mojang.blaze3d.vertex.PoseStack; 5 | import dev.lazurite.quadz.Quadz; 6 | import dev.lazurite.quadz.client.Config; 7 | import dev.lazurite.quadz.common.entity.Quadcopter; 8 | import dev.lazurite.quadz.common.util.Search; 9 | import net.minecraft.client.Minecraft; 10 | import net.minecraft.client.gui.Font; 11 | import net.minecraft.client.gui.GuiComponent; 12 | import net.minecraft.network.chat.Component; 13 | import net.minecraft.resources.ResourceLocation; 14 | 15 | public class OnScreenDisplay extends GuiComponent { 16 | 17 | private final Quadcopter quadcopter; 18 | private final Font font; 19 | 20 | public OnScreenDisplay(Quadcopter quadcopter) { 21 | this.quadcopter = quadcopter; 22 | this.font = Minecraft.getInstance().font; 23 | } 24 | 25 | public void renderVelocity(PoseStack poseStack, float tickDelta) { 26 | var client = Minecraft.getInstance(); 27 | var height = client.getWindow().getGuiScaledHeight() - 25; 28 | var unit = Config.velocityUnit; 29 | final var vel = Math.round(quadcopter.getRigidBody().getLinearVelocity(new Vector3f()).length() * unit.getFactor() * 10) / 10f; 30 | final var velocity = Component.literal(vel + " " + unit.getAbbreviation()); 31 | font.drawShadow(poseStack, velocity, 25, height, 0xFFFFFFFF); 32 | } 33 | 34 | public void renderSticks(PoseStack poseStack, float tickDelta) { 35 | Search.forPlayer(quadcopter).ifPresent(player -> { 36 | var pitch = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "pitch")); 37 | var yaw = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "yaw")); 38 | var roll = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "roll")); 39 | var throttle = (player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "throttle")) + 1.0f); 40 | var width = Minecraft.getInstance().getWindow().getGuiScaledWidth(); 41 | var height = Minecraft.getInstance().getWindow().getGuiScaledHeight(); 42 | renderSticks(poseStack, tickDelta, width / 2, height - 75, 25, 5, pitch, yaw, roll, throttle); 43 | }); 44 | } 45 | 46 | public static void renderSticks(PoseStack poseStack, float tickDelta, int x, int y, int scale, int spacing, float pitch, float yaw, float roll, float throttle) { 47 | var leftX = x - scale - spacing; 48 | var rightX = x + scale + spacing; 49 | var topY = y + scale; 50 | var bottomY = y - scale; 51 | 52 | // Draw crosses 53 | fill(poseStack, leftX, bottomY + 1, leftX + 1, topY, 0xFFFFFFFF); 54 | fill(poseStack, rightX, bottomY + 1, rightX + 1, topY, 0xFFFFFFFF); 55 | fill(poseStack, leftX - scale, y, leftX + scale + 1, y + 1, 0xFFFFFFFF); 56 | fill(poseStack, rightX - scale, y, rightX + scale + 1, y + 1, 0xFFFFFFFF); 57 | 58 | // Draw stick positions 59 | int dotSize = 2; 60 | int yawAdjusted = (int) (yaw * scale); 61 | int throttleAdjusted = (int) (throttle * scale) - scale; 62 | int rollAdjusted = (int) (roll * scale); 63 | int pitchAdjusted = (int) (pitch * scale); 64 | fill(poseStack, leftX + yawAdjusted - dotSize, y - throttleAdjusted - dotSize, leftX + yawAdjusted + dotSize, y - throttleAdjusted + dotSize, 0xFFFFFFFF); 65 | fill(poseStack, rightX + rollAdjusted - dotSize, y - pitchAdjusted - dotSize, rightX + rollAdjusted + dotSize, y - pitchAdjusted + dotSize, 0xFFFFFFFF); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/Quadz.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz; 2 | 3 | import dev.lazurite.form.api.event.TemplateEvents; 4 | import dev.lazurite.form.api.loader.TemplateLoader; 5 | import dev.lazurite.quadz.common.entity.Quadcopter; 6 | import dev.lazurite.quadz.common.hooks.ServerEventHooks; 7 | import dev.lazurite.quadz.common.hooks.ServerNetworkEventHooks; 8 | import dev.lazurite.quadz.common.item.GogglesItem; 9 | import dev.lazurite.quadz.common.item.QuadcopterItem; 10 | import dev.lazurite.quadz.common.item.RemoteItem; 11 | import dev.lazurite.toolbox.api.network.PacketRegistry; 12 | import net.fabricmc.api.ModInitializer; 13 | import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; 14 | import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents; 15 | import net.fabricmc.fabric.api.object.builder.v1.entity.FabricEntityTypeBuilder; 16 | import net.minecraft.core.Registry; 17 | import net.minecraft.core.registries.BuiltInRegistries; 18 | import net.minecraft.network.chat.Component; 19 | import net.minecraft.resources.ResourceLocation; 20 | import net.minecraft.world.entity.EntityDimensions; 21 | import net.minecraft.world.entity.EntityType; 22 | import net.minecraft.world.entity.LivingEntity; 23 | import net.minecraft.world.entity.MobCategory; 24 | import net.minecraft.world.item.CreativeModeTab; 25 | import net.minecraft.world.item.Item; 26 | import net.minecraft.world.item.ItemStack; 27 | import org.apache.logging.log4j.LogManager; 28 | import org.apache.logging.log4j.Logger; 29 | 30 | public class Quadz implements ModInitializer { 31 | 32 | public static final String MODID = "quadz"; 33 | public static final Logger LOGGER = LogManager.getLogger("Quadz"); 34 | 35 | // Items 36 | public static final Item GOGGLES_ITEM = Registry.register(BuiltInRegistries.ITEM, new ResourceLocation(MODID, "goggles"), new GogglesItem()); 37 | public static final Item QUADCOPTER_ITEM = Registry.register(BuiltInRegistries.ITEM, new ResourceLocation(MODID, "quadcopter"), new QuadcopterItem()); 38 | public static final Item REMOTE_ITEM = Registry.register(BuiltInRegistries.ITEM, new ResourceLocation(MODID, "remote"), new RemoteItem()); 39 | public static final CreativeModeTab CREATIVE_MODE_TAB = FabricItemGroup.builder(new ResourceLocation(MODID, "quadz")) 40 | .icon(() -> new ItemStack(GOGGLES_ITEM)) 41 | .title(Component.literal("Quadz")) 42 | .build(); 43 | 44 | // Entities 45 | public static final EntityType QUADCOPTER = Registry.register( 46 | BuiltInRegistries.ENTITY_TYPE, 47 | new ResourceLocation(MODID, "quadcopter"), 48 | FabricEntityTypeBuilder.createLiving() 49 | .entityFactory(Quadcopter::new) 50 | .spawnGroup(MobCategory.MISC) 51 | .dimensions(EntityDimensions.scalable(0.5f, 0.2f)) 52 | .defaultAttributes(LivingEntity::createLivingAttributes) 53 | .build()); 54 | 55 | @Override 56 | public void onInitialize() { 57 | LOGGER.info("Goggles down, thumbs up!"); 58 | 59 | // Events 60 | TemplateEvents.ENTITY_TEMPLATE_CHANGED.register(ServerEventHooks::onEntityTemplateChanged); 61 | TemplateEvents.TEMPLATE_LOADED.register(ServerEventHooks::onTemplateLoaded); 62 | ItemGroupEvents.modifyEntriesEvent(CREATIVE_MODE_TAB).register(ServerEventHooks::onRebuildCreativeTab); 63 | 64 | // Network events 65 | PacketRegistry.registerServerbound(Networking.JOYSTICK_INPUT, ServerNetworkEventHooks::onJoystickInput); 66 | PacketRegistry.registerServerbound(Networking.REQUEST_QUADCOPTER_VIEW, ServerNetworkEventHooks::onQuadcopterViewRequested); 67 | PacketRegistry.registerServerbound(Networking.REQUEST_PLAYER_VIEW, ServerNetworkEventHooks::onPlayerViewRequestReceived); 68 | 69 | // Load templates 70 | TemplateLoader.initialize(MODID); 71 | } 72 | 73 | public static class Networking { 74 | 75 | public static final ResourceLocation JOYSTICK_INPUT = new ResourceLocation(MODID, "joystick_input"); 76 | public static final ResourceLocation REQUEST_QUADCOPTER_VIEW = new ResourceLocation(MODID, "request_quadcopter_view"); 77 | public static final ResourceLocation REQUEST_PLAYER_VIEW = new ResourceLocation(MODID, "request_player_view"); 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/shaders/program/static.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | /* 4 | https://www.shadertoy.com/view/ldXGW4 5 | */ 6 | 7 | uniform sampler2D DiffuseSampler; 8 | uniform vec2 OutSize; 9 | uniform float Time; 10 | uniform float Amount; 11 | 12 | // change these values to 0.0 to turn off individual effects 13 | float vertJerkOpt = 1.0; 14 | float vertMovementOpt = 1.0; 15 | float bottomStaticOpt = 1.0; 16 | float scalinesOpt = 1.0; 17 | float rgbOffsetOpt = 0.0; // disabled 18 | float horzFuzzOpt = 1.0; 19 | 20 | // Noise generation functions borrowed from: 21 | // https://github.com/ashima/webgl-noise/blob/master/src/noise2D.glsl 22 | 23 | vec3 mod289(vec3 x) { 24 | return x - floor(x * (1.0 / 289.0)) * 289.0; 25 | } 26 | 27 | vec2 mod289(vec2 x) { 28 | return x - floor(x * (1.0 / 289.0)) * 289.0; 29 | } 30 | 31 | vec3 permute(vec3 x) { 32 | return mod289(((x*34.0)+1.0)*x); 33 | } 34 | 35 | float snoise(vec2 v) 36 | { 37 | const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0 38 | 0.366025403784439, // 0.5*(sqrt(3.0)-1.0) 39 | -0.577350269189626, // -1.0 + 2.0 * C.x 40 | 0.024390243902439); // 1.0 / 41.0 41 | // First corner 42 | vec2 i = floor(v + dot(v, C.yy) ); 43 | vec2 x0 = v - i + dot(i, C.xx); 44 | 45 | // Other corners 46 | vec2 i1; 47 | //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0 48 | //i1.y = 1.0 - i1.x; 49 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 50 | // x0 = x0 - 0.0 + 0.0 * C.xx ; 51 | // x1 = x0 - i1 + 1.0 * C.xx ; 52 | // x2 = x0 - 1.0 + 2.0 * C.xx ; 53 | vec4 x12 = x0.xyxy + C.xxzz; 54 | x12.xy -= i1; 55 | 56 | // Permutations 57 | i = mod289(i); // Avoid truncation effects in permutation 58 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) 59 | + i.x + vec3(0.0, i1.x, 1.0 )); 60 | 61 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); 62 | m = m*m ; 63 | m = m*m ; 64 | 65 | // Gradients: 41 points uniformly over a line, mapped onto a diamond. 66 | // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287) 67 | 68 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 69 | vec3 h = abs(x) - 0.5; 70 | vec3 ox = floor(x + 0.5); 71 | vec3 a0 = x - ox; 72 | 73 | // Normalise gradients implicitly by scaling m 74 | // Approximation of: m *= inversesqrt( a0*a0 + h*h ); 75 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 76 | 77 | // Compute final noise value at P 78 | vec3 g; 79 | g.x = a0.x * x0.x + h.x * x0.y; 80 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 81 | return 130.0 * dot(m, g) * Amount; 82 | } 83 | 84 | float staticV(vec2 uv) { 85 | float staticHeight = snoise(vec2(9.0,Time*1.2+3.0))*0.3+5.0; 86 | float staticAmount = snoise(vec2(1.0,Time*1.2-6.0))*0.1+0.3; 87 | float staticStrength = snoise(vec2(-9.75,Time*0.6-3.0))*2.0+2.0; 88 | return ((1.0-step(snoise(vec2(5.0*pow(Time,2.0)+pow(uv.x*7.0,1.2),pow((mod(Time,100.0)+100.0)*uv.y*0.3+3.0,staticHeight))),staticAmount))*staticStrength); 89 | } 90 | 91 | out vec4 fragColor; 92 | in vec2 texCoord; 93 | 94 | void main() 95 | { 96 | vec2 uv = gl_FragCoord.xy/OutSize.xy; 97 | 98 | float jerkOffset = (1.0-step(snoise(vec2(Time*1.3,5.0)),0.8))*0.05; 99 | 100 | float fuzzOffset = snoise(vec2(Time*15.0,uv.y*80.0))*0.003; 101 | float largeFuzzOffset = snoise(vec2(Time*1.0,uv.y*25.0))*0.004; 102 | 103 | float vertMovementOn = (1.0-step(snoise(vec2(Time*0.2,8.0)),0.4))*vertMovementOpt; 104 | float vertJerk = (1.0-step(snoise(vec2(Time*1.5,5.0)),0.6))*vertJerkOpt; 105 | float vertJerk2 = (1.0-step(snoise(vec2(Time*5.5,5.0)),0.2))*vertJerkOpt; 106 | float yOffset = abs(sin(Time)*4.0)*vertMovementOn+vertJerk*vertJerk2*0.3; 107 | float y = mod(uv.y+yOffset,1.0); 108 | 109 | float xOffset = (fuzzOffset + largeFuzzOffset) * horzFuzzOpt; 110 | float staticVal = 0.0; 111 | 112 | for (float y = -1.0; y <= 1.0; y += 1.0) { 113 | float maxDist = 5.0/200.0; 114 | float dist = y/200.0; 115 | staticVal += staticV(vec2(uv.x,uv.y+dist))*(maxDist-abs(dist))*1.5; 116 | } 117 | 118 | staticVal *= bottomStaticOpt; 119 | 120 | float red = texture( DiffuseSampler, vec2(uv.x + xOffset -0.01*rgbOffsetOpt,y)).r+staticVal; 121 | float green = texture( DiffuseSampler, vec2(uv.x + xOffset, y)).g+staticVal; 122 | float blue = texture( DiffuseSampler, vec2(uv.x + xOffset +0.01*rgbOffsetOpt,y)).b+staticVal; 123 | 124 | vec3 color = vec3(red,green,blue); 125 | float scanline = sin(uv.y*800.0)*0.04*scalinesOpt; 126 | color -= scanline; 127 | 128 | fragColor = vec4(color,1.0); 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/item/QuadcopterItem.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.item; 2 | 3 | import com.jme3.math.Quaternion; 4 | import com.jme3.math.Vector3f; 5 | import dev.lazurite.form.api.Templated; 6 | import dev.lazurite.form.api.render.FormRegistry; 7 | import dev.lazurite.quadz.Quadz; 8 | import dev.lazurite.quadz.common.util.Bindable; 9 | import dev.lazurite.quadz.client.render.entity.QuadcopterEntityRenderer; 10 | import dev.lazurite.rayon.impl.bullet.math.Convert; 11 | import dev.lazurite.toolbox.api.math.QuaternionHelper; 12 | import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; 13 | import net.minecraft.world.InteractionHand; 14 | import net.minecraft.world.InteractionResultHolder; 15 | import net.minecraft.world.entity.player.Player; 16 | import net.minecraft.world.item.Item; 17 | import net.minecraft.world.item.ItemStack; 18 | import net.minecraft.world.item.Items; 19 | import net.minecraft.world.level.ClipContext; 20 | import net.minecraft.world.level.Level; 21 | import net.minecraft.world.phys.HitResult; 22 | import org.jetbrains.annotations.NotNull; 23 | import software.bernie.geckolib.animatable.GeoItem; 24 | import software.bernie.geckolib.animatable.client.RenderProvider; 25 | import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; 26 | import software.bernie.geckolib.core.animation.AnimatableManager; 27 | import software.bernie.geckolib.core.animation.AnimationController; 28 | import software.bernie.geckolib.core.object.PlayState; 29 | import software.bernie.geckolib.util.GeckoLibUtil; 30 | 31 | import java.util.Random; 32 | import java.util.function.Consumer; 33 | import java.util.function.Supplier; 34 | 35 | /** 36 | * Represents a quadcopter, allows the player to spawn with right-click on the ground. 37 | * @see QuadcopterEntityRenderer 38 | */ 39 | public class QuadcopterItem extends Item implements GeoItem, Templated.Item { 40 | 41 | private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); 42 | private final Supplier renderProvider = GeoItem.makeRenderer(this); 43 | 44 | public QuadcopterItem() { 45 | super(new Properties().stacksTo(1)); 46 | } 47 | 48 | @Override 49 | public InteractionResultHolder use(Level level, Player player, InteractionHand interactionHand) { 50 | var itemStack = player.getItemInHand(interactionHand); 51 | var hitResult = getPlayerPOVHitResult(level, player, ClipContext.Fluid.NONE); 52 | 53 | if (level.isClientSide()) { 54 | return InteractionResultHolder.success(itemStack); // wave hand 55 | } else { 56 | var entity = Quadz.QUADCOPTER.create(level); 57 | entity.copyFrom(Templated.get(itemStack)); 58 | Bindable.get(itemStack).ifPresent(entity::copyFrom); 59 | 60 | if (hitResult.getType() == HitResult.Type.BLOCK) { 61 | entity.absMoveTo(hitResult.getLocation().x, hitResult.getLocation().y, hitResult.getLocation().z); 62 | entity.getRigidBody().setPhysicsRotation(Convert.toBullet(QuaternionHelper.rotateY(Convert.toMinecraft(new Quaternion()), -player.getYRot()))); 63 | } else { 64 | var random = new Random(); 65 | var direction = hitResult.getLocation().subtract(player.position()).add(0, player.getEyeHeight(), 0).normalize(); 66 | var pos = player.position().add(direction); 67 | 68 | entity.absMoveTo(pos.x, pos.y, pos.z); 69 | entity.getRigidBody().setLinearVelocity(Convert.toBullet(direction).multLocal(4).multLocal(new Vector3f(1, 3, 1))); 70 | entity.getRigidBody().setAngularVelocity(new Vector3f(random.nextFloat() * 2, random.nextFloat() * 2, random.nextFloat() * 2)); 71 | } 72 | 73 | level.addFreshEntity(entity); 74 | itemStack.shrink(1); 75 | itemStack = new ItemStack(Items.AIR); 76 | } 77 | 78 | return InteractionResultHolder.success(itemStack); // wave hand 79 | } 80 | 81 | @Override 82 | public void createRenderer(Consumer consumer) { 83 | consumer.accept(new RenderProvider() { 84 | @Override 85 | public BlockEntityWithoutLevelRenderer getCustomRenderer() { 86 | return FormRegistry.getItemRenderer(QuadcopterItem.this); 87 | } 88 | }); 89 | } 90 | 91 | @Override 92 | public Supplier getRenderProvider() { 93 | return this.renderProvider; 94 | } 95 | 96 | @Override 97 | public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { 98 | controllers.add(new AnimationController<>(this, 20, state -> PlayState.CONTINUE)); 99 | } 100 | 101 | @Override 102 | public AnimatableInstanceCache getAnimatableInstanceCache() { 103 | return this.cache; 104 | } 105 | 106 | @Override 107 | public @NotNull String getDescriptionId(ItemStack itemStack) { 108 | var template = Templated.get(itemStack).getTemplate(); 109 | return "template.quadz." + template; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/util/Search.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.util; 2 | 3 | import dev.lazurite.quadz.Quadz; 4 | import dev.lazurite.quadz.common.entity.Quadcopter; 5 | import dev.lazurite.toolbox.api.util.PlayerUtil; 6 | import net.minecraft.core.BlockPos; 7 | import net.minecraft.server.MinecraftServer; 8 | import net.minecraft.server.level.ServerLevel; 9 | import net.minecraft.world.entity.Entity; 10 | import net.minecraft.world.entity.LivingEntity; 11 | import net.minecraft.world.entity.ai.targeting.TargetingConditions; 12 | import net.minecraft.world.entity.player.Player; 13 | import net.minecraft.world.level.Level; 14 | import net.minecraft.world.phys.AABB; 15 | import net.minecraft.world.phys.Vec3; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.util.List; 19 | import java.util.Optional; 20 | import java.util.Set; 21 | import java.util.function.Predicate; 22 | import java.util.stream.Collectors; 23 | 24 | /** 25 | * A set of tools for finding {@link Quadcopter} objects. 26 | * @since 2.0.0 27 | */ 28 | public interface Search { 29 | 30 | /** 31 | * Returns a list of every {@link Quadcopter} that is being viewed by a player in the entire server. 32 | * @param server the {@link MinecraftServer} to use 33 | * @return {@link List} of {@link Quadcopter}s 34 | */ 35 | static Set forAllViewed(MinecraftServer server) { 36 | return server.getPlayerList().getPlayers().stream().filter(player -> player.getCamera() instanceof Quadcopter) 37 | .map(player -> (Quadcopter) player.getCamera()).collect(Collectors.toSet()); 38 | } 39 | 40 | /** 41 | * Returns a list of every {@link Quadcopter} that is being viewed by a player in the given world. 42 | * @param level the {@link ServerLevel} to use 43 | * @return {@link List} of {@link Quadcopter}s 44 | */ 45 | static Set forAllViewed(ServerLevel level) { 46 | return level.players().stream().filter(player -> player.getCamera() instanceof Quadcopter) 47 | .map(player -> (Quadcopter) player.getCamera()).collect(Collectors.toSet()); 48 | } 49 | 50 | /** 51 | * Finds all {@link Quadcopter} objects inside of a specific range. 52 | * @param level the level to search in 53 | * @param origin the point to search from 54 | * @param range the maximum range 55 | * @return a {@link List} of {@link Quadcopter} objects 56 | */ 57 | static List forAllWithinRange(Level level, Vec3 origin, int range) { 58 | return level.getEntitiesOfClass(Quadcopter.class, new AABB(new BlockPos(origin)).inflate(range), entity -> true); 59 | } 60 | 61 | /** 62 | * Finds a specific {@link Quadcopter} which is bound to the given bind ID. 63 | * @param level the level to search in 64 | * @param origin the point to search from 65 | * @param bindId the bind id of the transmitter 66 | * @param range the maximum range 67 | * @return the bound {@link Quadcopter} 68 | */ 69 | static Optional forQuadWithBindId(Level level, Vec3 origin, int bindId, int range) { 70 | var entities = level.getEntities((Entity) null, 71 | new AABB(new BlockPos(origin)).inflate(range), 72 | entity -> entity instanceof Quadcopter quadcopter && quadcopter.isBoundTo(bindId)); 73 | return entities.size() > 0 ? Optional.of((Quadcopter) entities.get(0)) : Optional.empty(); 74 | } 75 | 76 | /** 77 | * Finds the closest {@link Quadcopter} to the given origin. 78 | * @param level the level to search in 79 | * @param origin the point to search from 80 | * @param range the maximum range 81 | * @param predicate a predicate to narrow the search 82 | * @return the nearest {@link Quadcopter} 83 | */ 84 | static Optional forNearestQuad(Level level, Vec3 origin, int range, @Nullable Predicate predicate) { 85 | return Optional.ofNullable(level.getNearestEntity( 86 | Quadcopter.class, 87 | TargetingConditions.DEFAULT.selector(predicate), 88 | null, origin.x, origin.y, origin.z, 89 | new AABB(new BlockPos(origin)).inflate(range))); 90 | } 91 | 92 | /** 93 | * Finds a {@link Player} based on the given {@link Quadcopter} and its bind ID. 94 | * @param quadcopter the {@link Quadcopter} to use in the search 95 | * @return the matching {@link Player} 96 | */ 97 | static Optional forPlayer(Quadcopter quadcopter) { 98 | if (quadcopter.getLevel().isClientSide()) { 99 | return quadcopter.getLevel().players().stream() 100 | .filter(player -> Bindable.get(player.getMainHandItem(), quadcopter).isPresent() && player.getMainHandItem().getItem() == Quadz.REMOTE_ITEM) 101 | .findFirst(); 102 | } 103 | 104 | return PlayerUtil.tracking(quadcopter) 105 | .stream().filter(player -> Bindable.get(player.getMainHandItem(), quadcopter).isPresent()) 106 | .findFirst(); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/QuadcopterView.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render; 2 | 3 | import com.jme3.math.Quaternion; 4 | import com.jme3.math.Vector3f; 5 | import com.mojang.blaze3d.vertex.PoseStack; 6 | import dev.lazurite.corduroy.api.View; 7 | import dev.lazurite.form.api.loader.TemplateLoader; 8 | import dev.lazurite.form.impl.common.template.model.Template; 9 | import dev.lazurite.quadz.Quadz; 10 | import dev.lazurite.quadz.client.Config; 11 | import dev.lazurite.quadz.client.render.screen.osd.OnScreenDisplay; 12 | import dev.lazurite.quadz.common.entity.Quadcopter; 13 | import dev.lazurite.rayon.impl.bullet.math.Convert; 14 | import dev.lazurite.rayon.impl.bullet.thread.util.Clock; 15 | import dev.lazurite.toolbox.api.math.QuaternionHelper; 16 | import dev.lazurite.toolbox.api.math.VectorHelper; 17 | import ladysnake.satin.api.managed.ManagedShaderEffect; 18 | import ladysnake.satin.api.managed.ShaderEffectManager; 19 | import ladysnake.satin.api.managed.uniform.Uniform1f; 20 | import net.minecraft.client.Minecraft; 21 | import net.minecraft.resources.ResourceLocation; 22 | import net.minecraft.util.Mth; 23 | import net.minecraft.world.phys.Vec3; 24 | import org.joml.Quaternionf; 25 | 26 | public class QuadcopterView extends View implements View.Ticking { 27 | 28 | private static final ManagedShaderEffect STATIC_SHADER = ShaderEffectManager.getInstance().manage(new ResourceLocation(Quadz.MODID, "shaders/post/static.json")); 29 | private static final ManagedShaderEffect FISHEYE_SHADER = ShaderEffectManager.getInstance().manage(new ResourceLocation(Quadz.MODID, "shaders/post/fisheye.json")); 30 | private static final Uniform1f STATIC_AMOUNT = STATIC_SHADER.findUniform1f("Amount"); 31 | private static final Uniform1f STATIC_TIMER = STATIC_SHADER.findUniform1f("Time"); 32 | private static final Uniform1f FISHEYE_AMOUNT = FISHEYE_SHADER.findUniform1f("Amount"); 33 | 34 | private static final int SIGNAL_DISTANCE = 1024; 35 | 36 | private final Quadcopter quadcopter; 37 | private final OnScreenDisplay osd; 38 | private final Clock clock; 39 | private Template template; 40 | 41 | public QuadcopterView(Quadcopter quadcopter) { 42 | this.quadcopter = quadcopter; 43 | this.osd = new OnScreenDisplay(quadcopter); 44 | this.clock = new Clock(); 45 | } 46 | 47 | @Override 48 | public void tick() { 49 | FISHEYE_AMOUNT.set(Config.fisheyeAmount); 50 | 51 | var distance = getQuadcopter().distanceTo(Minecraft.getInstance().player); 52 | var lineOfSight = getQuadcopter().hasLineOfSight(Minecraft.getInstance().player); 53 | STATIC_AMOUNT.set(lineOfSight ? Mth.clamp(distance / (float) SIGNAL_DISTANCE, 0.0f, 1.0f) * 2.0f : 2.0f); 54 | } 55 | 56 | @Override 57 | public void onRender() { 58 | var options = Minecraft.getInstance().options; 59 | var camera = this.getCamera(); 60 | 61 | if (options.getCameraType().isFirstPerson()) { 62 | if (!Config.renderCameraInCenter) { 63 | var template = this.getTemplate(); 64 | float cameraX = template.metadata().get("cameraX").getAsFloat(); 65 | float cameraY = template.metadata().get("cameraY").getAsFloat(); 66 | camera.move(-cameraX, -cameraY, 0); 67 | } 68 | } else { 69 | camera.move(camera.getMaxZoom(4), 0, 0); 70 | } 71 | } 72 | 73 | public void onGuiRender(PoseStack poseStack, float tickDelta) { 74 | var firstPerson = Minecraft.getInstance().options.getCameraType().isFirstPerson(); 75 | 76 | if (firstPerson && Config.fisheyeEnabled) { 77 | FISHEYE_SHADER.render(tickDelta); 78 | } 79 | 80 | if (Config.osdEnabled) { 81 | this.osd.renderVelocity(poseStack, tickDelta); 82 | this.osd.renderSticks(poseStack, tickDelta); 83 | } 84 | 85 | if (firstPerson && Config.videoInterferenceEnabled) { 86 | STATIC_TIMER.set(clock.get()); 87 | STATIC_SHADER.render(tickDelta); 88 | } 89 | } 90 | 91 | @Override 92 | public Vec3 getPosition(float tickDelta) { 93 | return VectorHelper.toVec3(Convert.toMinecraft(this.quadcopter.getPhysicsLocation(new Vector3f(), tickDelta))); 94 | } 95 | 96 | @Override 97 | public Quaternionf getRotation(float tickDelta) { 98 | return QuaternionHelper.rotateY(QuaternionHelper.rotateX( 99 | Convert.toMinecraft(this.quadcopter.getPhysicsRotation(new Quaternion(), tickDelta)), 100 | -this.quadcopter.getEntityData().get(Quadcopter.CAMERA_ANGLE) 101 | ), 180); 102 | } 103 | 104 | @Override 105 | public boolean shouldRenderTarget() { 106 | return (!Config.renderCameraInCenter && Config.renderFirstPerson) || !Minecraft.getInstance().options.getCameraType().isFirstPerson(); 107 | } 108 | 109 | public Quadcopter getQuadcopter() { 110 | return this.quadcopter; 111 | } 112 | 113 | private Template getTemplate() { 114 | // cache the template object 115 | if (this.template == null) { 116 | this.template = TemplateLoader.getTemplateById(this.quadcopter.getTemplate()).orElse(null); 117 | } 118 | 119 | return this.template; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/Config.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParser; 5 | import com.google.gson.JsonPrimitive; 6 | import dev.lazurite.quadz.Quadz; 7 | import dev.lazurite.quadz.client.render.screen.osd.VelocityUnit; 8 | import net.fabricmc.loader.api.FabricLoader; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | 15 | public class Config { 16 | 17 | public static int controllerId = 0; 18 | 19 | public static int pitch = 1; 20 | public static int yaw = 3; 21 | public static int roll = 0; 22 | public static int throttle = 2; 23 | 24 | public static boolean pitchInverted = false; 25 | public static boolean yawInverted = false; 26 | public static boolean rollInverted = false; 27 | public static boolean throttleInverted = false; 28 | public static boolean throttleInCenter = false; 29 | 30 | public static float rate = 0.7f; 31 | public static float superRate = 0.8f; 32 | public static float expo = 0.0f; 33 | 34 | public static float deadzone = 0.05f; 35 | public static boolean followLOS = true; 36 | public static boolean renderFirstPerson = true; 37 | public static boolean renderCameraInCenter = false; 38 | public static boolean osdEnabled = true; 39 | public static VelocityUnit velocityUnit = VelocityUnit.METERS_PER_SECOND; 40 | public static boolean videoInterferenceEnabled = true; 41 | public static boolean fisheyeEnabled = true; 42 | public static float fisheyeAmount = 0.8f; 43 | 44 | public static Path getConfigPath() { 45 | return FabricLoader.getInstance().getConfigDir().resolve("quadz.json"); 46 | } 47 | 48 | public static void save() { 49 | final var path = getConfigPath(); 50 | final var config = new JsonObject(); 51 | 52 | config.add("pitch", new JsonPrimitive(pitch)); 53 | config.add("yaw", new JsonPrimitive(yaw)); 54 | config.add("roll", new JsonPrimitive(roll)); 55 | config.add("throttle", new JsonPrimitive(throttle)); 56 | config.add("pitchInverted", new JsonPrimitive(pitchInverted)); 57 | config.add("yawInverted", new JsonPrimitive(yawInverted)); 58 | config.add("rollInverted", new JsonPrimitive(rollInverted)); 59 | config.add("throttleInverted", new JsonPrimitive(throttleInverted)); 60 | config.add("throttleInCenter", new JsonPrimitive(throttleInCenter)); 61 | config.add("rate", new JsonPrimitive(rate)); 62 | config.add("superRate", new JsonPrimitive(superRate)); 63 | config.add("expo", new JsonPrimitive(expo)); 64 | config.add("controllerId", new JsonPrimitive(controllerId)); 65 | config.add("deadzone", new JsonPrimitive(deadzone)); 66 | config.add("followLOS", new JsonPrimitive(followLOS)); 67 | config.add("renderFirstPerson", new JsonPrimitive(renderFirstPerson)); 68 | config.add("renderCameraInCenter", new JsonPrimitive(renderCameraInCenter)); 69 | config.add("osdEnabled", new JsonPrimitive(osdEnabled)); 70 | config.add("velocityUnit", new JsonPrimitive(velocityUnit.toString())); 71 | config.add("videoInterferenceEnabled", new JsonPrimitive(videoInterferenceEnabled)); 72 | config.add("fisheyeEnabled", new JsonPrimitive(fisheyeEnabled)); 73 | config.add("fisheyeAmount", new JsonPrimitive(fisheyeAmount)); 74 | 75 | try { 76 | Files.writeString(path, config.toString()); 77 | } catch(IOException e) { 78 | Quadz.LOGGER.error(e); 79 | } 80 | } 81 | 82 | public static void load() { 83 | final var path = getConfigPath(); 84 | 85 | if (!Files.exists(path)) { 86 | save(); 87 | return; 88 | } 89 | 90 | try { 91 | final var config = JsonParser.parseReader(new InputStreamReader(Files.newInputStream(path))).getAsJsonObject(); 92 | pitch = config.get("pitch").getAsInt(); 93 | yaw = config.get("yaw").getAsInt(); 94 | roll = config.get("roll").getAsInt(); 95 | throttle = config.get("throttle").getAsInt(); 96 | pitchInverted = config.get("pitchInverted").getAsBoolean(); 97 | yawInverted = config.get("yawInverted").getAsBoolean(); 98 | rollInverted = config.get("rollInverted").getAsBoolean(); 99 | throttleInverted = config.get("throttleInverted").getAsBoolean(); 100 | throttleInCenter = config.get("throttleInCenter").getAsBoolean(); 101 | rate = config.get("rate").getAsFloat(); 102 | superRate = config.get("superRate").getAsFloat(); 103 | expo = config.get("expo").getAsFloat(); 104 | controllerId = config.get("controllerId").getAsInt(); 105 | deadzone = config.get("deadzone").getAsFloat(); 106 | followLOS = config.get("followLOS").getAsBoolean(); 107 | renderFirstPerson = config.get("renderFirstPerson").getAsBoolean(); 108 | renderCameraInCenter = config.get("renderCameraInCenter").getAsBoolean(); 109 | osdEnabled = config.get("osdEnabled").getAsBoolean(); 110 | velocityUnit = VelocityUnit.valueOf(config.get("velocityUnit").getAsString()); 111 | videoInterferenceEnabled = config.get("videoInterferenceEnabled").getAsBoolean(); 112 | fisheyeEnabled = config.get("fisheyeEnabled").getAsBoolean(); 113 | fisheyeAmount = config.get("fisheyeAmount").getAsFloat(); 114 | } catch(IOException e) { 115 | Quadz.LOGGER.error(e); 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/screen/MainConfigScreen.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.screen; 2 | 3 | import dev.lazurite.quadz.client.Config; 4 | import dev.lazurite.quadz.client.render.screen.osd.VelocityUnit; 5 | import me.shedaniel.clothconfig2.api.ConfigBuilder; 6 | import net.minecraft.client.gui.components.Button; 7 | import net.minecraft.client.gui.screens.Screen; 8 | import net.minecraft.network.chat.Component; 9 | 10 | public interface MainConfigScreen { 11 | 12 | static Screen get(Screen parent) { 13 | var builder = ConfigBuilder.create() 14 | .setParentScreen(parent) 15 | .setTitle(Component.translatable("quadz.config.title")) 16 | .setSavingRunnable(Config::save); 17 | 18 | var entryBuilder = builder.entryBuilder(); 19 | var controllerCategory = builder.getOrCreateCategory(Component.translatable("quadz.config.controller.title")); 20 | var visualsCategory = builder.getOrCreateCategory(Component.translatable("quadz.config.visuals.title")); 21 | 22 | controllerCategory.addEntry( 23 | entryBuilder.startFloatField(Component.translatable("quadz.config.controller.rate"), Config.rate) 24 | .setDefaultValue(Config.rate) 25 | .setSaveConsumer(value -> Config.rate = value) 26 | .build() 27 | ); 28 | 29 | controllerCategory.addEntry( 30 | entryBuilder.startFloatField(Component.translatable("quadz.config.controller.superRate"), Config.superRate) 31 | .setDefaultValue(Config.superRate) 32 | .setSaveConsumer(value -> Config.superRate = value) 33 | .build() 34 | ); 35 | 36 | controllerCategory.addEntry( 37 | entryBuilder.startFloatField(Component.translatable("quadz.config.controller.expo"), Config.expo) 38 | .setDefaultValue(Config.expo) 39 | .setSaveConsumer(value -> Config.expo = value) 40 | .build() 41 | ); 42 | 43 | controllerCategory.addEntry( 44 | entryBuilder.startFloatField(Component.translatable("quadz.config.controller.deadzone"), Config.deadzone) 45 | .setDefaultValue(Config.deadzone) 46 | .setSaveConsumer(value -> Config.deadzone = value) 47 | .build() 48 | ); 49 | 50 | visualsCategory.addEntry( 51 | entryBuilder.startBooleanToggle(Component.translatable("quadz.config.visuals.osd_toggle"), Config.osdEnabled) 52 | .setDefaultValue(Config.osdEnabled) 53 | .setSaveConsumer(value -> Config.osdEnabled = value) 54 | .build() 55 | ); 56 | 57 | visualsCategory.addEntry( 58 | entryBuilder.startEnumSelector(Component.translatable("quadz.config.visuals.velocity_unit"), VelocityUnit.class, Config.velocityUnit) 59 | .setEnumNameProvider(unit -> ((VelocityUnit) unit).getTranslation()) 60 | .setDefaultValue(Config.velocityUnit) 61 | .setSaveConsumer(value -> Config.velocityUnit = value) 62 | .build() 63 | ); 64 | 65 | visualsCategory.addEntry( 66 | entryBuilder.startBooleanToggle(Component.translatable("quadz.config.visuals.follow_los_toggle"), Config.followLOS) 67 | .setDefaultValue(Config.followLOS) 68 | .setSaveConsumer(value -> Config.followLOS = value) 69 | .build() 70 | ); 71 | 72 | visualsCategory.addEntry( 73 | entryBuilder.startBooleanToggle(Component.translatable("quadz.config.visuals.static_toggle"), Config.videoInterferenceEnabled) 74 | .setDefaultValue(Config.videoInterferenceEnabled) 75 | .setSaveConsumer(value -> Config.videoInterferenceEnabled = value) 76 | .build() 77 | ); 78 | 79 | visualsCategory.addEntry( 80 | entryBuilder.startBooleanToggle(Component.translatable("quadz.config.visuals.fisheye_toggle"), Config.fisheyeEnabled) 81 | .setDefaultValue(Config.fisheyeEnabled) 82 | .setSaveConsumer(value -> Config.fisheyeEnabled = value) 83 | .build() 84 | ); 85 | 86 | visualsCategory.addEntry( 87 | entryBuilder.startIntSlider(Component.translatable("quadz.config.visuals.fisheye_amount"), (int) (Config.fisheyeAmount * 100), 0, 100) 88 | .setDefaultValue((int) (Config.fisheyeAmount * 100)) 89 | .setSaveConsumer(value -> Config.fisheyeAmount = value * 0.01f) 90 | .build() 91 | ); 92 | 93 | visualsCategory.addEntry( 94 | entryBuilder.startBooleanToggle(Component.translatable("quadz.config.visuals.camera_in_center_toggle"), Config.renderCameraInCenter) 95 | .setDefaultValue(Config.renderCameraInCenter) 96 | .setSaveConsumer(value -> Config.renderCameraInCenter = value) 97 | .build() 98 | ); 99 | 100 | visualsCategory.addEntry( 101 | entryBuilder.startBooleanToggle(Component.translatable("quadz.config.visuals.first_person_render_toggle"), Config.renderFirstPerson) 102 | .setDefaultValue(Config.renderFirstPerson) 103 | .setSaveConsumer(value -> Config.renderFirstPerson = value) 104 | .build() 105 | ); 106 | 107 | builder.setGlobalized(true); 108 | var screen = builder.build(); 109 | screen.addRenderableWidget(Button.builder(Component.literal("TEST"), button -> {}).pos(100, 100).size(60, 20).build()); 110 | return screen; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/geo/item/quadcopter.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:geometry": [ 4 | { 5 | "description": { 6 | "identifier": "geometry.unknown", 7 | "texture_width": 32, 8 | "texture_height": 32, 9 | "visible_bounds_width": 2, 10 | "visible_bounds_height": 1.5, 11 | "visible_bounds_offset": [0, 0.25, 0] 12 | }, 13 | "bones": [ 14 | { 15 | "name": "bb_main", 16 | "pivot": [0, 0, 0], 17 | "cubes": [ 18 | { 19 | "origin": [5.6, 0.7, -0.65], 20 | "size": [1.3, 0.5, 1.3], 21 | "pivot": [0, 0.7, 0], 22 | "rotation": [0, -45, 0], 23 | "uv": { 24 | "north": {"uv": [14, 15], "uv_size": [1, 1]}, 25 | "east": {"uv": [15, 15], "uv_size": [1, 1]}, 26 | "south": {"uv": [6, 16], "uv_size": [1, 1]}, 27 | "west": {"uv": [7, 16], "uv_size": [1, 1]}, 28 | "up": {"uv": [8, 16], "uv_size": [1, 1]}, 29 | "down": {"uv": [16, 9], "uv_size": [1, -1]} 30 | } 31 | }, 32 | { 33 | "origin": [-7, 0, -0.75], 34 | "size": [5, 0.7, 1.5], 35 | "pivot": [0, 0, 0], 36 | "rotation": [0, 45, 0], 37 | "uv": { 38 | "north": {"uv": [6, 12], "uv_size": [5, 1]}, 39 | "east": {"uv": [15, 4], "uv_size": [2, 1]}, 40 | "south": {"uv": [12, 10], "uv_size": [5, 1]}, 41 | "west": {"uv": [15, 5], "uv_size": [2, 1]}, 42 | "up": {"uv": [4, 0], "uv_size": [5, 2]}, 43 | "down": {"uv": [4, 4], "uv_size": [5, -2]} 44 | } 45 | }, 46 | { 47 | "origin": [3.76942, 0.7, -5.06942], 48 | "size": [1.3, 0.5, 1.3], 49 | "pivot": [4.41942, 0.95, -4.41942], 50 | "rotation": [0, 45, 0], 51 | "uv": { 52 | "north": {"uv": [9, 16], "uv_size": [1, 1]}, 53 | "east": {"uv": [16, 9], "uv_size": [1, 1]}, 54 | "south": {"uv": [10, 16], "uv_size": [1, 1]}, 55 | "west": {"uv": [11, 16], "uv_size": [1, 1]}, 56 | "up": {"uv": [16, 11], "uv_size": [1, 1]}, 57 | "down": {"uv": [12, 17], "uv_size": [1, -1]} 58 | } 59 | }, 60 | { 61 | "origin": [-6.9, 0.7, -0.65], 62 | "size": [1.3, 0.5, 1.3], 63 | "pivot": [0, 0.7, 0], 64 | "rotation": [0, -45, 0], 65 | "uv": { 66 | "north": {"uv": [16, 12], "uv_size": [1, 1]}, 67 | "east": {"uv": [13, 16], "uv_size": [1, 1]}, 68 | "south": {"uv": [16, 13], "uv_size": [1, 1]}, 69 | "west": {"uv": [14, 16], "uv_size": [1, 1]}, 70 | "up": {"uv": [15, 16], "uv_size": [1, 1]}, 71 | "down": {"uv": [16, 16], "uv_size": [1, -1]} 72 | } 73 | }, 74 | { 75 | "origin": [-2, 0.1, -2], 76 | "size": [4, 0.75, 4], 77 | "uv": { 78 | "north": {"uv": [14, 3], "uv_size": [4, 1]}, 79 | "east": {"uv": [6, 14], "uv_size": [4, 1]}, 80 | "south": {"uv": [10, 14], "uv_size": [4, 1]}, 81 | "west": {"uv": [14, 14], "uv_size": [4, 1]}, 82 | "up": {"uv": [0, 0], "uv_size": [4, 4]}, 83 | "down": {"uv": [0, 8], "uv_size": [4, -4]} 84 | } 85 | }, 86 | { 87 | "origin": [-5.06943, 0.7, 3.76941], 88 | "size": [1.3, 0.5, 1.3], 89 | "pivot": [-4.41943, 0.95, 4.41941], 90 | "rotation": [0, -45, 0], 91 | "uv": { 92 | "north": {"uv": [16, 16], "uv_size": [1, 1]}, 93 | "east": {"uv": [0, 17], "uv_size": [1, 1]}, 94 | "south": {"uv": [1, 17], "uv_size": [1, 1]}, 95 | "west": {"uv": [2, 17], "uv_size": [1, 1]}, 96 | "up": {"uv": [3, 17], "uv_size": [1, 1]}, 97 | "down": {"uv": [17, 5], "uv_size": [1, -1]} 98 | } 99 | }, 100 | { 101 | "origin": [2, 0, -0.75], 102 | "size": [5, 0.7, 1.5], 103 | "pivot": [0, 0, 0], 104 | "rotation": [0, 45, 0], 105 | "uv": { 106 | "north": {"uv": [11, 12], "uv_size": [5, 1]}, 107 | "east": {"uv": [6, 15], "uv_size": [2, 1]}, 108 | "south": {"uv": [6, 13], "uv_size": [5, 1]}, 109 | "west": {"uv": [15, 6], "uv_size": [2, 1]}, 110 | "up": {"uv": [4, 4], "uv_size": [5, 2]}, 111 | "down": {"uv": [4, 8], "uv_size": [5, -2]} 112 | } 113 | }, 114 | { 115 | "origin": [2, 0, -0.75], 116 | "size": [5, 0.7, 1.5], 117 | "pivot": [0, 0, 0], 118 | "rotation": [0, -45, 0], 119 | "uv": { 120 | "north": {"uv": [11, 13], "uv_size": [5, 1]}, 121 | "east": {"uv": [15, 7], "uv_size": [2, 1]}, 122 | "south": {"uv": [14, 0], "uv_size": [5, 1]}, 123 | "west": {"uv": [8, 15], "uv_size": [2, 1]}, 124 | "up": {"uv": [0, 8], "uv_size": [5, 2]}, 125 | "down": {"uv": [5, 10], "uv_size": [5, -2]} 126 | } 127 | }, 128 | { 129 | "origin": [-7, 0, -0.75], 130 | "size": [5, 0.7, 1.5], 131 | "pivot": [0, 0, 0], 132 | "rotation": [0, -45, 0], 133 | "uv": { 134 | "north": {"uv": [14, 1], "uv_size": [5, 1]}, 135 | "east": {"uv": [10, 15], "uv_size": [2, 1]}, 136 | "south": {"uv": [14, 2], "uv_size": [5, 1]}, 137 | "west": {"uv": [12, 15], "uv_size": [2, 1]}, 138 | "up": {"uv": [9, 0], "uv_size": [5, 2]}, 139 | "down": {"uv": [9, 4], "uv_size": [5, -2]} 140 | } 141 | }, 142 | { 143 | "origin": [-6.6962, 1.2, 3.41114], 144 | "size": [6.3, 0.3, 1], 145 | "pivot": [-4.59634, 1.2, 3.11116], 146 | "rotation": [0, -45, 0], 147 | "uv": { 148 | "north": {"uv": [9, 4], "uv_size": [6, 1]}, 149 | "east": {"uv": [17, 5], "uv_size": [1, 1]}, 150 | "south": {"uv": [9, 5], "uv_size": [6, 1]}, 151 | "west": {"uv": [6, 17], "uv_size": [1, 1]}, 152 | "up": {"uv": [9, 6], "uv_size": [6, 1]}, 153 | "down": {"uv": [9, 8], "uv_size": [6, -1]} 154 | } 155 | }, 156 | { 157 | "origin": [-4.91942, 1.2, -7.5694], 158 | "size": [1, 0.3, 6.3], 159 | "pivot": [-4.41942, 1.35, -4.41942], 160 | "rotation": [0, -45, 0], 161 | "uv": { 162 | "north": {"uv": [17, 6], "uv_size": [1, 1]}, 163 | "east": {"uv": [0, 10], "uv_size": [6, 1]}, 164 | "south": {"uv": [7, 17], "uv_size": [1, 1]}, 165 | "west": {"uv": [6, 10], "uv_size": [6, 1]}, 166 | "up": {"uv": [0, 11], "uv_size": [1, 6]}, 167 | "down": {"uv": [1, 17], "uv_size": [1, -6]} 168 | } 169 | }, 170 | { 171 | "origin": [3.91941, 1.2, -7.56941], 172 | "size": [1, 0.3, 6.3], 173 | "pivot": [4.41941, 1.35, -4.41941], 174 | "rotation": [0, 45, 0], 175 | "uv": { 176 | "north": {"uv": [17, 7], "uv_size": [1, 1]}, 177 | "east": {"uv": [10, 8], "uv_size": [6, 1]}, 178 | "south": {"uv": [8, 17], "uv_size": [1, 1]}, 179 | "west": {"uv": [10, 9], "uv_size": [6, 1]}, 180 | "up": {"uv": [2, 11], "uv_size": [1, 6]}, 181 | "down": {"uv": [3, 17], "uv_size": [1, -6]} 182 | } 183 | }, 184 | { 185 | "origin": [3.91942, 1.2, 1.26942], 186 | "size": [1, 0.3, 6.3], 187 | "pivot": [4.41942, 1.35, 4.41942], 188 | "rotation": [0, -45, 0], 189 | "uv": { 190 | "north": {"uv": [17, 8], "uv_size": [1, 1]}, 191 | "east": {"uv": [4, 11], "uv_size": [6, 1]}, 192 | "south": {"uv": [9, 17], "uv_size": [1, 1]}, 193 | "west": {"uv": [10, 11], "uv_size": [6, 1]}, 194 | "up": {"uv": [4, 12], "uv_size": [1, 6]}, 195 | "down": {"uv": [5, 18], "uv_size": [1, -6]} 196 | } 197 | } 198 | ] 199 | } 200 | ] 201 | } 202 | ] 203 | } -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/client/render/screen/ControllerSetupScreen.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.client.render.screen; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import dev.lazurite.quadz.Quadz; 5 | import dev.lazurite.quadz.client.Config; 6 | import dev.lazurite.quadz.client.render.screen.osd.OnScreenDisplay; 7 | import dev.lazurite.quadz.common.util.FloatBufferUtil; 8 | import dev.lazurite.quadz.common.util.JoystickOutput; 9 | import net.minecraft.client.gui.components.Button; 10 | import net.minecraft.client.gui.screens.Screen; 11 | import net.minecraft.network.chat.Component; 12 | import net.minecraft.resources.ResourceLocation; 13 | 14 | import java.nio.FloatBuffer; 15 | import java.util.List; 16 | import java.util.function.Consumer; 17 | 18 | public class ControllerSetupScreen extends Screen { 19 | 20 | private final Screen parent; 21 | 22 | private final ResourceLocation pitchLocation = new ResourceLocation(Quadz.MODID, "pitch"); 23 | private final ResourceLocation yawLocation = new ResourceLocation(Quadz.MODID, "yaw"); 24 | private final ResourceLocation rollLocation = new ResourceLocation(Quadz.MODID, "roll"); 25 | private final ResourceLocation throttleLocation = new ResourceLocation(Quadz.MODID, "throttle"); 26 | 27 | private Button saveButton; 28 | private Button cancelButton; 29 | private Button configButton; 30 | private Button pitchButton; 31 | private Button yawButton; 32 | private Button rollButton; 33 | private Button throttleButton; 34 | private Button invertPitchButton; 35 | private Button invertYawButton; 36 | private Button invertRollButton; 37 | private Button invertThrottleButton; 38 | private Consumer axisConsumer; 39 | 40 | private FloatBuffer axes; 41 | private List previousAxes; 42 | 43 | public ControllerSetupScreen(Screen parent) { 44 | super(Component.translatable("quadz.config.controller_setup.title")); 45 | this.parent = parent; 46 | } 47 | 48 | @Override 49 | public void init() { 50 | var spacing = 10; 51 | var bWidth = 60; 52 | var bHeight = 20; 53 | 54 | this.saveButton = Button.builder(Component.translatable("quadz.config.controller_setup.save"), this::onSaveButton) 55 | .pos(spacing, height - bHeight - spacing) 56 | .size(bWidth, bHeight) 57 | .build(); 58 | 59 | this.cancelButton = Button.builder(Component.translatable("quadz.config.controller_setup.cancel"), this::onCancelButton) 60 | .pos(spacing + bWidth + spacing / 2, height - bHeight - spacing) 61 | .size(bWidth, bHeight) 62 | .build(); 63 | 64 | this.configButton = Button.builder(Component.translatable("quadz.config.controller_setup.config"), this::onConfigButton) 65 | .pos(width - spacing - bWidth, height - bHeight - spacing) 66 | .size(bWidth, bHeight) 67 | .build(); 68 | 69 | this.pitchButton = Button.builder(Component.translatable("quadz.config.controller_setup.pitch"), button -> onAxisButton(axis -> Config.pitch = axis)) 70 | .pos(width / 2 + bWidth + spacing, bHeight + spacing) 71 | .size(bWidth, bHeight) 72 | .build(); 73 | 74 | this.yawButton = Button.builder(Component.translatable("quadz.config.controller_setup.yaw"), button -> onAxisButton(axis -> Config.yaw = axis)) 75 | .pos(width / 2 - bWidth - spacing / 3, bHeight + spacing) 76 | .size(bWidth, bHeight) 77 | .build(); 78 | 79 | this.rollButton = Button.builder(Component.translatable("quadz.config.controller_setup.roll"), button -> onAxisButton(axis -> Config.roll = axis)) 80 | .pos(width / 2 + spacing / 3, bHeight + spacing) 81 | .size(bWidth, bHeight) 82 | .build(); 83 | 84 | this.throttleButton = Button.builder(Component.translatable("quadz.config.controller_setup.throttle"), button -> onAxisButton(axis -> Config.throttle = axis)) 85 | .pos(width / 2 - bWidth * 2 - spacing, bHeight + spacing) 86 | .size(bWidth, bHeight) 87 | .build(); 88 | 89 | this.invertPitchButton = Button.builder(Component.translatable("quadz.config.controller_setup.invert"), button -> Config.pitchInverted = !Config.pitchInverted) 90 | .pos(width / 2 + bWidth + spacing, bHeight * 2 + spacing) 91 | .size(bWidth, bHeight) 92 | .build(); 93 | 94 | this.invertYawButton = Button.builder(Component.translatable("quadz.config.controller_setup.invert"), button -> Config.yawInverted = !Config.yawInverted) 95 | .pos(width / 2 - bWidth - spacing / 3, bHeight * 2 + spacing) 96 | .size(bWidth, bHeight) 97 | .build(); 98 | 99 | this.invertRollButton = Button.builder(Component.translatable("quadz.config.controller_setup.invert"), button -> Config.rollInverted = !Config.rollInverted) 100 | .pos(width / 2 + spacing / 3, bHeight * 2 + spacing) 101 | .size(bWidth, bHeight) 102 | .build(); 103 | 104 | this.invertThrottleButton = Button.builder(Component.translatable("quadz.config.controller_setup.invert"), button -> Config.throttleInverted = !Config.throttleInverted) 105 | .pos(width / 2 - bWidth * 2 - spacing, bHeight * 2 + spacing) 106 | .size(bWidth, bHeight) 107 | .build(); 108 | 109 | this.addRenderableWidget(this.saveButton); 110 | this.addRenderableWidget(this.cancelButton); 111 | this.addRenderableWidget(this.configButton); 112 | this.addRenderableWidget(this.pitchButton); 113 | this.addRenderableWidget(this.yawButton); 114 | this.addRenderableWidget(this.rollButton); 115 | this.addRenderableWidget(this.throttleButton); 116 | this.addRenderableWidget(this.invertPitchButton); 117 | this.addRenderableWidget(this.invertYawButton); 118 | this.addRenderableWidget(this.invertRollButton); 119 | this.addRenderableWidget(this.invertThrottleButton); 120 | } 121 | 122 | private void onSaveButton(Button button) { 123 | Config.save(); 124 | this.minecraft.setScreen(this.parent); 125 | } 126 | 127 | private void onCancelButton(Button button) { 128 | this.minecraft.setScreen(this.parent); 129 | } 130 | 131 | private void onConfigButton(Button button) { 132 | this.minecraft.setScreen(MainConfigScreen.get(this)); 133 | } 134 | 135 | private void onAxisButton(Consumer axisConsumer) { 136 | this.axisConsumer = axisConsumer; 137 | this.pitchButton.active = false; 138 | this.yawButton.active = false; 139 | this.rollButton.active = false; 140 | this.throttleButton.active = false; 141 | this.saveButton.active = false; 142 | this.invertPitchButton.active = false; 143 | this.invertYawButton.active = false; 144 | this.invertRollButton.active = false; 145 | this.invertThrottleButton.active = false; 146 | } 147 | 148 | @Override 149 | public void tick() { 150 | while (this.axisConsumer != null && this.axes != null && this.axes.hasRemaining() && this.previousAxes != null) { 151 | var currentAxis = Math.round(this.axes.get() * 10f) / 10f; 152 | var previousAxis = Math.round(this.previousAxes.get(this.axes.position() - 1) * 10f) / 10f; 153 | 154 | if (currentAxis != previousAxis) { 155 | this.axisConsumer.accept(this.axes.position() - 1); 156 | this.axisConsumer = null; 157 | this.pitchButton.active = true; 158 | this.yawButton.active = true; 159 | this.rollButton.active = true; 160 | this.throttleButton.active = true; 161 | this.saveButton.active = true; 162 | this.invertPitchButton.active = true; 163 | this.invertYawButton.active = true; 164 | this.invertRollButton.active = true; 165 | this.invertThrottleButton.active = true; 166 | break; 167 | } 168 | } 169 | 170 | if (this.axes != null && this.axes.remaining() > 0) 171 | this.previousAxes = FloatBufferUtil.toArray(this.axes); 172 | this.axes = JoystickOutput.getAllAxisValues(); 173 | } 174 | 175 | @Override 176 | public void render(PoseStack poseStack, int i, int j, float f) { 177 | super.renderDirtBackground(0); 178 | super.render(poseStack, i, j, f); 179 | 180 | var pitch = JoystickOutput.getAxisValue(null, Config.pitch, this.pitchLocation, Config.pitchInverted, false); 181 | var yaw = JoystickOutput.getAxisValue(null, Config.yaw, this.yawLocation, Config.yawInverted, false); 182 | var roll = JoystickOutput.getAxisValue(null, Config.roll, this.rollLocation, Config.rollInverted, false); 183 | var throttle = JoystickOutput.getAxisValue(null, Config.throttle, this.throttleLocation, Config.throttleInverted, Config.throttleInCenter) + 1.0f; 184 | OnScreenDisplay.renderSticks(poseStack, f, width / 2, height / 2 + 20, 40, 10, pitch, yaw, roll, throttle); 185 | 186 | // An axis has been selected. Time to listen... 187 | if (this.axisConsumer != null) { 188 | drawCenteredString(poseStack, this.font, Component.translatable("quadz.config.controller_setup.prompt"), this.width / 2, 85, 0x00FF00); 189 | } 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/dev/lazurite/quadz/common/entity/Quadcopter.java: -------------------------------------------------------------------------------- 1 | package dev.lazurite.quadz.common.entity; 2 | 3 | import com.jme3.math.Quaternion; 4 | import com.jme3.math.Transform; 5 | import com.jme3.math.Vector3f; 6 | import dev.lazurite.form.api.Templated; 7 | import dev.lazurite.form.api.loader.TemplateLoader; 8 | import dev.lazurite.quadz.Quadz; 9 | import dev.lazurite.quadz.client.render.QuadcopterView; 10 | import dev.lazurite.quadz.common.util.Bindable; 11 | import dev.lazurite.quadz.common.util.Search; 12 | import dev.lazurite.quadz.common.item.GogglesItem; 13 | import dev.lazurite.quadz.common.item.RemoteItem; 14 | import dev.lazurite.quadz.common.util.BetaflightHelper; 15 | import dev.lazurite.quadz.common.util.Matrix4fHelper; 16 | import dev.lazurite.rayon.api.EntityPhysicsElement; 17 | import dev.lazurite.rayon.impl.bullet.collision.body.ElementRigidBody; 18 | import dev.lazurite.rayon.impl.bullet.collision.body.EntityRigidBody; 19 | import dev.lazurite.rayon.impl.bullet.math.Convert; 20 | import dev.lazurite.toolbox.api.math.QuaternionHelper; 21 | import dev.lazurite.toolbox.api.math.VectorHelper; 22 | import net.minecraft.core.Direction; 23 | import net.minecraft.nbt.CompoundTag; 24 | import net.minecraft.network.chat.Component; 25 | import net.minecraft.network.syncher.EntityDataAccessor; 26 | import net.minecraft.network.syncher.EntityDataSerializers; 27 | import net.minecraft.network.syncher.SynchedEntityData; 28 | import net.minecraft.resources.ResourceLocation; 29 | import net.minecraft.server.level.ServerPlayer; 30 | import net.minecraft.world.InteractionHand; 31 | import net.minecraft.world.InteractionResult; 32 | import net.minecraft.world.damagesource.DamageSource; 33 | import net.minecraft.world.entity.EntityType; 34 | import net.minecraft.world.entity.EquipmentSlot; 35 | import net.minecraft.world.entity.HumanoidArm; 36 | import net.minecraft.world.entity.LivingEntity; 37 | import net.minecraft.world.entity.player.Player; 38 | import net.minecraft.world.item.ItemStack; 39 | import net.minecraft.world.item.Items; 40 | import net.minecraft.world.level.Level; 41 | import net.minecraft.world.level.block.BushBlock; 42 | import net.minecraft.world.level.block.VineBlock; 43 | import net.minecraft.world.phys.Vec3; 44 | import org.jetbrains.annotations.NotNull; 45 | import org.joml.Matrix4f; 46 | import org.joml.Quaternionf; 47 | import software.bernie.geckolib.animatable.GeoEntity; 48 | import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; 49 | import software.bernie.geckolib.core.animation.AnimatableManager; 50 | import software.bernie.geckolib.core.animation.AnimationController; 51 | import software.bernie.geckolib.core.animation.RawAnimation; 52 | import software.bernie.geckolib.core.object.PlayState; 53 | import software.bernie.geckolib.util.GeckoLibUtil; 54 | 55 | import java.util.ArrayList; 56 | import java.util.Optional; 57 | 58 | public class Quadcopter extends LivingEntity implements EntityPhysicsElement, Templated, GeoEntity, Bindable { 59 | 60 | public static final DamageSource PROP_DAMAGE = new DamageSource("quadcopter").setProjectile().damageHelmet(); 61 | public static final EntityDataAccessor TEMPLATE = SynchedEntityData.defineId(Quadcopter.class, EntityDataSerializers.STRING); 62 | public static final EntityDataAccessor PREV_TEMPLATE = SynchedEntityData.defineId(Quadcopter.class, EntityDataSerializers.STRING); 63 | public static final EntityDataAccessor ARMED = SynchedEntityData.defineId(Quadcopter.class, EntityDataSerializers.BOOLEAN); 64 | public static final EntityDataAccessor BIND_ID = SynchedEntityData.defineId(Quadcopter.class, EntityDataSerializers.INT); 65 | public static final EntityDataAccessor CAMERA_ANGLE = SynchedEntityData.defineId(Quadcopter.class, EntityDataSerializers.INT); 66 | 67 | private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); 68 | private final EntityRigidBody rigidBody = new EntityRigidBody(this); 69 | private final QuadcopterView view = new QuadcopterView(this); 70 | 71 | public Quadcopter(EntityType entityType, Level level) { 72 | super(entityType, level); 73 | this.noCulling = true; 74 | this.rigidBody.setBuoyancyType(ElementRigidBody.BuoyancyType.NONE); 75 | this.rigidBody.setDragType(ElementRigidBody.DragType.SIMPLE); 76 | } 77 | 78 | @Override 79 | public void tick() { 80 | super.tick(); 81 | 82 | // Update template if a change is detected 83 | if (!getTemplate().equals(getEntityData().get(PREV_TEMPLATE))) { 84 | getEntityData().set(PREV_TEMPLATE, getTemplate()); 85 | this.refreshDimensions(); 86 | } 87 | 88 | if (!this.level.isClientSide) { 89 | // Server-side only prioritization 90 | Optional.ofNullable(getRigidBody().getPriorityPlayer()).ifPresent(player -> { 91 | if (!((ServerPlayer) player).getCamera().equals(this)) { 92 | getRigidBody().prioritize(null); 93 | } 94 | }); 95 | 96 | // Break grass, flowers, etc if the quadcopter is above a certain weight. 97 | if (this.getRigidBody().getMass() > 0.1f) { 98 | var block = this.level.getBlockState(this.blockPosition()).getBlock(); 99 | 100 | if (block instanceof BushBlock || block instanceof VineBlock) { 101 | this.level.destroyBlock(this.blockPosition(), false, this); 102 | } 103 | } 104 | 105 | // Hurt entities on collision 106 | this.level.getEntities(this, this.getBoundingBox(), entity -> entity instanceof LivingEntity).forEach(entity -> { 107 | entity.hurt(PROP_DAMAGE, 2.0f); 108 | }); 109 | } 110 | 111 | Search.forPlayer(this).ifPresentOrElse(player -> { 112 | this.setArmed(true); 113 | player.quadz$syncJoystick(); 114 | 115 | if (player instanceof ServerPlayer serverPlayer && serverPlayer.getCamera() == this && !player.equals(this.getRigidBody().getPriorityPlayer())) { 116 | this.getRigidBody().prioritize(player); 117 | } 118 | 119 | var pitch = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "pitch")); 120 | var yaw = -1 * player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "yaw")); 121 | var roll = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "roll")); 122 | var throttle = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "throttle")) + 1.0f; 123 | 124 | var rate = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "rate")); 125 | var superRate = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "super_rate")); 126 | var expo = player.quadz$getJoystickValue(new ResourceLocation(Quadz.MODID, "expo")); 127 | 128 | this.rotate( 129 | (float) BetaflightHelper.calculateRates(pitch, rate, expo, superRate, 0.05f), 130 | (float) BetaflightHelper.calculateRates(yaw, rate, expo, superRate, 0.05f), 131 | (float) BetaflightHelper.calculateRates(roll, rate, expo, superRate, 0.05f) 132 | ); 133 | 134 | // Decrease angular velocity 135 | if (throttle > 0.1f) { 136 | var correction = getRigidBody().getAngularVelocity(new Vector3f()).multLocal(0.5f * throttle); 137 | 138 | if (Float.isFinite(correction.lengthSquared())) { 139 | getRigidBody().setAngularVelocity(correction); 140 | } 141 | } 142 | 143 | // Get the thrust unit vector 144 | // TODO make this into it's own class 145 | var mat = new Matrix4f(); 146 | Matrix4fHelper.fromQuaternion(mat, QuaternionHelper.rotateX(Convert.toMinecraft(getRigidBody().getPhysicsRotation(new Quaternion())), 90)); 147 | var unit = Convert.toBullet(Matrix4fHelper.matrixToVector(mat)); 148 | 149 | // Calculate basic thrust 150 | var thrust = new Vector3f().set(unit).multLocal((float) (getThrust() * (Math.pow(throttle, getThrustCurve())))); 151 | 152 | // Calculate thrust from yaw spin 153 | var yawThrust = new Vector3f().set(unit).multLocal(Math.abs(yaw * getThrust() * 0.002f)); 154 | 155 | // Add up the net thrust and apply the force 156 | if (Float.isFinite(thrust.length())) { 157 | getRigidBody().applyCentralForce(thrust.add(yawThrust).multLocal(-1)); 158 | } else { 159 | Quadz.LOGGER.warn("Infinite thrust force!"); 160 | } 161 | }, () -> { 162 | this.setArmed(false); 163 | 164 | if (!this.level.isClientSide) { 165 | this.getRigidBody().prioritize(null); 166 | } 167 | }); 168 | } 169 | 170 | public float getThrust() { 171 | return TemplateLoader.getTemplateById(this.getTemplate()) 172 | .map(template -> template.metadata().get("thrust").getAsFloat()) 173 | .orElse(0.0f); 174 | } 175 | 176 | public float getThrustCurve() { 177 | return TemplateLoader.getTemplateById(this.getTemplate()) 178 | .map(template -> template.metadata().get("thrustCurve").getAsFloat()) 179 | .orElse(0.0f); 180 | } 181 | 182 | public void rotate(float x, float y, float z) { 183 | var rot = new Quaternionf(0, 0, 0, 1); 184 | QuaternionHelper.rotateX(rot, x); 185 | QuaternionHelper.rotateY(rot, y); 186 | QuaternionHelper.rotateZ(rot, z); 187 | 188 | var trans = getRigidBody().getTransform(new Transform()); 189 | trans.getRotation().set(trans.getRotation().mult(Convert.toBullet(rot))); 190 | getRigidBody().setPhysicsTransform(trans); 191 | } 192 | 193 | @Override 194 | public InteractionResult interact(Player player, InteractionHand hand) { 195 | if (!level.isClientSide()) { 196 | final var stack = player.getInventory().getSelected(); 197 | 198 | if (stack.getItem() instanceof RemoteItem) { 199 | Bindable.get(stack).ifPresent(bindable -> Bindable.bind(this, bindable)); 200 | player.displayClientMessage(Component.translatable("quadz.message.bound"), true); 201 | } 202 | } 203 | 204 | return InteractionResult.SUCCESS; 205 | } 206 | 207 | @Override 208 | public void kill() { 209 | var itemStack = new ItemStack(Quadz.QUADCOPTER_ITEM); 210 | Bindable.get(itemStack).ifPresent(bindable -> bindable.copyFrom(this)); 211 | Templated.get(itemStack).copyFrom(this); 212 | this.spawnAtLocation(itemStack); 213 | this.remove(RemovalReason.KILLED); 214 | } 215 | 216 | @Override 217 | public boolean hurt(@NotNull DamageSource source, float amount) { 218 | if (!level.isClientSide() && source.getEntity() instanceof ServerPlayer) { 219 | this.kill(); 220 | return true; 221 | } 222 | 223 | return false; 224 | } 225 | 226 | @Override 227 | public void readAdditionalSaveData(CompoundTag tag) { 228 | super.readAdditionalSaveData(tag); 229 | getEntityData().set(TEMPLATE, tag.getString("template")); 230 | getEntityData().set(BIND_ID, tag.getInt("bind_id")); 231 | getEntityData().set(CAMERA_ANGLE, tag.getInt("camera_angle")); 232 | } 233 | 234 | @Override 235 | public void addAdditionalSaveData(CompoundTag tag) { 236 | super.addAdditionalSaveData(tag); 237 | tag.putString("template", getTemplate()); 238 | tag.putInt("bind_id", getEntityData().get(BIND_ID)); 239 | tag.putInt("camera_angle", getEntityData().get(CAMERA_ANGLE)); 240 | } 241 | 242 | @Override 243 | public Vec3 getPosition(float tickDelta) { 244 | return VectorHelper.toVec3(Convert.toMinecraft(getPhysicsLocation(new Vector3f(), tickDelta))); 245 | } 246 | 247 | @Override 248 | public float getViewYRot(float tickDelta) { 249 | return QuaternionHelper.getYaw(Convert.toMinecraft(getPhysicsRotation(new Quaternion(), tickDelta))); 250 | } 251 | 252 | @Override 253 | public float getViewXRot(float tickDelta) { 254 | return QuaternionHelper.getPitch(Convert.toMinecraft(getPhysicsRotation(new Quaternion(), tickDelta))); 255 | } 256 | 257 | @Override 258 | public Direction getDirection() { 259 | return Direction.fromYRot(QuaternionHelper.getYaw(Convert.toMinecraft(getPhysicsRotation(new Quaternion(), 1.0f)))); 260 | } 261 | 262 | @Override 263 | protected void defineSynchedData() { 264 | super.defineSynchedData(); 265 | getEntityData().define(TEMPLATE, ""); 266 | getEntityData().define(PREV_TEMPLATE, ""); 267 | getEntityData().define(ARMED, false); 268 | getEntityData().define(BIND_ID, 0); 269 | getEntityData().define(CAMERA_ANGLE, 0); 270 | } 271 | 272 | // @Override 273 | // public boolean shouldRenderPlayer() { 274 | // return true; 275 | // } 276 | 277 | // @Override 278 | public boolean shouldPlayerBeViewing(Player player) { 279 | return player != null && player.getInventory().armor.get(3).getItem() instanceof GogglesItem; 280 | } 281 | 282 | @Override 283 | public boolean shouldRenderAtSqrDistance(double distance) { 284 | return true; 285 | } 286 | 287 | @Override 288 | public boolean isPickable() { 289 | return true; 290 | } 291 | 292 | @Override 293 | public Iterable getArmorSlots() { 294 | return new ArrayList<>(); 295 | } 296 | 297 | @Override 298 | public ItemStack getItemBySlot(EquipmentSlot equipmentSlot) { 299 | return new ItemStack(Items.AIR); 300 | } 301 | 302 | @Override 303 | public void setItemSlot(EquipmentSlot equipmentSlot, ItemStack itemStack) { 304 | 305 | } 306 | 307 | @Override 308 | public HumanoidArm getMainArm() { 309 | return null; 310 | } 311 | 312 | @Override 313 | public EntityRigidBody getRigidBody() { 314 | return this.rigidBody; 315 | } 316 | 317 | @Override 318 | public void registerControllers(AnimatableManager.ControllerRegistrar controllerRegistrar) { 319 | controllerRegistrar.add(new AnimationController<>(this, 0, state -> { 320 | if (this.isArmed()) { 321 | return state.setAndContinue(RawAnimation.begin().thenLoop("armed")); 322 | } 323 | 324 | return PlayState.STOP; 325 | })); 326 | } 327 | 328 | @Override 329 | public AnimatableInstanceCache getAnimatableInstanceCache() { 330 | return this.cache; 331 | } 332 | 333 | @Override 334 | public void setBindId(int bindId) { 335 | this.getEntityData().set(BIND_ID, bindId); 336 | } 337 | 338 | @Override 339 | public int getBindId() { 340 | return this.getEntityData().get(BIND_ID); 341 | } 342 | 343 | @Override 344 | public String getTemplate() { 345 | return this.getEntityData().get(TEMPLATE); 346 | } 347 | 348 | @Override 349 | public void setTemplate(String template) { 350 | this.getEntityData().set(TEMPLATE, template); 351 | } 352 | 353 | public void setArmed(boolean armed) { 354 | this.getEntityData().set(ARMED, armed); 355 | } 356 | 357 | public boolean isArmed() { 358 | return this.getEntityData().get(ARMED); 359 | } 360 | 361 | public QuadcopterView getView() { 362 | return this.view; 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voyager/geo/voyager.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:geometry": [ 4 | { 5 | "description": { 6 | "identifier": "geometry.voyager", 7 | "texture_width": 32, 8 | "texture_height": 32, 9 | "visible_bounds_width": 3, 10 | "visible_bounds_height": 1.5, 11 | "visible_bounds_offset": [0, 0.25, 0] 12 | }, 13 | "bones": [ 14 | { 15 | "name": "Battery", 16 | "pivot": [10.86272, 1.3, -8.66381], 17 | "cubes": [ 18 | { 19 | "origin": [-1.25, 2.3, -2.6], 20 | "size": [2.5, 1.5, 5.9], 21 | "uv": { 22 | "north": {"uv": [2, 23], "uv_size": [3, 2]}, 23 | "east": {"uv": [9, 6], "uv_size": [6, 2]}, 24 | "south": {"uv": [9, 23], "uv_size": [3, 2]}, 25 | "west": {"uv": [12, 8], "uv_size": [6, 2]}, 26 | "up": {"uv": [6, 0], "uv_size": [3, 6]}, 27 | "down": {"uv": [9, 15], "uv_size": [3, -6]} 28 | } 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "Frame", 34 | "pivot": [0, 0, 0] 35 | }, 36 | { 37 | "name": "Camera", 38 | "parent": "Frame", 39 | "pivot": [0, -0.35, -7.7], 40 | "rotation": [-27.5, 0, 0], 41 | "cubes": [ 42 | { 43 | "origin": [-2, 0.79217, -8.01032], 44 | "size": [4, 3, 1.8], 45 | "uv": { 46 | "north": {"uv": [9, 0], "uv_size": [4, 3]}, 47 | "east": {"uv": [3, 0], "uv_size": [2, 3]}, 48 | "south": {"uv": [9, 3], "uv_size": [4, 3]}, 49 | "west": {"uv": [0, 23], "uv_size": [2, 3]}, 50 | "up": {"uv": [5, 14], "uv_size": [4, 2]}, 51 | "down": {"uv": [8, 20], "uv_size": [4, -2]} 52 | } 53 | }, 54 | { 55 | "origin": [0, 1.79217, -8.71032], 56 | "size": [2, 2, 0.7], 57 | "uv": { 58 | "north": {"uv": [22, 23], "uv_size": [2, 2]}, 59 | "east": {"uv": [5, 0], "uv_size": [1, 2]}, 60 | "south": {"uv": [23, 0], "uv_size": [2, 2]}, 61 | "west": {"uv": [5, 11], "uv_size": [1, 2]}, 62 | "up": {"uv": [25, 7], "uv_size": [2, 1]}, 63 | "down": {"uv": [25, 10], "uv_size": [2, -1]} 64 | } 65 | } 66 | ] 67 | }, 68 | { 69 | "name": "Motors", 70 | "parent": "Frame", 71 | "pivot": [10.86272, 1.3, -8.66381], 72 | "cubes": [ 73 | { 74 | "origin": [-8.32089, 0.6, -5.91167], 75 | "size": [1.6, 0.5, 1.6], 76 | "pivot": [-7.52089, 0.6, -6.11167], 77 | "rotation": [0, -17.5, 0], 78 | "uv": { 79 | "north": {"uv": [21, 25], "uv_size": [2, 1]}, 80 | "east": {"uv": [25, 0], "uv_size": [2, 1]}, 81 | "south": {"uv": [25, 1], "uv_size": [2, 1]}, 82 | "west": {"uv": [25, 2], "uv_size": [2, 1]}, 83 | "up": {"uv": [14, 23], "uv_size": [2, 2]}, 84 | "down": {"uv": [16, 25], "uv_size": [2, -2]} 85 | } 86 | }, 87 | { 88 | "origin": [6.90303, 0.6, 4.91346], 89 | "size": [1.6, 0.5, 1.6], 90 | "pivot": [4.70303, 0.9, 5.71346], 91 | "rotation": [0, -35, 0], 92 | "uv": { 93 | "north": {"uv": [26, 17], "uv_size": [2, 1]}, 94 | "east": {"uv": [26, 19], "uv_size": [2, 1]}, 95 | "south": {"uv": [26, 24], "uv_size": [2, 1]}, 96 | "west": {"uv": [26, 25], "uv_size": [2, 1]}, 97 | "up": {"uv": [2, 25], "uv_size": [2, 2]}, 98 | "down": {"uv": [4, 27], "uv_size": [2, -2]} 99 | } 100 | }, 101 | { 102 | "origin": [6.59273, 0.6, 2.04558], 103 | "size": [1.6, 0.5, 1.6], 104 | "pivot": [-0.60727, 0.9, 2.84558], 105 | "rotation": [0, -145, 0], 106 | "uv": { 107 | "north": {"uv": [0, 27], "uv_size": [2, 1]}, 108 | "east": {"uv": [2, 27], "uv_size": [2, 1]}, 109 | "south": {"uv": [4, 27], "uv_size": [2, 1]}, 110 | "west": {"uv": [9, 27], "uv_size": [2, 1]}, 111 | "up": {"uv": [9, 25], "uv_size": [2, 2]}, 112 | "down": {"uv": [11, 27], "uv_size": [2, -2]} 113 | } 114 | }, 115 | { 116 | "origin": [7.94417, 0.6, -4.06045], 117 | "size": [1.6, 0.5, 1.6], 118 | "pivot": [2.64628, 0.4, -1.39929], 119 | "rotation": [0, 17.5, 0], 120 | "uv": { 121 | "north": {"uv": [27, 2], "uv_size": [2, 1]}, 122 | "east": {"uv": [27, 3], "uv_size": [2, 1]}, 123 | "south": {"uv": [27, 4], "uv_size": [2, 1]}, 124 | "west": {"uv": [27, 5], "uv_size": [2, 1]}, 125 | "up": {"uv": [13, 25], "uv_size": [2, 2]}, 126 | "down": {"uv": [15, 27], "uv_size": [2, -2]} 127 | } 128 | } 129 | ] 130 | }, 131 | { 132 | "name": "Arms", 133 | "parent": "Frame", 134 | "pivot": [0, 0, 0] 135 | }, 136 | { 137 | "name": "back_left", 138 | "parent": "Arms", 139 | "pivot": [0, 0, 0], 140 | "cubes": [ 141 | { 142 | "origin": [1, 0.1, 2.6], 143 | "size": [7, 0.6, 1.2], 144 | "pivot": [2, 0.4, 2.6], 145 | "rotation": [0, -35, 0], 146 | "uv": { 147 | "north": {"uv": [5, 16], "uv_size": [7, 1]}, 148 | "east": {"uv": [5, 2], "uv_size": [1, 1]}, 149 | "south": {"uv": [18, 9], "uv_size": [7, 1]}, 150 | "west": {"uv": [5, 13], "uv_size": [1, 1]}, 151 | "up": {"uv": [0, 19], "uv_size": [7, 1]}, 152 | "down": {"uv": [12, 20], "uv_size": [7, -1]} 153 | } 154 | }, 155 | { 156 | "origin": [6.70303, 0.1, 4.71346], 157 | "size": [2, 0.6, 2], 158 | "pivot": [4.70303, 0.4, 5.71346], 159 | "rotation": [0, -35, 0], 160 | "uv": { 161 | "north": {"uv": [23, 26], "uv_size": [2, 1]}, 162 | "east": {"uv": [25, 26], "uv_size": [2, 1]}, 163 | "south": {"uv": [26, 8], "uv_size": [2, 1]}, 164 | "west": {"uv": [26, 12], "uv_size": [2, 1]}, 165 | "up": {"uv": [24, 24], "uv_size": [2, 2]}, 166 | "down": {"uv": [24, 14], "uv_size": [2, -2]} 167 | } 168 | } 169 | ] 170 | }, 171 | { 172 | "name": "back_right", 173 | "parent": "Arms", 174 | "pivot": [0, 0, 0], 175 | "cubes": [ 176 | { 177 | "origin": [-3, 0.1, 1.4], 178 | "size": [7, 0.6, 1.2], 179 | "pivot": [-2, 0.4, 2.6], 180 | "rotation": [0, -145, 0], 181 | "uv": { 182 | "north": {"uv": [19, 19], "uv_size": [7, 1]}, 183 | "east": {"uv": [7, 19], "uv_size": [1, 1]}, 184 | "south": {"uv": [0, 20], "uv_size": [7, 1]}, 185 | "west": {"uv": [6, 22], "uv_size": [1, 1]}, 186 | "up": {"uv": [7, 20], "uv_size": [7, 1]}, 187 | "down": {"uv": [14, 21], "uv_size": [7, -1]} 188 | } 189 | }, 190 | { 191 | "origin": [6.39273, 0.1, 1.84558], 192 | "size": [2, 0.6, 2], 193 | "pivot": [-0.60727, 0.2, 2.84558], 194 | "rotation": [0, -145, 0], 195 | "uv": { 196 | "north": {"uv": [26, 13], "uv_size": [2, 1]}, 197 | "east": {"uv": [26, 14], "uv_size": [2, 1]}, 198 | "south": {"uv": [26, 15], "uv_size": [2, 1]}, 199 | "west": {"uv": [26, 16], "uv_size": [2, 1]}, 200 | "up": {"uv": [24, 14], "uv_size": [2, 2]}, 201 | "down": {"uv": [24, 18], "uv_size": [2, -2]} 202 | } 203 | } 204 | ] 205 | }, 206 | { 207 | "name": "front_left", 208 | "parent": "Arms", 209 | "pivot": [0, 0, 1], 210 | "cubes": [ 211 | { 212 | "origin": [1.69788, 0.1, -4.56115], 213 | "size": [6, 0.6, 1.2], 214 | "pivot": [3.6, -0.1, -1.7], 215 | "rotation": [0, 17.5, 0], 216 | "uv": { 217 | "north": {"uv": [9, 21], "uv_size": [6, 1]}, 218 | "east": {"uv": [23, 25], "uv_size": [1, 1]}, 219 | "south": {"uv": [15, 21], "uv_size": [6, 1]}, 220 | "west": {"uv": [27, 6], "uv_size": [1, 1]}, 221 | "up": {"uv": [21, 21], "uv_size": [6, 1]}, 222 | "down": {"uv": [21, 21], "uv_size": [6, -1]} 223 | } 224 | }, 225 | { 226 | "origin": [7.74417, 0.1, -4.26045], 227 | "size": [2, 0.6, 2], 228 | "pivot": [2.64628, -0.1, -1.39929], 229 | "rotation": [0, 17.5, 0], 230 | "uv": { 231 | "north": {"uv": [10, 17], "uv_size": [2, 1]}, 232 | "east": {"uv": [22, 17], "uv_size": [2, 1]}, 233 | "south": {"uv": [17, 25], "uv_size": [2, 1]}, 234 | "west": {"uv": [19, 25], "uv_size": [2, 1]}, 235 | "up": {"uv": [5, 23], "uv_size": [2, 2]}, 236 | "down": {"uv": [12, 25], "uv_size": [2, -2]} 237 | } 238 | } 239 | ] 240 | }, 241 | { 242 | "name": "front_right", 243 | "parent": "Arms", 244 | "pivot": [0, 0, 1], 245 | "cubes": [ 246 | { 247 | "origin": [4.55372, 0.1, -2.09929], 248 | "size": [2, 0.6, 2], 249 | "pivot": [-1.44628, -0.1, -2.09929], 250 | "rotation": [0, 162.5, 0], 251 | "uv": { 252 | "north": {"uv": [25, 3], "uv_size": [2, 1]}, 253 | "east": {"uv": [25, 4], "uv_size": [2, 1]}, 254 | "south": {"uv": [25, 5], "uv_size": [2, 1]}, 255 | "west": {"uv": [25, 6], "uv_size": [2, 1]}, 256 | "up": {"uv": [18, 23], "uv_size": [2, 2]}, 257 | "down": {"uv": [20, 25], "uv_size": [2, -2]} 258 | } 259 | }, 260 | { 261 | "origin": [-3.4, 0.1, -1.6], 262 | "size": [6, 0.6, 1.2], 263 | "pivot": [-2.4, 0.1, -2.4], 264 | "rotation": [0, 162.5, 0], 265 | "uv": { 266 | "north": {"uv": [0, 22], "uv_size": [6, 1]}, 267 | "east": {"uv": [27, 7], "uv_size": [1, 1]}, 268 | "south": {"uv": [9, 22], "uv_size": [6, 1]}, 269 | "west": {"uv": [27, 9], "uv_size": [1, 1]}, 270 | "up": {"uv": [15, 22], "uv_size": [6, 1]}, 271 | "down": {"uv": [21, 23], "uv_size": [6, -1]} 272 | } 273 | } 274 | ] 275 | }, 276 | { 277 | "name": "Antenna", 278 | "parent": "Frame", 279 | "pivot": [0, 0, -0.75], 280 | "rotation": [-7.5, 0, 0], 281 | "cubes": [ 282 | { 283 | "origin": [-0.25, 4.30005, 2.42088], 284 | "size": [0.5, 0.5, 6.6], 285 | "pivot": [0, 4.55005, 5.72088], 286 | "rotation": [37.5, 0, 0], 287 | "uv": { 288 | "north": {"uv": [2, 28], "uv_size": [1, 1]}, 289 | "east": {"uv": [20, 18], "uv_size": [7, 1]}, 290 | "south": {"uv": [3, 28], "uv_size": [1, 1]}, 291 | "west": {"uv": [0, 21], "uv_size": [7, 1]}, 292 | "up": {"uv": [7, 21], "uv_size": [1, 7]}, 293 | "down": {"uv": [8, 28], "uv_size": [1, -7]} 294 | } 295 | }, 296 | { 297 | "origin": [-0.45, -0.6303, 10], 298 | "size": [0.9, 0.8, 1.5], 299 | "pivot": [0, -0.18293, -0.60721], 300 | "rotation": [37.5, 0, 0], 301 | "uv": { 302 | "north": {"uv": [13, 28], "uv_size": [1, 1]}, 303 | "east": {"uv": [14, 28], "uv_size": [1, 1]}, 304 | "south": {"uv": [28, 28], "uv_size": [1, 1]}, 305 | "west": {"uv": [28, 0], "uv_size": [1, 1]}, 306 | "up": {"uv": [28, 1], "uv_size": [1, 1]}, 307 | "down": {"uv": [28, 7], "uv_size": [1, -1]} 308 | } 309 | } 310 | ] 311 | }, 312 | { 313 | "name": "Body", 314 | "parent": "Frame", 315 | "pivot": [0, 0, 0], 316 | "cubes": [ 317 | { 318 | "origin": [-1.5, 0, -4.4], 319 | "size": [3, 0.6, 8], 320 | "uv": { 321 | "north": {"uv": [9, 8], "uv_size": [3, 1]}, 322 | "east": {"uv": [14, 17], "uv_size": [8, 1]}, 323 | "south": {"uv": [9, 15], "uv_size": [3, 1]}, 324 | "west": {"uv": [0, 18], "uv_size": [8, 1]}, 325 | "up": {"uv": [0, 0], "uv_size": [3, 8]}, 326 | "down": {"uv": [3, 11], "uv_size": [3, -8]} 327 | } 328 | }, 329 | { 330 | "origin": [-1, 0, -6.2], 331 | "size": [2, 0.5, 2], 332 | "pivot": [1, 0.5, -4.2], 333 | "rotation": [-15, 0, 0], 334 | "uv": { 335 | "north": {"uv": [0, 26], "uv_size": [2, 1]}, 336 | "east": {"uv": [17, 26], "uv_size": [2, 1]}, 337 | "south": {"uv": [19, 26], "uv_size": [2, 1]}, 338 | "west": {"uv": [21, 26], "uv_size": [2, 1]}, 339 | "up": {"uv": [23, 2], "uv_size": [2, 2]}, 340 | "down": {"uv": [23, 6], "uv_size": [2, -2]} 341 | } 342 | }, 343 | { 344 | "origin": [-1.5, 1.8, -4.4], 345 | "size": [3, 0.5, 8], 346 | "uv": { 347 | "north": {"uv": [0, 16], "uv_size": [3, 1]}, 348 | "east": {"uv": [12, 18], "uv_size": [8, 1]}, 349 | "south": {"uv": [24, 23], "uv_size": [3, 1]}, 350 | "west": {"uv": [18, 8], "uv_size": [8, 1]}, 351 | "up": {"uv": [6, 6], "uv_size": [3, 8]}, 352 | "down": {"uv": [0, 16], "uv_size": [3, -8]} 353 | } 354 | }, 355 | { 356 | "origin": [-1.2, 0.5, -3.2], 357 | "size": [2.4, 0.6, 5.5], 358 | "uv": { 359 | "north": {"uv": [11, 27], "uv_size": [2, 1]}, 360 | "east": {"uv": [22, 10], "uv_size": [6, 1]}, 361 | "south": {"uv": [13, 27], "uv_size": [2, 1]}, 362 | "west": {"uv": [22, 11], "uv_size": [6, 1]}, 363 | "up": {"uv": [3, 11], "uv_size": [2, 6]}, 364 | "down": {"uv": [12, 18], "uv_size": [2, -6]} 365 | } 366 | }, 367 | { 368 | "origin": [0.95, 0.5, 3], 369 | "size": [0.5, 1.5, 0.5], 370 | "uv": { 371 | "north": {"uv": [6, 25], "uv_size": [1, 2]}, 372 | "east": {"uv": [6, 27], "uv_size": [1, 2]}, 373 | "south": {"uv": [15, 27], "uv_size": [1, 2]}, 374 | "west": {"uv": [16, 27], "uv_size": [1, 2]}, 375 | "up": {"uv": [4, 28], "uv_size": [1, 1]}, 376 | "down": {"uv": [5, 29], "uv_size": [1, -1]} 377 | } 378 | }, 379 | { 380 | "origin": [-1.45, 0.5, 3], 381 | "size": [0.5, 1.5, 0.5], 382 | "uv": { 383 | "north": {"uv": [17, 27], "uv_size": [1, 2]}, 384 | "east": {"uv": [18, 27], "uv_size": [1, 2]}, 385 | "south": {"uv": [19, 27], "uv_size": [1, 2]}, 386 | "west": {"uv": [20, 27], "uv_size": [1, 2]}, 387 | "up": {"uv": [7, 28], "uv_size": [1, 1]}, 388 | "down": {"uv": [8, 29], "uv_size": [1, -1]} 389 | } 390 | }, 391 | { 392 | "origin": [-1.45, 0.5, -4.3], 393 | "size": [0.5, 1.5, 0.5], 394 | "uv": { 395 | "north": {"uv": [25, 27], "uv_size": [1, 2]}, 396 | "east": {"uv": [26, 27], "uv_size": [1, 2]}, 397 | "south": {"uv": [27, 27], "uv_size": [1, 2]}, 398 | "west": {"uv": [27, 0], "uv_size": [1, 2]}, 399 | "up": {"uv": [11, 28], "uv_size": [1, 1]}, 400 | "down": {"uv": [12, 29], "uv_size": [1, -1]} 401 | } 402 | }, 403 | { 404 | "origin": [0.95, 0.5, -4.3], 405 | "size": [0.5, 1.5, 0.5], 406 | "uv": { 407 | "north": {"uv": [21, 27], "uv_size": [1, 2]}, 408 | "east": {"uv": [22, 27], "uv_size": [1, 2]}, 409 | "south": {"uv": [23, 27], "uv_size": [1, 2]}, 410 | "west": {"uv": [24, 27], "uv_size": [1, 2]}, 411 | "up": {"uv": [9, 28], "uv_size": [1, 1]}, 412 | "down": {"uv": [10, 29], "uv_size": [1, -1]} 413 | } 414 | } 415 | ] 416 | }, 417 | { 418 | "name": "Propellers", 419 | "pivot": [10.75, 2.3, -7.5] 420 | }, 421 | { 422 | "name": "front_left_prop", 423 | "parent": "Propellers", 424 | "pivot": [7.9023, 2.3, -5.008], 425 | "cubes": [ 426 | { 427 | "origin": [2.90227, 1.1, -5.508], 428 | "size": [10, 0.3, 1], 429 | "pivot": [7.90227, 1.7, -5.008], 430 | "rotation": [0, -37.5, 0], 431 | "uv": { 432 | "north": {"uv": [12, 10], "uv_size": [10, 1]}, 433 | "east": {"uv": [27, 18], "uv_size": [1, 1]}, 434 | "south": {"uv": [12, 11], "uv_size": [10, 1]}, 435 | "west": {"uv": [27, 20], "uv_size": [1, 1]}, 436 | "up": {"uv": [13, 0], "uv_size": [10, 1]}, 437 | "down": {"uv": [13, 2], "uv_size": [10, -1]} 438 | } 439 | } 440 | ] 441 | }, 442 | { 443 | "name": "front_right_prop", 444 | "parent": "Propellers", 445 | "pivot": [-7.8205, 0, -5.1648], 446 | "cubes": [ 447 | { 448 | "origin": [-12.82054, 1.1, -5.66484], 449 | "size": [10, 0.3, 1], 450 | "pivot": [-7.82054, 1.6, -5.16484], 451 | "rotation": [0, 37.5, 0], 452 | "uv": { 453 | "north": {"uv": [13, 2], "uv_size": [10, 1]}, 454 | "east": {"uv": [27, 21], "uv_size": [1, 1]}, 455 | "south": {"uv": [13, 3], "uv_size": [10, 1]}, 456 | "west": {"uv": [27, 22], "uv_size": [1, 1]}, 457 | "up": {"uv": [13, 4], "uv_size": [10, 1]}, 458 | "down": {"uv": [13, 6], "uv_size": [10, -1]} 459 | } 460 | } 461 | ] 462 | }, 463 | { 464 | "name": "back_left_prop", 465 | "parent": "Propellers", 466 | "pivot": [7.1605, 0, 7.4342], 467 | "cubes": [ 468 | { 469 | "origin": [2.16051, 1.1, 6.93415], 470 | "size": [10, 0.3, 1], 471 | "pivot": [7.1605, 2.1, 7.43415], 472 | "rotation": [0, 20, 0], 473 | "uv": { 474 | "north": {"uv": [14, 14], "uv_size": [10, 1]}, 475 | "east": {"uv": [27, 23], "uv_size": [1, 1]}, 476 | "south": {"uv": [14, 12], "uv_size": [10, 1]}, 477 | "west": {"uv": [27, 26], "uv_size": [1, 1]}, 478 | "up": {"uv": [14, 13], "uv_size": [10, 1]}, 479 | "down": {"uv": [14, 16], "uv_size": [10, -1]} 480 | } 481 | } 482 | ] 483 | }, 484 | { 485 | "name": "back_right_prop", 486 | "parent": "Propellers", 487 | "pivot": [-7.1605, 2.3, 7.4342], 488 | "cubes": [ 489 | { 490 | "origin": [-12.16049, 1.1, 6.93419], 491 | "size": [10, 0.3, 1], 492 | "pivot": [-7.16049, 2.1, 7.43419], 493 | "rotation": [0, 160, 0], 494 | "uv": { 495 | "north": {"uv": [15, 6], "uv_size": [10, 1]}, 496 | "east": {"uv": [0, 28], "uv_size": [1, 1]}, 497 | "south": {"uv": [15, 7], "uv_size": [10, 1]}, 498 | "west": {"uv": [1, 28], "uv_size": [1, 1]}, 499 | "up": {"uv": [14, 16], "uv_size": [10, 1]}, 500 | "down": {"uv": [0, 18], "uv_size": [10, -1]} 501 | } 502 | } 503 | ] 504 | } 505 | ] 506 | } 507 | ] 508 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/voxel_racer_one/geo/voxel_racer_one.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:geometry": [ 4 | { 5 | "description": { 6 | "identifier": "geometry.voxel_racer_one", 7 | "texture_width": 32, 8 | "texture_height": 32, 9 | "visible_bounds_width": 2, 10 | "visible_bounds_height": 1.5, 11 | "visible_bounds_offset": [0, 0.25, 0] 12 | }, 13 | "bones": [ 14 | { 15 | "name": "unknown_bone", 16 | "pivot": [0, 0, 0], 17 | "cubes": [ 18 | { 19 | "origin": [-1.5, 0, -2.3], 20 | "size": [3, 1.8, 4.5], 21 | "uv": { 22 | "north": {"uv": [11, 14], "uv_size": [3, 2]}, 23 | "east": {"uv": [6, 12], "uv_size": [5, 2]}, 24 | "south": {"uv": [14, 14], "uv_size": [3, 2]}, 25 | "west": {"uv": [0, 13], "uv_size": [5, 2]}, 26 | "up": {"uv": [0, 8], "uv_size": [3, 5]}, 27 | "down": {"uv": [3, 13], "uv_size": [3, -5]} 28 | } 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "props", 34 | "pivot": [0, 0, 0] 35 | }, 36 | { 37 | "name": "front_right", 38 | "parent": "props", 39 | "pivot": [-4.4194, 1.35, -4.4194], 40 | "cubes": [ 41 | { 42 | "origin": [-4.91942, 2.9, -7.5694], 43 | "size": [1, 0.3, 6.3], 44 | "pivot": [-4.41942, 3.05, -4.41942], 45 | "rotation": [0, -45, 0], 46 | "uv": { 47 | "north": {"uv": [4, 24], "uv_size": [1, 1]}, 48 | "east": {"uv": [13, 2], "uv_size": [6, 1]}, 49 | "south": {"uv": [5, 24], "uv_size": [1, 1]}, 50 | "west": {"uv": [13, 3], "uv_size": [6, 1]}, 51 | "up": {"uv": [5, 13], "uv_size": [1, 6]}, 52 | "down": {"uv": [6, 20], "uv_size": [1, -6]} 53 | } 54 | }, 55 | { 56 | "origin": [-7.56942, 2.9, -4.9194], 57 | "size": [2.65, 0.3, 1], 58 | "pivot": [-4.41942, 3.05, -4.41942], 59 | "rotation": [0, -45, 0], 60 | "uv": { 61 | "north": {"uv": [9, 20], "uv_size": [3, 1]}, 62 | "east": {"uv": [14, 24], "uv_size": [1, 1]}, 63 | "south": {"uv": [12, 20], "uv_size": [3, 1]}, 64 | "west": {"uv": [15, 24], "uv_size": [1, 1]}, 65 | "up": {"uv": [15, 20], "uv_size": [3, 1]}, 66 | "down": {"uv": [18, 21], "uv_size": [3, -1]} 67 | } 68 | }, 69 | { 70 | "origin": [-3.9194, 2.9, -4.9194], 71 | "size": [2.65, 0.3, 1], 72 | "pivot": [-4.41942, 3.05, -4.41942], 73 | "rotation": [0, -45, 0], 74 | "uv": { 75 | "north": {"uv": [0, 21], "uv_size": [3, 1]}, 76 | "east": {"uv": [16, 24], "uv_size": [1, 1]}, 77 | "south": {"uv": [3, 21], "uv_size": [3, 1]}, 78 | "west": {"uv": [17, 24], "uv_size": [1, 1]}, 79 | "up": {"uv": [6, 21], "uv_size": [3, 1]}, 80 | "down": {"uv": [9, 22], "uv_size": [3, -1]} 81 | } 82 | } 83 | ] 84 | }, 85 | { 86 | "name": "back_right", 87 | "parent": "props", 88 | "pivot": [-4.5, 0, 4.5], 89 | "cubes": [ 90 | { 91 | "origin": [-4.8774, 2.9, 3.5066], 92 | "size": [2.65, 0.3, 1], 93 | "pivot": [-4.40015, 2.9, 3.05666], 94 | "rotation": [0, 45, 0], 95 | "uv": { 96 | "north": {"uv": [0, 19], "uv_size": [3, 1]}, 97 | "east": {"uv": [23, 20], "uv_size": [1, 1]}, 98 | "south": {"uv": [3, 19], "uv_size": [3, 1]}, 99 | "west": {"uv": [23, 22], "uv_size": [1, 1]}, 100 | "up": {"uv": [19, 19], "uv_size": [3, 1]}, 101 | "down": {"uv": [19, 1], "uv_size": [3, -1]} 102 | } 103 | }, 104 | { 105 | "origin": [-8.4133, 2.9, 3.4821], 106 | "size": [2.65, 0.3, 1], 107 | "pivot": [-4.31343, 2.9, 3.18212], 108 | "rotation": [0, 45, 0], 109 | "uv": { 110 | "north": {"uv": [19, 1], "uv_size": [3, 1]}, 111 | "east": {"uv": [0, 24], "uv_size": [1, 1]}, 112 | "south": {"uv": [19, 2], "uv_size": [3, 1]}, 113 | "west": {"uv": [1, 24], "uv_size": [1, 1]}, 114 | "up": {"uv": [19, 3], "uv_size": [3, 1]}, 115 | "down": {"uv": [19, 5], "uv_size": [3, -1]} 116 | } 117 | }, 118 | { 119 | "origin": [-6.6962, 2.9, 3.41114], 120 | "size": [6.3, 0.3, 1], 121 | "pivot": [-4.59634, 2.9, 3.11116], 122 | "rotation": [0, -45, 0], 123 | "uv": { 124 | "north": {"uv": [11, 10], "uv_size": [6, 1]}, 125 | "east": {"uv": [2, 24], "uv_size": [1, 1]}, 126 | "south": {"uv": [11, 13], "uv_size": [6, 1]}, 127 | "west": {"uv": [3, 24], "uv_size": [1, 1]}, 128 | "up": {"uv": [13, 0], "uv_size": [6, 1]}, 129 | "down": {"uv": [13, 2], "uv_size": [6, -1]} 130 | } 131 | } 132 | ] 133 | }, 134 | { 135 | "name": "back_left", 136 | "parent": "props", 137 | "pivot": [4.4194, 0, 4.4194], 138 | "cubes": [ 139 | { 140 | "origin": [3.91942, 2.9, 1.26942], 141 | "size": [1, 0.3, 6.3], 142 | "pivot": [4.41942, 3.05, 4.41942], 143 | "rotation": [0, -45, 0], 144 | "uv": { 145 | "north": {"uv": [8, 24], "uv_size": [1, 1]}, 146 | "east": {"uv": [13, 6], "uv_size": [6, 1]}, 147 | "south": {"uv": [9, 24], "uv_size": [1, 1]}, 148 | "west": {"uv": [13, 7], "uv_size": [6, 1]}, 149 | "up": {"uv": [9, 14], "uv_size": [1, 6]}, 150 | "down": {"uv": [10, 20], "uv_size": [1, -6]} 151 | } 152 | }, 153 | { 154 | "origin": [4.9194, 2.9, 3.9194], 155 | "size": [2.65, 0.3, 1], 156 | "pivot": [4.41942, 3.05, 4.41942], 157 | "rotation": [0, -45, 0], 158 | "uv": { 159 | "north": {"uv": [19, 5], "uv_size": [3, 1]}, 160 | "east": {"uv": [10, 24], "uv_size": [1, 1]}, 161 | "south": {"uv": [19, 6], "uv_size": [3, 1]}, 162 | "west": {"uv": [11, 24], "uv_size": [1, 1]}, 163 | "up": {"uv": [19, 7], "uv_size": [3, 1]}, 164 | "down": {"uv": [19, 18], "uv_size": [3, -1]} 165 | } 166 | }, 167 | { 168 | "origin": [1.2694, 2.9, 3.9194], 169 | "size": [2.65, 0.3, 1], 170 | "pivot": [4.41942, 3.05, 4.41942], 171 | "rotation": [0, -45, 0], 172 | "uv": { 173 | "north": {"uv": [19, 18], "uv_size": [3, 1]}, 174 | "east": {"uv": [12, 24], "uv_size": [1, 1]}, 175 | "south": {"uv": [0, 20], "uv_size": [3, 1]}, 176 | "west": {"uv": [13, 24], "uv_size": [1, 1]}, 177 | "up": {"uv": [3, 20], "uv_size": [3, 1]}, 178 | "down": {"uv": [6, 21], "uv_size": [3, -1]} 179 | } 180 | } 181 | ] 182 | }, 183 | { 184 | "name": "front_left", 185 | "parent": "props", 186 | "pivot": [4.4194, 0, -4.4194], 187 | "cubes": [ 188 | { 189 | "origin": [3.91941, 2.9, -7.56941], 190 | "size": [1, 0.3, 6.3], 191 | "pivot": [4.41941, 3.05, -4.41941], 192 | "rotation": [0, 45, 0], 193 | "uv": { 194 | "north": {"uv": [6, 24], "uv_size": [1, 1]}, 195 | "east": {"uv": [13, 4], "uv_size": [6, 1]}, 196 | "south": {"uv": [7, 24], "uv_size": [1, 1]}, 197 | "west": {"uv": [13, 5], "uv_size": [6, 1]}, 198 | "up": {"uv": [7, 14], "uv_size": [1, 6]}, 199 | "down": {"uv": [8, 20], "uv_size": [1, -6]} 200 | } 201 | }, 202 | { 203 | "origin": [4.9194, 2.9, -4.9194], 204 | "size": [2.65, 0.3, 1], 205 | "pivot": [4.41941, 3.05, -4.41941], 206 | "rotation": [0, 45, 0], 207 | "uv": { 208 | "north": {"uv": [12, 21], "uv_size": [3, 1]}, 209 | "east": {"uv": [18, 24], "uv_size": [1, 1]}, 210 | "south": {"uv": [15, 21], "uv_size": [3, 1]}, 211 | "west": {"uv": [19, 24], "uv_size": [1, 1]}, 212 | "up": {"uv": [18, 21], "uv_size": [3, 1]}, 213 | "down": {"uv": [21, 22], "uv_size": [3, -1]} 214 | } 215 | }, 216 | { 217 | "origin": [1.2694, 2.9, -4.9194], 218 | "size": [2.65, 0.3, 1], 219 | "pivot": [4.41941, 3.05, -4.41941], 220 | "rotation": [0, 45, 0], 221 | "uv": { 222 | "north": {"uv": [21, 8], "uv_size": [3, 1]}, 223 | "east": {"uv": [20, 24], "uv_size": [1, 1]}, 224 | "south": {"uv": [21, 9], "uv_size": [3, 1]}, 225 | "west": {"uv": [21, 24], "uv_size": [1, 1]}, 226 | "up": {"uv": [21, 10], "uv_size": [3, 1]}, 227 | "down": {"uv": [21, 12], "uv_size": [3, -1]} 228 | } 229 | } 230 | ] 231 | }, 232 | { 233 | "name": "frame", 234 | "pivot": [0, 0, 0], 235 | "cubes": [ 236 | { 237 | "origin": [-2, 1.8, -2], 238 | "size": [4, 0.5, 4], 239 | "uv": { 240 | "north": {"uv": [0, 17], "uv_size": [4, 1]}, 241 | "east": {"uv": [11, 17], "uv_size": [4, 1]}, 242 | "south": {"uv": [15, 17], "uv_size": [4, 1]}, 243 | "west": {"uv": [17, 10], "uv_size": [4, 1]}, 244 | "up": {"uv": [0, 0], "uv_size": [4, 4]}, 245 | "down": {"uv": [0, 8], "uv_size": [4, -4]} 246 | } 247 | }, 248 | { 249 | "origin": [-1.1, 2.3, -1.1], 250 | "size": [2.3, 0.4, 2.2], 251 | "uv": { 252 | "north": {"uv": [19, 22], "uv_size": [2, 1]}, 253 | "east": {"uv": [21, 22], "uv_size": [2, 1]}, 254 | "south": {"uv": [22, 0], "uv_size": [2, 1]}, 255 | "west": {"uv": [22, 1], "uv_size": [2, 1]}, 256 | "up": {"uv": [11, 18], "uv_size": [2, 2]}, 257 | "down": {"uv": [13, 20], "uv_size": [2, -2]} 258 | } 259 | }, 260 | { 261 | "origin": [-2, 4, -2.4], 262 | "size": [4, 0.5, 4.4], 263 | "pivot": [0, 3.9, 0], 264 | "rotation": [-14, 0, 0], 265 | "uv": { 266 | "north": {"uv": [17, 13], "uv_size": [4, 1]}, 267 | "east": {"uv": [17, 14], "uv_size": [4, 1]}, 268 | "south": {"uv": [17, 15], "uv_size": [4, 1]}, 269 | "west": {"uv": [0, 18], "uv_size": [4, 1]}, 270 | "up": {"uv": [4, 4], "uv_size": [4, 4]}, 271 | "down": {"uv": [4, 4], "uv_size": [4, -4]} 272 | } 273 | } 274 | ] 275 | }, 276 | { 277 | "name": "camera", 278 | "parent": "frame", 279 | "pivot": [0, 0, 0], 280 | "cubes": [ 281 | { 282 | "origin": [-0.75, 2.9, -2.4], 283 | "size": [1.5, 1.5, 1.2], 284 | "pivot": [0.3, 2.9, -2.4], 285 | "rotation": [-27.5, 0, 0], 286 | "uv": { 287 | "north": {"uv": [15, 18], "uv_size": [2, 2]}, 288 | "east": {"uv": [22, 2], "uv_size": [1, 2]}, 289 | "south": {"uv": [17, 18], "uv_size": [2, 2]}, 290 | "west": {"uv": [22, 4], "uv_size": [1, 2]}, 291 | "up": {"uv": [22, 6], "uv_size": [2, 1]}, 292 | "down": {"uv": [22, 8], "uv_size": [2, -1]} 293 | } 294 | }, 295 | { 296 | "origin": [-0.4875, 3.1, -2.8], 297 | "size": [1, 1, 0.4], 298 | "pivot": [0.3, 2.9, -2.4], 299 | "rotation": [-27.5, 0, 0], 300 | "uv": { 301 | "north": {"uv": [24, 5], "uv_size": [1, 1]}, 302 | "east": {"uv": [24, 6], "uv_size": [1, 1]}, 303 | "south": {"uv": [24, 7], "uv_size": [1, 1]}, 304 | "west": {"uv": [24, 8], "uv_size": [1, 1]}, 305 | "up": {"uv": [24, 9], "uv_size": [1, 1]}, 306 | "down": {"uv": [24, 11], "uv_size": [1, -1]} 307 | } 308 | }, 309 | { 310 | "origin": [-1.1, 3, -1.8], 311 | "size": [0.4, 0.6, 0.7], 312 | "uv": { 313 | "north": {"uv": [24, 11], "uv_size": [1, 1]}, 314 | "east": {"uv": [24, 12], "uv_size": [1, 1]}, 315 | "south": {"uv": [24, 13], "uv_size": [1, 1]}, 316 | "west": {"uv": [24, 14], "uv_size": [1, 1]}, 317 | "up": {"uv": [24, 15], "uv_size": [1, 1]}, 318 | "down": {"uv": [24, 17], "uv_size": [1, -1]} 319 | } 320 | }, 321 | { 322 | "origin": [0.7, 3, -1.8], 323 | "size": [0.4, 0.6, 0.7], 324 | "uv": { 325 | "north": {"uv": [24, 17], "uv_size": [1, 1]}, 326 | "east": {"uv": [24, 18], "uv_size": [1, 1]}, 327 | "south": {"uv": [24, 19], "uv_size": [1, 1]}, 328 | "west": {"uv": [24, 20], "uv_size": [1, 1]}, 329 | "up": {"uv": [24, 21], "uv_size": [1, 1]}, 330 | "down": {"uv": [24, 23], "uv_size": [1, -1]} 331 | } 332 | } 333 | ] 334 | }, 335 | { 336 | "name": "arms", 337 | "parent": "frame", 338 | "pivot": [0, 0, 0], 339 | "cubes": [ 340 | { 341 | "origin": [-7, 1.7, -0.75], 342 | "size": [5, 0.7, 1.5], 343 | "pivot": [0, 1.7, 0], 344 | "rotation": [0, -45, 0], 345 | "uv": { 346 | "north": {"uv": [16, 11], "uv_size": [5, 1]}, 347 | "east": {"uv": [0, 22], "uv_size": [2, 1]}, 348 | "south": {"uv": [16, 12], "uv_size": [5, 1]}, 349 | "west": {"uv": [2, 22], "uv_size": [2, 1]}, 350 | "up": {"uv": [11, 11], "uv_size": [5, 2]}, 351 | "down": {"uv": [11, 10], "uv_size": [5, -2]} 352 | } 353 | }, 354 | { 355 | "origin": [-7, 1.7, -0.75], 356 | "size": [5, 0.7, 1.5], 357 | "pivot": [0, 1.7, 0], 358 | "rotation": [0, 45, 0], 359 | "uv": { 360 | "north": {"uv": [0, 15], "uv_size": [5, 1]}, 361 | "east": {"uv": [21, 12], "uv_size": [2, 1]}, 362 | "south": {"uv": [0, 16], "uv_size": [5, 1]}, 363 | "west": {"uv": [21, 13], "uv_size": [2, 1]}, 364 | "up": {"uv": [6, 8], "uv_size": [5, 2]}, 365 | "down": {"uv": [8, 2], "uv_size": [5, -2]} 366 | } 367 | }, 368 | { 369 | "origin": [2, 1.7, -0.75], 370 | "size": [5, 0.7, 1.5], 371 | "pivot": [0, 1.7, 0], 372 | "rotation": [0, 45, 0], 373 | "uv": { 374 | "north": {"uv": [11, 16], "uv_size": [5, 1]}, 375 | "east": {"uv": [21, 14], "uv_size": [2, 1]}, 376 | "south": {"uv": [16, 16], "uv_size": [5, 1]}, 377 | "west": {"uv": [21, 15], "uv_size": [2, 1]}, 378 | "up": {"uv": [8, 2], "uv_size": [5, 2]}, 379 | "down": {"uv": [8, 6], "uv_size": [5, -2]} 380 | } 381 | }, 382 | { 383 | "origin": [2, 1.7, -0.75], 384 | "size": [5, 0.7, 1.5], 385 | "pivot": [0, 1.7, 0], 386 | "rotation": [0, -45, 0], 387 | "uv": { 388 | "north": {"uv": [16, 8], "uv_size": [5, 1]}, 389 | "east": {"uv": [21, 16], "uv_size": [2, 1]}, 390 | "south": {"uv": [16, 9], "uv_size": [5, 1]}, 391 | "west": {"uv": [21, 20], "uv_size": [2, 1]}, 392 | "up": {"uv": [8, 6], "uv_size": [5, 2]}, 393 | "down": {"uv": [6, 12], "uv_size": [5, -2]} 394 | } 395 | } 396 | ] 397 | }, 398 | { 399 | "name": "motors", 400 | "parent": "frame", 401 | "pivot": [0, 0, 0], 402 | "cubes": [ 403 | { 404 | "origin": [-6.9, 2.4, -0.65], 405 | "size": [1.3, 0.5, 1.3], 406 | "pivot": [0, 2.4, 0], 407 | "rotation": [0, -45, 0], 408 | "uv": { 409 | "north": {"uv": [23, 2], "uv_size": [1, 1]}, 410 | "east": {"uv": [23, 3], "uv_size": [1, 1]}, 411 | "south": {"uv": [23, 4], "uv_size": [1, 1]}, 412 | "west": {"uv": [23, 5], "uv_size": [1, 1]}, 413 | "up": {"uv": [23, 12], "uv_size": [1, 1]}, 414 | "down": {"uv": [23, 14], "uv_size": [1, -1]} 415 | } 416 | }, 417 | { 418 | "origin": [5.6, 2.4, -0.65], 419 | "size": [1.3, 0.5, 1.3], 420 | "pivot": [0, 2.4, 0], 421 | "rotation": [0, -45, 0], 422 | "uv": { 423 | "north": {"uv": [22, 17], "uv_size": [1, 1]}, 424 | "east": {"uv": [22, 18], "uv_size": [1, 1]}, 425 | "south": {"uv": [22, 19], "uv_size": [1, 1]}, 426 | "west": {"uv": [0, 23], "uv_size": [1, 1]}, 427 | "up": {"uv": [1, 23], "uv_size": [1, 1]}, 428 | "down": {"uv": [2, 24], "uv_size": [1, -1]} 429 | } 430 | }, 431 | { 432 | "origin": [3.76942, 2.4, -5.06942], 433 | "size": [1.3, 0.5, 1.3], 434 | "pivot": [4.41942, 2.65, -4.41942], 435 | "rotation": [0, 45, 0], 436 | "uv": { 437 | "north": {"uv": [3, 23], "uv_size": [1, 1]}, 438 | "east": {"uv": [19, 23], "uv_size": [1, 1]}, 439 | "south": {"uv": [20, 23], "uv_size": [1, 1]}, 440 | "west": {"uv": [21, 23], "uv_size": [1, 1]}, 441 | "up": {"uv": [22, 23], "uv_size": [1, 1]}, 442 | "down": {"uv": [23, 24], "uv_size": [1, -1]} 443 | } 444 | }, 445 | { 446 | "origin": [-5.06943, 2.4, 3.76941], 447 | "size": [1.3, 0.5, 1.3], 448 | "pivot": [-4.41943, 2.65, 4.41941], 449 | "rotation": [0, -45, 0], 450 | "uv": { 451 | "north": {"uv": [23, 14], "uv_size": [1, 1]}, 452 | "east": {"uv": [23, 15], "uv_size": [1, 1]}, 453 | "south": {"uv": [23, 16], "uv_size": [1, 1]}, 454 | "west": {"uv": [23, 17], "uv_size": [1, 1]}, 455 | "up": {"uv": [23, 18], "uv_size": [1, 1]}, 456 | "down": {"uv": [23, 20], "uv_size": [1, -1]} 457 | } 458 | } 459 | ] 460 | }, 461 | { 462 | "name": "standoffs", 463 | "parent": "frame", 464 | "pivot": [0, 0, 0], 465 | "cubes": [ 466 | { 467 | "origin": [1.1, 2.3, 1.1], 468 | "size": [0.7, 1.6, 0.7], 469 | "uv": { 470 | "north": {"uv": [4, 17], "uv_size": [1, 2]}, 471 | "east": {"uv": [4, 22], "uv_size": [1, 2]}, 472 | "south": {"uv": [5, 22], "uv_size": [1, 2]}, 473 | "west": {"uv": [6, 22], "uv_size": [1, 2]}, 474 | "up": {"uv": [22, 24], "uv_size": [1, 1]}, 475 | "down": {"uv": [23, 25], "uv_size": [1, -1]} 476 | } 477 | }, 478 | { 479 | "origin": [-1.8, 2.3, 1.1], 480 | "size": [0.7, 1.6, 0.7], 481 | "uv": { 482 | "north": {"uv": [7, 22], "uv_size": [1, 2]}, 483 | "east": {"uv": [8, 22], "uv_size": [1, 2]}, 484 | "south": {"uv": [9, 22], "uv_size": [1, 2]}, 485 | "west": {"uv": [10, 22], "uv_size": [1, 2]}, 486 | "up": {"uv": [24, 24], "uv_size": [1, 1]}, 487 | "down": {"uv": [24, 1], "uv_size": [1, -1]} 488 | } 489 | }, 490 | { 491 | "origin": [-1.8, 2.3, -1.8], 492 | "size": [0.7, 2.3, 0.7], 493 | "uv": { 494 | "north": {"uv": [11, 22], "uv_size": [1, 2]}, 495 | "east": {"uv": [12, 22], "uv_size": [1, 2]}, 496 | "south": {"uv": [13, 22], "uv_size": [1, 2]}, 497 | "west": {"uv": [14, 22], "uv_size": [1, 2]}, 498 | "up": {"uv": [24, 1], "uv_size": [1, 1]}, 499 | "down": {"uv": [24, 3], "uv_size": [1, -1]} 500 | } 501 | }, 502 | { 503 | "origin": [1.1, 2.3, -1.8], 504 | "size": [0.7, 2.3, 0.7], 505 | "uv": { 506 | "north": {"uv": [15, 22], "uv_size": [1, 2]}, 507 | "east": {"uv": [16, 22], "uv_size": [1, 2]}, 508 | "south": {"uv": [17, 22], "uv_size": [1, 2]}, 509 | "west": {"uv": [18, 22], "uv_size": [1, 2]}, 510 | "up": {"uv": [24, 3], "uv_size": [1, 1]}, 511 | "down": {"uv": [24, 5], "uv_size": [1, -1]} 512 | } 513 | } 514 | ] 515 | } 516 | ] 517 | } 518 | ] 519 | } -------------------------------------------------------------------------------- /src/main/resources/assets/quadz/templates/pixel/geo/pixel.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:geometry": [ 4 | { 5 | "description": { 6 | "identifier": "geometry.pixel", 7 | "texture_width": 32, 8 | "texture_height": 32, 9 | "visible_bounds_width": 2, 10 | "visible_bounds_height": 1.5, 11 | "visible_bounds_offset": [0, 0.25, 0] 12 | }, 13 | "bones": [ 14 | { 15 | "name": "unknown_bone", 16 | "pivot": [0, 0, 0], 17 | "cubes": [ 18 | { 19 | "origin": [-0.3, 0.7, -1.9], 20 | "size": [0.6, 0.1, 3.8], 21 | "pivot": [0.00508, 1.02338, 0.00463], 22 | "rotation": [-180, -45, 180], 23 | "uv": { 24 | "north": {"uv": [0, 16], "uv_size": [1, 1]}, 25 | "east": {"uv": [0, 0], "uv_size": [4, 1]}, 26 | "south": {"uv": [1, 16], "uv_size": [1, 1]}, 27 | "west": {"uv": [0, 1], "uv_size": [4, 1]}, 28 | "up": {"uv": [0, 2], "uv_size": [1, 4]}, 29 | "down": {"uv": [1, 6], "uv_size": [1, -4]} 30 | } 31 | }, 32 | { 33 | "origin": [-1.9, 0.7, -0.3], 34 | "size": [1.6, 0.1, 0.6], 35 | "pivot": [0.00508, 1.02338, 0.00463], 36 | "rotation": [-180, -45, 180], 37 | "uv": { 38 | "north": {"uv": [9, 0], "uv_size": [2, 1]}, 39 | "east": {"uv": [2, 16], "uv_size": [1, 1]}, 40 | "south": {"uv": [9, 1], "uv_size": [2, 1]}, 41 | "west": {"uv": [3, 16], "uv_size": [1, 1]}, 42 | "up": {"uv": [9, 6], "uv_size": [2, 1]}, 43 | "down": {"uv": [9, 8], "uv_size": [2, -1]} 44 | } 45 | }, 46 | { 47 | "origin": [0.3, 0.7, -0.3], 48 | "size": [1.6, 0.1, 0.6], 49 | "pivot": [0.00508, 1.02338, 0.00463], 50 | "rotation": [-180, -45, 180], 51 | "uv": { 52 | "north": {"uv": [0, 10], "uv_size": [2, 1]}, 53 | "east": {"uv": [4, 16], "uv_size": [1, 1]}, 54 | "south": {"uv": [2, 10], "uv_size": [2, 1]}, 55 | "west": {"uv": [5, 16], "uv_size": [1, 1]}, 56 | "up": {"uv": [4, 10], "uv_size": [2, 1]}, 57 | "down": {"uv": [6, 11], "uv_size": [2, -1]} 58 | } 59 | }, 60 | { 61 | "origin": [-0.7, 0.8, -0.7], 62 | "size": [1.4, 0.2, 1.4], 63 | "pivot": [0.00508, 1.02338, 0.00463], 64 | "rotation": [-180, -45, 180], 65 | "uv": { 66 | "north": {"uv": [6, 16], "uv_size": [1, 1]}, 67 | "east": {"uv": [7, 16], "uv_size": [1, 1]}, 68 | "south": {"uv": [8, 16], "uv_size": [1, 1]}, 69 | "west": {"uv": [9, 16], "uv_size": [1, 1]}, 70 | "up": {"uv": [10, 16], "uv_size": [1, 1]}, 71 | "down": {"uv": [11, 17], "uv_size": [1, -1]} 72 | } 73 | }, 74 | { 75 | "origin": [-0.45176, 0.92078, 0.02313], 76 | "size": [0.9, 0.9, 0.4], 77 | "pivot": [0.00508, 1.02338, 0.00463], 78 | "rotation": [-152, 0, 180], 79 | "uv": { 80 | "north": {"uv": [12, 16], "uv_size": [1, 1]}, 81 | "east": {"uv": [13, 16], "uv_size": [1, 1]}, 82 | "south": {"uv": [14, 16], "uv_size": [1, 1]}, 83 | "west": {"uv": [15, 16], "uv_size": [1, 1]}, 84 | "up": {"uv": [16, 16], "uv_size": [1, 1]}, 85 | "down": {"uv": [16, 1], "uv_size": [1, -1]} 86 | } 87 | }, 88 | { 89 | "origin": [-0.2518, 1.1208, 0.42313], 90 | "size": [0.5, 0.5, 0.2], 91 | "pivot": [0.00508, 1.02338, 0.00463], 92 | "rotation": [-152, 0, 180], 93 | "uv": { 94 | "north": {"uv": [16, 13], "uv_size": [1, 1]}, 95 | "east": {"uv": [16, 14], "uv_size": [1, 1]}, 96 | "south": {"uv": [16, 15], "uv_size": [1, 1]}, 97 | "west": {"uv": [0, 17], "uv_size": [1, 1]}, 98 | "up": {"uv": [1, 17], "uv_size": [1, 1]}, 99 | "down": {"uv": [2, 18], "uv_size": [1, -1]} 100 | } 101 | }, 102 | { 103 | "origin": [-0.30179, 1.02078, -0.67687], 104 | "size": [0.6, 0.3, 0.9], 105 | "pivot": [0.00508, 1.12338, 0.00463], 106 | "rotation": [-152, 0, 180], 107 | "uv": { 108 | "north": {"uv": [16, 7], "uv_size": [1, 1]}, 109 | "east": {"uv": [16, 8], "uv_size": [1, 1]}, 110 | "south": {"uv": [16, 9], "uv_size": [1, 1]}, 111 | "west": {"uv": [16, 10], "uv_size": [1, 1]}, 112 | "up": {"uv": [16, 11], "uv_size": [1, 1]}, 113 | "down": {"uv": [16, 13], "uv_size": [1, -1]} 114 | } 115 | }, 116 | { 117 | "origin": [0.09824, 1.2301, 0.34622], 118 | "size": [0.1, 1.3, 0.2], 119 | "pivot": [0.00508, 1.02338, 0.00463], 120 | "rotation": [-127, 0, 180], 121 | "uv": { 122 | "north": {"uv": [16, 1], "uv_size": [1, 1]}, 123 | "east": {"uv": [16, 2], "uv_size": [1, 1]}, 124 | "south": {"uv": [16, 3], "uv_size": [1, 1]}, 125 | "west": {"uv": [16, 4], "uv_size": [1, 1]}, 126 | "up": {"uv": [16, 5], "uv_size": [1, 1]}, 127 | "down": {"uv": [16, 7], "uv_size": [1, -1]} 128 | } 129 | }, 130 | { 131 | "origin": [-0.6, 0, -1.2], 132 | "size": [1.2, 0.7, 2.4], 133 | "pivot": [0, -0.3, 0], 134 | "rotation": [-180, 0, 180], 135 | "uv": { 136 | "north": {"uv": [3, 17], "uv_size": [1, 1]}, 137 | "east": {"uv": [8, 10], "uv_size": [2, 1]}, 138 | "south": {"uv": [4, 17], "uv_size": [1, 1]}, 139 | "west": {"uv": [10, 10], "uv_size": [2, 1]}, 140 | "up": {"uv": [10, 2], "uv_size": [1, 2]}, 141 | "down": {"uv": [10, 6], "uv_size": [1, -2]} 142 | } 143 | } 144 | ] 145 | }, 146 | { 147 | "name": "guard", 148 | "pivot": [-1.2, 0.275, -3], 149 | "rotation": [0, -45, 0], 150 | "cubes": [ 151 | { 152 | "origin": [-0.72583, 0.8, -2.42453], 153 | "size": [0.2, 0.35, 1.4], 154 | "pivot": [1.77672, 0.32338, -1.72752], 155 | "rotation": [-180, 0, 180], 156 | "uv": { 157 | "north": {"uv": [10, 8], "uv_size": [1, 1]}, 158 | "east": {"uv": [10, 9], "uv_size": [1, 1]}, 159 | "south": {"uv": [0, 11], "uv_size": [1, 1]}, 160 | "west": {"uv": [1, 11], "uv_size": [1, 1]}, 161 | "up": {"uv": [2, 11], "uv_size": [1, 1]}, 162 | "down": {"uv": [3, 12], "uv_size": [1, -1]} 163 | } 164 | }, 165 | { 166 | "origin": [-0.72628, 0.8, -2.43007], 167 | "size": [0.2, 0.35, 1.4], 168 | "pivot": [1.77672, 0.32338, -1.72752], 169 | "rotation": [0, -90, 0], 170 | "uv": { 171 | "north": {"uv": [8, 12], "uv_size": [1, 1]}, 172 | "east": {"uv": [9, 12], "uv_size": [1, 1]}, 173 | "south": {"uv": [10, 12], "uv_size": [1, 1]}, 174 | "west": {"uv": [11, 12], "uv_size": [1, 1]}, 175 | "up": {"uv": [12, 12], "uv_size": [1, 1]}, 176 | "down": {"uv": [12, 1], "uv_size": [1, -1]} 177 | } 178 | }, 179 | { 180 | "origin": [-0.72074, 0.8, -2.43052], 181 | "size": [0.2, 0.35, 1.4], 182 | "pivot": [1.77672, 0.32338, -1.72752], 183 | "rotation": [0, 0, 0], 184 | "uv": { 185 | "north": {"uv": [11, 8], "uv_size": [1, 1]}, 186 | "east": {"uv": [11, 9], "uv_size": [1, 1]}, 187 | "south": {"uv": [0, 12], "uv_size": [1, 1]}, 188 | "west": {"uv": [1, 12], "uv_size": [1, 1]}, 189 | "up": {"uv": [2, 12], "uv_size": [1, 1]}, 190 | "down": {"uv": [3, 13], "uv_size": [1, -1]} 191 | } 192 | }, 193 | { 194 | "origin": [-0.72029, 0.8, -2.42497], 195 | "size": [0.2, 0.35, 1.4], 196 | "pivot": [1.77672, 0.32338, -1.72752], 197 | "rotation": [0, 90, 0], 198 | "uv": { 199 | "north": {"uv": [10, 11], "uv_size": [1, 1]}, 200 | "east": {"uv": [11, 11], "uv_size": [1, 1]}, 201 | "south": {"uv": [11, 0], "uv_size": [1, 1]}, 202 | "west": {"uv": [11, 1], "uv_size": [1, 1]}, 203 | "up": {"uv": [11, 2], "uv_size": [1, 1]}, 204 | "down": {"uv": [11, 4], "uv_size": [1, -1]} 205 | } 206 | }, 207 | { 208 | "origin": [-0.72583, 0.8, -1.02453], 209 | "size": [1.6, 0.35, 0.2], 210 | "pivot": [1.77672, 0.32338, -1.72752], 211 | "rotation": [-180, 0, 180], 212 | "uv": { 213 | "north": {"uv": [7, 0], "uv_size": [2, 1]}, 214 | "east": {"uv": [4, 11], "uv_size": [1, 1]}, 215 | "south": {"uv": [7, 1], "uv_size": [2, 1]}, 216 | "west": {"uv": [5, 11], "uv_size": [1, 1]}, 217 | "up": {"uv": [0, 8], "uv_size": [2, 1]}, 218 | "down": {"uv": [2, 9], "uv_size": [2, -1]} 219 | } 220 | }, 221 | { 222 | "origin": [-0.72628, 0.8, -1.03007], 223 | "size": [1.6, 0.35, 0.2], 224 | "pivot": [1.77672, 0.32338, -1.72752], 225 | "rotation": [0, -90, 0], 226 | "uv": { 227 | "north": {"uv": [2, 9], "uv_size": [2, 1]}, 228 | "east": {"uv": [6, 12], "uv_size": [1, 1]}, 229 | "south": {"uv": [4, 9], "uv_size": [2, 1]}, 230 | "west": {"uv": [7, 12], "uv_size": [1, 1]}, 231 | "up": {"uv": [6, 9], "uv_size": [2, 1]}, 232 | "down": {"uv": [8, 10], "uv_size": [2, -1]} 233 | } 234 | }, 235 | { 236 | "origin": [-0.02628, 0.8, -1.93007], 237 | "size": [0.4, 0.2, 0.4], 238 | "pivot": [1.77672, 0.32338, -1.72752], 239 | "rotation": [0, -90, 0], 240 | "uv": { 241 | "north": {"uv": [12, 3], "uv_size": [1, 1]}, 242 | "east": {"uv": [12, 4], "uv_size": [1, 1]}, 243 | "south": {"uv": [12, 5], "uv_size": [1, 1]}, 244 | "west": {"uv": [12, 6], "uv_size": [1, 1]}, 245 | "up": {"uv": [12, 7], "uv_size": [1, 1]}, 246 | "down": {"uv": [12, 9], "uv_size": [1, -1]} 247 | } 248 | }, 249 | { 250 | "origin": [3.17372, 0.8, -1.93007], 251 | "size": [0.4, 0.2, 0.4], 252 | "pivot": [1.77672, 0.32338, -1.72752], 253 | "rotation": [0, -90, 0], 254 | "uv": { 255 | "north": {"uv": [12, 9], "uv_size": [1, 1]}, 256 | "east": {"uv": [12, 10], "uv_size": [1, 1]}, 257 | "south": {"uv": [12, 11], "uv_size": [1, 1]}, 258 | "west": {"uv": [0, 13], "uv_size": [1, 1]}, 259 | "up": {"uv": [1, 13], "uv_size": [1, 1]}, 260 | "down": {"uv": [2, 14], "uv_size": [1, -1]} 261 | } 262 | }, 263 | { 264 | "origin": [1.57372, 0.8, -3.53007], 265 | "size": [0.4, 0.2, 0.4], 266 | "pivot": [1.77672, 0.32338, -1.72752], 267 | "rotation": [0, -90, 0], 268 | "uv": { 269 | "north": {"uv": [3, 13], "uv_size": [1, 1]}, 270 | "east": {"uv": [4, 13], "uv_size": [1, 1]}, 271 | "south": {"uv": [5, 13], "uv_size": [1, 1]}, 272 | "west": {"uv": [6, 13], "uv_size": [1, 1]}, 273 | "up": {"uv": [7, 13], "uv_size": [1, 1]}, 274 | "down": {"uv": [8, 14], "uv_size": [1, -1]} 275 | } 276 | }, 277 | { 278 | "origin": [1.57372, 0.8, -0.33007], 279 | "size": [0.4, 0.2, 0.4], 280 | "pivot": [1.77672, 0.32338, -1.72752], 281 | "rotation": [0, -90, 0], 282 | "uv": { 283 | "north": {"uv": [9, 13], "uv_size": [1, 1]}, 284 | "east": {"uv": [10, 13], "uv_size": [1, 1]}, 285 | "south": {"uv": [11, 13], "uv_size": [1, 1]}, 286 | "west": {"uv": [12, 13], "uv_size": [1, 1]}, 287 | "up": {"uv": [13, 13], "uv_size": [1, 1]}, 288 | "down": {"uv": [13, 1], "uv_size": [1, -1]} 289 | } 290 | }, 291 | { 292 | "origin": [-0.72074, 0.8, -1.03052], 293 | "size": [1.6, 0.35, 0.2], 294 | "pivot": [1.77672, 0.32338, -1.72752], 295 | "rotation": [0, 0, 0], 296 | "uv": { 297 | "north": {"uv": [8, 3], "uv_size": [2, 1]}, 298 | "east": {"uv": [4, 12], "uv_size": [1, 1]}, 299 | "south": {"uv": [8, 4], "uv_size": [2, 1]}, 300 | "west": {"uv": [5, 12], "uv_size": [1, 1]}, 301 | "up": {"uv": [8, 5], "uv_size": [2, 1]}, 302 | "down": {"uv": [0, 10], "uv_size": [2, -1]} 303 | } 304 | }, 305 | { 306 | "origin": [-0.72029, 0.8, -1.02497], 307 | "size": [1.6, 0.35, 0.2], 308 | "pivot": [1.77672, 0.32338, -1.72752], 309 | "rotation": [0, 90, 0], 310 | "uv": { 311 | "north": {"uv": [4, 8], "uv_size": [2, 1]}, 312 | "east": {"uv": [8, 11], "uv_size": [1, 1]}, 313 | "south": {"uv": [6, 8], "uv_size": [2, 1]}, 314 | "west": {"uv": [9, 11], "uv_size": [1, 1]}, 315 | "up": {"uv": [8, 8], "uv_size": [2, 1]}, 316 | "down": {"uv": [8, 3], "uv_size": [2, -1]} 317 | } 318 | }, 319 | { 320 | "origin": [-0.72583, 0.8, -2.62453], 321 | "size": [3.2, 0.35, 0.2], 322 | "pivot": [1.77672, 0.32338, -1.72752], 323 | "rotation": [-180, 0, 180], 324 | "uv": { 325 | "north": {"uv": [2, 2], "uv_size": [3, 1]}, 326 | "east": {"uv": [6, 11], "uv_size": [1, 1]}, 327 | "south": {"uv": [2, 3], "uv_size": [3, 1]}, 328 | "west": {"uv": [7, 11], "uv_size": [1, 1]}, 329 | "up": {"uv": [2, 4], "uv_size": [3, 1]}, 330 | "down": {"uv": [4, 1], "uv_size": [3, -1]} 331 | } 332 | }, 333 | { 334 | "origin": [-0.72628, 0.8, -2.63007], 335 | "size": [3.2, 0.35, 0.2], 336 | "pivot": [1.77672, 0.32338, -1.72752], 337 | "rotation": [0, -90, 0], 338 | "uv": { 339 | "north": {"uv": [6, 6], "uv_size": [3, 1]}, 340 | "east": {"uv": [12, 1], "uv_size": [1, 1]}, 341 | "south": {"uv": [0, 7], "uv_size": [3, 1]}, 342 | "west": {"uv": [12, 2], "uv_size": [1, 1]}, 343 | "up": {"uv": [3, 7], "uv_size": [3, 1]}, 344 | "down": {"uv": [6, 8], "uv_size": [3, -1]} 345 | } 346 | }, 347 | { 348 | "origin": [-0.72074, 0.8, -2.63052], 349 | "size": [3.2, 0.35, 0.2], 350 | "pivot": [1.77672, 0.32338, -1.72752], 351 | "rotation": [0, 0, 0], 352 | "uv": { 353 | "north": {"uv": [5, 3], "uv_size": [3, 1]}, 354 | "east": {"uv": [11, 6], "uv_size": [1, 1]}, 355 | "south": {"uv": [5, 4], "uv_size": [3, 1]}, 356 | "west": {"uv": [11, 7], "uv_size": [1, 1]}, 357 | "up": {"uv": [0, 6], "uv_size": [3, 1]}, 358 | "down": {"uv": [3, 7], "uv_size": [3, -1]} 359 | } 360 | }, 361 | { 362 | "origin": [-0.72029, 0.8, -2.62497], 363 | "size": [3.2, 0.35, 0.2], 364 | "pivot": [1.77672, 0.32338, -1.72752], 365 | "rotation": [0, 90, 0], 366 | "uv": { 367 | "north": {"uv": [4, 1], "uv_size": [3, 1]}, 368 | "east": {"uv": [11, 4], "uv_size": [1, 1]}, 369 | "south": {"uv": [2, 5], "uv_size": [3, 1]}, 370 | "west": {"uv": [11, 5], "uv_size": [1, 1]}, 371 | "up": {"uv": [5, 5], "uv_size": [3, 1]}, 372 | "down": {"uv": [5, 3], "uv_size": [3, -1]} 373 | } 374 | } 375 | ] 376 | }, 377 | { 378 | "name": "prop1", 379 | "pivot": [-1.12237, 1.05, -1.12706], 380 | "cubes": [ 381 | { 382 | "origin": [-1.22237, 1, -1.72706], 383 | "size": [0.2, 0.1, 1.2], 384 | "pivot": [-1.12237, 1.05, -1.12706], 385 | "rotation": [-180, -45, 180], 386 | "uv": { 387 | "north": {"uv": [13, 1], "uv_size": [1, 1]}, 388 | "east": {"uv": [13, 2], "uv_size": [1, 1]}, 389 | "south": {"uv": [13, 3], "uv_size": [1, 1]}, 390 | "west": {"uv": [13, 4], "uv_size": [1, 1]}, 391 | "up": {"uv": [13, 5], "uv_size": [1, 1]}, 392 | "down": {"uv": [13, 7], "uv_size": [1, -1]} 393 | } 394 | }, 395 | { 396 | "origin": [-1.61986, 1, -0.97957], 397 | "size": [0.5, 0.1, 0.2], 398 | "pivot": [-1.36986, 1.05, -0.87957], 399 | "rotation": [-180, -45, 180], 400 | "uv": { 401 | "north": {"uv": [13, 7], "uv_size": [1, 1]}, 402 | "east": {"uv": [13, 8], "uv_size": [1, 1]}, 403 | "south": {"uv": [13, 9], "uv_size": [1, 1]}, 404 | "west": {"uv": [13, 10], "uv_size": [1, 1]}, 405 | "up": {"uv": [13, 11], "uv_size": [1, 1]}, 406 | "down": {"uv": [13, 13], "uv_size": [1, -1]} 407 | } 408 | }, 409 | { 410 | "origin": [-1.12488, 1, -1.47454], 411 | "size": [0.5, 0.1, 0.2], 412 | "pivot": [-0.87488, 1.05, -1.37454], 413 | "rotation": [-180, -45, 180], 414 | "uv": { 415 | "north": {"uv": [0, 14], "uv_size": [1, 1]}, 416 | "east": {"uv": [1, 14], "uv_size": [1, 1]}, 417 | "south": {"uv": [2, 14], "uv_size": [1, 1]}, 418 | "west": {"uv": [3, 14], "uv_size": [1, 1]}, 419 | "up": {"uv": [4, 14], "uv_size": [1, 1]}, 420 | "down": {"uv": [5, 15], "uv_size": [1, -1]} 421 | } 422 | } 423 | ] 424 | }, 425 | { 426 | "name": "prop2", 427 | "pivot": [1.14037, 1.05, 1.13569], 428 | "cubes": [ 429 | { 430 | "origin": [1.04037, 1, 0.53569], 431 | "size": [0.2, 0.1, 1.2], 432 | "pivot": [1.14037, 1.05, 1.13569], 433 | "rotation": [-180, -45, 180], 434 | "uv": { 435 | "north": {"uv": [6, 14], "uv_size": [1, 1]}, 436 | "east": {"uv": [7, 14], "uv_size": [1, 1]}, 437 | "south": {"uv": [8, 14], "uv_size": [1, 1]}, 438 | "west": {"uv": [9, 14], "uv_size": [1, 1]}, 439 | "up": {"uv": [10, 14], "uv_size": [1, 1]}, 440 | "down": {"uv": [11, 15], "uv_size": [1, -1]} 441 | } 442 | }, 443 | { 444 | "origin": [0.64289, 1, 1.28317], 445 | "size": [0.5, 0.1, 0.2], 446 | "pivot": [0.89289, 1.05, 1.38317], 447 | "rotation": [-180, -45, 180], 448 | "uv": { 449 | "north": {"uv": [12, 14], "uv_size": [1, 1]}, 450 | "east": {"uv": [13, 14], "uv_size": [1, 1]}, 451 | "south": {"uv": [14, 14], "uv_size": [1, 1]}, 452 | "west": {"uv": [14, 0], "uv_size": [1, 1]}, 453 | "up": {"uv": [14, 1], "uv_size": [1, 1]}, 454 | "down": {"uv": [14, 3], "uv_size": [1, -1]} 455 | } 456 | }, 457 | { 458 | "origin": [1.13786, 1, 0.7882], 459 | "size": [0.5, 0.1, 0.2], 460 | "pivot": [1.38786, 1.05, 0.8882], 461 | "rotation": [-177.5, -45, 180], 462 | "uv": { 463 | "north": {"uv": [14, 3], "uv_size": [1, 1]}, 464 | "east": {"uv": [14, 4], "uv_size": [1, 1]}, 465 | "south": {"uv": [14, 5], "uv_size": [1, 1]}, 466 | "west": {"uv": [14, 6], "uv_size": [1, 1]}, 467 | "up": {"uv": [14, 7], "uv_size": [1, 1]}, 468 | "down": {"uv": [14, 9], "uv_size": [1, -1]} 469 | } 470 | } 471 | ] 472 | }, 473 | { 474 | "name": "prop3", 475 | "pivot": [1.14037, 1.05, -1.12706], 476 | "cubes": [ 477 | { 478 | "origin": [1.04037, 1, -1.72706], 479 | "size": [0.2, 0.1, 1.2], 480 | "pivot": [1.14037, 1.05, -1.12706], 481 | "rotation": [-180, -45, 180], 482 | "uv": { 483 | "north": {"uv": [14, 9], "uv_size": [1, 1]}, 484 | "east": {"uv": [14, 10], "uv_size": [1, 1]}, 485 | "south": {"uv": [14, 11], "uv_size": [1, 1]}, 486 | "west": {"uv": [14, 12], "uv_size": [1, 1]}, 487 | "up": {"uv": [14, 13], "uv_size": [1, 1]}, 488 | "down": {"uv": [0, 16], "uv_size": [1, -1]} 489 | } 490 | }, 491 | { 492 | "origin": [0.64289, 1, -0.97957], 493 | "size": [0.5, 0.1, 0.2], 494 | "pivot": [0.89289, 1.05, -0.87957], 495 | "rotation": [-180, -45, 180], 496 | "uv": { 497 | "north": {"uv": [1, 15], "uv_size": [1, 1]}, 498 | "east": {"uv": [2, 15], "uv_size": [1, 1]}, 499 | "south": {"uv": [3, 15], "uv_size": [1, 1]}, 500 | "west": {"uv": [4, 15], "uv_size": [1, 1]}, 501 | "up": {"uv": [5, 15], "uv_size": [1, 1]}, 502 | "down": {"uv": [6, 16], "uv_size": [1, -1]} 503 | } 504 | }, 505 | { 506 | "origin": [1.13786, 1, -1.47454], 507 | "size": [0.5, 0.1, 0.2], 508 | "pivot": [1.38786, 1.05, -1.37454], 509 | "rotation": [-180, -45, 180], 510 | "uv": { 511 | "north": {"uv": [7, 15], "uv_size": [1, 1]}, 512 | "east": {"uv": [8, 15], "uv_size": [1, 1]}, 513 | "south": {"uv": [9, 15], "uv_size": [1, 1]}, 514 | "west": {"uv": [10, 15], "uv_size": [1, 1]}, 515 | "up": {"uv": [11, 15], "uv_size": [1, 1]}, 516 | "down": {"uv": [12, 16], "uv_size": [1, -1]} 517 | } 518 | } 519 | ] 520 | }, 521 | { 522 | "name": "prop4", 523 | "pivot": [-1.12237, 1.05, 1.13569], 524 | "cubes": [ 525 | { 526 | "origin": [-1.22237, 1, 0.53569], 527 | "size": [0.2, 0.1, 1.2], 528 | "pivot": [-1.12237, 1.05, 1.13569], 529 | "rotation": [-180, -45, 180], 530 | "uv": { 531 | "north": {"uv": [13, 15], "uv_size": [1, 1]}, 532 | "east": {"uv": [14, 15], "uv_size": [1, 1]}, 533 | "south": {"uv": [15, 15], "uv_size": [1, 1]}, 534 | "west": {"uv": [15, 0], "uv_size": [1, 1]}, 535 | "up": {"uv": [15, 1], "uv_size": [1, 1]}, 536 | "down": {"uv": [15, 3], "uv_size": [1, -1]} 537 | } 538 | }, 539 | { 540 | "origin": [-1.61986, 1, 1.28317], 541 | "size": [0.5, 0.1, 0.2], 542 | "pivot": [-1.36986, 1.05, 1.38317], 543 | "rotation": [-180, -45, 180], 544 | "uv": { 545 | "north": {"uv": [15, 3], "uv_size": [1, 1]}, 546 | "east": {"uv": [15, 4], "uv_size": [1, 1]}, 547 | "south": {"uv": [15, 5], "uv_size": [1, 1]}, 548 | "west": {"uv": [15, 6], "uv_size": [1, 1]}, 549 | "up": {"uv": [15, 7], "uv_size": [1, 1]}, 550 | "down": {"uv": [15, 9], "uv_size": [1, -1]} 551 | } 552 | }, 553 | { 554 | "origin": [-1.12488, 1, 0.7882], 555 | "size": [0.5, 0.1, 0.2], 556 | "pivot": [-0.87488, 1.05, 0.8882], 557 | "rotation": [-180, -45, 180], 558 | "uv": { 559 | "north": {"uv": [15, 9], "uv_size": [1, 1]}, 560 | "east": {"uv": [15, 10], "uv_size": [1, 1]}, 561 | "south": {"uv": [15, 11], "uv_size": [1, 1]}, 562 | "west": {"uv": [15, 12], "uv_size": [1, 1]}, 563 | "up": {"uv": [15, 13], "uv_size": [1, 1]}, 564 | "down": {"uv": [15, 15], "uv_size": [1, -1]} 565 | } 566 | } 567 | ] 568 | } 569 | ] 570 | } 571 | ] 572 | } --------------------------------------------------------------------------------