filename:
common/src/main/java/rearth/oritech/block/entity/generators/SteamEngineEntity.java
branch:
1.21
back to repo
package rearth.oritech.block.entity.generators;
import dev.architectury.fluid.FluidStack;
import net.minecraft.block.BlockState;
import net.minecraft.fluid.Fluid;
import net.minecraft.fluid.Fluids;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.Vec3i;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.Oritech;
import rearth.oritech.api.fluid.FluidApi;
import rearth.oritech.block.base.entity.FluidMultiblockGeneratorBlockEntity;
import rearth.oritech.block.base.entity.MachineBlockEntity;
import rearth.oritech.block.base.entity.MultiblockGeneratorBlockEntity;
import rearth.oritech.client.init.ModScreens;
import rearth.oritech.client.init.ParticleContent;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.init.FluidContent;
import rearth.oritech.init.recipes.OritechRecipe;
import rearth.oritech.init.recipes.OritechRecipeType;
import rearth.oritech.init.recipes.RecipeContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.Geometry;
import rearth.oritech.util.InventorySlotAssignment;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
// progress is abused to sync active speed.
public class SteamEngineEntity extends MultiblockGeneratorBlockEntity implements FluidApi.BlockProvider {
private static final int MAX_SPEED = 10;
private static final int MAX_CHAIN_SIZE = 20;
private static final float WATER_RATIO = 0.9f;
// how chaining works:
// (non-chained non-empty) generator checks neighbors in both sides in N dist
// generator marks neighbors as chained, with timestamp
// slaved generator shows only chain notice in popup, moves all inserted steam to master (get api returns master entries)
// master processes at X rate, shows chained count in UI
// progress is used to store/sync animation speed
public long masterHeartbeat; // set from master, used by slave
public SteamEngineEntity master;
private final Set<SteamEngineEntity> slaves = new HashSet<>();
// client only
public NetworkContent.SteamEngineSyncPacket clientStats;
public SteamEngineEntity(BlockPos pos, BlockState state) {
super(BlockEntitiesContent.STEAM_ENGINE_ENTITY, pos, state, Oritech.CONFIG.generators.steamEngineData.steamToRfRatio());
}
@Override
public void tick(World world, BlockPos pos, BlockState state, MachineBlockEntity blockEntity) {
if (world.isClient || !isActive(state)) return;
var slaved = inSlaveMode();
var hasInput = !boilerStorage.getInStack().isEmpty();
if (world.getTime() % 80 == 0 && !slaved && hasInput)
setupMaster();
if (!slaved && hasInput) tickMaster();
if (slaved) tickSlave();
outputEnergy();
if (networkDirty)
updateNetwork();
}
// this is only called when steam is available
private void tickMaster() {
var steamTank = boilerStorage.getInputContainer();
var waterTank = boilerStorage.getOutputContainer();
// optional config stops (energy full / water full)
if (energyStorage.getAmount() >= energyStorage.getCapacity() && Oritech.CONFIG.generators.steamEngineData.stopOnEnergyFull())
return;
if (waterTank.getStack().getAmount() >= waterTank.getCapacity() && Oritech.CONFIG.generators.steamEngineData.stopOnWaterFull())
return;
// if not recipe is currently set, or it does not match the steam tank, search for a recipe
if (currentRecipe == OritechRecipe.DUMMY || !currentRecipe.getFluidInput().isFluidEqual(steamTank.getStack())) {
var candidate = FluidMultiblockGeneratorBlockEntity.getRecipe(steamTank, world, getOwnRecipeType());
candidate.ifPresent(recipe -> currentRecipe = recipe.value());
if (candidate.isEmpty()) return;
currentRecipe = candidate.get().value();
}
var speed = getSteamProcessingSpeed(steamTank);
var workerCount = slaves.size() + 1;
var consumedCount = currentRecipe.getFluidInput().getAmount() * speed * workerCount;
var producedCount = consumedCount * WATER_RATIO;
// update tanks
steamTank.extract(currentRecipe.getFluidInput().copyWithAmount((long) consumedCount), false);
waterTank.insert(FluidStack.create(Fluids.WATER, (long) producedCount), false);
// produce energy
var energyEfficiency = getSteamEnergyEfficiency(speed);
var energyProduced = consumedCount * energyEfficiency * energyPerTick;
energyStorage.insertIgnoringLimit((long) energyProduced, false);
spawnParticles();
lastWorkedAt = world.getTime();
// used for animation speed
progress = (int) (speed * 100f);
// order/data: speed, efficiency, rf produced, steam consumed, slave count
clientStats = new NetworkContent.SteamEngineSyncPacket(pos, speed, energyEfficiency, (long) energyProduced, (long) consumedCount, slaves.size());
this.markDirty();
}
private void tickSlave() {
// check if master is actually working
var masterStats = master.clientStats;
var wasWorking = master.isActivelyWorking();
var speed = masterStats.speed();
if (wasWorking) {
spawnParticles();
this.lastWorkedAt = world.getTime();
this.markDirty();
}
// used for animation speed
progress = (int) (speed * 100f);
}
private void setupMaster() {
slaves.clear();
for (int direction = -1; direction <= 1; direction++) {
if (direction == 0) continue;
for (int i = 1; i <= MAX_CHAIN_SIZE; i++) {
var checkPos = new BlockPos(Geometry.offsetToWorldPosition(getFacing(), new Vec3i(i * direction, 0, 0), pos));
var coreCandidate = world.getBlockEntity(checkPos, BlockEntitiesContent.MACHINE_CORE_ENTITY);
if (coreCandidate.isPresent() && coreCandidate.get().getCachedController() != null)
checkPos = coreCandidate.get().getControllerPos();
var candidate = world.getBlockEntity(checkPos, BlockEntitiesContent.STEAM_ENGINE_ENTITY);
if (candidate.isEmpty() || !candidate.get().isActive(candidate.get().getCachedState())) {
break;
} else if (!candidate.get().boilerStorage.getInStack().isEmpty()) {
break;
} else {
var slave = candidate.get();
slaves.add(slave);
slave.masterHeartbeat = world.getTime();
slave.master = this;
}
}
}
}
public boolean inSlaveMode() {
var heartbeatAge = world.getTime() - masterHeartbeat;
return heartbeatAge <= 100 && master != null && !master.isRemoved();
}
@Override
public boolean boilerAcceptsInput(Fluid fluid) {
return fluid.equals(FluidContent.STILL_STEAM.get());
}
private void spawnParticles() {
if (world.random.nextFloat() > 0.5) return;
// emit particles
var facing = getFacing();
var offsetLocal = Geometry.rotatePosition(new Vec3d(0, 0, -0.5), facing);
var emitPosition = Vec3d.ofCenter(pos).add(offsetLocal);
ParticleContent.STEAM_ENGINE_WORKING.spawn(world, emitPosition, 1);
}
private float getSteamEnergyEfficiency(float x) {
// basically a curve that goes through 0:0.5, 7:1 and 10:0.2
return (float) (0.5f - 0.1966667f * x + 0.09166667f * Math.pow(x, 2) - 0.0075f * Math.pow(x, 3)) + 0.4f;
}
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
}
private float getSteamProcessingSpeed(FluidApi.SingleSlotStorage usedTank) {
var fillPercentage = usedTank.getStack().getAmount() / (float) usedTank.getCapacity();
return fillPercentage * MAX_SPEED;
}
@Override
protected float getAnimationSpeed() {
if (progress == 0) return 1;
return (float) progress / 100f;
}
@Override
public BarConfiguration getFluidConfiguration() {
return new BarConfiguration(149, 10, 18, 64);
}
@Override
protected void sendNetworkEntry() {
super.sendNetworkEntry();
NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.GeneratorSteamSyncPacket(pos, boilerStorage.getInStack().getAmount(), boilerStorage.getOutStack().getAmount()));
if (clientStats != null) NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(clientStats);
}
@Override
protected OritechRecipeType getOwnRecipeType() {
return RecipeContent.STEAM_ENGINE;
}
@Override
public InventorySlotAssignment getSlotAssignments() {
return new InventorySlotAssignment(0, 0, 0, 0);
}
@Override
public List<GuiSlot> getGuiSlots() {
return List.of();
}
@Override
public ScreenHandlerType<?> getScreenHandlerType() {
return ModScreens.STEAM_ENGINE_SCREEN;
}
@Override
public int getInventorySize() {
return 0;
}
@Override
public long getDefaultCapacity() {
return Oritech.CONFIG.generators.steamEngineData.energyCapacity();
}
@Override
public long getDefaultExtractionRate() {
return Oritech.CONFIG.generators.steamEngineData.maxEnergyExtraction();
}
@Override
protected Set<Pair<BlockPos, Direction>> getOutputTargets(BlockPos pos, World world) {
var res = new HashSet<Pair<BlockPos, Direction>>();
var facing = getFacingForAddon();
var posA = new Vec3i(0, 0, 1); // front
var posB = new Vec3i(-1, 0, 0); // right
var posC = new Vec3i(1, 0, 0); // left
var posD = new Vec3i(-1, 0, -1); // back left
var posE = new Vec3i(1, 0, -1); // back right
var posF = new Vec3i(0, 0, -2); // back
var worldPosA = (BlockPos) Geometry.offsetToWorldPosition(facing, posA, pos);
var worldPosB = (BlockPos) Geometry.offsetToWorldPosition(facing, posB, pos);
var worldPosC = (BlockPos) Geometry.offsetToWorldPosition(facing, posC, pos);
var worldPosD = (BlockPos) Geometry.offsetToWorldPosition(facing, posD, pos);
var worldPosE = (BlockPos) Geometry.offsetToWorldPosition(facing, posE, pos);
var worldPosF = (BlockPos) Geometry.offsetToWorldPosition(facing, posF, pos);
res.add(new Pair<>(worldPosA, Geometry.fromVector(Geometry.getForward(facing))));
res.add(new Pair<>(worldPosB, Geometry.fromVector(Geometry.getLeft(facing))));
res.add(new Pair<>(worldPosC, Geometry.fromVector(Geometry.getRight(facing))));
res.add(new Pair<>(worldPosD, Geometry.fromVector(Geometry.getLeft(facing))));
res.add(new Pair<>(worldPosE, Geometry.fromVector(Geometry.getRight(facing))));
res.add(new Pair<>(worldPosF, Geometry.fromVector(Geometry.getBackward(facing))));
return res;
}
@Override
public List<Vec3i> getAddonSlots() {
return List.of();
}
@Override
public List<Vec3i> getCorePositions() {
return List.of(
new Vec3i(0, 1, 0),
new Vec3i(0, 0, -1),
new Vec3i(0, 1, -1)
);
}
@Override
public boolean showProgress() {
return false;
}
@Override
public FluidApi.FluidStorage getFluidStorage(@Nullable Direction direction) {
if (inSlaveMode()) return master.boilerStorage.getStorageForDirection(direction);
return boilerStorage.getStorageForDirection(direction);
}
}