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

Commit a67a5e3

Browse files
committed
feat: the carbon has an exit, and the suit covers a stale zone
- add a jettison port: an unpowered airlock that vents its slot - a blocked port HOLDS its cargo rather than voiding it - extract EjectionPort, shared with the thermal dump to come - clearance is block-frame; the spawn crosses into the world frame - pin the suit fallback for a pressurised room that went stale - assert force-tick found a tile, so a lost chunk stops reading as refusal
1 parent bd6262d commit a67a5e3

12 files changed

Lines changed: 636 additions & 1 deletion

File tree

‎src/main/java/zmaster587/advancedRocketry/AdvancedRocketry.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ public void preInit(FMLPreInitializationEvent event) {
423423
GameRegistry.registerTileEntity(TileGasSeparator.class, "ARgasSeparator");
424424
GameRegistry.registerTileEntity(zmaster587.advancedRocketry.tile.atmosphere.TileLifeSupportPlant.class, "ARlifeSupportPlant");
425425
GameRegistry.registerTileEntity(zmaster587.advancedRocketry.tile.atmosphere.TileVentilationDuct.class, "ARventilationDuct");
426+
GameRegistry.registerTileEntity(zmaster587.advancedRocketry.tile.infrastructure.TileJettisonPort.class, "ARjettisonPort");
426427
GameRegistry.registerTileEntity(TileWarpController.class, "ARStationMonitor");
427428
GameRegistry.registerTileEntity(TileAtmosphereDetector.class, "AROxygenDetector");
428429
GameRegistry.registerTileEntity(TileStationOrientationController.class, "AROrientationControl");
@@ -784,6 +785,7 @@ public void registerBlocks(RegistryEvent.Register<Block> evt) {
784785
AdvancedRocketryBlocks.blockGasSeparator = new zmaster587.advancedRocketry.block.BlockGasSeparator(TileGasSeparator.class, GuiHandler.guiId.MODULAR.ordinal()).setCreativeTab(tabAdvRocketry).setUnlocalizedName("gasSeparator").setHardness(3f);
785786
AdvancedRocketryBlocks.blockLifeSupportPlant = new BlockTile(zmaster587.advancedRocketry.tile.atmosphere.TileLifeSupportPlant.class, GuiHandler.guiId.MODULAR.ordinal()).setCreativeTab(tabAdvRocketry).setUnlocalizedName("lifeSupportPlant").setHardness(3f);
786787
AdvancedRocketryBlocks.blockVentilationDuct = new zmaster587.advancedRocketry.block.BlockVentilationDuct().setCreativeTab(tabAdvRocketry).setUnlocalizedName("ventilationDuct").setHardness(1f);
788+
AdvancedRocketryBlocks.blockJettisonPort = new BlockTile(zmaster587.advancedRocketry.tile.infrastructure.TileJettisonPort.class, GuiHandler.guiId.MODULAR.ordinal()).setCreativeTab(tabAdvRocketry).setUnlocalizedName("jettisonPort").setHardness(3f);
787789
AdvancedRocketryBlocks.blockOxygenVent = new BlockTile(TileOxygenVent.class, GuiHandler.guiId.MODULAR.ordinal()).setUnlocalizedName("oxygenVent").setCreativeTab(tabAdvRocketry).setHardness(3f);
788790
AdvancedRocketryBlocks.blockOxygenCharger = new BlockHalfTile(TileGasChargePad.class, GuiHandler.guiId.MODULAR.ordinal()).setUnlocalizedName("oxygenCharger").setCreativeTab(tabAdvRocketry).setHardness(3f);
789791
AdvancedRocketryBlocks.blockOxygenDetection = new BlockRedstoneEmitter(Material.IRON, "advancedrocketry:atmosphereDetector_active").setUnlocalizedName("atmosphereDetector").setHardness(3f).setCreativeTab(tabAdvRocketry);
@@ -979,6 +981,7 @@ public void registerBlocks(RegistryEvent.Register<Block> evt) {
979981
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockGasSeparator.setRegistryName("gasSeparator"));
980982
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockLifeSupportPlant.setRegistryName("lifeSupportPlant"));
981983
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockVentilationDuct.setRegistryName("ventilationDuct"));
984+
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockJettisonPort.setRegistryName("jettisonPort"));
982985
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockOxygenVent.setRegistryName("oxygenVent"));
983986
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockOxygenCharger.setRegistryName("oxygenCharger"));
984987
LibVulpesBlocks.registerBlock(AdvancedRocketryBlocks.blockOxygenDetection.setRegistryName("oxygenDetection"));

‎src/main/java/zmaster587/advancedRocketry/api/ARConfiguration.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ public class ARConfiguration {
274274
@ConfigProperty
275275
public int lifeSupportCarbonPerDust;
276276
@ConfigProperty
277+
public int jettisonPortIntervalTicks;
278+
@ConfigProperty
279+
public int jettisonPortClearance;
280+
@ConfigProperty
277281
public int lifeSupportFluidPerAtmBlock;
278282
@ConfigProperty
279283
public int lifeSupportSeparatorRate;
@@ -535,6 +539,8 @@ public static void loadPreInit() {
535539
arConfig.lifeSupportRecirculatorRate = config.get(OXYGEN, "lifeSupportRecirculatorRate", 6000, "CO2 a single recirculator turns back into oxygen each second, in millionths of an atmosphere. At the default it keeps up with three crew in a room of any size.", 0, Integer.MAX_VALUE).getInt();
536540
arConfig.lifeSupportRecirculatorPower = config.get(OXYGEN, "lifeSupportRecirculatorPower", 400, "Power a recirculator draws per operation. Reversing combustion is endothermic: the energy cost is the point, not a tax.", 0, Integer.MAX_VALUE).getInt();
537541
arConfig.lifeSupportCarbonPerDust = config.get(OXYGEN, "lifeSupportCarbonPerDust", 60000, "CO2 that must be regenerated before one carbon dust is produced, in millionths of an atmosphere. At the defaults a recirculator running flat out yields a dust every ten seconds.", 1, Integer.MAX_VALUE).getInt();
542+
arConfig.jettisonPortIntervalTicks = config.get(OXYGEN, "jettisonPortIntervalTicks", 20, "How often a jettison port tries to throw its contents overboard, in ticks. This is a duty cycle, not a throttle on how much leaves: the port ejects whatever stack it holds, so a faster port empties a busier scrubber line rather than exporting more per firing.", 1, Integer.MAX_VALUE).getInt();
543+
arConfig.jettisonPortClearance = config.get(OXYGEN, "jettisonPortClearance", 3, "How many blocks in front of a jettison port must be empty before it will fire. The port refuses rather than firing into a wall, and it HOLDS its cargo while blocked instead of voiding it, so the only cost of a badly placed port is that nothing leaves.", 1, 64).getInt();
538544
arConfig.lifeSupportFluidPerAtmBlock = config.get(OXYGEN, "lifeSupportFluidPerAtmBlock", 1000, "How many millibuckets of gas one whole atmosphere of partial pressure amounts to in ONE block of room. This is the exchange rate between air in a room and gas in a pipe: at the default, emptying a 20-block cabin of its 0.21 atm of oxygen yields 4200 mB.", 1, Integer.MAX_VALUE).getInt();
539545
arConfig.lifeSupportSeparatorRate = config.get(OXYGEN, "lifeSupportSeparatorRate", 20000, "Partial pressure a separator moves between room and tank each second, in millionths of an atmosphere. At the default it clears a badly stale room in under ten seconds.", 0, Integer.MAX_VALUE).getInt();
540546
arConfig.lifeSupportSeparatorPower = config.get(OXYGEN, "lifeSupportSeparatorPower", 300, "Power a separator draws per operation.", 0, Integer.MAX_VALUE).getInt();

‎src/main/java/zmaster587/advancedRocketry/api/AdvancedRocketryBlocks.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public class AdvancedRocketryBlocks {
9191
public static Block blockGasSeparator;
9292
public static Block blockLifeSupportPlant;
9393
public static Block blockVentilationDuct;
94+
public static Block blockJettisonPort;
9495
public static Block blockCircleLight;
9596
public static Block blockSolarGenerator;
9697
public static Block blockDockingPort;

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ public void execute(MinecraftServer server, ICommandSender sender, String[] args
175175
case "vent":
176176
handleVent(server, sender, tail(args));
177177
break;
178+
case "jettison":
179+
handleJettison(server, sender, tail(args));
180+
break;
178181
case "item":
179182
handleItem(server, sender, tail(args));
180183
break;
@@ -16457,6 +16460,80 @@ private static net.minecraftforge.fluids.capability.IFluidHandler findFluidHandl
1645716460
return null;
1645816461
}
1645916462

16463+
// Jettison-port probe ----------------------------------------------
16464+
16465+
/**
16466+
* {@code /artest jettison load <dim> <x> <y> <z> <itemId> <count>} — put a stack in the port's
16467+
* slot, the way a hopper would.
16468+
* <p>
16469+
* {@code /artest jettison info <dim> <x> <y> <z>} — what the port holds, whether its exit is
16470+
* clear, and how many loose item entities are floating within a few blocks of it:
16471+
*
16472+
* <pre>
16473+
* {"ok":true,"held":"advancedrocketry:carbonDust","heldCount":1,"obstruction":0,"ejected":0}
16474+
* </pre>
16475+
*
16476+
* <p>{@code ejected} is the assertion the contract actually needs — "the dust LEFT" is a claim
16477+
* about the world, not about the slot, and a port that merely voided its contents would empty
16478+
* its slot exactly as convincingly. {@code obstruction} is reported as a DISTANCE rather than a
16479+
* flag so a red test says which block to go and look at.</p>
16480+
*/
16481+
private void handleJettison(MinecraftServer server, ICommandSender sender, String[] args) {
16482+
if (args.length < 5
16483+
|| !("load".equalsIgnoreCase(args[0]) || "info".equalsIgnoreCase(args[0]))) {
16484+
send(sender, "{\"error\":\"unknown jettison subcommand — try load <dim> <x> <y> <z>"
16485+
+ " <itemId> <count> | info <dim> <x> <y> <z>\"}");
16486+
return;
16487+
}
16488+
int dim = parseIntOr(args[1], Integer.MIN_VALUE);
16489+
int x = parseIntOr(args[2], 0);
16490+
int y = parseIntOr(args[3], 0);
16491+
int z = parseIntOr(args[4], 0);
16492+
net.minecraft.world.WorldServer world = server.getWorld(dim);
16493+
if (world == null) {
16494+
send(sender, "{\"error\":\"world not loaded\",\"dim\":" + dim + "}");
16495+
return;
16496+
}
16497+
BlockPos pos = new BlockPos(x, y, z);
16498+
TileEntity tile = world.getTileEntity(pos);
16499+
if (!(tile instanceof zmaster587.advancedRocketry.tile.infrastructure.TileJettisonPort)) {
16500+
send(sender, "{\"error\":\"not a TileJettisonPort\",\"tileClass\":\""
16501+
+ escapeJson(tile == null ? "null" : tile.getClass().getName()) + "\"}");
16502+
return;
16503+
}
16504+
zmaster587.advancedRocketry.tile.infrastructure.TileJettisonPort port =
16505+
(zmaster587.advancedRocketry.tile.infrastructure.TileJettisonPort) tile;
16506+
16507+
if ("load".equalsIgnoreCase(args[0])) {
16508+
if (args.length < 7) {
16509+
send(sender, "{\"error\":\"usage: load <dim> <x> <y> <z> <itemId> <count>\"}");
16510+
return;
16511+
}
16512+
net.minecraft.item.Item item =
16513+
ForgeRegistries.ITEMS.getValue(new ResourceLocation(args[5]));
16514+
if (item == null) {
16515+
send(sender, "{\"error\":\"unknown item id\",\"id\":\"" + escapeJson(args[5]) + "\"}");
16516+
return;
16517+
}
16518+
port.setInventorySlotContents(0,
16519+
new net.minecraft.item.ItemStack(item, Math.max(1, parseIntOr(args[6], 1))));
16520+
send(sender, "{\"ok\":true,\"loaded\":\"" + escapeJson(args[5]) + "\"}");
16521+
return;
16522+
}
16523+
16524+
net.minecraft.item.ItemStack held = port.getStackInSlot(0);
16525+
// Loose items only, and only close by: the port's own muzzle offset puts an ejected stack
16526+
// well under a block away, so a wider box would start counting the world's litter.
16527+
java.util.List<net.minecraft.entity.item.EntityItem> loose = world.getEntitiesWithinAABB(
16528+
net.minecraft.entity.item.EntityItem.class,
16529+
new net.minecraft.util.math.AxisAlignedBB(pos).grow(4.0D));
16530+
send(sender, "{\"ok\":true,\"held\":\""
16531+
+ escapeJson(held.isEmpty() ? "" : held.getItem().getRegistryName().toString())
16532+
+ "\",\"heldCount\":" + held.getCount()
16533+
+ ",\"obstruction\":" + port.getObstruction()
16534+
+ ",\"ejected\":" + loose.size() + "}");
16535+
}
16536+
1646016537
// Subsystem-network probe ------------------------------------------
1646116538

1646216539
/**
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package zmaster587.advancedRocketry.subsystem.ejection;
2+
3+
import net.minecraft.entity.item.EntityItem;
4+
import net.minecraft.item.ItemStack;
5+
import net.minecraft.util.EnumFacing;
6+
import net.minecraft.util.math.BlockPos;
7+
import net.minecraft.world.World;
8+
import zmaster587.advancedRocketry.integration.vs.VSIntegration;
9+
10+
/**
11+
* Throwing matter out of a hull, done once for everyone who needs to.
12+
* <p>
13+
* Two subsystems eject solid objects overboard — life support sheds the carbon its scrubbers pull
14+
* out of the air, and the thermal system fires charged heat slugs when rejection cannot win. They
15+
* are deliberately DIFFERENT machines: charging a slug to just under its melting point needs
16+
* materials and a duty cycle an ordinary airlock has no business carrying, and the thermal dump is
17+
* required to be unusable as a steady state while the carbon sink IS one. What they share is this —
18+
* the act itself, which is fiddlier than it looks aboard a ship.
19+
* <p>
20+
* <b>The clearance walk needs no frame conversion, and the ejection does.</b> A ship's blocks live
21+
* in a stationary subspace and that is exactly where the world stores them, so asking "are the next
22+
* few blocks along my facing empty?" is ordinary block arithmetic and is correct on a flying ship
23+
* without touching a transform. An item ENTITY, on the other hand, lives in the world frame, so the
24+
* spawn crosses the seam and every part of it must be mapped.
25+
*/
26+
public final class EjectionPort {
27+
28+
/** Clear of the port's mouth before the item is released, so it never spawns inside the hull. */
29+
private static final double MUZZLE_OFFSET = 0.6D;
30+
31+
private EjectionPort() {
32+
}
33+
34+
/**
35+
* How far along {@code facing} the first obstruction sits, or 0 when the exit is clear.
36+
* <p>
37+
* Reported as a distance rather than a boolean because a blocked port is something a player has
38+
* to go and FIND: "blocked at 2" sends them to the right block, "blocked" sends them around the
39+
* ship. Air and replaceable blocks (grass, snow) do not count as obstructions — a port that
40+
* refused to fire because of a snow layer would read as broken.
41+
*/
42+
public static int obstructionDistance(World world, BlockPos pos, EnumFacing facing, int clearance) {
43+
if (world == null || pos == null || facing == null) {
44+
return 1;
45+
}
46+
for (int step = 1; step <= Math.max(1, clearance); step++) {
47+
BlockPos ahead = pos.offset(facing, step);
48+
if (!world.isAirBlock(ahead)
49+
&& !world.getBlockState(ahead).getBlock().isReplaceable(world, ahead)) {
50+
return step;
51+
}
52+
}
53+
return 0;
54+
}
55+
56+
/**
57+
* Releases one stack out of the port, in the world frame, carrying the ship's own motion.
58+
* <p>
59+
* The carry matters as much as the direction: an item released with the ship's velocity
60+
* subtracted out simply reappears inside the hull a tick later, because the hull is moving and
61+
* the item is not. Sampling the velocity at the port's own world point rather than at the
62+
* ship's centre also gets rotation right — a port far out on a spinning hull is moving faster
63+
* than the hub it turns around.
64+
*
65+
* @return false when the stack was empty or the world refused the spawn; the caller keeps the
66+
* stack in that case rather than losing it
67+
*/
68+
public static boolean eject(World world, BlockPos pos, EnumFacing facing, ItemStack stack) {
69+
if (world == null || world.isRemote || pos == null || facing == null
70+
|| stack == null || stack.isEmpty()) {
71+
return false;
72+
}
73+
74+
double[] mouth = worldPointOf(world, pos);
75+
double[] direction = worldDirectionOf(world, pos, facing);
76+
77+
double x = mouth[0] + 0.5D + direction[0] * MUZZLE_OFFSET;
78+
double y = mouth[1] + 0.5D + direction[1] * MUZZLE_OFFSET;
79+
double z = mouth[2] + 0.5D + direction[2] * MUZZLE_OFFSET;
80+
81+
EntityItem item = new EntityItem(world, x, y, z, stack.copy());
82+
item.setPickupDelay(20);
83+
84+
double[] carry = VSIntegration.shipVelocityAtPoint(world, x, y, z);
85+
double carryX = carry == null ? 0.0D : carry[0];
86+
double carryY = carry == null ? 0.0D : carry[1];
87+
double carryZ = carry == null ? 0.0D : carry[2];
88+
89+
item.motionX = carryX + direction[0] * EJECT_SPEED;
90+
item.motionY = carryY + direction[1] * EJECT_SPEED;
91+
item.motionZ = carryZ + direction[2] * EJECT_SPEED;
92+
93+
return world.spawnEntity(item);
94+
}
95+
96+
/** Slow enough to read as "pushed out", fast enough to clear a hull that is turning. */
97+
private static final double EJECT_SPEED = 0.35D;
98+
99+
/**
100+
* Where this block actually IS right now. On a ship the block's own coordinates are a subspace
101+
* address that has nothing to do with where the hull is flying, so they are mapped; off a ship
102+
* the two frames coincide and the mapper answers null.
103+
*/
104+
private static double[] worldPointOf(World world, BlockPos pos) {
105+
double[] mapped = VSIntegration.getShipWorldPosition(world, pos);
106+
return mapped != null ? mapped : new double[]{pos.getX(), pos.getY(), pos.getZ()};
107+
}
108+
109+
/**
110+
* Which way "out of this face" points in the world.
111+
* <p>
112+
* Taken as the difference between two SUBSPACE points mapped through the same transform, rather
113+
* than by rotating a vector: it needs no ship identity, it is exact for any hull attitude, and
114+
* it degrades to the plain facing vector by construction when there is no ship, because then
115+
* both points map to themselves.
116+
*/
117+
private static double[] worldDirectionOf(World world, BlockPos pos, EnumFacing facing) {
118+
double[] here = VSIntegration.getShipWorldPosition(world, pos);
119+
double[] ahead = VSIntegration.getShipWorldPosition(world, pos.offset(facing));
120+
if (here != null && ahead != null) {
121+
double dx = ahead[0] - here[0];
122+
double dy = ahead[1] - here[1];
123+
double dz = ahead[2] - here[2];
124+
double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
125+
if (length > 1.0E-6D) {
126+
return new double[]{dx / length, dy / length, dz / length};
127+
}
128+
}
129+
return new double[]{facing.getFrontOffsetX(), facing.getFrontOffsetY(), facing.getFrontOffsetZ()};
130+
}
131+
}

0 commit comments

Comments
 (0)