Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8a7ead04 | ||
|
|
9e7aae5773 |
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package dev.lumareth.improvedAerodynamics;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
||||
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
||||
@@ -21,6 +22,7 @@ public class ImprovedAerodynamics {
|
||||
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
||||
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC);
|
||||
Network.register();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
||||
|
||||
/**
|
||||
* Flight of a Warium shell, which is a vanilla arrow: every tick it moves by its velocity, the velocity is then scaled
|
||||
* by the drag and gravity is subtracted. That is linear in the launch velocity, so the position after n ticks is
|
||||
* <pre>M + D(n) * v0 - (0, G(n), 0)</pre>
|
||||
* with D(n) the sum of the drag factors and G(n) the drop gravity causes on its own. Both are tabulated once, which
|
||||
* makes both the impact point and the lead solution cheap enough to run every frame.
|
||||
* <p>
|
||||
* Plain doubles and no Minecraft classes, so it can be tested on its own. Units are blocks and ticks.
|
||||
*/
|
||||
public final class Ballistics {
|
||||
public static final int MAX_TICKS = 400;
|
||||
|
||||
private final double[] d = new double[MAX_TICKS + 1];
|
||||
private final double[] g = new double[MAX_TICKS + 1];
|
||||
|
||||
public Ballistics(double drag, double gravity) {
|
||||
double y = 0.0;
|
||||
double vy = 0.0;
|
||||
double sum = 0.0;
|
||||
double factor = 1.0;
|
||||
for (int n = 1; n <= MAX_TICKS; n++) {
|
||||
y += vy;
|
||||
vy = vy * drag - gravity;
|
||||
sum += factor;
|
||||
factor *= drag;
|
||||
d[n] = sum;
|
||||
g[n] = -y;
|
||||
}
|
||||
}
|
||||
|
||||
/** Sum of the drag factors after n ticks, n may be fractional. */
|
||||
public double d(double n) {
|
||||
return interpolate(d, n);
|
||||
}
|
||||
|
||||
/** Drop in blocks after n ticks, n may be fractional. */
|
||||
public double g(double n) {
|
||||
return interpolate(g, n);
|
||||
}
|
||||
|
||||
private static double interpolate(double[] table, double n) {
|
||||
if (n <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if (n >= MAX_TICKS) {
|
||||
return table[MAX_TICKS];
|
||||
}
|
||||
int i = (int) n;
|
||||
double f = n - i;
|
||||
return table[i] + (table[i + 1] - table[i]) * f;
|
||||
}
|
||||
|
||||
/** Height of the terrain a shell would hit at a column. */
|
||||
public interface Ground {
|
||||
double heightAt(double x, double z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a shell fired from the muzzle with the given world velocity (blocks per tick, already including the
|
||||
* speed of the shooter if shells inherit it) comes down. Returns the impact point as {x, y, z} or null if it
|
||||
* stays in the air for the whole table.
|
||||
*/
|
||||
public double[] impact(double mx, double my, double mz, double vx, double vy, double vz, Ground ground) {
|
||||
double px = mx;
|
||||
double py = my;
|
||||
double pz = mz;
|
||||
for (int n = 1; n <= MAX_TICKS; n++) {
|
||||
double nx = mx + d[n] * vx;
|
||||
double ny = my + d[n] * vy - g[n];
|
||||
double nz = mz + d[n] * vz;
|
||||
double floor = ground.heightAt(nx, nz);
|
||||
if (ny <= floor) {
|
||||
// Linear crossing inside this step
|
||||
double before = py - ground.heightAt(px, pz);
|
||||
double after = ny - floor;
|
||||
double f = before - after > 1.0E-9 ? Math.min(1.0, Math.max(0.0, before / (before - after))) : 1.0;
|
||||
return new double[] {px + (nx - px) * f, py + (ny - py) * f, pz + (nz - pz) * f};
|
||||
}
|
||||
px = nx;
|
||||
py = ny;
|
||||
pz = nz;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Result of {@link #lead}: aim direction, time of flight in ticks and the range to the target's current position. */
|
||||
public record Lead(double dirX, double dirY, double dirZ, double ticks, double range) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Aim direction that makes a shell meet a moving target.
|
||||
*
|
||||
* @param speed muzzle speed in blocks per tick
|
||||
* @param t* target position now
|
||||
* @param tv* target velocity in blocks per tick
|
||||
* @param sv* velocity the shell inherits from the shooter in blocks per tick, zero if it does not
|
||||
* @return the solution or null if the shell cannot reach the target within the table
|
||||
*/
|
||||
public Lead lead(double mx, double my, double mz, double speed, double tx, double ty, double tz,
|
||||
double tvx, double tvy, double tvz, double svx, double svy, double svz) {
|
||||
double range = Math.sqrt(sq(tx - mx) + sq(ty - my) + sq(tz - mz));
|
||||
double previousN = 0.0;
|
||||
for (int n = 1; n <= MAX_TICKS; n++) {
|
||||
double f = miss(n, mx, my, mz, speed, tx, ty, tz, tvx, tvy, tvz, svx, svy, svz);
|
||||
if (f <= 0.0) {
|
||||
double lo = previousN;
|
||||
double hi = n;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
double mid = 0.5 * (lo + hi);
|
||||
if (miss(mid, mx, my, mz, speed, tx, ty, tz, tvx, tvy, tvz, svx, svy, svz) > 0.0) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
double t = 0.5 * (lo + hi);
|
||||
double dn = d(t);
|
||||
double rx = tx + tvx * t - mx - dn * svx;
|
||||
double ry = ty + tvy * t - my - dn * svy + g(t);
|
||||
double rz = tz + tvz * t - mz - dn * svz;
|
||||
double length = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
||||
if (length < 1.0E-6) {
|
||||
return null;
|
||||
}
|
||||
return new Lead(rx / length, ry / length, rz / length, t, range);
|
||||
}
|
||||
previousN = n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Distance the shell can still cover short of where the target will be, positive means it has not got there yet. */
|
||||
private double miss(double n, double mx, double my, double mz, double speed, double tx, double ty, double tz,
|
||||
double tvx, double tvy, double tvz, double svx, double svy, double svz) {
|
||||
double dn = d(n);
|
||||
double rx = tx + tvx * n - mx - dn * svx;
|
||||
double ry = ty + tvy * n - my - dn * svy + g(n);
|
||||
double rz = tz + tvz * n - mz - dn * svz;
|
||||
return Math.sqrt(rx * rx + ry * ry + rz * rz) - speed * dn;
|
||||
}
|
||||
|
||||
private static double sq(double v) {
|
||||
return v * v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
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<Block> BARREL = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "acbarrel"));
|
||||
private static final TagKey<Block> 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<BlockPos> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.RenderLevelStageEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3f;
|
||||
import org.valkyrienskies.core.api.ships.ClientShip;
|
||||
import org.valkyrienskies.core.api.ships.Ship;
|
||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
|
||||
/**
|
||||
* Draws two markers for the first light autocannon of the aircraft you sit in:
|
||||
* an orange ring where its shells come down on the ground, and, for the radar target selected on the seat, a green
|
||||
* ring you put on the target to hit it (the lead pipper): green from the radar, cyan while following the ship itself,
|
||||
* yellow while only extrapolating a lost contact. Both come from the same ballistics Warium's shells follow.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
|
||||
public final class GunPipperRenderer {
|
||||
private static final int IMPACT_COLOR = 0xFFFF9020;
|
||||
private static final int LEAD_COLOR = 0xFF40FF60;
|
||||
private static final int VISUAL_COLOR = 0xFF40E8FF;
|
||||
private static final int COAST_COLOR = 0xFFFFE040;
|
||||
private static final float DISTANCE = 2.0F;
|
||||
|
||||
private static final GunFinder FINDER = new GunFinder();
|
||||
private static final TargetTrack TRACK = new TargetTrack();
|
||||
private static Ballistics ballistics;
|
||||
private static double ballisticsDrag = Double.NaN;
|
||||
private static double ballisticsGravity = Double.NaN;
|
||||
|
||||
private GunPipperRenderer() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRenderLevel(RenderLevelStageEvent event) {
|
||||
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES || !PipperConfig.SPEC.isLoaded()
|
||||
|| !PipperConfig.ENABLED.get() || !ModList.get().isLoaded("valkyrien_warium")) {
|
||||
return;
|
||||
}
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
ClientLevel level = minecraft.level;
|
||||
if (level == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
Entity seat = WariumSeat.seatOf(minecraft.player);
|
||||
if (seat == null) {
|
||||
TRACK.reset();
|
||||
return;
|
||||
}
|
||||
BlockPos seatPos = BlockPos.containing(seat.getX(), seat.getY(), seat.getZ());
|
||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, seatPos);
|
||||
if (ship == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GunFinder.Shot shot = FINDER.find(level, ship.getId(), ship.getShipAABB(), seatPos);
|
||||
if (shot == null) {
|
||||
return;
|
||||
}
|
||||
org.joml.Matrix4dc shipToWorld = ship instanceof ClientShip clientShip
|
||||
? clientShip.getRenderTransform().getShipToWorld() : ship.getShipToWorld();
|
||||
Vector3d muzzle = shipToWorld.transformPosition(new Vector3d(shot.mx(), shot.my(), shot.mz()));
|
||||
Vector3d direction = shipToWorld.transformDirection(new Vector3d(shot.dx(), shot.dy(), shot.dz())).normalize();
|
||||
|
||||
double inherit = PipperConfig.INHERIT_SHIP_VELOCITY.get() ? 1.0 / 20.0 : 0.0;
|
||||
org.joml.Vector3dc velocity = ship.getVelocity();
|
||||
double svx = velocity.x() * inherit;
|
||||
double svy = velocity.y() * inherit;
|
||||
double svz = velocity.z() * inherit;
|
||||
|
||||
Ballistics table = table();
|
||||
Camera camera = event.getCamera();
|
||||
Matrix4f matrix = event.getPoseStack().last().pose();
|
||||
|
||||
BufferBuilder builder = Tesselator.getInstance().getBuilder();
|
||||
RenderSystem.disableDepthTest();
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.defaultBlendFunc();
|
||||
RenderSystem.disableCull();
|
||||
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
||||
boolean drawn = false;
|
||||
|
||||
if (PipperConfig.SHOW_IMPACT.get()) {
|
||||
double[] impact = table.impact(muzzle.x, muzzle.y, muzzle.z,
|
||||
direction.x * shot.speed() + svx, direction.y * shot.speed() + svy, direction.z * shot.speed() + svz,
|
||||
(x, z) -> level.getHeight(Heightmap.Types.MOTION_BLOCKING, Mth.floor(x), Mth.floor(z)));
|
||||
if (impact != null) {
|
||||
drawn |= marker(builder, matrix, camera, impact[0], impact[1], impact[2], IMPACT_COLOR, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (PipperConfig.SHOW_LEAD.get()) {
|
||||
drawn |= lead(builder, matrix, camera, level, seat, seatPos, ship.getId(), table, muzzle, shot.speed(), svx, svy, svz);
|
||||
} else {
|
||||
TRACK.reset();
|
||||
}
|
||||
|
||||
if (drawn) {
|
||||
BufferUploader.drawWithShader(builder.end());
|
||||
} else {
|
||||
builder.end().release();
|
||||
}
|
||||
RenderSystem.enableCull();
|
||||
RenderSystem.enableDepthTest();
|
||||
RenderSystem.disableBlend();
|
||||
}
|
||||
|
||||
private static boolean lead(BufferBuilder builder, Matrix4f matrix, Camera camera, ClientLevel level, Entity seat, BlockPos seatPos,
|
||||
long ownShipId, Ballistics table, Vector3d muzzle, double speed, double svx, double svy, double svz) {
|
||||
int selected = WariumSeat.selectedTarget(seat);
|
||||
double[] radar = selected > 0 ? radarPosition(level, seatPos, selected) : null;
|
||||
TargetTrack.Fix fix = TRACK.resolve(level, ownShipId, selected, radar, level.getGameTime(),
|
||||
PipperConfig.VISUAL_TRACK.get(), PipperConfig.COAST_SECONDS.get());
|
||||
if (fix == null) {
|
||||
return false;
|
||||
}
|
||||
Ballistics.Lead lead = table.lead(muzzle.x, muzzle.y, muzzle.z, speed, fix.x(), fix.y(), fix.z(),
|
||||
fix.vx(), fix.vy(), fix.vz(), svx, svy, svz);
|
||||
if (lead == null) {
|
||||
return false;
|
||||
}
|
||||
int color = switch (fix.source()) {
|
||||
case RADAR -> LEAD_COLOR;
|
||||
case VISUAL -> VISUAL_COLOR;
|
||||
case COAST -> COAST_COLOR;
|
||||
};
|
||||
return marker(builder, matrix, camera,
|
||||
muzzle.x + lead.dirX() * lead.range(), muzzle.y + lead.dirY() * lead.range(), muzzle.z + lead.dirZ() * lead.range(),
|
||||
color, true);
|
||||
}
|
||||
|
||||
/** Position the node reports for a target index, or null if it reports none. */
|
||||
private static double[] radarPosition(ClientLevel level, BlockPos seatPos, int selected) {
|
||||
BlockEntity seatEntity = level.getBlockEntity(seatPos);
|
||||
if (seatEntity == null) {
|
||||
return null;
|
||||
}
|
||||
var seatData = seatEntity.getPersistentData();
|
||||
BlockEntity node = level.getBlockEntity(BlockPos.containing(seatData.getDouble("ControlX"), seatData.getDouble("ControlY"), seatData.getDouble("ControlZ")));
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
var data = node.getPersistentData();
|
||||
// Warium builds these keys from a double, so they read TargetX1.0, TargetY1.0 and so on
|
||||
double tx = data.getDouble("TargetX" + (double) selected);
|
||||
double ty = data.getDouble("TargetY" + (double) selected);
|
||||
double tz = data.getDouble("TargetZ" + (double) selected);
|
||||
if (tx == 0.0 && ty == 0.0 && tz == 0.0) {
|
||||
return null;
|
||||
}
|
||||
return new double[] {tx, ty, tz};
|
||||
}
|
||||
|
||||
private static Ballistics table() {
|
||||
double drag = PipperConfig.DRAG.get();
|
||||
double gravity = PipperConfig.GRAVITY.get();
|
||||
if (ballistics == null || drag != ballisticsDrag || gravity != ballisticsGravity) {
|
||||
ballistics = new Ballistics(drag, gravity);
|
||||
ballisticsDrag = drag;
|
||||
ballisticsGravity = gravity;
|
||||
}
|
||||
return ballistics;
|
||||
}
|
||||
|
||||
/** A ring on the screen in the direction of a world point, with ticks on it for the lead pipper. */
|
||||
private static boolean marker(BufferBuilder builder, Matrix4f matrix, Camera camera, double x, double y, double z, int color, boolean ticks) {
|
||||
Vec3 cameraPosition = camera.getPosition();
|
||||
Vector3f toPoint = new Vector3f((float) (x - cameraPosition.x), (float) (y - cameraPosition.y), (float) (z - cameraPosition.z));
|
||||
if (toPoint.lengthSquared() < 1.0E-6F) {
|
||||
return false;
|
||||
}
|
||||
toPoint.normalize();
|
||||
if (toPoint.dot(camera.getLookVector()) <= 0.05F) {
|
||||
return false;
|
||||
}
|
||||
Vector3f center = new Vector3f(toPoint).mul(DISTANCE);
|
||||
Vector3f left = new Vector3f(camera.getLeftVector());
|
||||
Vector3f up = new Vector3f(camera.getUpVector());
|
||||
float radius = (float) (PipperConfig.MARKER_SIZE.get() * DISTANCE);
|
||||
float thickness = radius * 0.12F;
|
||||
|
||||
ring(builder, matrix, center, left, up, radius, thickness, color);
|
||||
quad(builder, matrix, center, left, up, thickness * 1.6F, thickness * 1.6F, color);
|
||||
if (ticks) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
double angle = i * Math.PI / 2.0;
|
||||
float cx = (float) Math.cos(angle);
|
||||
float cy = (float) Math.sin(angle);
|
||||
Vector3f tickCenter = new Vector3f(center)
|
||||
.fma(cx * radius * 1.45F, left).fma(cy * radius * 1.45F, up);
|
||||
float halfX = Math.abs(cx) > 0.5F ? radius * 0.35F : thickness;
|
||||
float halfY = Math.abs(cy) > 0.5F ? radius * 0.35F : thickness;
|
||||
quad(builder, matrix, tickCenter, left, up, halfX, halfY, color);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ring(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float radius, float thickness, int color) {
|
||||
final int segments = 32;
|
||||
float inner = radius - thickness;
|
||||
float outer = radius + thickness;
|
||||
for (int i = 0; i < segments; i++) {
|
||||
double a0 = i * 2.0 * Math.PI / segments;
|
||||
double a1 = (i + 1) * 2.0 * Math.PI / segments;
|
||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a0) * inner, (float) Math.sin(a0) * inner, color);
|
||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a0) * outer, (float) Math.sin(a0) * outer, color);
|
||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a1) * outer, (float) Math.sin(a1) * outer, color);
|
||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a1) * inner, (float) Math.sin(a1) * inner, color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void quad(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float halfX, float halfY, int color) {
|
||||
vertex(builder, matrix, center, left, up, -halfX, -halfY, color);
|
||||
vertex(builder, matrix, center, left, up, halfX, -halfY, color);
|
||||
vertex(builder, matrix, center, left, up, halfX, halfY, color);
|
||||
vertex(builder, matrix, center, left, up, -halfX, halfY, color);
|
||||
}
|
||||
|
||||
private static void vertex(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float a, float b, int color) {
|
||||
// The left vector points to the left of the screen, so a positive offset along it is drawn to the left
|
||||
float vx = center.x - left.x * a + up.x * b;
|
||||
float vy = center.y - left.y * a + up.y * b;
|
||||
float vz = center.z - left.z * a + up.z * b;
|
||||
builder.vertex(matrix, vx, vy, vz)
|
||||
.color(color >> 16 & 255, color >> 8 & 255, color & 255, color >>> 24)
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
/** Client side settings of the gun pipper. Lives in improved_aerodynamics-client.toml. */
|
||||
public final class PipperConfig {
|
||||
public static final ForgeConfigSpec SPEC;
|
||||
|
||||
public static final ForgeConfigSpec.BooleanValue ENABLED;
|
||||
public static final ForgeConfigSpec.BooleanValue SHOW_IMPACT;
|
||||
public static final ForgeConfigSpec.BooleanValue SHOW_LEAD;
|
||||
public static final ForgeConfigSpec.BooleanValue INHERIT_SHIP_VELOCITY;
|
||||
public static final ForgeConfigSpec.BooleanValue VISUAL_TRACK;
|
||||
public static final ForgeConfigSpec.DoubleValue COAST_SECONDS;
|
||||
public static final ForgeConfigSpec.DoubleValue MUZZLE_SPEED_OVERRIDE;
|
||||
public static final ForgeConfigSpec.DoubleValue GRAVITY;
|
||||
public static final ForgeConfigSpec.DoubleValue DRAG;
|
||||
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
|
||||
|
||||
static {
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
builder.comment("Gun pipper for Warium light autocannons, drawn while seated in a Warium seat").push("pipper");
|
||||
ENABLED = builder.comment("Master switch")
|
||||
.define("enabled", true);
|
||||
SHOW_IMPACT = builder.comment("Show where the shells of the gun come down on the ground (orange ring)")
|
||||
.define("showImpact", true);
|
||||
SHOW_LEAD = builder.comment("Show the lead pipper for the radar target you selected (green ring)")
|
||||
.define("showLead", true);
|
||||
VISUAL_TRACK = builder.comment("Follow the selected target's ship itself once the radar has shown it, so the lead pipper keeps working when",
|
||||
"the radar loses it (notching, speed gate, ground clutter). Only while the ship is loaded, which is within render distance",
|
||||
"(cyan ring)")
|
||||
.define("visualTrack", true);
|
||||
COAST_SECONDS = builder.comment("Seconds a lost radar contact is extrapolated from its last position and velocity when its ship is not loaded",
|
||||
"(yellow ring), 0 turns it off")
|
||||
.defineInRange("coastSeconds", 8.0, 0.0, 60.0);
|
||||
INHERIT_SHIP_VELOCITY = builder.comment("Add the speed of your aircraft to the shells. Warium does not do this itself, only turn it",
|
||||
"on if a test shows your shells keep the speed of the jet (fire level at full speed, then hovering)")
|
||||
.define("inheritShipVelocity", false);
|
||||
MUZZLE_SPEED_OVERRIDE = builder.comment("Muzzle speed in blocks per tick, 0 works it out from the barrel length like Warium does")
|
||||
.defineInRange("muzzleSpeedOverride", 0.0, 0.0, 100.0);
|
||||
GRAVITY = builder.comment("Gravity of the shells in blocks per tick squared")
|
||||
.defineInRange("gravity", 0.05, 0.0, 1.0);
|
||||
DRAG = builder.comment("Speed the shells keep each tick, 0.99 is a vanilla arrow")
|
||||
.defineInRange("drag", 0.99, 0.5, 1.0);
|
||||
MARKER_SIZE = builder.comment("Radius of the markers on screen")
|
||||
.defineInRange("markerSize", 0.03, 0.005, 0.2);
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
private PipperConfig() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.primitives.AABBdc;
|
||||
import org.valkyrienskies.core.api.ships.ClientShip;
|
||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
|
||||
/**
|
||||
* The target the lead pipper aims at. The radar is only the way in: its contact is matched to the closest loaded ship
|
||||
* and from then on that ship is followed directly, with its true position and velocity, so the pipper survives the
|
||||
* radar losing the contact (notching, the speed gate, ground clutter). Without a loaded ship, which happens beyond
|
||||
* render distance, the last radar position is extrapolated for a limited time.
|
||||
* <p>
|
||||
* The selected index is no help for this: the radar rebuilds its list, so after a loss the index can point at another
|
||||
* contact or at an empty slot.
|
||||
*/
|
||||
final class TargetTrack {
|
||||
/** A radar position this close to a ship is taken to be that ship. */
|
||||
private static final double SNAP_BLOCKS = 80.0;
|
||||
/** A fresh radar position this far from the followed ship means the player moved on to another target. */
|
||||
private static final double REBIND_BLOCKS = 250.0;
|
||||
private static final long NONE = Long.MIN_VALUE;
|
||||
|
||||
enum Source {
|
||||
RADAR, VISUAL, COAST
|
||||
}
|
||||
|
||||
/** Position in blocks, velocity in blocks per tick. */
|
||||
record Fix(double x, double y, double z, double vx, double vy, double vz, Source source) {
|
||||
}
|
||||
|
||||
private final TargetTracker radar = new TargetTracker();
|
||||
private int index = -1;
|
||||
private long boundId = NONE;
|
||||
|
||||
void reset() {
|
||||
radar.reset();
|
||||
index = -1;
|
||||
boundId = NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param radarPosition the position the node reports for the selected index, or null if it reports none
|
||||
*/
|
||||
Fix resolve(ClientLevel level, long ownShipId, int selected, double[] radarPosition, long tick,
|
||||
boolean visualTrack, double coastSeconds) {
|
||||
if (selected <= 0) {
|
||||
reset();
|
||||
return null;
|
||||
}
|
||||
if (selected != index) {
|
||||
reset();
|
||||
index = selected;
|
||||
}
|
||||
if (radarPosition != null) {
|
||||
radar.update(selected, radarPosition[0], radarPosition[1], radarPosition[2], tick);
|
||||
}
|
||||
|
||||
if (visualTrack) {
|
||||
ClientShip bound = boundId == NONE ? null : shipById(level, boundId);
|
||||
if (radarPosition != null && radar.age(tick) <= 1) {
|
||||
// Fresh contact: pick the ship it belongs to, or let go of the old one if the player switched
|
||||
if (bound == null || distance(center(bound), radarPosition) > REBIND_BLOCKS) {
|
||||
bound = nearest(level, ownShipId, radarPosition);
|
||||
boundId = bound == null ? NONE : bound.getId();
|
||||
}
|
||||
}
|
||||
if (bound != null) {
|
||||
Vector3d center = center(bound);
|
||||
var velocity = bound.getVelocity();
|
||||
return new Fix(center.x, center.y, center.z, velocity.x() / 20.0, velocity.y() / 20.0, velocity.z() / 20.0, Source.VISUAL);
|
||||
}
|
||||
boundId = NONE;
|
||||
}
|
||||
|
||||
if (!radar.hasPosition()) {
|
||||
return null;
|
||||
}
|
||||
long age = radar.age(tick);
|
||||
if (age > TargetTracker.STALE_TICKS) {
|
||||
if (coastSeconds <= 0.0 || !radar.hasVelocity() || age > coastSeconds * 20.0) {
|
||||
return null;
|
||||
}
|
||||
double[] p = radar.predicted(tick);
|
||||
return new Fix(p[0], p[1], p[2], radar.vx(), radar.vy(), radar.vz(), Source.COAST);
|
||||
}
|
||||
double[] p = radar.predicted(tick);
|
||||
// A contact is only extrapolated up to the age of its last update
|
||||
return new Fix(p[0], p[1], p[2], radar.vx(), radar.vy(), radar.vz(), Source.RADAR);
|
||||
}
|
||||
|
||||
private static ClientShip shipById(ClientLevel level, long id) {
|
||||
for (ClientShip ship : VSGameUtilsKt.getShipObjectWorld(level).getLoadedShips()) {
|
||||
if (ship.getId() == id) {
|
||||
return ship;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ClientShip nearest(ClientLevel level, long ownShipId, double[] position) {
|
||||
ClientShip best = null;
|
||||
double bestDistance = SNAP_BLOCKS;
|
||||
for (ClientShip ship : VSGameUtilsKt.getShipObjectWorld(level).getLoadedShips()) {
|
||||
if (ship.getId() == ownShipId) {
|
||||
continue;
|
||||
}
|
||||
double distance = distance(center(ship), position);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = ship;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** The same point Warium's radar reports: the center of the world bounding box. */
|
||||
private static Vector3d center(ClientShip ship) {
|
||||
AABBdc box = ship.getWorldAABB();
|
||||
return box.center(new Vector3d());
|
||||
}
|
||||
|
||||
private static double distance(Vector3d a, double[] b) {
|
||||
return Math.sqrt(sq(a.x - b[0]) + sq(a.y - b[1]) + sq(a.z - b[2]));
|
||||
}
|
||||
|
||||
private static double sq(double v) {
|
||||
return v * v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
||||
|
||||
/**
|
||||
* Target velocity from the radar. The node only stores positions, and they change when the radar updates, which is
|
||||
* not every tick. So the velocity is the change between two distinct positions divided by the ticks between them,
|
||||
* smoothed. When the radar stops updating the last velocity is kept so the track can be extrapolated.
|
||||
*/
|
||||
final class TargetTracker {
|
||||
/** Ticks without a new position after which the contact counts as lost and is only extrapolated. */
|
||||
static final long STALE_TICKS = 40;
|
||||
private static final double SMOOTHING = 0.5;
|
||||
private static final double JUMP_BLOCKS_PER_TICK = 15.0;
|
||||
|
||||
private int index = -1;
|
||||
private double px;
|
||||
private double py;
|
||||
private double pz;
|
||||
private long sampleTick;
|
||||
private double vx;
|
||||
private double vy;
|
||||
private double vz;
|
||||
private boolean hasVelocity;
|
||||
|
||||
void update(int selected, double x, double y, double z, long tick) {
|
||||
if (selected != index || tick < sampleTick) {
|
||||
index = selected;
|
||||
px = x;
|
||||
py = y;
|
||||
pz = z;
|
||||
sampleTick = tick;
|
||||
hasVelocity = false;
|
||||
vx = 0.0;
|
||||
vy = 0.0;
|
||||
vz = 0.0;
|
||||
return;
|
||||
}
|
||||
if (x != px || y != py || z != pz) {
|
||||
long dt = Math.max(1L, tick - sampleTick);
|
||||
double nx = (x - px) / dt;
|
||||
double ny = (y - py) / dt;
|
||||
double nz = (z - pz) / dt;
|
||||
if (Math.sqrt(nx * nx + ny * ny + nz * nz) > JUMP_BLOCKS_PER_TICK) {
|
||||
// Teleport or a different contact reusing the slot
|
||||
hasVelocity = false;
|
||||
} else if (hasVelocity) {
|
||||
vx += (nx - vx) * SMOOTHING;
|
||||
vy += (ny - vy) * SMOOTHING;
|
||||
vz += (nz - vz) * SMOOTHING;
|
||||
} else {
|
||||
vx = nx;
|
||||
vy = ny;
|
||||
vz = nz;
|
||||
hasVelocity = true;
|
||||
}
|
||||
px = x;
|
||||
py = y;
|
||||
pz = z;
|
||||
sampleTick = tick;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
index = -1;
|
||||
hasVelocity = false;
|
||||
vx = 0.0;
|
||||
vy = 0.0;
|
||||
vz = 0.0;
|
||||
}
|
||||
|
||||
boolean hasPosition() {
|
||||
return index >= 0;
|
||||
}
|
||||
|
||||
/** Ticks since the radar last gave a new position. */
|
||||
long age(long tick) {
|
||||
return tick - sampleTick;
|
||||
}
|
||||
|
||||
/** The last radar position moved on by the velocity over the time since. */
|
||||
double[] predicted(long tick) {
|
||||
long dt = Math.max(0L, tick - sampleTick);
|
||||
return new double[] {px + vx * dt, py + vy * dt, pz + vz * dt};
|
||||
}
|
||||
|
||||
double lastX() {
|
||||
return px;
|
||||
}
|
||||
|
||||
double lastY() {
|
||||
return py;
|
||||
}
|
||||
|
||||
double lastZ() {
|
||||
return pz;
|
||||
}
|
||||
|
||||
/** Velocity in blocks per tick, zero when unknown. */
|
||||
double vx() {
|
||||
return vx;
|
||||
}
|
||||
|
||||
double vy() {
|
||||
return vy;
|
||||
}
|
||||
|
||||
double vz() {
|
||||
return vz;
|
||||
}
|
||||
|
||||
boolean hasVelocity() {
|
||||
return hasVelocity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
||||
|
||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
/** Everything that touches Warium VS classes, so nothing else loads them when Warium is missing. */
|
||||
final class WariumSeat {
|
||||
private WariumSeat() {
|
||||
}
|
||||
|
||||
static Entity seatOf(Entity rider) {
|
||||
Entity root = rider.getRootVehicle();
|
||||
return root instanceof SeatEntityEntity ? root : null;
|
||||
}
|
||||
|
||||
/** Index of the radar target selected on the seat, 1 based, 0 for none. */
|
||||
static int selectedTarget(Entity seat) {
|
||||
return seat.getEntityData().get(SeatEntityEntity.DATA_SelectedTarget);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user