filename:
common/src/main/java/rearth/oritech/util/MachineAddonController.java
branch:
1.21
back to repo
package rearth.oritech.util;
import io.wispforest.endec.Endec;
import io.wispforest.endec.impl.StructEndecBuilder;
import io.wispforest.owo.serialization.endec.MinecraftEndecs;
import net.minecraft.block.BlockState;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtList;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3i;
import net.minecraft.world.World;
import rearth.oritech.api.energy.containers.DynamicEnergyStorage;
import rearth.oritech.api.item.ItemApi;
import rearth.oritech.block.blocks.addons.MachineAddonBlock;
import rearth.oritech.block.entity.addons.AddonBlockEntity;
import java.util.*;
public interface MachineAddonController {
// list of where actually connected addons are
List<BlockPos> getConnectedAddons();
// a list of where addons could be placed
List<BlockPos> getOpenAddonSlots();
BlockPos getPosForAddon();
World getWorldForAddon();
Direction getFacingForAddon();
DynamicEnergyStorage getStorageForAddon();
ItemApi.InventoryStorage getInventoryForAddon();
ScreenProvider getScreenProvider();
List<Vec3i> getAddonSlots();
BaseAddonData getBaseAddonData();
void setBaseAddonData(BaseAddonData data);
long getDefaultCapacity();
long getDefaultInsertRate();
default float getCoreQuality() {
return 1f;
}
// to initialize everything, should be called when right-clicked
default void initAddons(BlockPos brokenAddon) {
var foundAddons = getAllAddons(brokenAddon);
gatherAddonStats(foundAddons);
writeAddons(foundAddons);
updateEnergyContainer();
removeOldAddons(foundAddons);
getConnectedAddons().clear();
updateEnergyContainer();
for (var addon : foundAddons) {
getConnectedAddons().add(addon.pos());
}
}
private void removeOldAddons(List<AddonBlock> foundAddons) {
// remove/reset all old addons that are not connected anymore
for (var addon : getConnectedAddons()) {
if (foundAddons.stream().noneMatch(newAddon -> newAddon.pos().equals(addon))) {
var state = Objects.requireNonNull(getWorldForAddon()).getBlockState(addon);
if (state.getBlock() instanceof MachineAddonBlock) {
getWorldForAddon().setBlockState(addon, state.with(MachineAddonBlock.ADDON_USED, false));
getWorldForAddon().updateNeighborsAlways(addon, state.getBlock());
}
}
}
}
default void initAddons() {
initAddons(null);
}
// to be called if controller or one of the addons has been broken
default void resetAddons() {
for (var addon : getConnectedAddons()) {
var state = Objects.requireNonNull(getWorldForAddon()).getBlockState(addon);
if (state.getBlock() instanceof MachineAddonBlock) {
getWorldForAddon().setBlockState(addon, state.with(MachineAddonBlock.ADDON_USED, false));
getWorldForAddon().updateNeighborsAlways(addon, state.getBlock());
}
}
getConnectedAddons().clear();
updateEnergyContainer();
}
// addon loading algorithm, called during init
default List<AddonBlock> getAllAddons(BlockPos brokenAddon) {
var maxIterationCount = (int) getCoreQuality() + 1;
// start with base slots (on machine itself)
// repeat N times (dependent on core quality?):
// go through all slots
// check if slot is occupied by MachineAddonBlock, check if block is not used
// if valid and extender: add all neighboring positions to search set
var world = getWorldForAddon();
var pos = getPosForAddon();
assert world != null;
var openSlots = getOpenAddonSlots();
openSlots.clear();
var baseSlots = getAddonSlots(); // available addon slots on machine itself (includes multiblocks)
var searchedPositions = new HashSet<BlockPos>(baseSlots.size()); // all positions ever checked, to avoid adding duplicates
var queuedPositions = new ArrayList<BlockPos>(baseSlots.size());
var result = new ArrayList<AddonBlock>(baseSlots.size()); // results, unused addon blocks
// fill initial spots
for (var initialSpot : baseSlots) {
queuedPositions.add((BlockPos) Geometry.offsetToWorldPosition(getFacingForAddon(), initialSpot, pos));
}
// to allow loops where we modify the content
var toAdd = new HashSet<BlockPos>();
var toRemove = new HashSet<BlockPos>();
//everything done in world space
for (int i = 0; i < maxIterationCount; i++) {
if (queuedPositions.isEmpty()) break;
for (var candidatePos : queuedPositions) {
if (searchedPositions.contains(candidatePos)) continue;
searchedPositions.add(candidatePos);
toRemove.add(candidatePos);
var candidate = world.getBlockState(candidatePos);
var candidateEntity = world.getBlockEntity(candidatePos);
// if the candidate is the broken addon, skip it
if (candidatePos.equals(brokenAddon)) {
openSlots.add(candidatePos);
continue;
}
// if the candidate is not an addon
if (!(candidate.getBlock() instanceof MachineAddonBlock addonBlock) || !(candidateEntity instanceof AddonBlockEntity candidateAddonEntity)) {
// if the block is not part of the machine itself
if (!candidatePos.equals(pos))
openSlots.add(candidatePos);
continue;
}
// if the candidate is in use with another controller
if (candidate.get(MachineAddonBlock.ADDON_USED) && !candidateAddonEntity.getControllerPos().equals(pos)) {
openSlots.add(candidatePos);
continue;
}
var entry = new AddonBlock(addonBlock, candidate, candidatePos, candidateAddonEntity);
result.add(entry);
if (addonBlock.getAddonSettings().extender()) {
var neighbors = getNeighbors(candidatePos);
for (var neighbor : neighbors) {
if (!searchedPositions.contains(neighbor)) toAdd.add(neighbor);
}
}
}
queuedPositions.addAll(toAdd);
queuedPositions.removeAll(toRemove);
toAdd.clear();
toRemove.clear();
}
return result;
}
// can be overridden to allow custom addon loading (e.g. custom stat, or check for specific addon existence)
default void gatherAddonStats(List<AddonBlock> addons) {
var speed = 1f;
var efficiency = 1f;
var energyAmount = 0L;
var energyInsert = 0L;
var extraChambers = 0;
for (var addon : addons) {
var addonSettings = addon.addonBlock().getAddonSettings();
speed *= addonSettings.speedMultiplier();
efficiency *= addonSettings.efficiencyMultiplier();
energyAmount += addonSettings.addedCapacity();
energyInsert += addonSettings.addedInsert();
extraChambers += addonSettings.chamberCount();
getAdditionalStatFromAddon(addon);
}
var baseData = new BaseAddonData(speed, efficiency, energyAmount, energyInsert, extraChambers);
setBaseAddonData(baseData);
}
// used to check for specific addons, or do something if a specific addon has been found
default void getAdditionalStatFromAddon(AddonBlock addonBlock) {
}
// update state of the found addons
default void writeAddons(List<AddonBlock> addons) {
var world = getWorldForAddon();
var pos = getPosForAddon();
assert world != null;
for (var addon : addons) {
var newState = addon.state()
.with(MachineAddonBlock.ADDON_USED, true);
// Set controller before setting block state, otherwise the addon will think
// it's not connected to a machine the first time neighbor blocks are being updated.
addon.addonEntity().setControllerPos(pos);
world.setBlockState(addon.pos(), newState);
}
}
// part of init/break, updates the energy container size
default void updateEnergyContainer() {
var energyStorage = getStorageForAddon();
var addonData = getBaseAddonData();
energyStorage.capacity = getDefaultCapacity() + addonData.energyBonusCapacity;
energyStorage.maxInsert = getDefaultInsertRate() + addonData.energyBonusTransfer;
energyStorage.amount = Math.min(energyStorage.amount, energyStorage.capacity);
}
default void writeAddonToNbt(NbtCompound nbt) {
var data = getBaseAddonData();
nbt.putFloat("speed", data.speed);
nbt.putFloat("efficiency", data.efficiency);
nbt.putLong("energyBonusCapacity", data.energyBonusCapacity);
nbt.putLong("energyBonusTransfer", data.energyBonusTransfer);
nbt.putInt("extraChambers", data.extraChambers);
var posList = new NbtList();
for (var pos : getConnectedAddons()) {
var posTag = new NbtCompound();
posTag.putInt("x", pos.getX());
posTag.putInt("y", pos.getY());
posTag.putInt("z", pos.getZ());
posList.add(posTag);
}
nbt.put("connectedAddons", posList);
}
default void loadAddonNbtData(NbtCompound nbt) {
var data = new BaseAddonData(nbt.getFloat("speed"), nbt.getFloat("efficiency"), nbt.getLong("energyBonusCapacity"), nbt.getLong("energyBonusTransfer"), nbt.getInt("extraChambers"));
setBaseAddonData(data);
var posList = nbt.getList("connectedAddons", NbtElement.COMPOUND_TYPE);
var connectedAddons = getConnectedAddons();
for (var posTag : posList) {
var posCompound = (NbtCompound) posTag;
var x = posCompound.getInt("x");
var y = posCompound.getInt("y");
var z = posCompound.getInt("z");
var pos = new BlockPos(x, y, z);
connectedAddons.add(pos);
}
}
default AddonUiData getUiData() {
var data = getBaseAddonData();
return new AddonUiData(getConnectedAddons(), getOpenAddonSlots(), data.efficiency, data.speed, getPosForAddon(), data.extraChambers);
}
private static Set<BlockPos> getNeighbors(BlockPos pos) {
return Set.of(
pos.add(-1, 0, 0),
pos.add(1, 0, 0),
pos.add(0, 0, -1),
pos.add(0, 0, 1),
pos.add(0, -1, 0),
pos.add(0, 1, 0)
);
}
record AddonBlock(MachineAddonBlock addonBlock, BlockState state, BlockPos pos, AddonBlockEntity addonEntity) {
}
record BaseAddonData(float speed, float efficiency, long energyBonusCapacity, long energyBonusTransfer, int extraChambers) {
}
record AddonUiData(List<BlockPos> positions, List<BlockPos> openSlots, float efficiency, float speed,
BlockPos ownPosition, int extraChambers) {
}
BaseAddonData DEFAULT_ADDON_DATA = new BaseAddonData(1, 1, 0, 0, 0);
Endec<AddonUiData> ADDON_UI_ENDEC = StructEndecBuilder.of(
MinecraftEndecs.BLOCK_POS.listOf().fieldOf("addon_positions", AddonUiData::positions),
MinecraftEndecs.BLOCK_POS.listOf().fieldOf("open_slots", AddonUiData::openSlots),
Endec.FLOAT.fieldOf("efficiency", AddonUiData::efficiency),
Endec.FLOAT.fieldOf("speed", AddonUiData::speed),
MinecraftEndecs.BLOCK_POS.fieldOf("ownPosition", AddonUiData::ownPosition),
Endec.INT.fieldOf("extra_chambers", AddonUiData::extraChambers),
AddonUiData::new
);
}