Skip to content
This repository was archived by the owner on Aug 31, 2026. It is now read-only.

Commit 72d907a

Browse files
committed
test(fixtures): move the shared player stand-spot onto surveyed ground
- old spot was a 1x1 block over open ocean; the player clipped and sank - seven client tests share one surveyed plains site, nothing is placed - site guard checks each site with its own pad parameters - new probe: artest player damage-log names the damage source
1 parent 7d69fdd commit 72d907a

10 files changed

Lines changed: 260 additions & 76 deletions

src/main/java/zmaster587/advancedRocketry/command/test/TestProbeCommand.java

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14120,6 +14120,46 @@ private void handlePlayer(MinecraftServer server, ICommandSender sender, String[
1412014120
+ ",\"slots\":[" + slotJson + "]}");
1412114121
return;
1412214122
}
14123+
if ("damage-log".equals(sub)) {
14124+
// /artest player damage-log [reset]
14125+
//
14126+
// Every damage EVENT applied to a player since the last reset:
14127+
// source, amount, and the world state that explains it. A health
14128+
// delta alone says a player lost hearts; it never says to what.
14129+
// Suffocation in a hillside, a 4-block fall off a stand-spot and
14130+
// the vacuum tick all read as "health went down" — they differ
14131+
// only in damageType, and a fixture bug is exactly the case where
14132+
// the source is NOT the one the test is about.
14133+
//
14134+
// Cumulative counters + bounded history (never a snapshot static):
14135+
// the interesting damage is transient and a "last damage" field
14136+
// is empty 40 ticks later (EntityLivingBase.getLastDamageSource
14137+
// expires), which is how the first pass of this diagnosis read
14138+
// lastDamage:"" for a player that had demonstrably been hurt.
14139+
if (args.length >= 2 && "reset".equals(args[1].toLowerCase(java.util.Locale.ROOT))) {
14140+
DamageRecorder.ensureRegistered();
14141+
DamageRecorder.reset();
14142+
send(sender, "{\"ok\":true,\"action\":\"reset\""
14143+
+ ",\"worldTime\":" + player.world.getTotalWorldTime()
14144+
+ ",\"health\":" + player.getHealth() + "}");
14145+
return;
14146+
}
14147+
DamageRecorder.ensureRegistered();
14148+
send(sender, "{\"ok\":true"
14149+
+ ",\"registered\":true"
14150+
+ ",\"events\":" + DamageRecorder.eventCount
14151+
+ ",\"totalAmount\":" + DamageRecorder.totalAmount
14152+
+ ",\"worldTime\":" + player.world.getTotalWorldTime()
14153+
+ ",\"health\":" + player.getHealth()
14154+
+ ",\"posX\":" + player.posX
14155+
+ ",\"posY\":" + player.posY
14156+
+ ",\"posZ\":" + player.posZ
14157+
+ ",\"onGround\":" + player.onGround
14158+
+ ",\"inWater\":" + player.isInWater()
14159+
+ ",\"fallDistance\":" + player.fallDistance
14160+
+ ",\"log\":\"" + escapeJson(DamageRecorder.history()) + "\"}");
14161+
return;
14162+
}
1412314163
if ("set-fall-distance".equals(sub) && args.length >= 2) {
1412414164
// set the player's server-side fallDistance field.
1412514165
// Used to set up a non-zero baseline so AreaGravityController's
@@ -15110,7 +15150,7 @@ private void handlePlayer(MinecraftServer server, ICommandSender sender, String[
1511015150
+ escapeJson(player.getName()) + "\"}");
1511115151
return;
1511215152
}
15113-
send(sender, "{\"error\":\"unknown player subcommand — try inv-bypass <add|remove|status> | open-container | health | set-health <hp> | held-air | suit-diag | give-suit-chest [air] | equip-airsuit [air] | clear-armor | advancement <id> | advancement reset <id> | last-chat | chat-clear | try-seal-detect <dim> <x> <y> <z> | try-atm-analyze <dim> | try-hovercraft <dim> <px> <py> <pz> <yaw> <pitch> | try-biomechanger-rclick <dim>\"}");
15153+
send(sender, "{\"error\":\"unknown player subcommand — try inv-bypass <add|remove|status> | open-container | health | set-health <hp> | held-air | suit-diag | damage-log [reset] | give-suit-chest [air] | equip-airsuit [air] | clear-armor | advancement <id> | advancement reset <id> | last-chat | chat-clear | try-seal-detect <dim> <x> <y> <z> | try-atm-analyze <dim> | try-hovercraft <dim> <px> <py> <pz> <yaw> <pitch> | try-biomechanger-rclick <dim>\"}");
1511415154
}
1511515155

1511615156
// ── chat-tap ──────────────────────────────────────
@@ -16649,6 +16689,73 @@ public void onDeOrbiting(
1664916689
}
1665016690
}
1665116691

16692+
/**
16693+
* Global event-bus listener recording every damage event applied to a
16694+
* player, for `/artest player damage-log`. Registered lazily on first
16695+
* query; a test that wants a clean window calls `damage-log reset`
16696+
* first (which also registers, so nothing before the reset is missed
16697+
* afterwards).
16698+
*
16699+
* <p>Cumulative counters plus a BOUNDED history — the shape a transient
16700+
* needs. `getLastDamageSource()` reports only what happened within the
16701+
* last 40 ticks, so a snapshot read at the end of an 80-tick window
16702+
* says "no damage source" about a player who lost hearts at tick 3.</p>
16703+
*/
16704+
public static final class DamageRecorder {
16705+
/** Cap: the history is carried inside a JUnit assert message. */
16706+
private static final int MAX_ENTRIES = 24;
16707+
16708+
public static volatile int eventCount = 0;
16709+
public static volatile float totalAmount = 0f;
16710+
private static final java.util.ArrayDeque<String> ENTRIES = new java.util.ArrayDeque<String>();
16711+
16712+
private static volatile boolean registered = false;
16713+
16714+
public static synchronized void ensureRegistered() {
16715+
if (registered) return;
16716+
net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(new DamageRecorder());
16717+
registered = true;
16718+
}
16719+
16720+
public static synchronized void reset() {
16721+
eventCount = 0;
16722+
totalAmount = 0f;
16723+
ENTRIES.clear();
16724+
}
16725+
16726+
/** Whole window on one line: `t=<worldTime> <type> -<amount> hp=<before> @y=<y> …`. */
16727+
public static synchronized String history() {
16728+
StringBuilder sb = new StringBuilder();
16729+
for (String e : ENTRIES) {
16730+
if (sb.length() > 0) sb.append(" | ");
16731+
sb.append(e);
16732+
}
16733+
return sb.toString();
16734+
}
16735+
16736+
@net.minecraftforge.fml.common.eventhandler.SubscribeEvent
16737+
public void onDamage(net.minecraftforge.event.entity.living.LivingDamageEvent e) {
16738+
if (!(e.getEntityLiving() instanceof net.minecraft.entity.player.EntityPlayer)) return;
16739+
net.minecraft.entity.EntityLivingBase victim = e.getEntityLiving();
16740+
String entry = "t=" + victim.world.getTotalWorldTime()
16741+
+ " " + e.getSource().getDamageType()
16742+
+ " -" + String.format(java.util.Locale.ROOT, "%.2f", e.getAmount())
16743+
+ " hp=" + String.format(java.util.Locale.ROOT, "%.1f", victim.getHealth())
16744+
+ " pos=" + String.format(java.util.Locale.ROOT, "%.1f,%.1f,%.1f",
16745+
victim.posX, victim.posY, victim.posZ)
16746+
+ " ground=" + victim.onGround
16747+
+ " water=" + victim.isInWater()
16748+
+ " fall=" + String.format(java.util.Locale.ROOT, "%.1f", victim.fallDistance)
16749+
+ " air=" + victim.getAir();
16750+
synchronized (DamageRecorder.class) {
16751+
eventCount++;
16752+
totalAmount += e.getAmount();
16753+
if (ENTRIES.size() >= MAX_ENTRIES) ENTRIES.pollFirst();
16754+
ENTRIES.addLast(entry);
16755+
}
16756+
}
16757+
}
16758+
1665216759
// ── TileDockingPort probes (Gap 5 — NBT + network packet round-trip) ──
1665316760
//
1665416761
// TileDockingPort stores two strings (myIdStr, targetIdStr) that

src/test/java/zmaster587/advancedRocketry/test/client/ElevatorCapsuleRideE2ETest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,14 @@ private int spawnCapsuleAt(double x, double y, double z) throws Exception {
7070

7171
@Test
7272
public void playerMountsElevatorCapsuleViaStartRiding() throws Exception {
73-
// Pad + tp pattern is from HovercraftRideE2ETest — keeps the
73+
// Site + tp pattern is from HovercraftRideE2ETest — keeps the
7474
// bot in the same chunk as the spawned entity so id resolution
7575
// through world.getEntityByID stays in-tick.
76-
exec("artest place 0 108 78 8 minecraft:stone");
77-
exec("tp @a 108.5 79 8.5");
76+
exec(HarnessPlayerSite.tpCommand());
7877
bot().waitTicks(5);
7978

80-
int capsuleId = spawnCapsuleAt(108.5, 79, 10.5);
79+
int capsuleId = spawnCapsuleAt(HarnessPlayerSite.standX(),
80+
HarnessPlayerSite.STAND_Y, HarnessPlayerSite.frontZ());
8181

8282
String mount = exec("artest player mount-entity " + capsuleId);
8383
assertTrue("mount-entity probe must succeed: " + mount,
@@ -104,11 +104,11 @@ public void playerMountsElevatorCapsuleViaStartRiding() throws Exception {
104104

105105
@Test
106106
public void playerDismountClearsRidingEntity() throws Exception {
107-
exec("artest place 0 128 78 8 minecraft:stone");
108-
exec("tp @a 128.5 79 8.5");
107+
exec(HarnessPlayerSite.tpCommand());
109108
bot().waitTicks(5);
110109

111-
int capsuleId = spawnCapsuleAt(128.5, 79, 10.5);
110+
int capsuleId = spawnCapsuleAt(HarnessPlayerSite.standX(),
111+
HarnessPlayerSite.STAND_Y, HarnessPlayerSite.frontZ());
112112
exec("artest player mount-entity " + capsuleId);
113113
com.google.gson.JsonObject mounted = waitForClientRiding(true);
114114
assertEquals("arrange: client must be riding the capsule first",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package zmaster587.advancedRocketry.test.client;
2+
3+
/**
4+
* The surveyed natural stand-spot shared by every client fixture that just needs the player to be
5+
* standing on solid ground somewhere.
6+
*
7+
* <p>These fixtures used to stand the player on a single stone block placed in mid-air at
8+
* {@code (8.5, 79, 8.5)}. With the harness world seed pinned, that column is open OCEAN: the block
9+
* was a 1×1 pillar ~16 blocks above the water, the player clipped its own corner for a point of
10+
* {@code inWall} suffocation damage and then fell in and sank. Everything downstream followed from
11+
* that — health assertions that read "the suit stopped protecting" when the player had merely
12+
* suffocated, a vacuum tick that never fires because the atmosphere handler skips a submerged
13+
* entity, and a suit that drains air per tick because the in-water branch of the player tick asks
14+
* whether the suit protects (and that question commits a decrement).</p>
15+
*
16+
* <p>So the spot is now real, natural, flat ground, surveyed once against the pinned seed with
17+
* {@code /artest worldgen find-biome} + {@code find-site} and cross-checked with {@code site-check}:
18+
* plains, pad radius 4, headroom 6, deviation 0. Nothing is placed and nothing is levelled — the
19+
* player is simply teleported onto ground that is already there.</p>
20+
*
21+
* <p>Pinned by {@code HarnessFixtureSitesTest}: if the seed or the generator changes, that guard
22+
* fails with "the fixture site is no longer flat" instead of half a dozen client tests failing with
23+
* symptoms that read like production bugs.</p>
24+
*
25+
* <p>All these tests get a FRESH world per test method ({@code AbstractClientE2ETest} starts a new
26+
* server harness in a new temp directory in {@code @Before}), so they can share one spot: the
27+
* per-method x-offsets the old fixtures carried bought no isolation.</p>
28+
*/
29+
public final class HarnessPlayerSite {
30+
31+
/** Surveyed site (plains, spread 0). */
32+
public static final int X = 736;
33+
public static final int Z = 2036;
34+
35+
/** Y of the topmost ground block — the player stands on top of it. */
36+
public static final int GROUND_Y = 64;
37+
38+
/** Y the player's feet occupy. */
39+
public static final int STAND_Y = GROUND_Y + 1;
40+
41+
/** Teleports every player onto the site, centred on the block. */
42+
public static String tpCommand() {
43+
return "tp @a " + (X + 0.5) + " " + STAND_Y + " " + (Z + 0.5);
44+
}
45+
46+
/** Centre X of the stand block. */
47+
public static double standX() {
48+
return X + 0.5;
49+
}
50+
51+
/** Centre Z of the stand block. */
52+
public static double standZ() {
53+
return Z + 0.5;
54+
}
55+
56+
/** Z two blocks in front of the player — where the ride fixtures spawn their craft. */
57+
public static double frontZ() {
58+
return Z + 2.5;
59+
}
60+
61+
private HarnessPlayerSite() {
62+
}
63+
}

src/test/java/zmaster587/advancedRocketry/test/client/HovercraftRideE2ETest.java

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,13 @@ private int spawnHovercraftAt(double x, double y, double z) throws Exception {
7676

7777
@Test
7878
public void playerMountsHovercraftViaStartRiding() throws Exception {
79-
// Spawn craft adjacent to a known stone pad so the bot is
80-
// close enough to be in the same world tick + chunk.
81-
exec("artest place 0 8 78 8 minecraft:stone");
82-
// (8, 79, 8) is ordinary terrain height — clear the body volume so a
83-
// hillside seed can't suffocate the player mid-test.
84-
exec("artest fill 0 7 79 7 9 82 9 minecraft:air");
85-
exec("tp @a 8.5 79 8.5");
79+
// Surveyed natural ground (HarnessPlayerSite); the craft spawns two blocks in front, so
80+
// the bot is in the same chunk and world tick as the entity it mounts.
81+
exec(HarnessPlayerSite.tpCommand());
8682
bot().waitTicks(5);
8783

88-
int craftId = spawnHovercraftAt(8.5, 79, 10.5);
84+
int craftId = spawnHovercraftAt(HarnessPlayerSite.standX(),
85+
HarnessPlayerSite.STAND_Y, HarnessPlayerSite.frontZ());
8986

9087
String mount = exec("artest player mount-entity " + craftId);
9188
assertTrue("mount-entity probe must succeed: " + mount,
@@ -112,11 +109,11 @@ public void playerMountsHovercraftViaStartRiding() throws Exception {
112109

113110
@Test
114111
public void playerDismountClearsRidingEntity() throws Exception {
115-
exec("artest place 0 28 78 8 minecraft:stone");
116-
exec("tp @a 28.5 79 8.5");
112+
exec(HarnessPlayerSite.tpCommand());
117113
bot().waitTicks(5);
118114

119-
int craftId = spawnHovercraftAt(28.5, 79, 10.5);
115+
int craftId = spawnHovercraftAt(HarnessPlayerSite.standX(),
116+
HarnessPlayerSite.STAND_Y, HarnessPlayerSite.frontZ());
120117
exec("artest player mount-entity " + craftId);
121118
com.google.gson.JsonObject mounted = waitForClientRiding(true);
122119
assertEquals("arrange: client must be riding the craft first",
@@ -146,11 +143,11 @@ public void forwardThrottleMovesHovercraftLaterally() throws Exception {
146143
// next to it. The craft's onUpdate reads player.moveForward
147144
// each tick — setting it via probe drives acceleration in the
148145
// direction of the craft's yaw.
149-
exec("artest place 0 48 78 8 minecraft:stone");
150-
exec("tp @a 48.5 79 8.5");
146+
exec(HarnessPlayerSite.tpCommand());
151147
bot().waitTicks(5);
152148

153-
int craftId = spawnHovercraftAt(48.5, 79, 10.5);
149+
int craftId = spawnHovercraftAt(HarnessPlayerSite.standX(),
150+
HarnessPlayerSite.STAND_Y, HarnessPlayerSite.frontZ());
154151
exec("artest player mount-entity " + craftId);
155152
waitForClientRiding(true);
156153

@@ -200,11 +197,11 @@ public void unmountedHovercraftDoesNotMoveLaterally() throws Exception {
200197
// getPassengerMovingForward returns 0 -> no lateral acceleration.
201198
// The Y position may drift (gravity/hover), but X+Z should
202199
// stay stable.
203-
exec("artest place 0 68 78 8 minecraft:stone");
204-
exec("tp @a 68.5 79 8.5");
200+
exec(HarnessPlayerSite.tpCommand());
205201
bot().waitTicks(5);
206202

207-
int craftId = spawnHovercraftAt(68.5, 79, 10.5);
203+
int craftId = spawnHovercraftAt(HarnessPlayerSite.standX(),
204+
HarnessPlayerSite.STAND_Y, HarnessPlayerSite.frontZ());
208205
// Confirm unmounted state.
209206
String riding = exec("artest player riding-entity");
210207
assertNotEquals("baseline: player must NOT be riding the craft "

src/test/java/zmaster587/advancedRocketry/test/client/ItemSpaceArmorUseFluidE2ETest.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,16 @@ private int readChestAir() throws Exception {
9292
/** Reset the player to a known location + bare-skinned state so each
9393
* test has a clean baseline regardless of order. */
9494
private void resetPlayer() throws Exception {
95-
exec("artest place 0 8 78 8 minecraft:stone");
96-
// Clear the volume the player's body occupies (y 79..81) plus a margin.
97-
// (8, 79, 8) is ordinary overworld terrain height: on some world seeds a
98-
// hillside fills it, the player spawns INSIDE a block and takes
99-
// suffocation ("inWall") damage — which the health assertions below
100-
// would otherwise misread as the space suit failing to grant immunity.
101-
String clear = exec("artest fill 0 7 79 7 9 82 9 minecraft:air");
102-
assertTrue("stand-spot pre-clear failed: " + clear, clear.contains("\"ok\":true"));
103-
exec("tp @a 8.5 79 8.5");
95+
// Open a damage-recording window covering the WHOLE test, arrangement
96+
// included: a health assertion that fails needs the damage SOURCE, and
97+
// the player's own last-damage field has expired by the time an 80-tick
98+
// window ends. An empty log next to a below-max health reading is itself
99+
// the answer — the hearts were lost before this server booted.
100+
exec("artest player damage-log reset");
101+
// Natural, surveyed, flat ground (see HarnessPlayerSite) — nothing placed, nothing
102+
// levelled. The health assertions below only mean "the suit protected" if the player
103+
// cannot lose hearts to the arrangement itself.
104+
exec(HarnessPlayerSite.tpCommand());
104105
exec("artest player clear-armor");
105106
exec("gamerule naturalRegeneration false");
106107
exec("gamemode survival @a");
@@ -173,7 +174,8 @@ public void suitedPlayerInVacuumLosesChestAirOverTime() throws Exception {
173174
assertTrue("suited player must not take vacuum damage; "
174175
+ "healthStart=" + healthStart
175176
+ " healthAfter=" + healthAfter
176-
+ " diag=" + exec("artest player suit-diag"),
177+
+ " diag=" + exec("artest player suit-diag")
178+
+ " damage=" + exec("artest player damage-log"),
177179
healthAfter >= healthStart);
178180
} finally {
179181
restoreDim(originalDensity);
@@ -212,7 +214,13 @@ public void suitedPlayerInBreathableDimDoesNotLoseChestAir() throws Exception {
212214
bot().waitTicks(80);
213215

214216
int chestAirAfter = readChestAir();
215-
assertEquals("client-rendered chest air must hold in breathable atmosphere",
217+
// Report the SERVER value beside the client one: they disagree only
218+
// if the client's copy is stale, and they agree only if the drain
219+
// really ran — one number cannot tell those apart.
220+
assertEquals("client-rendered chest air must hold in breathable atmosphere; "
221+
+ "server=" + chestAirAfter
222+
+ " diag=" + exec("artest player suit-diag")
223+
+ " where=" + exec("artest player damage-log"),
216224
1000, clientChestAir());
217225
assertEquals("chest air must be unchanged in breathable atmosphere; "
218226
+ "before=1000 after=" + chestAirAfter,
@@ -238,7 +246,8 @@ public void unsuitedPlayerInVacuumLosesNoAirAndTakesDamage() throws Exception {
238246
-1, readChestAir());
239247

240248
double healthStart = health(bot().reportState());
241-
assertTrue("player must start at full health, got " + healthStart,
249+
assertTrue("player must start at full health, got " + healthStart
250+
+ " damage=" + exec("artest player damage-log"),
242251
healthStart >= 20.0);
243252

244253
exec("artest atmosphere set-density 0 0");

0 commit comments

Comments
 (0)