Skip to content

Commit

Permalink
Improve structure error feedback
Browse files Browse the repository at this point in the history
This adds a mechanism for easily adding validation error messages for
unique structure requirements. Many multis have specific requirements,
but these requirements aren't communicated anywhere and there's no
feedback for when the requirements aren't met. This PR allows multis to
add error messages easily.

For now, it's only implemented on the cryo freezer and gt++ multis, but
if this is accepted other multis can add their own validations easily.
  • Loading branch information
RecursivePineapple committed Feb 3, 2025
1 parent e97c0d0 commit 37023cd
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 22 deletions.
14 changes: 14 additions & 0 deletions src/main/java/gregtech/api/enums/StructureError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gregtech.api.enums;

/**
* Used as a bit set for {@link gregtech.api.metatileentity.implementations.MTEMultiBlockBase#mStructureStatus}.
* You can reorder these as needed.
*/
public enum StructureError {
MISSING_MAINTENANCE,
MISSING_MUFFLER,
UNNEEDED_MUFFLER,
TOO_FEW_CASINGS,
MISSING_CRYO_HATCH,
TOO_MANY_CRYO_HATCHES;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -65,6 +67,7 @@
import cpw.mods.fml.relauncher.SideOnly;
import gregtech.GTMod;
import gregtech.api.enums.SoundResource;
import gregtech.api.enums.StructureError;
import gregtech.api.enums.VoidingMode;
import gregtech.api.gui.modularui.GTUIInfos;
import gregtech.api.gui.modularui.GTUITextures;
Expand Down Expand Up @@ -170,6 +173,9 @@ public abstract class MTEMultiBlockBase extends MetaTileEntity
private static final int CHECK_INTERVAL = 100; // How often should we check for a new recipe on an idle machine?
private final int randomTickOffset = (int) (Math.random() * CHECK_INTERVAL + 1);

/** A list of unparameterized structure errors. */
protected EnumSet<StructureError> mStructureStatus = EnumSet.noneOf(StructureError.class);

protected static final byte INTERRUPT_SOUND_INDEX = 8;
protected static final byte PROCESS_START_SOUND_INDEX = 1;

Expand Down Expand Up @@ -437,6 +443,7 @@ public void clearHatches() {
mMaintenanceHatches.clear();
mDualInputHatches.clear();
mSmartInputHatches.clear();
mStructureStatus = EnumSet.noneOf(StructureError.class);
}

public boolean checkStructure(boolean aForceReset) {
Expand All @@ -449,11 +456,40 @@ public boolean checkStructure(boolean aForceReset, IGregTechTileEntity aBaseMeta
if ((mStructureChanged || aForceReset)) {
clearHatches();
mMachine = checkMachine(aBaseMetaTileEntity, mInventory[1]);
// intentionally not predicated on checkMachine
validateStructure();
if (!mStructureStatus.isEmpty()) mMachine = false;
}
mStructureChanged = false;
return mMachine;
}

/**
* Validates this multi's structure (hatch/casing counts mainly) for any errors.
* Runs regardless of whether the structure is complete. Check or update {@link #mMachine} as needed.
* Should update {@link #mStructureStatus} as needed.
*/
protected void validateStructure() {

}

/**
* Scans {@link #mStructureStatus} or other fields as needed and emits localized structure error messages.
* mStructureStatus is synced already, but any newly introduced fields need to be synced manually.
*/
@SideOnly(Side.CLIENT)
protected void getStructureErrors(ArrayList<String> lines) {

}

/**
* Controls whether the error message widget is shown. If you have any new structure status fields, make sure to
* check them here.
*/
protected boolean hasStructureErrors() {
return !mStructureStatus.isEmpty();
}

@Override
public void onPostTick(IGregTechTileEntity aBaseMetaTileEntity, long aTick) {
if (aBaseMetaTileEntity.isServerSide()) {
Expand Down Expand Up @@ -2653,6 +2689,50 @@ protected String generateCurrentRecipeInfoString() {
protected void drawTexts(DynamicPositionedColumn screenElements, SlotWidget inventorySlot) {
screenElements.setSynced(false)
.setSpace(0);

screenElements.widget(
new FakeSyncWidget<EnumSet<StructureError>>(
() -> mStructureStatus,
status -> mStructureStatus = status,
(packetBuffer, structureErrors) -> {
BitSet bits = new BitSet();

for (StructureError error : structureErrors) {
bits.set(error.ordinal());
}

byte[] data = bits.toByteArray();

packetBuffer.writeVarIntToBuffer(data.length);
packetBuffer.writeBytes(data);
},
packetBuffer -> {
byte[] data = new byte[packetBuffer.readVarIntFromBuffer()];
packetBuffer.readBytes(data);

BitSet bits = BitSet.valueOf(data);

EnumSet<StructureError> out = EnumSet.noneOf(StructureError.class);

for (StructureError error : StructureError.values()) {
if (bits.get(error.ordinal())) {
out.add(error);
}
}

return out;
}));

screenElements.widgets(TextWidget.dynamicString(() -> {
ArrayList<String> lines = new ArrayList<>();
getStructureErrors(lines);
return String.join("\n", lines);
})
.setSynced(false)
.setTextAlignment(Alignment.CenterLeft)
.setDefaultColor(EnumChatFormatting.DARK_RED)
.setEnabled(w -> hasStructureErrors()));

if (supportsMachineModeSwitch()) {
screenElements.widget(
TextWidget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import javax.annotation.Nullable;

import net.minecraft.block.Block;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
Expand All @@ -43,8 +44,11 @@
import com.gtnewhorizons.modularui.common.widget.FakeSyncWidget;
import com.gtnewhorizons.modularui.common.widget.TextWidget;

import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import gregtech.api.enums.GTValues;
import gregtech.api.enums.Materials;
import gregtech.api.enums.StructureError;
import gregtech.api.enums.Textures;
import gregtech.api.enums.VoidingMode;
import gregtech.api.gui.modularui.GTUITextures;
Expand Down Expand Up @@ -490,8 +494,42 @@ public void updateSlots() {
super.updateSlots();
}

@SideOnly(Side.CLIENT)
protected void getStructureErrors(ArrayList<String> lines) {
super.getStructureErrors(lines);

if (mStructureStatus.contains(StructureError.MISSING_MAINTENANCE)) {
lines.add(I18n.format("GT5U.gui.text.no_maintenance"));
}

if (mStructureStatus.contains(StructureError.MISSING_MUFFLER)) {
lines.add(I18n.format("GT5U.gui.text.no_muffler"));
}

if (mStructureStatus.contains(StructureError.UNNEEDED_MUFFLER)) {
lines.add(I18n.format("GT5U.gui.text.unneeded_muffler"));
}
}

@Override
protected void validateStructure() {
super.validateStructure();

if (mMaintenanceHatches.isEmpty()) {
mStructureStatus.add(StructureError.MISSING_MAINTENANCE);
}

if (this.getPollutionPerSecond(null) > 0 && mMufflerHatches.isEmpty()) {
mStructureStatus.add(StructureError.MISSING_MUFFLER);
}

if (this.getPollutionPerSecond(null) == 0 && !mMufflerHatches.isEmpty()) {
mStructureStatus.add(StructureError.UNNEEDED_MUFFLER);
}
}

public boolean checkHatch() {
return mMaintenanceHatches.size() <= 1 && (this.getPollutionPerSecond(null) <= 0 || !mMufflerHatches.isEmpty());
return true;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,24 @@
import java.util.ArrayList;
import java.util.Objects;

import net.minecraft.client.resources.I18n;
import net.minecraft.item.ItemStack;
import net.minecraftforge.fluids.FluidStack;

import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable;
import com.gtnewhorizon.structurelib.structure.IStructureDefinition;
import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
import com.gtnewhorizon.structurelib.structure.StructureDefinition;
import com.gtnewhorizons.modularui.common.widget.DynamicPositionedColumn;
import com.gtnewhorizons.modularui.common.widget.FakeSyncWidget;
import com.gtnewhorizons.modularui.common.widget.SlotWidget;

import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import gregtech.api.GregTechAPI;
import gregtech.api.enums.MetaTileEntityIDs;
import gregtech.api.enums.SoundResource;
import gregtech.api.enums.StructureError;
import gregtech.api.enums.TAE;
import gregtech.api.interfaces.IIconContainer;
import gregtech.api.interfaces.metatileentity.IMetaTileEntity;
Expand All @@ -40,6 +47,7 @@
import gtPlusPlus.api.recipe.GTPPRecipeMaps;
import gtPlusPlus.core.block.ModBlocks;
import gtPlusPlus.core.util.minecraft.FluidUtils;
import gtPlusPlus.xmod.gregtech.api.enums.GregtechItemList;
import gtPlusPlus.xmod.gregtech.api.metatileentity.implementations.base.GTPPMultiBlockBase;
import gtPlusPlus.xmod.gregtech.api.metatileentity.implementations.base.MTEHatchCustomFluidBase;
import gtPlusPlus.xmod.gregtech.common.blocks.textures.TexturesGtBlock;
Expand All @@ -48,30 +56,35 @@ public class MTEIndustrialVacuumFreezer extends GTPPMultiBlockBase<MTEIndustrial
implements ISurvivalConstructable {

public static int CASING_TEXTURE_ID;
public static String mCryoFuelName = "Gelid Cryotheum";
public static String mCasingName = "Advanced Cryogenic Casing";
public static String mHatchName = "Cryotheum Hatch";
public static FluidStack mFuelStack;
private int mCasing;
public static String CASING_NAME;
public static String HATCH_NAME;
public static FluidStack CRYO_STACK;
private static IStructureDefinition<MTEIndustrialVacuumFreezer> STRUCTURE_DEFINITION = null;

private int mCasing;

private final ArrayList<MTEHatchCustomFluidBase> mCryotheumHatches = new ArrayList<>();

public MTEIndustrialVacuumFreezer(final int aID, final String aName, final String aNameRegional) {
super(aID, aName, aNameRegional);
mFuelStack = FluidUtils.getFluidStack("cryotheum", 1);
CASING_TEXTURE_ID = TAE.getIndexFromPage(2, 10);

GregTechAPI.sAfterGTLoad.add(() -> {
CRYO_STACK = FluidUtils.getFluidStack("cryotheum", 1);
CASING_NAME = GregtechItemList.Casing_AdvancedVacuum.get(1)
.getDisplayName();
HATCH_NAME = GregtechItemList.Hatch_Input_Cryotheum.get(1)
.getDisplayName();
});
}

public MTEIndustrialVacuumFreezer(final String aName) {
super(aName);
mFuelStack = FluidUtils.getFluidStack("cryotheum", 1);
CASING_TEXTURE_ID = TAE.getIndexFromPage(2, 10);
protected MTEIndustrialVacuumFreezer(MTEIndustrialVacuumFreezer prototype) {
super(prototype.mName);
}

@Override
public IMetaTileEntity newMetaEntity(final IGregTechTileEntity aTileEntity) {
return new MTEIndustrialVacuumFreezer(this.mName);
return new MTEIndustrialVacuumFreezer(this);
}

@Override
Expand All @@ -85,20 +98,20 @@ protected MultiblockTooltipBuilder createTooltip() {
tt.addMachineType(getMachineType())
.addInfo("Factory Grade Advanced Vacuum Freezer")
.addInfo("Speed: +100% | EU Usage: 100% | Parallel: 4")
.addInfo("Consumes 10L of " + mCryoFuelName + "/s during operation")
.addInfo("Consumes 10L of " + CRYO_STACK.getLocalizedName() + "/s during operation")
.addInfo("Constructed exactly the same as a normal Vacuum Freezer")
.addPollutionAmount(getPollutionPerSecond(null))
.beginStructureBlock(3, 3, 3, true)
.addController("Front Center")
.addCasingInfoMin(mCasingName, 10, false)
.addCasingInfoMin(CASING_NAME, 10, false)
.addInputBus("Any Casing", 1)
.addOutputBus("Any Casing", 1)
.addInputHatch("Any Casing", 1)
.addOutputHatch("Any Casing", 1)
.addEnergyHatch("Any Casing", 1)
.addMufflerHatch("Any Casing", 1)
.addMaintenanceHatch("Any Casing", 1)
.addOtherStructurePart(mHatchName, "Any Casing", 1)
.addOtherStructurePart(HATCH_NAME, "Any Casing", 1)
.toolTipFinisher();
return tt;
}
Expand All @@ -116,8 +129,7 @@ public IStructureDefinition<MTEIndustrialVacuumFreezer> getStructureDefinition()
ofChain(
buildHatchAdder(MTEIndustrialVacuumFreezer.class)
.adder(MTEIndustrialVacuumFreezer::addCryotheumHatch)
.hatchId(967)
.shouldReject(t -> !t.mCryotheumHatches.isEmpty())
.hatchId(MetaTileEntityIDs.Hatch_Input_Cryotheum.ID)
.casingIndex(CASING_TEXTURE_ID)
.dot(1)
.build(),
Expand All @@ -144,15 +156,57 @@ public int survivalConstruct(ItemStack stackSize, int elementBudget, ISurvivalBu
}

@Override
public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
public void clearHatches() {
super.clearHatches();
mCasing = 0;
mCryotheumHatches.clear();
return checkPiece(mName, 1, 1, 0) && mCasing >= 10 && checkHatch();
}

@Override
public boolean checkHatch() {
return super.checkHatch() && !mCryotheumHatches.isEmpty();
public boolean checkMachine(IGregTechTileEntity aBaseMetaTileEntity, ItemStack aStack) {
return checkPiece(mName, 1, 1, 0);
}

@Override
public void validateStructure() {
super.validateStructure();

if (mCasing < 10) {
mStructureStatus.add(StructureError.TOO_FEW_CASINGS);
}

if (mCryotheumHatches.isEmpty()) {
mStructureStatus.add(StructureError.MISSING_CRYO_HATCH);
}

if (mCryotheumHatches.size() > 1) {
mStructureStatus.add(StructureError.TOO_MANY_CRYO_HATCHES);
}
}

@Override
protected void drawTexts(DynamicPositionedColumn screenElements, SlotWidget inventorySlot) {
super.drawTexts(screenElements, inventorySlot);

screenElements.widgets(new FakeSyncWidget.IntegerSyncer(() -> mCasing, casings -> mCasing = casings));
}

@Override
@SideOnly(Side.CLIENT)
protected void getStructureErrors(ArrayList<String> lines) {
super.getStructureErrors(lines);

if (mStructureStatus.contains(StructureError.TOO_FEW_CASINGS)) {
lines.add(I18n.format("GT5U.gui.missing_casings", 10, mCasing));
}

if (mStructureStatus.contains(StructureError.MISSING_CRYO_HATCH)) {
lines.add(I18n.format("GT5U.gui.missing_hatch", HATCH_NAME));
}

if (mStructureStatus.contains(StructureError.TOO_MANY_CRYO_HATCHES)) {
lines.add(I18n.format("GT5U.gui.too_many_hatches", HATCH_NAME, 1));
}
}

private boolean addCryotheumHatch(IGregTechTileEntity aTileEntity, int aBaseCasingIndex) {
Expand All @@ -161,7 +215,7 @@ private boolean addCryotheumHatch(IGregTechTileEntity aTileEntity, int aBaseCasi
} else {
IMetaTileEntity aMetaTileEntity = aTileEntity.getMetaTileEntity();
if (aMetaTileEntity instanceof MTEHatchCustomFluidBase && aMetaTileEntity.getBaseMetaTileEntity()
.getMetaTileID() == 967) {
.getMetaTileID() == MetaTileEntityIDs.Hatch_Input_Cryotheum.ID) {
return addToMachineListInternal(mCryotheumHatches, aTileEntity, aBaseCasingIndex);
}
}
Expand Down
Loading

0 comments on commit 37023cd

Please sign in to comment.