Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
Unreleased

- Check generator redstone before the first power pulse so a disabled solar panel cannot produce a startup burst.
- Stop the electric still consuming oil and power when its full output will not fit.
- Drain waterlogged slabs and stairs without destroying the blocks, and only charge pump power after a successful pickup.
- Keep growth controllers from requesting more electricity than they can store or use.
- Read light above the solar panel so its solid body does not block its own light reading.
- Let turret beams leave their own mounting post while still respecting nearby cover and their existing range.
- Keep hydroturbines anchored to their generators so flowing water cannot build up invalid movement across a restart.
- Sample a machine's redstone interlock on its first server tick, then retain the normal eight-tick refresh.
- Send machine sounds to nearby connected players without loading surrounding chunks.
- Use an empty or full-scale hydroelectric power display without changing the amount of electricity generated.
- Add operating and persistence regression tests, including blocked outputs, refused pickups and unchanged waterlogged host blocks.

Electric Advantage 2.2.7.115021

- Target Minecraft 1.15.2, Forge 31.2.57 and snapshot 20200514-1.15.1/MCP 20200515.085601.
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ElectricAdvantage

**Minecraft 1.15.2, version 2.2.7.115021.** See the
[shared test record](https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/blob/master-1.15.2/docs/1.15/CURRENT-QUALIFICATION.md)
[shared test record](https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/blob/master-1.15.2/docs/1.15/TEST-COVERAGE-BACKPORT.md)
for exact jars, automated machine and upgrade results, and remaining manual
checks. Visuals and recipe unlocks were accepted on 27 October 2026. Historical
[1.14 test results](https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/blob/master-1.14.4/docs/1.14/BROADER-QUALIFICATION.md)
Expand Down Expand Up @@ -73,3 +73,32 @@ The implementation namespace is `zone.moddev.mc.electricadvantage`. Runtime iden

The RF- and Tech Reborn-labelled converters belong to Power Advantage and now use Forge Energy.
The historical [1.13 migration guide](https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/blob/master-1.13.2/docs/1.13/MIGRATION.md) explains the legacy conversion and backup process; the shared 1.15 test record lists the completed upgrade routes and their limits. Always make a whole-world backup before upgrading, keep the complete set of migration backups, and test a disposable copy first. Check that every other mod supports the target version; bounded Advantage tests do not qualify unrelated mods or an entire modpack.

## Generator Follow-Up

The [generator follow-up](../../PowerAdvantage/docs/1.15/TEST-COVERAGE-BACKPORT.md#generator-follow-up) records the older `pa115-candidate-b` inputs.
Its 126 operating jobs and 126 cold checks, 39 GUI windows, 351 caption cases,
355 rejected broken controls, thirteen frozen routes, ten connected routes and
paired 30-minute runs passed. This does not extend the earlier fourteen-route
proof. It is a pre-final Steam-control checkpoint, not qualification of the newer
SteamAdvantage jars or harness. Those still need the final sweep.
Manual play-testing and release review remain separate.

## Final T Candidate

The [final-t checkpoint](../../PowerAdvantage/docs/1.15/TEST-COVERAGE-BACKPORT.md#final-t-candidate)
records profile `pa115-candidate-d` and its exact jar hashes. All 132 operating
jobs and 132 cold-restart comparisons pass, including an independent audit of
six Steam controls and their nine tile-hold records. GUI checks pass for 39
windows, 351 caption cases and 355 rejected broken controls.
Thirteen frozen routes passed. The verified connected aggregate passes all ten
declared routes across both five-route shards.
Fresh and migrated-copy 30-minute runs pass. Power and Electric retain their
exact-hash release and reproducibility proof; final-t Steam passes its full suite
and two independent export builds. All 18 mod/version release and reproducibility
gates pass across the six maintained Minecraft lines. Both requested broad
profiles and their independent audit pass; the reporter-only unit/replay
qualification preserves the original native proof. Survival harvest checks pass
before and after restart. The report's extra network/drill scopes are now
qualified; manual acceptance remains separate. Earlier checkpoints keep their original scope;
No release publication is included.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public IPacket<?> createSpawnPacket() {
return NetworkHooks.getEntitySpawningPacket(this);
}

@Override
public boolean isPushedByWater() {
// Anchored to the generator: flow drives rotation, not linear movement.
return false;
}

@Override
public void tick(){
super.tick();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,23 @@ public static void drawIndicatorLight(boolean on,GUIContainer gc, int xOffset, i
}

public static void drawProgressBar(int x, int y, float progress, GUIContainer gc){
int n = (int)(8 * (progress + 0.0625f) );
if(n <= 0) return;
int w = 1 + (n * LED_BAR_W8);
int w = progressBarWidth(progress);
if(w == 0) return;
gc.blit(x, y, LED_BAR_X, LED_BAR_Y, w, LED_BAR_H);
}

static int progressBarWidth(float progress) {
progress = Float.isFinite(progress) ? Math.max(0F, Math.min(1F, progress)) : 0F;
int n = (int)(8 * (progress + 0.0625f));
return n <= 0 ? 0 : 1 + n * LED_BAR_W8;
}


public static void drawFluidBar(FluidStack fs, float barHeight, int xPos, int yPos,
GUIContainer guiContainer, int x, int y, float z,
ResourceLocation displayImage){

barHeight = Float.isFinite(barHeight) ? Math.max(0F, Math.min(1F, barHeight)) : 0F;
final int w = 16;
final int barSlotHeight = 60;
final int h = (int)(barSlotHeight * barHeight);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import zone.moddev.mc.electricadvantage.init.Power;
import cyano.poweradvantage.api.ConduitType;
import net.minecraft.block.BlockState;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundNBT;
Expand All @@ -18,7 +19,6 @@
import zone.moddev.mc.electricadvantage.ElectricAdvantage;

import java.util.Arrays;
import java.util.List;

public abstract class ElectricGeneratorTileEntity extends cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine {

Expand Down Expand Up @@ -46,19 +46,26 @@ public ElectricGeneratorTileEntity(TileEntityType<?> tileType, String name,int n


private boolean redstone = false;
private boolean redstoneInitialized = false;
private float oldEnergy = 0f;
@Override
public void powerUpdate(){
super.powerUpdate();

redstone = getWorld().isBlockPowered(getPos());
redstoneInitialized = true;

if(oldEnergy != this.getEnergy(Power.ELECTRIC_POWER)){
this.sync();
}
}

protected boolean hasRedstoneSignal(){
// Generators can check the interlock before their first power-update refresh.
if(!redstoneInitialized && getWorld() != null && !getWorld().isRemote){
redstone = getWorld().isBlockPowered(getPos());
redstoneInitialized = true;
}
return redstone;
}

Expand All @@ -74,10 +81,14 @@ protected void setActive(boolean active){
protected void playSoundEffect(double x, double y, double z, SoundEvent sound, float volume, float pitch){
if(getWorld().isRemote) return;
final double range = 16;
List<ServerPlayerEntity> players = getWorld().getEntitiesWithinAABB(ServerPlayerEntity.class, new AxisAlignedBB(
AxisAlignedBB bounds = new AxisAlignedBB(
x - range, y - range, z - range,
x + range, y + range, z + range));
for(ServerPlayerEntity player : players){
x + range, y + range, z + range);
// Broadcast to existing players without loading chunks for a spatial entity query.
for(PlayerEntity candidate : getWorld().getPlayers()){
if(!(candidate instanceof ServerPlayerEntity)) continue;
ServerPlayerEntity player = (ServerPlayerEntity)candidate;
if(player.connection == null || player.isSpectator() || !player.getBoundingBox().intersects(bounds)) continue;
player.connection.sendPacket(new SPlaySoundPacket(sound.getRegistryName(), SoundCategory.BLOCKS,
new net.minecraft.util.math.Vec3d(x, y, z), volume, pitch));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import zone.moddev.mc.electricadvantage.init.Power;
import cyano.poweradvantage.api.ConduitType;
import net.minecraft.block.BlockState;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
Expand All @@ -22,7 +23,6 @@
import zone.moddev.mc.electricadvantage.ElectricAdvantage;

import java.util.Arrays;
import java.util.List;

public abstract class ElectricMachineTileEntity extends cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine{

Expand Down Expand Up @@ -71,6 +71,7 @@ public ElectricMachineTileEntity(TileEntityType<?> tileType, String name,int num
}

private boolean redstone = false;
private boolean redstoneInitialized = false;
private float oldEnergy = 0f;
private int[] syncArrayOld = null;
private int[] syncArrayNew = null;
Expand All @@ -79,6 +80,7 @@ public void powerUpdate(){
super.powerUpdate();

redstone = getWorld().isBlockPowered(getPos());
redstoneInitialized = true;

this.setPowerState(this.isPowered());

Expand All @@ -99,6 +101,11 @@ public void powerUpdate(){


protected boolean hasRedstoneSignal(){
// tickUpdate can run before the first scheduled powerUpdate after loading.
if(!redstoneInitialized && getWorld() != null && !getWorld().isRemote){
redstone = getWorld().isBlockPowered(getPos());
redstoneInitialized = true;
}
return redstone;
}

Expand Down Expand Up @@ -343,10 +350,14 @@ public ItemStack insertItemToInputSlots(ItemStack itemStack){
protected void playSoundEffect(double x, double y, double z, SoundEvent sound, float volume, float pitch){
if(getWorld().isRemote) return;
final double range = 16;
List<ServerPlayerEntity> players = getWorld().getEntitiesWithinAABB(ServerPlayerEntity.class, new AxisAlignedBB(
AxisAlignedBB bounds = new AxisAlignedBB(
x - range, y - range, z - range,
x + range, y + range, z + range));
for(ServerPlayerEntity player : players){
x + range, y + range, z + range);
// Broadcast to existing players without loading chunks for a spatial entity query.
for(PlayerEntity candidate : getWorld().getPlayers()){
if(!(candidate instanceof ServerPlayerEntity)) continue;
ServerPlayerEntity player = (ServerPlayerEntity)candidate;
if(player.connection == null || player.isSpectator() || !player.getBoundingBox().intersects(bounds)) continue;
player.connection.sendPacket(new SPlaySoundPacket(sound.getRegistryName(), SoundCategory.BLOCKS,
new net.minecraft.util.math.Vec3d(x, y, z), volume, pitch));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import zone.moddev.mc.poweradvantage.init.Fluids;
import zone.moddev.mc.poweradvantage.util.FluidIdHelper;
import net.minecraft.block.BlockState;
import net.minecraft.block.IBucketPickupHandler;
import net.minecraft.util.SoundEvents;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundNBT;
Expand Down Expand Up @@ -82,12 +83,14 @@ public void tickUpdate(boolean isServerWorld) {
float cost = ENERGY_COST_PUMP+ENERGY_COST_VERTICAL*(getPos().getY()-fluidSource.getY());
// Found a fluid to suck-up!
BlockState blockstate = w.getBlockState(fluidSource);
Fluid f = getFluid(blockstate);
if(f != null && getEnergy() >= cost){
this.getTank().fill(new FluidStack(f, FluidTank.BUCKET_VOLUME), true);
this.subtractEnergy(cost, Power.ELECTRIC_POWER);
w.setBlockState(fluidSource, net.minecraft.block.Blocks.AIR.getDefaultState(), 3);
success = true;
if(blockstate.getBlock() instanceof IBucketPickupHandler && getEnergy() >= cost){
// Native pickup drains waterlogging without deleting the containing block.
Fluid f = ((IBucketPickupHandler)blockstate.getBlock()).pickupFluid(w, fluidSource, blockstate);
if(f != null && f != net.minecraft.fluid.Fluids.EMPTY){
this.getTank().fill(new FluidStack(f, FluidTank.BUCKET_VOLUME), true);
this.subtractEnergy(cost, Power.ELECTRIC_POWER);
success = true;
}
}
}
timeUntilNextPump = PUMP_INTERVAL;
Expand Down Expand Up @@ -129,12 +132,6 @@ protected boolean isValidInputItem(ItemStack item) {
}


private Fluid getFluid(BlockState blockstate) {
return blockstate.getFluidState().isEmpty() ? null : blockstate.getFluidState().getFluid();
}



private BlockPos scan(Set<BlockPos> searchSpace, BlockPos coord, int limit) {
if(isFluidBlock(coord) == false) return null;
do{
Expand All @@ -161,7 +158,8 @@ private BlockPos scan(Set<BlockPos> searchSpace, BlockPos coord, int limit) {
}

private boolean isSourceBlock( BlockPos coord){
return getWorld().getFluidState(coord).isSource();
BlockState state = getWorld().getBlockState(coord);
return state.getBlock() instanceof IBucketPickupHandler && state.getFluidState().isSource();
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public void tickUpdate(boolean isServerWorld) {
if(!this.hasRedstoneSignal()){
if(getInputTank().getFluidAmount() > 0
&& getEnergy() >= electricityPerDistill
&& canDistill(getInputTank().getFluid())){
&& canDistill(getInputTank().getFluid())
&& hasSpaceForDistillation()){
distill();
if(!wasActive){
this.setActiveState(true);
Expand Down Expand Up @@ -93,6 +94,14 @@ private boolean canDistill(FluidStack fs) {
}
}

private boolean hasSpaceForDistillation() {
DistillationRecipe recipe = DistillationRecipeRegistry.getInstance()
.getDistillationRecipeForFluid(getInputTank().getFluid().getFluid());
// applyRecipe consumes its input, so preview a copy before committing either tank.
FluidStack output = recipe.applyRecipe(getInputTank().getFluid().copy(), speed);
return output != null && getOutputTank().fill(output, false) == output.amount;
}

private void distill(){
DistillationRecipe recipe = DistillationRecipeRegistry.getInstance()
.getDistillationRecipeForFluid(getInputTank().getFluid().getFluid());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,10 @@ public PowerRequest getPowerRequest(ConduitType offer) {
float powerWanted = (this.getEnergyCapacity(Power.GROWTHCHAMBER_POWER) - this.getEnergy(Power.GROWTHCHAMBER_POWER));
powerWanted = Math.min(powerWanted, soil / SOIL_PER_UNIT);
powerWanted = Math.min(powerWanted, tank.getFluidAmount() / WATER_PER_UNIT);
return new PowerRequest(PowerRequest.MEDIUM_PRIORITY,ELECTRICITY_PER_UNIT * powerWanted,this);
float stored = getEnergy(Power.ELECTRIC_POWER);
float room = getEnergyCapacity(Power.ELECTRIC_POWER) - stored;
float electricityWanted = Math.max(0, Math.min(room, ELECTRICITY_PER_UNIT * powerWanted - stored));
return new PowerRequest(PowerRequest.MEDIUM_PRIORITY,electricityWanted,this);
} else {
return PowerRequest.REQUEST_NOTHING;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public float getPowerOutput() {
}
}

private final float[] progress = new float[1];

public float[] getProgress() {
progress[0] = isActive() ? 1F : 0F;
return progress;
}

@Override
protected boolean isValidInputItem(ItemStack item) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import zone.moddev.mc.electricadvantage.ElectricAdvantage;
import zone.moddev.mc.electricadvantage.init.Power;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.EntityClassification;
Expand All @@ -18,6 +19,9 @@
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.*;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.util.math.shapes.VoxelShapes;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
Expand Down Expand Up @@ -293,9 +297,15 @@ private Vec3d calculateLaserTerminus(){
return followRayToSolidBlock(getOpticPosition(),dir,ATTACK_RANGE);
}
private Vec3d followRayToSolidBlock(Vec3d origin, Vec3d dir, double maxRange){
Vec3d max = origin.add(dir).add(maxRange * dir.x, maxRange * dir.y, maxRange * dir.z);
Vec3d max = origin.add(maxRange * dir.x, maxRange * dir.y, maxRange * dir.z);
BlockRayTraceResult impact = getWorld().rayTraceBlocks(new RayTraceContext(origin, max,
RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.SOURCE_ONLY, null));
RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.SOURCE_ONLY, null) {
@Override public VoxelShape getBlockShape(BlockState state, IBlockReader world, BlockPos pos) {
// The optic is inside our post; nearby cover must still block the beam.
return pos.equals(LaserTurretTileEntity.this.getPos()) ? VoxelShapes.empty()
: super.getBlockShape(state, world, pos);
}
});
if(impact.getType() == RayTraceResult.Type.BLOCK){
return impact.getHitVec();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ public void powerUpdate(){
}

public static float getLightIntensityAt(BlockPos pos, World w){
// Sample incident light above the solid panel, not inside its body.
BlockPos lightPos = pos.up();
float light;
if(w.canSeeSky(pos) && w.dimension.hasSkyLight()){
if(w.canSeeSky(lightPos) && w.dimension.hasSkyLight()){
// outdoor light imposes no penalty
light = MathHelper.sin((float)w.getDayTime() * 2.61799E-4f); // (worldTime * pi / 12000);
// handle weather
Expand All @@ -103,7 +105,7 @@ public static float getLightIntensityAt(BlockPos pos, World w){
light *= 0.35f;
}
} else {
float blockLight = w.getLight(pos) * 6.66666E-2f; // block light / 15
float blockLight = w.getLight(lightPos) * 6.66666E-2f; // block light / 15
light = BLOCKLIGHT_FACTOR * blockLight;
}
return light;
Expand Down
Loading
Loading