|
| 1 | +package zmaster587.advancedRocketry.test.server; |
| 2 | + |
| 3 | +import org.junit.Assume; |
| 4 | +import org.junit.Test; |
| 5 | + |
| 6 | +import java.util.regex.Matcher; |
| 7 | +import java.util.regex.Pattern; |
| 8 | + |
| 9 | +import static org.junit.Assert.assertTrue; |
| 10 | + |
| 11 | +/** |
| 12 | + * A shot that meets a SHIP — the case the whole substrate exists for, and the one its world-block |
| 13 | + * tests cannot reach. |
| 14 | + * |
| 15 | + * <p>A ship's blocks are not where the ship appears to be: they sit at fixed addresses in a shipyard |
| 16 | + * subspace millions of blocks away while the hull flies around. So a swept segment computed in world |
| 17 | + * coordinates crosses <b>nothing</b> — the world frame is empty air where the hull visibly is. The |
| 18 | + * substrate therefore maps both ends of the segment into each candidate ship's frame, traverses |
| 19 | + * there, and maps the crossing point back out. Three conversions, none of which announces itself when |
| 20 | + * it is wrong: a frame error here is not an exception, it is a round that flies through a hull.</p> |
| 21 | + * |
| 22 | + * <h3>What makes this evidence rather than a coincidence</h3> |
| 23 | + * <p>Two controls, both asserted before any conclusion is drawn. The world frame at the target must |
| 24 | + * genuinely hold <b>air</b>, so a hit cannot have come from the world-frame traversal; and the |
| 25 | + * subject block must be undamaged at its subspace address beforehand, so "damaged afterwards" is |
| 26 | + * about this shot. The end point is then checked against the ship's WORLD position — with the |
| 27 | + * mapping-back-out leg deleted, a shot would report ending five million blocks away in a shipyard |
| 28 | + * nobody can see, and every other assertion here would still pass.</p> |
| 29 | + */ |
| 30 | +public class ShotHitsShipHullE2ETest extends AbstractSharedServerTest { |
| 31 | + |
| 32 | + private static final Pattern BUILDER_POS = |
| 33 | + Pattern.compile("\"builderPos\":\\[(-?\\d+),(-?\\d+),(-?\\d+)]"); |
| 34 | + |
| 35 | + /** A build site of this class's own, clear of the other ship scenarios on this shared server. */ |
| 36 | + private static final int SRC_X = 6400, SRC_Y = 80, SRC_Z = 6400; |
| 37 | + /** Where the ship is moved to: in the air, INSIDE build height, so "the world is air there" is a |
| 38 | + * measurement rather than a consequence of being above the world's ceiling. */ |
| 39 | + private static final int FAR_X = 6400, FAR_Y = 150, FAR_Z = 8800; |
| 40 | + |
| 41 | + /** Fast enough that one tick's segment crosses the whole hull — the case a point test misses. */ |
| 42 | + private static final double SPEED = 40.0D; |
| 43 | + /** Enough budget to be spent on more than the first block it meets. */ |
| 44 | + private static final int ENERGY = 200000; |
| 45 | + |
| 46 | + @Test |
| 47 | + public void aShotStopsAtTheHullOfAMovedShipAndDamagesItsOwnBlock() throws Exception { |
| 48 | + Assume.assumeTrue("needs Valkyrien Skies on the server classpath", serverHasVs()); |
| 49 | + exec("artest vs permaload true"); |
| 50 | + exec("artest damage clear-impacts"); |
| 51 | + exec("artest shot clear 0"); |
| 52 | + |
| 53 | + String shipId = buildAndMoveShip(); |
| 54 | + |
| 55 | + // A block of this ship whose subspace address we know: its pilot seat. |
| 56 | + String seat = exec("artest vs find-seat 0 id " + shipId); |
| 57 | + assertTrue("could not locate the ship's seat, so there is no known block to aim at: " + seat, |
| 58 | + seat.contains("\"seatFound\":true")); |
| 59 | + int subX = extractInt(seat, "seatX"), subY = extractInt(seat, "seatY"), |
| 60 | + subZ = extractInt(seat, "seatZ"); |
| 61 | + |
| 62 | + String mapped = exec("artest vs to-world 0 " + FAR_X + " " + FAR_Y + " " + FAR_Z |
| 63 | + + " " + subX + " " + subY + " " + subZ); |
| 64 | + assertTrue("the seat's subspace address could not be mapped to a world point: " + mapped, |
| 65 | + mapped.contains("\"ok\":true")); |
| 66 | + double worldX = extractDouble(mapped, "worldX"); |
| 67 | + double worldY = extractDouble(mapped, "worldY"); |
| 68 | + double worldZ = extractDouble(mapped, "worldZ"); |
| 69 | + |
| 70 | + // ARRANGEMENT CONTROL — the mapped point is actually on the ship as the world sees it. If it |
| 71 | + // is not, everything below measures a broken fixture rather than the substrate. |
| 72 | + String moved = exec("artest vs ship-info 0 " + FAR_X + " " + FAR_Y + " " + FAR_Z); |
| 73 | + assertTrue("the moved ship is not managed at its new position: " + moved, |
| 74 | + moved.contains("\"managed\":true")); |
| 75 | + double shipX = extractDouble(moved, "posX"), shipY = extractDouble(moved, "posY"), |
| 76 | + shipZ = extractDouble(moved, "posZ"); |
| 77 | + double offHull = Math.sqrt(sq(worldX - shipX) + sq(worldY - shipY) + sq(worldZ - shipZ)); |
| 78 | + assertTrue("the seat's mapped world point (" + worldX + "," + worldY + "," + worldZ + ") is " |
| 79 | + + offHull + " blocks from the ship's own world position: the fixture, not the" |
| 80 | + + " substrate, is what this run would be measuring. mapped=" + mapped, offHull < 64.0D); |
| 81 | + |
| 82 | + // CONTROL 1 — the WORLD frame is air along the line of fire. A hit therefore cannot have come |
| 83 | + // from the world-frame traversal, which is the only other way this substrate finds anything. |
| 84 | + for (int drop = -4; drop <= 4; drop++) { |
| 85 | + String worldBlock = exec("artest damage stage 0 " + (int) Math.floor(worldX) + " " |
| 86 | + + ((int) Math.floor(worldY) + drop) + " " + (int) Math.floor(worldZ)); |
| 87 | + assertTrue("the world frame holds a block at the target, " + drop + " blocks off the seat: " |
| 88 | + + worldBlock + ". A shot stopping here would prove nothing about ship frames", |
| 89 | + worldBlock.contains("\"block\":\"minecraft:air\"")); |
| 90 | + } |
| 91 | + |
| 92 | + // CONTROL 2 — the subject is undamaged at its SUBSPACE address, where the ship's blocks are. |
| 93 | + String before = stage(subX, subY, subZ); |
| 94 | + assertTrue("the seat's subspace address holds no block, so nothing below is about the ship: " |
| 95 | + + before, !before.contains("\"block\":\"minecraft:air\"")); |
| 96 | + assertTrue("the subject block is already damaged before the shot: " + before, |
| 97 | + readLong(before, "stage") == 0); |
| 98 | + |
| 99 | + // Fire straight down through the seat's WORLD position, from clear air above it. |
| 100 | + long id = readLong(exec("artest shot fire 0 " + worldX + " " + (worldY + 30.0D) + " " + worldZ |
| 101 | + + " 0 " + (-SPEED) + " 0 " + ENERGY + " 40"), "id"); |
| 102 | + assertTrue("the launch was refused, so nothing else here means anything", id > 0); |
| 103 | + exec("artest shield tick 0"); |
| 104 | + |
| 105 | + String after = exec("artest shot read 0 " + id); |
| 106 | + assertTrue("the shot is still in flight after a step that crossed the hull — a segment computed" |
| 107 | + + " in the world frame finds nothing where a ship visibly is, which is exactly what" |
| 108 | + + " this substrate maps around: " + after, after.contains("\"present\":false")); |
| 109 | + assertTrue("the shot stopped, but not by meeting structure: " + after, |
| 110 | + "STRUCTURE_IMPACT".equals(extractString(after, "ended"))); |
| 111 | + |
| 112 | + // The damage landed on the SHIP's own block, at its subspace address. |
| 113 | + String hull = stage(subX, subY, subZ); |
| 114 | + boolean staged = readLong(hull, "stage") > 0; |
| 115 | + boolean destroyed = hull.contains("\"wasDestroyed\":true") |
| 116 | + || hull.contains("\"block\":\"minecraft:air\""); |
| 117 | + assertTrue("the shot reported hitting structure but the ship's own block is untouched at its" |
| 118 | + + " subspace address (before=" + before + " after=" + hull + "): the impact was handed" |
| 119 | + + " over in the wrong frame, or to the wrong target", staged || destroyed); |
| 120 | + |
| 121 | + // And the shot ended in WORLD coordinates. Without the mapping back out it would report |
| 122 | + // ending at a shipyard address millions of blocks from anything a player can see — and every |
| 123 | + // assertion above would still have passed. |
| 124 | + double endX = readDouble(after, "endX"), endY = readDouble(after, "endY"), |
| 125 | + endZ = readDouble(after, "endZ"); |
| 126 | + double offSeat = Math.sqrt(sq(endX - worldX) + sq(endY - worldY) + sq(endZ - worldZ)); |
| 127 | + assertTrue("the shot ended at (" + endX + "," + endY + "," + endZ + "), " + offSeat |
| 128 | + + " blocks from the world point it was fired through: the crossing point was never" |
| 129 | + + " mapped out of the ship's frame", offSeat < 16.0D); |
| 130 | + } |
| 131 | + |
| 132 | + /** Build the fixture, assemble it into a ship and move it far from where it was built. */ |
| 133 | + private String buildAndMoveShip() throws Exception { |
| 134 | + clearArea(SRC_X, SRC_Z); |
| 135 | + String coords = placeFixture(SRC_X, SRC_Y, SRC_Z, "with-pilot-seat"); |
| 136 | + String asm = exec("artest rocket assemble 0 " + coords); |
| 137 | + assertTrue("with VS an AFC-bearing build must become a ship, not a rocket: " + asm, |
| 138 | + asm.contains("\"rocketCount\":0")); |
| 139 | + |
| 140 | + String info = null; |
| 141 | + for (int attempt = 0; attempt < 40; attempt++) { |
| 142 | + exec("artest vs load-ships 0"); |
| 143 | + info = exec("artest vs ship-info 0 " + SRC_X + " " + SRC_Y + " " + SRC_Z); |
| 144 | + if (info.contains("\"managed\":true")) { |
| 145 | + break; |
| 146 | + } |
| 147 | + Thread.sleep(250); |
| 148 | + } |
| 149 | + assertTrue("the build never became a ship managed at its build site: " + info, |
| 150 | + info != null && info.contains("\"managed\":true")); |
| 151 | + |
| 152 | + String tp = exec("artest vs teleport-ship 0 " + SRC_X + " " + SRC_Y + " " + SRC_Z |
| 153 | + + " " + FAR_X + " " + FAR_Y + " " + FAR_Z); |
| 154 | + assertTrue("the ship could not be moved, so it never left the world blocks it was built from: " |
| 155 | + + tp, tp.contains("\"ok\":true")); |
| 156 | + exec("artest vs unpark 0 " + FAR_X + " " + FAR_Y + " " + FAR_Z); |
| 157 | + return extractString(info, "id"); |
| 158 | + } |
| 159 | + |
| 160 | + private String stage(int x, int y, int z) throws Exception { |
| 161 | + return exec("artest damage stage 0 " + x + " " + y + " " + z); |
| 162 | + } |
| 163 | + |
| 164 | + private void clearArea(int baseX, int baseZ) throws Exception { |
| 165 | + int cx1 = (baseX - 4) >> 4, cz1 = (baseZ - 4) >> 4; |
| 166 | + int cx2 = (baseX + 20) >> 4, cz2 = (baseZ + 20) >> 4; |
| 167 | + assertTrue("chunk warmup failed", exec("artest chunk warmup 0 " + cx1 + " " + cz1 + " " |
| 168 | + + cx2 + " " + cz2).contains("\"ok\":true")); |
| 169 | + assertTrue("pre-clear failed", exec("artest fill 0 " + (baseX - 4) + " " + (SRC_Y - 2) + " " |
| 170 | + + (baseZ - 4) + " " + (baseX + 20) + " " + (SRC_Y + 12) + " " + (baseZ + 20) |
| 171 | + + " minecraft:air").contains("\"ok\":true")); |
| 172 | + } |
| 173 | + |
| 174 | + private String placeFixture(int baseX, int baseY, int baseZ, String variant) throws Exception { |
| 175 | + String fixture = exec("artest fixture rocket 0 " + baseX + " " + baseY + " " + baseZ + " " |
| 176 | + + variant); |
| 177 | + assertTrue("fixture (" + variant + ") failed: " + fixture, fixture.contains("\"ok\":true")); |
| 178 | + Matcher bp = BUILDER_POS.matcher(fixture); |
| 179 | + assertTrue("fixture (" + variant + ") missing builderPos: " + fixture, bp.find()); |
| 180 | + return bp.group(1) + " " + bp.group(2) + " " + bp.group(3); |
| 181 | + } |
| 182 | + |
| 183 | + private boolean serverHasVs() throws Exception { |
| 184 | + return exec("artest vs available").contains("\"available\":true"); |
| 185 | + } |
| 186 | + |
| 187 | + private static double sq(double v) { |
| 188 | + return v * v; |
| 189 | + } |
| 190 | + |
| 191 | + private String exec(String cmd) throws Exception { |
| 192 | + return String.join("\n", client().execute(cmd)); |
| 193 | + } |
| 194 | + |
| 195 | + private static long readLong(String json, String key) { |
| 196 | + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+)").matcher(json); |
| 197 | + assertTrue("no " + key + " field in: " + json, m.find()); |
| 198 | + return Long.parseLong(m.group(1)); |
| 199 | + } |
| 200 | + |
| 201 | + private static double readDouble(String json, String key) { |
| 202 | + Matcher m = Pattern.compile("\"" + key + "\":(-?[0-9][0-9.eE+-]*)").matcher(json); |
| 203 | + assertTrue("no " + key + " field in: " + json, m.find()); |
| 204 | + return Double.parseDouble(m.group(1)); |
| 205 | + } |
| 206 | + |
| 207 | + private static int extractInt(String json, String key) { |
| 208 | + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+)").matcher(json); |
| 209 | + return m.find() ? Integer.parseInt(m.group(1)) : Integer.MIN_VALUE; |
| 210 | + } |
| 211 | + |
| 212 | + private static double extractDouble(String json, String key) { |
| 213 | + Matcher m = Pattern.compile("\"" + key + "\":(-?\\d+(?:\\.\\d+)?)").matcher(json); |
| 214 | + return m.find() ? Double.parseDouble(m.group(1)) : 0.0; |
| 215 | + } |
| 216 | + |
| 217 | + private static String extractString(String json, String key) { |
| 218 | + Matcher m = Pattern.compile("\"" + key + "\":\"([^\"]*)\"").matcher(json); |
| 219 | + return m.find() ? m.group(1) : null; |
| 220 | + } |
| 221 | +} |
0 commit comments