package dev.lumareth.improvedAerodynamics.client.pipper; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.DirectionProperty; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkStatus; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraftforge.fml.ModList; import net.minecraftforge.registries.ForgeRegistries; import org.joml.primitives.AABBic; import java.util.ArrayList; import java.util.List; /** * Finds the light autocannon of a ship and works out what it will shoot, the same way Warium's LACFireScript does: * barrel count, muzzle, aim direction and muzzle speed. */ final class GunFinder { private static final ResourceLocation LIGHT_AUTOCANNON = new ResourceLocation("crusty_chunks", "light_autocannon"); private static final TagKey BARREL = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "acbarrel")); private static final TagKey PASSTHROUGH = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "barrelpassthrough")); private static final int RESCAN_TICKS = 100; private static final int MAX_GUNS = 16; /** Muzzle and direction in ship space and the speed of the shell in blocks per tick. */ record Shot(double mx, double my, double mz, double dx, double dy, double dz, double speed) { } private long shipId = Long.MIN_VALUE; private long lastScan = Long.MIN_VALUE; private final List guns = new ArrayList<>(); /** The gun closest to the seat, or null if the ship has none loaded. */ Shot find(ClientLevel level, long shipId, AABBic aabb, BlockPos seat) { if (shipId != this.shipId || level.getGameTime() - lastScan >= RESCAN_TICKS || level.getGameTime() < lastScan) { this.shipId = shipId; lastScan = level.getGameTime(); scan(level, aabb); } BlockPos best = null; double bestDistance = Double.MAX_VALUE; for (BlockPos pos : guns) { double distance = pos.distSqr(seat); if (distance < bestDistance) { bestDistance = distance; best = pos; } } return best == null ? null : shotOf(level, best); } private void scan(ClientLevel level, AABBic aabb) { guns.clear(); Block gun = ForgeRegistries.BLOCKS.getValue(LIGHT_AUTOCANNON); if (gun == null || aabb == null) { return; } BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int cx = aabb.minX() >> 4; cx <= aabb.maxX() >> 4; cx++) { for (int cz = aabb.minZ() >> 4; cz <= aabb.maxZ() >> 4; cz++) { ChunkAccess chunk = level.getChunk(cx, cz, ChunkStatus.FULL, false); if (chunk == null) { continue; } LevelChunkSection[] sections = chunk.getSections(); for (int i = 0; i < sections.length; i++) { LevelChunkSection section = sections[i]; if (section.hasOnlyAir() || !section.maybeHas(state -> state.is(gun))) { continue; } int baseY = level.getSectionYFromSectionIndex(i) << 4; for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { if (section.getBlockState(x, y, z).is(gun) && guns.size() < MAX_GUNS) { guns.add(pos.set((cx << 4) + x, baseY + y, (cz << 4) + z).immutable()); } } } } } } } } private static Shot shotOf(ClientLevel level, BlockPos gun) { BlockState state = level.getBlockState(gun); Property property = state.getBlock().getStateDefinition().getProperty("facing"); if (!(property instanceof DirectionProperty facingProperty)) { return null; } Direction facing = state.getValue(facingProperty); int fx = facing.getStepX(); int fy = facing.getStepY(); int fz = facing.getStepZ(); // Barrels, counted like Warium does: up to five more behind the first one double barrels = 1.0; for (int i = 0; i < 5 && level.getBlockState(gun.offset(fx * (int) (1 + barrels), fy * (int) (1 + barrels), fz * (int) (1 + barrels))).is(BARREL); i++) { barrels++; } // Muzzle: past the barrels, skipping through blocks the shell passes double position = barrels; for (int i = 0; i < 10; i++) { BlockState at = level.getBlockState(gun.offset(fx * (int) (1 + position), fy * (int) (1 + position), fz * (int) (1 + position))); if ((!at.isAir() && !at.canOcclude()) || at.is(PASSTHROUGH)) { position++; } else { break; } } double mx = gun.getX() + fx * (1.5 + position) + 0.5; double my = gun.getY() + fy * (1.5 + position) + 0.5; double mz = gun.getZ() + fz * (1.5 + position) + 0.5; // Aim, from the tags of the gun itself double aimX = 0.0; double aimY = 0.0; double aimZ = 0.0; BlockEntity entity = level.getBlockEntity(gun); double gx = entity == null ? 0.0 : entity.getPersistentData().getDouble("X"); double gz = entity == null ? 0.0 : entity.getPersistentData().getDouble("Z"); double gpitch = entity == null ? 0.0 : entity.getPersistentData().getDouble("Pitch"); if (1.0 > Math.abs(fx - gx) || 1.0 > Math.abs(fz - gz)) { aimY = gpitch; aimX = gx + fx; aimZ = gz + fz; } if (aimX * aimZ == 0.0) { aimX = fx; aimZ = fz; } double length = Math.sqrt(aimX * aimX + aimY * aimY + aimZ * aimZ); if (length < 1.0E-6) { return null; } double speed = PipperConfig.MUZZLE_SPEED_OVERRIDE.get(); if (speed <= 0.0) { double multiplier = ModList.get().isLoaded("ritchiesprojectilelib") ? 2.0 : 1.0; speed = (5.0 + barrels / 1.5) * multiplier; } return new Shot(mx, my, mz, aimX / length, aimY / length, aimZ / length, speed); } }