From b31052879e7601dbfea4c7fb53b94119c7bbf5c6 Mon Sep 17 00:00:00 2001 From: querns <33518699+querns@users.noreply.github.com> Date: Wed, 13 Sep 2023 10:03:17 -0500 Subject: Adds Metrics Transmitter Cover and Advanced GT Sensor Card for Information Panels (#2289) * Adds several UI elements to multiblock drills * Adds metrics transmitter cover and associated sensor card * Fixes item icons and item name * Adds tooltips for adv. sensor card and metrics panel, fixes card/overlay icons, adds recipe for metrics cover * Refactor cover tab sync to send much smaller ISerializable payloads * Remove unused variable * Additional master merge cleanup * Adds interface for custom metrics export, adds oil drill support * Adds support for metrics covers retaining attached machine name for tooltip, metrics * * Adds discrete coordinates to Metrics Cover data * Adds machine to advanced sensor card tooltip * Adds cycle time and minimum energy hatch tier to multiblock ore drill tooltips * Moves many tooltip strings to i10n * Remove old GT_DisabledWhileActiveButton.java file that crept in during a messy merge * Spotless * Improve oil and ore drill metrics, more i18n * Addresses PR review concerns, adds more situations for self-destructing sensor cards * Remediates further PR concerns --- src/main/java/gregtech/common/GT_Proxy.java | 2 + .../java/gregtech/common/covers/CoverInfo.java | 8 + .../covers/GT_Cover_Metrics_Transmitter.java | 212 +++++++++ .../common/events/BaseMetricsCoverEvent.java | 21 + .../common/events/MetricsCoverDataEvent.java | 37 ++ .../events/MetricsCoverHostDeconstructedEvent.java | 16 + .../events/MetricsCoverSelfDestructEvent.java | 15 + .../common/items/GT_AdvancedSensorCard_Item.java | 323 ++++++++++++++ .../common/items/GT_MetaGenerated_Item_03.java | 20 + .../gregtech/common/items/GT_SensorCard_Item.java | 2 +- .../items/behaviors/Behaviour_Cover_Tool.java | 24 +- .../common/misc/GlobalMetricsCoverDatabase.java | 492 +++++++++++++++++++++ .../multi/GT_MetaTileEntity_DrillerBase.java | 27 +- .../multi/GT_MetaTileEntity_OilDrillBase.java | 51 ++- .../GT_MetaTileEntity_OreDrillingPlantBase.java | 59 ++- 15 files changed, 1283 insertions(+), 26 deletions(-) create mode 100644 src/main/java/gregtech/common/covers/GT_Cover_Metrics_Transmitter.java create mode 100644 src/main/java/gregtech/common/events/BaseMetricsCoverEvent.java create mode 100644 src/main/java/gregtech/common/events/MetricsCoverDataEvent.java create mode 100644 src/main/java/gregtech/common/events/MetricsCoverHostDeconstructedEvent.java create mode 100644 src/main/java/gregtech/common/events/MetricsCoverSelfDestructEvent.java create mode 100644 src/main/java/gregtech/common/items/GT_AdvancedSensorCard_Item.java create mode 100644 src/main/java/gregtech/common/misc/GlobalMetricsCoverDatabase.java (limited to 'src/main/java/gregtech/common') diff --git a/src/main/java/gregtech/common/GT_Proxy.java b/src/main/java/gregtech/common/GT_Proxy.java index d853ea5f0a..2da2b883ca 100644 --- a/src/main/java/gregtech/common/GT_Proxy.java +++ b/src/main/java/gregtech/common/GT_Proxy.java @@ -166,6 +166,7 @@ import gregtech.common.entities.GT_Entity_Arrow; import gregtech.common.items.GT_MetaGenerated_Item_98; import gregtech.common.items.GT_MetaGenerated_Tool_01; import gregtech.common.misc.GlobalEnergyWorldSavedData; +import gregtech.common.misc.GlobalMetricsCoverDatabase; import gregtech.common.misc.spaceprojects.SpaceProjectWorldSavedData; public abstract class GT_Proxy implements IGT_Mod, IGuiHandler, IFuelHandler, IGlobalWirelessEnergy { @@ -1095,6 +1096,7 @@ public abstract class GT_Proxy implements IGT_Mod, IGuiHandler, IFuelHandler, IG MinecraftForge.EVENT_BUS.register(new GlobalEnergyWorldSavedData("")); MinecraftForge.EVENT_BUS.register(new SpaceProjectWorldSavedData()); MinecraftForge.EVENT_BUS.register(new GT_Worldgenerator.OregenPatternSavedData("")); + MinecraftForge.EVENT_BUS.register(new GlobalMetricsCoverDatabase()); FMLCommonHandler.instance() .bus() .register(new GT_Worldgenerator.OregenPatternSavedData("")); diff --git a/src/main/java/gregtech/common/covers/CoverInfo.java b/src/main/java/gregtech/common/covers/CoverInfo.java index e63fe6e176..0fac00218c 100644 --- a/src/main/java/gregtech/common/covers/CoverInfo.java +++ b/src/main/java/gregtech/common/covers/CoverInfo.java @@ -1,6 +1,7 @@ package gregtech.common.covers; import java.lang.ref.WeakReference; +import java.util.List; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; @@ -9,6 +10,8 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.ForgeDirection; import net.minecraftforge.fluids.Fluid; +import org.jetbrains.annotations.NotNull; + import com.gtnewhorizons.modularui.api.screen.ModularWindow; import gregtech.api.GregTech_API; @@ -238,4 +241,9 @@ public final class CoverInfo { public int getFacadeMeta() { return getCoverBehavior().getFacadeMeta(coverSide, coverID, coverData, coveredTile.get()); } + + @NotNull + public List getAdditionalTooltip(ISerializableObject data) { + return getCoverBehavior().getAdditionalTooltip(data); + } } diff --git a/src/main/java/gregtech/common/covers/GT_Cover_Metrics_Transmitter.java b/src/main/java/gregtech/common/covers/GT_Cover_Metrics_Transmitter.java new file mode 100644 index 0000000000..6d3cf529d3 --- /dev/null +++ b/src/main/java/gregtech/common/covers/GT_Cover_Metrics_Transmitter.java @@ -0,0 +1,212 @@ +package gregtech.common.covers; + +import java.util.List; +import java.util.UUID; + +import net.minecraft.entity.item.EntityItem; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.util.ForgeDirection; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteArrayDataInput; + +import gregtech.api.enums.ItemList; +import gregtech.api.interfaces.ITexture; +import gregtech.api.interfaces.metatileentity.IMetricsExporter; +import gregtech.api.interfaces.tileentity.ICoverable; +import gregtech.api.interfaces.tileentity.IGregTechDeviceInformation; +import gregtech.api.metatileentity.BaseMetaTileEntity; +import gregtech.api.util.GT_CoverBehaviorBase; +import gregtech.api.util.ISerializableObject; +import gregtech.common.events.MetricsCoverDataEvent; +import gregtech.common.events.MetricsCoverSelfDestructEvent; +import gregtech.common.misc.GlobalMetricsCoverDatabase; +import gregtech.common.misc.GlobalMetricsCoverDatabase.State; +import io.netty.buffer.ByteBuf; + +/** + * Used to transmit Nuclear Control information across dimensions. The only reason this is a cover is to artificially + * limit the number of total machines that transmit this data, for performance reasons. + *

+ * This cover will retrieve information, preferentially, using {@link IMetricsExporter#reportMetrics()}. Absent this + * method, it will resort to {@link BaseMetaTileEntity#getInfoData()} instead. + */ +public class GT_Cover_Metrics_Transmitter + extends GT_CoverBehaviorBase { + + @SuppressWarnings("SpellCheckingInspection") + public static final String FREQUENCY_MSB_KEY = "gt.metricscover.freq_msb"; + @SuppressWarnings("SpellCheckingInspection") + public static final String FREQUENCY_LSB_KEY = "gt.metricscover.freq_lsb"; + public static final String MACHINE_KEY = "machine_name"; + public static final String CARD_STATE_KEY = "card_state"; + + @SuppressWarnings("unused") + public GT_Cover_Metrics_Transmitter() { + this(null); + } + + public GT_Cover_Metrics_Transmitter(ITexture coverTexture) { + super(MetricsTransmitterData.class, coverTexture); + } + + @Override + public MetricsTransmitterData createDataObject(int aLegacyData) { + // As a new cover, this shouldn't fire. + return new MetricsTransmitterData(); + } + + @Override + public MetricsTransmitterData createDataObject() { + return new MetricsTransmitterData(); + } + + @Override + protected int getTickRateImpl(ForgeDirection side, int aCoverID, MetricsTransmitterData aCoverVariable, + ICoverable aTileEntity) { + return 20; + } + + @Override + public boolean isCoverPlaceable(ForgeDirection side, ItemStack aStack, ICoverable aTileEntity) { + return aTileEntity instanceof final IGregTechDeviceInformation device && device.isGivingInformation(); + } + + @Override + protected MetricsTransmitterData doCoverThingsImpl(ForgeDirection side, byte aInputRedstone, int aCoverID, + MetricsTransmitterData aCoverVariable, ICoverable aTileEntity, long aTimer) { + if (aTileEntity instanceof final BaseMetaTileEntity baseMTE && baseMTE.isGivingInformation()) { + final List payload; + + if (baseMTE.getMetaTileEntity() instanceof final IMetricsExporter metricsExporter) { + payload = metricsExporter.reportMetrics(); + } else { + payload = ImmutableList.copyOf(baseMTE.getInfoData()); + } + + MinecraftForge.EVENT_BUS.post(new MetricsCoverDataEvent( + aCoverVariable.frequency, + payload, + new GlobalMetricsCoverDatabase.Coordinates( + baseMTE.getWorld().provider.getDimensionName(), + baseMTE.getXCoord(), + baseMTE.getYCoord(), + baseMTE.getZCoord() + ) + )); + } + + return aCoverVariable; + } + + @Override + protected void onDroppedImpl(ForgeDirection side, int aCoverID, MetricsTransmitterData aCoverVariable, + ICoverable aTileEntity) { + MinecraftForge.EVENT_BUS.post(new MetricsCoverSelfDestructEvent(aCoverVariable.frequency)); + } + + @Override + public void onPlayerAttach(EntityPlayer player, ItemStack aCover, ICoverable aTileEntity, ForgeDirection side) { + final UUID newFrequency = UUID.randomUUID(); + final ItemStack cardStack = ItemList.NC_AdvancedSensorCard.get(1); + + if (cardStack == null) { + return; + } + + final NBTTagCompound tagCompound = new NBTTagCompound(); + tagCompound.setLong(FREQUENCY_MSB_KEY, newFrequency.getMostSignificantBits()); + tagCompound.setLong(FREQUENCY_LSB_KEY, newFrequency.getLeastSignificantBits()); + tagCompound.setInteger(CARD_STATE_KEY, State.OPERATIONAL.getType()); + + if (aTileEntity instanceof final BaseMetaTileEntity baseMTE) { + final ItemStack baseMTEStack = baseMTE.getStackForm(1); + if (baseMTEStack != null) { + tagCompound.setTag(MACHINE_KEY, baseMTEStack.writeToNBT(new NBTTagCompound())); + } + } + + cardStack.setTagCompound(tagCompound); + aTileEntity.getCoverInfoAtSide(side) + .setCoverData(new MetricsTransmitterData(newFrequency)); + + final EntityItem cardEntity = new EntityItem(player.worldObj, player.posX, player.posY, player.posZ, cardStack); + cardEntity.delayBeforeCanPickup = 0; + player.worldObj.spawnEntityInWorld(cardEntity); + } + + @Override + public boolean allowsCopyPasteTool() { + return false; + } + + @Override + public List getAdditionalTooltipImpl(MetricsTransmitterData data) { + return ImmutableList.of( + StatCollector.translateToLocalFormatted( + "gt.item.adv_sensor_card.tooltip.frequency", + EnumChatFormatting.UNDERLINE.toString() + EnumChatFormatting.YELLOW + data.frequency.toString())); + } + + public static class MetricsTransmitterData implements ISerializableObject { + + private UUID frequency; + + public MetricsTransmitterData() { + this.frequency = UUID.randomUUID(); + } + + public MetricsTransmitterData(@NotNull UUID frequency) { + this.frequency = frequency; + } + + @NotNull + public UUID getFrequency() { + return frequency; + } + + @NotNull + @Override + public ISerializableObject copy() { + return new MetricsTransmitterData(frequency); + } + + @NotNull + @Override + public NBTBase saveDataToNBT() { + NBTTagCompound tag = new NBTTagCompound(); + tag.setLong(FREQUENCY_MSB_KEY, frequency.getMostSignificantBits()); + tag.setLong(FREQUENCY_LSB_KEY, frequency.getLeastSignificantBits()); + return tag; + } + + @Override + public void loadDataFromNBT(NBTBase aNBT) { + if (aNBT instanceof final NBTTagCompound tag) { + frequency = new UUID(tag.getLong(FREQUENCY_MSB_KEY), tag.getLong(FREQUENCY_LSB_KEY)); + } + } + + @Override + public void writeToByteBuf(ByteBuf aBuf) { + aBuf.writeLong(frequency.getMostSignificantBits()); + aBuf.writeLong(frequency.getLeastSignificantBits()); + } + + @NotNull + @Override + public ISerializableObject readFromPacket(ByteArrayDataInput aBuf, @Nullable EntityPlayerMP aPlayer) { + return new MetricsTransmitterData(new UUID(aBuf.readLong(), aBuf.readLong())); + } + } +} diff --git a/src/main/java/gregtech/common/events/BaseMetricsCoverEvent.java b/src/main/java/gregtech/common/events/BaseMetricsCoverEvent.java new file mode 100644 index 0000000000..953d6fbfcf --- /dev/null +++ b/src/main/java/gregtech/common/events/BaseMetricsCoverEvent.java @@ -0,0 +1,21 @@ +package gregtech.common.events; + +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +import cpw.mods.fml.common.eventhandler.Event; + +public abstract class BaseMetricsCoverEvent extends Event { + + protected final UUID frequency; + + public BaseMetricsCoverEvent(@NotNull UUID frequency) { + this.frequency = frequency; + } + + @NotNull + public UUID getFrequency() { + return frequency; + } +} diff --git a/src/main/java/gregtech/common/events/MetricsCoverDataEvent.java b/src/main/java/gregtech/common/events/MetricsCoverDataEvent.java new file mode 100644 index 0000000000..92a5aa663a --- /dev/null +++ b/src/main/java/gregtech/common/events/MetricsCoverDataEvent.java @@ -0,0 +1,37 @@ +package gregtech.common.events; + +import java.util.List; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +import gregtech.common.misc.GlobalMetricsCoverDatabase; + +/** + * Event fired when the Metrics Transmitter cover sends an information packet. + */ +public class MetricsCoverDataEvent extends BaseMetricsCoverEvent { + + @NotNull + private final List payload; + + @NotNull + private final GlobalMetricsCoverDatabase.Coordinates coordinates; + + public MetricsCoverDataEvent(@NotNull UUID frequency, @NotNull List payload, + @NotNull GlobalMetricsCoverDatabase.Coordinates coordinates) { + super(frequency); + this.payload = payload; + this.coordinates = coordinates; + } + + @NotNull + public List getPayload() { + return payload; + } + + @NotNull + public GlobalMetricsCoverDatabase.Coordinates getCoordinates() { + return coordinates; + } +} diff --git a/src/main/java/gregtech/common/events/MetricsCoverHostDeconstructedEvent.java b/src/main/java/gregtech/common/events/MetricsCoverHostDeconstructedEvent.java new file mode 100644 index 0000000000..490bd7d0b0 --- /dev/null +++ b/src/main/java/gregtech/common/events/MetricsCoverHostDeconstructedEvent.java @@ -0,0 +1,16 @@ +package gregtech.common.events; + +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +/** + * Event fired when the machine housing a Metrics Transmitter cover is deconstructed, but the cover remains + * attached. + */ +public class MetricsCoverHostDeconstructedEvent extends BaseMetricsCoverEvent { + + public MetricsCoverHostDeconstructedEvent(@NotNull UUID frequency) { + super(frequency); + } +} diff --git a/src/main/java/gregtech/common/events/MetricsCoverSelfDestructEvent.java b/src/main/java/gregtech/common/events/MetricsCoverSelfDestructEvent.java new file mode 100644 index 0000000000..d554cbcc2b --- /dev/null +++ b/src/main/java/gregtech/common/events/MetricsCoverSelfDestructEvent.java @@ -0,0 +1,15 @@ +package gregtech.common.events; + +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +/** + * Event fired when a Metrics Transmitter cover is detached from its machine with a crowbar. + */ +public class MetricsCoverSelfDestructEvent extends BaseMetricsCoverEvent { + + public MetricsCoverSelfDestructEvent(@NotNull UUID frequency) { + super(frequency); + } +} diff --git a/src/main/java/gregtech/common/items/GT_AdvancedSensorCard_Item.java b/src/main/java/gregtech/common/items/GT_AdvancedSensorCard_Item.java new file mode 100644 index 0000000000..820fe59952 --- /dev/null +++ b/src/main/java/gregtech/common/items/GT_AdvancedSensorCard_Item.java @@ -0,0 +1,323 @@ +package gregtech.common.items; + +import static gregtech.api.enums.Mods.GregTech; +import static gregtech.common.covers.GT_Cover_Metrics_Transmitter.CARD_STATE_KEY; +import static gregtech.common.covers.GT_Cover_Metrics_Transmitter.FREQUENCY_LSB_KEY; +import static gregtech.common.covers.GT_Cover_Metrics_Transmitter.FREQUENCY_MSB_KEY; +import static gregtech.common.covers.GT_Cover_Metrics_Transmitter.MACHINE_KEY; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import net.minecraft.client.renderer.texture.IIconRegister; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IIcon; +import net.minecraft.util.StatCollector; +import net.minecraft.world.World; + +import org.jetbrains.annotations.NotNull; + +import com.google.common.collect.ImmutableList; + +import cpw.mods.fml.common.registry.GameRegistry; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import gregtech.common.misc.GlobalMetricsCoverDatabase; +import gregtech.common.misc.GlobalMetricsCoverDatabase.State; +import shedar.mods.ic2.nuclearcontrol.api.CardState; +import shedar.mods.ic2.nuclearcontrol.api.ICardWrapper; +import shedar.mods.ic2.nuclearcontrol.api.IPanelDataSource; +import shedar.mods.ic2.nuclearcontrol.api.PanelSetting; +import shedar.mods.ic2.nuclearcontrol.api.PanelString; + +@SuppressWarnings("unused") +public class GT_AdvancedSensorCard_Item extends Item implements IPanelDataSource { + + public static final UUID CARD_TYPE_ID = UUID.fromString("ff952e84-7608-4c4a-85af-dd6e1aa27fc7"); + + // This has obfuscated formatting, so no need to localize it. + private static final ImmutableList SELF_DESTRUCTED_OUTPUT = ImmutableList + .of(prebakePanelString(EnumChatFormatting.OBFUSCATED + "critical error" + EnumChatFormatting.RESET, true)); + + private static final ImmutableList DECONSTRUCTED_OUTPUT = ImmutableList.of( + prebakePanelString(StatCollector.translateToLocal("gt.item.adv_sensor_card.error.deconstructed.1"), true), + prebakePanelString(StatCollector.translateToLocal("gt.item.adv_sensor_card.error.deconstructed.2"), true)); + + private static final ImmutableList NO_DATA_FOUND = ImmutableList + .of(prebakePanelString(StatCollector.translateToLocal("gt.item.adv_sensor_card.error.no_data"), true)); + + private int payloadSize = 0; + + @SideOnly(Side.CLIENT) + private IIcon normalIcon; + @SideOnly(Side.CLIENT) + private IIcon selfDestructedIcon; + + @SuppressWarnings("unused") + public GT_AdvancedSensorCard_Item() { + super(); + + GameRegistry.registerItem(this, "gt.advancedsensorcard", GregTech.ID); + setUnlocalizedName("gt.advancedsensorcard"); + setMaxStackSize(1); + setNoRepair(); + } + + @Override + public void addInformation(final ItemStack itemStack, final EntityPlayer player, final List tooltip, + final boolean p_77624_4_) { + super.addInformation(itemStack, player, tooltip, p_77624_4_); + + final Optional cardState = getCardState(itemStack); + if (cardState.isPresent()) { + final State state = cardState.get(); + + if (state == State.SELF_DESTRUCTED) { + tooltip.add(StatCollector.translateToLocal("gt.item.adv_sensor_card.tooltip.fried.1")); + tooltip.add(StatCollector.translateToLocal("gt.item.adv_sensor_card.tooltip.fried.2")); + tooltip.add(StatCollector.translateToLocal("gt.item.adv_sensor_card.tooltip.fried.3")); + } else { + getMachineName(itemStack).ifPresent( + machineName -> tooltip.add( + StatCollector + .translateToLocalFormatted("gt.item.adv_sensor_card.tooltip.machine", machineName))); + getUUID(itemStack).ifPresent( + uuid -> tooltip.add( + StatCollector + .translateToLocalFormatted("gt.item.adv_sensor_card.tooltip.frequency", uuid.toString()))); + } + } else { + tooltip.add(StatCollector.translateToLocal("gt.item.adv_sensor_card.tooltip.recipe_hint")); + } + } + + @Override + @SideOnly(Side.CLIENT) + public void getSubItems(Item aItem, CreativeTabs aCreativeTab, List aOutputSubItems) {} + + @Override + @SideOnly(Side.CLIENT) + public void registerIcons(final IIconRegister aIconRegister) { + super.registerIcons(aIconRegister); + itemIcon = aIconRegister.registerIcon(GregTech.ID + ":gt.advancedsensorcard"); + normalIcon = itemIcon; + selfDestructedIcon = aIconRegister.registerIcon(GregTech.ID + ":gt.advancedsensorcardburned"); + } + + @Override + @SideOnly(Side.CLIENT) + public IIcon getIcon(final ItemStack stack, final int renderPass) { + return getIconIndex(stack); + } + + @Override + public IIcon getIconIndex(final ItemStack itemStack) { + return getCardState(itemStack).filter(Predicate.isEqual(State.SELF_DESTRUCTED)) + .map(ignored -> selfDestructedIcon) + .orElse(normalIcon); + } + + @Override + public CardState update(TileEntity tileEntity, ICardWrapper card, int maxRange) { + return update(tileEntity.getWorldObj(), card, maxRange); + } + + @Override + public CardState update(World world, ICardWrapper card, int maxRange) { + getDataFromDatabase(card).ifPresent(data -> { + reconcileSelfDestructedCard(card.getItemStack(), data.getState()); + card.setInt( + CARD_STATE_KEY, + data.getState() + .getType()); + payloadSize = switch (data.getState()) { + case SELF_DESTRUCTED -> SELF_DESTRUCTED_OUTPUT.size(); + case HOST_DECONSTRUCTED -> DECONSTRUCTED_OUTPUT.size() + getMachineName(card.getItemStack()).map(x -> 1) + .orElse(0); + case OPERATIONAL -> data.getPayload() + .map(List::size) + .orElse(0) + + getMachineName(card.getItemStack()).map(x -> 1) + .orElse(0) + + data.getCoordinates() + .map(x -> 2) + .orElse(0); + }; + }); + return CardState.OK; + } + + @Override + public List getStringData(final int displaySettings, final ICardWrapper card, + final boolean showLabels) { + // This method needs to return a mutable list, since the calling routine in NuclearCraft appends an item to the + // head of the list. Hence, all the array copying. + + return getCardState(card).map(state -> switch (state) { + case SELF_DESTRUCTED -> new ArrayList<>(SELF_DESTRUCTED_OUTPUT); + case HOST_DECONSTRUCTED -> { + final ArrayList list = new ArrayList<>(); + getMachineName(card.getItemStack()).ifPresent(name -> list.add(prebakePanelString(name, true))); + list.addAll(DECONSTRUCTED_OUTPUT); + yield list; + } + case OPERATIONAL -> getDataFromDatabase(card).map(data -> { + final ImmutableList.Builder builder = ImmutableList.builder(); + + getMachineName(card.getItemStack()).ifPresent(builder::add); + data.getCoordinates() + .ifPresent( + coordinates -> builder.add( + StatCollector.translateToLocalFormatted( + "gt.item.adv_sensor_card.dimension", + coordinates.getDimension()), + StatCollector.translateToLocalFormatted( + "gt.item.adv_sensor_card.coords", + coordinates.getLocalizedCoordinates()))); + + data.getPayload() + .ifPresent(builder::addAll); + + return builder.build(); + }) + .filter(payload -> !payload.isEmpty()) + .map( + payload -> IntStream.range(0, payload.size()) + .filter(i -> (displaySettings & (1 << i)) != 0) + .mapToObj(i -> prebakePanelString(payload.get(i), i == 0)) + .collect(Collectors.toCollection(ArrayList::new))) + .orElse(null); + }) + .orElse(new ArrayList<>(NO_DATA_FOUND)); + } + + @Override + public List getSettingsList() { + return payloadSize == 0 ? ImmutableList.of() + : ImmutableList.copyOf( + IntStream.range(0, Math.min(payloadSize, 31)) + .mapToObj(i -> new PanelSetting(String.valueOf(i + 1), 1 << i, getCardType())) + .iterator()); + } + + @Override + public UUID getCardType() { + return CARD_TYPE_ID; + } + + @Override + public void onUpdate(ItemStack stack, World worldIn, Entity entityIn, int slot, boolean isHeld) { + super.onUpdate(stack, worldIn, entityIn, slot, isHeld); + // At the time of this comment's writing, there are 52 matches of the regex: + // /% \d+0 \)?\s*== 0/ in the code base, indicating an over-reliance on events happening on either the 10th or + // 20th tick. Let's tick on something slightly off of that. A prime number will do nicely. + if ((worldIn.getWorldTime() % 20) == 13) { + getDataFromDatabase(stack).ifPresent(data -> { + reconcileSelfDestructedCard(stack, data.getState()); + if (!stack.hasTagCompound()) { + stack.setTagCompound(new NBTTagCompound()); + } + + stack.getTagCompound() + .setInteger( + CARD_STATE_KEY, + data.getState() + .getType()); + }); + } + } + + private void reconcileSelfDestructedCard(ItemStack stack, State newState) { + getUUID(stack).ifPresent(uuid -> getCardState(stack).ifPresent(oldState -> { + if (newState == State.SELF_DESTRUCTED && oldState != State.SELF_DESTRUCTED) { + GlobalMetricsCoverDatabase.clearSelfDestructedFrequency(uuid); + } + })); + + } + + @NotNull + private Optional getCardState(ICardWrapper card) { + return getCardState(card.getItemStack()); + } + + @NotNull + private Optional getCardState(ItemStack itemStack) { + if (itemStack.hasTagCompound() && itemStack.getTagCompound() + .hasKey(CARD_STATE_KEY)) { + return State.find( + itemStack.getTagCompound() + .getInteger(CARD_STATE_KEY)); + } + + return Optional.empty(); + } + + @NotNull + private Optional getUUID(ItemStack stack) { + if (stack.hasTagCompound()) { + NBTTagCompound nbt = stack.getTagCompound(); + if (nbt.hasKey(FREQUENCY_LSB_KEY) && nbt.hasKey(FREQUENCY_MSB_KEY)) { + return Optional.of(new UUID(nbt.getLong(FREQUENCY_MSB_KEY), nbt.getLong(FREQUENCY_LSB_KEY))); + } + } + + return Optional.empty(); + } + + @NotNull + private Optional getMachineName(ItemStack stack) { + if (stack.hasTagCompound() && stack.getTagCompound() + .hasKey(MACHINE_KEY)) { + try { + final ItemStack machine = ItemStack.loadItemStackFromNBT( + stack.getTagCompound() + .getCompoundTag(MACHINE_KEY)); + if (machine != null) { + return Optional.of(machine.getDisplayName()); + } + } catch (Exception ignored) {} + } + + return Optional.empty(); + } + + @NotNull + private Optional getDataFromDatabase(ICardWrapper card) { + return getDataFromDatabase(card.getItemStack()); + } + + @NotNull + private Optional getDataFromDatabase(ItemStack stack) { + return getUUID(stack).flatMap(GlobalMetricsCoverDatabase::getData); + } + + @NotNull + private static PanelString prebakePanelString(String info) { + return prebakePanelString(info, false); + } + + @NotNull + private static PanelString prebakePanelString(String info, boolean center) { + final PanelString panelString = new PanelString(); + if (center) { + panelString.textCenter = info; + } else { + panelString.textLeft = info; + } + return panelString; + + } +} diff --git a/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java b/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java index bede46adc9..f980c8031b 100644 --- a/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java +++ b/src/main/java/gregtech/common/items/GT_MetaGenerated_Item_03.java @@ -1,5 +1,7 @@ package gregtech.common.items; +import static gregtech.api.enums.Textures.BlockIcons.MACHINE_CASINGS; +import static gregtech.api.enums.Textures.BlockIcons.OVERLAY_METRICS_TRANSMITTER; import static gregtech.api.enums.Textures.BlockIcons.SOLARPANEL_UEV; import static gregtech.api.enums.Textures.BlockIcons.SOLARPANEL_UHV; import static gregtech.api.enums.Textures.BlockIcons.SOLARPANEL_UIV; @@ -21,6 +23,7 @@ import static gregtech.client.GT_TooltipHandler.Tier.ZPM; import static gregtech.client.GT_TooltipHandler.registerTieredTooltip; import gregtech.api.GregTech_API; +import gregtech.api.enums.GT_Values; import gregtech.api.enums.ItemList; import gregtech.api.enums.Materials; import gregtech.api.enums.OrePrefixes; @@ -29,6 +32,7 @@ import gregtech.api.enums.TC_Aspects; import gregtech.api.items.GT_MetaGenerated_Item_X32; import gregtech.api.render.TextureFactory; import gregtech.api.util.GT_OreDictUnificator; +import gregtech.common.covers.GT_Cover_Metrics_Transmitter; import gregtech.common.covers.GT_Cover_SolarPanel; public class GT_MetaGenerated_Item_03 extends GT_MetaGenerated_Item_X32 { @@ -911,6 +915,22 @@ public class GT_MetaGenerated_Item_03 extends GT_MetaGenerated_Item_X32 { "By the powers of Greg, I command this star to be really hot.", SubTag.NO_UNIFICATION)); + ItemList.Cover_Metrics_Transmitter.set( + addItem( + 232, + "Metrics Transmitter Cover", + String.join( + "/n ", + "Taking Information Panels to the next level!", + "Creates a GregTech Advanced Sensor Card when attached", + "Works across dimensions or if machine is dismantled", + "Removing this cover will destroy the linked card", + GT_Values.AuthorQuerns))); + GregTech_API.registerCover( + ItemList.Cover_Metrics_Transmitter.get(1L), + TextureFactory.of(MACHINE_CASINGS[2][0], TextureFactory.of(OVERLAY_METRICS_TRANSMITTER)), + new GT_Cover_Metrics_Transmitter(TextureFactory.of(OVERLAY_METRICS_TRANSMITTER))); + ItemList.Optical_Cpu_Containment_Housing.set(addItem(727, "Optical CPU Containment Housing", "CPU Housing", o)); ItemList.Optically_Perfected_CPU.set(addItem(726, "Optically Perfected CPU", "Perfected CPU!", o)); ItemList.Optically_Compatible_Memory.set(addItem(725, "Optically Compatible Memory", "Its in the name!", o)); diff --git a/src/main/java/gregtech/common/items/GT_SensorCard_Item.java b/src/main/java/gregtech/common/items/GT_SensorCard_Item.java index 024b089812..67e5b24a70 100644 --- a/src/main/java/gregtech/common/items/GT_SensorCard_Item.java +++ b/src/main/java/gregtech/common/items/GT_SensorCard_Item.java @@ -43,7 +43,7 @@ public class GT_SensorCard_Item extends GT_Generic_Item implements IRemoteSensor if (aStack != null) { NBTTagCompound tNBT = aStack.getTagCompound(); if (tNBT == null) { - aList.add(transItem("014", "Missing Coodinates!")); + aList.add(transItem("014", "Missing Coordinates!")); } else { aList.add(transItem("015", "Device at:")); aList.add( diff --git a/src/main/java/gregtech/common/items/behaviors/Behaviour_Cover_Tool.java b/src/main/java/gregtech/common/items/behaviors/Behaviour_Cover_Tool.java index bb37e1e933..5688872796 100644 --- a/src/main/java/gregtech/common/items/behaviors/Behaviour_Cover_Tool.java +++ b/src/main/java/gregtech/common/items/behaviors/Behaviour_Cover_Tool.java @@ -112,14 +112,22 @@ public class Behaviour_Cover_Tool extends Behaviour_None { ? GT_Utility.determineWrenchingSide(side, hitX, hitY, hitZ) : ForgeDirection.UNKNOWN; if (tSide != ForgeDirection.UNKNOWN) { - mStoredData = tCoverable.getComplexCoverDataAtSide(tSide); - mCoverType = tCoverable.getCoverIDAtSide(tSide); - aList.add("Block Side: " + EnumChatFormatting.AQUA + tSide.name() + EnumChatFormatting.RESET); - aList.add( - "Cover Type: " + EnumChatFormatting.GREEN - + tCoverable.getCoverItemAtSide(tSide) - .getDisplayName() - + EnumChatFormatting.RESET); + if (tCoverable.getCoverBehaviorAtSideNew(tSide) + .allowsCopyPasteTool()) { + mStoredData = tCoverable.getComplexCoverDataAtSide(tSide); + mCoverType = tCoverable.getCoverIDAtSide(tSide); + + aList.add("Block Side: " + EnumChatFormatting.AQUA + tSide.name() + EnumChatFormatting.RESET); + aList.add( + "Cover Type: " + EnumChatFormatting.GREEN + + tCoverable.getCoverItemAtSide(tSide) + .getDisplayName() + + EnumChatFormatting.RESET); + } else { + mStoredData = GregTech_API.sNoBehavior.createDataObject(); + mCoverType = 0; + aList.add("Copy unavailable for this cover type"); + } } else { mStoredData = GregTech_API.sNoBehavior.createDataObject(); mCoverType = 0; diff --git a/src/main/java/gregtech/common/misc/GlobalMetricsCoverDatabase.java b/src/main/java/gregtech/common/misc/GlobalMetricsCoverDatabase.java new file mode 100644 index 0000000000..33e8198bd6 --- /dev/null +++ b/src/main/java/gregtech/common/misc/GlobalMetricsCoverDatabase.java @@ -0,0 +1,492 @@ +package gregtech.common.misc; + +import static net.minecraftforge.common.util.Constants.NBT.TAG_BYTE_ARRAY; +import static net.minecraftforge.common.util.Constants.NBT.TAG_COMPOUND; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagByteArray; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.StatCollector; +import net.minecraft.world.WorldSavedData; +import net.minecraft.world.storage.MapStorage; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.item.ItemExpireEvent; +import net.minecraftforge.event.world.BlockEvent; +import net.minecraftforge.event.world.ExplosionEvent.Detonate; +import net.minecraftforge.event.world.WorldEvent; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import gregtech.api.enums.GT_Values; +import gregtech.api.util.GT_Utility; +import gregtech.common.covers.CoverInfo; +import gregtech.common.covers.GT_Cover_Metrics_Transmitter; +import gregtech.common.events.MetricsCoverDataEvent; +import gregtech.common.events.MetricsCoverHostDeconstructedEvent; +import gregtech.common.events.MetricsCoverSelfDestructEvent; + +/** + * Catches and provides data transmitted from deployed Metrics Transmitter covers. Only stores one result per frequency + * at a time. Metrics covers are intended to overwrite an old result every time they emit a new event. + *
+ *
+ * This information is only partially persisted; frequencies that are in a non-operational state will be written to + * disk, while operational frequencies are volatile. The assumption is that any frequency with a broadcasting card will, + * fairly quickly, re-assert its presence. Conversely, one-time events like deconstruction or self-destruction can occur + * while the card is in a container, rotting on the ground, etc. + */ +public class GlobalMetricsCoverDatabase extends WorldSavedData { + + private static GlobalMetricsCoverDatabase INSTANCE; + + /** Holds received metrics. */ + private static final Map DATABASE = new ConcurrentHashMap<>(); + /** Used to speed up event handlers dealing with block breaking and explosions. Not persisted. */ + private static final Map> REVERSE_LOOKUP = new ConcurrentHashMap<>(); + + private static final String DATA_NAME = "GregTech_MetricsCoverDatabase"; + private static final String DECONSTRUCTED_KEY = "GregTech_MetricsCoverDatabase_Deconstructed"; + private static final String SELF_DESTRUCTED_KEY = "GregTech_MetricsCoverDatabase_SelfDestructed"; + + public GlobalMetricsCoverDatabase() { + this(DATA_NAME); + } + + public GlobalMetricsCoverDatabase(String name) { + super(name); + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void receiveMetricsData(MetricsCoverDataEvent event) { + final Coordinates coordinates = event.getCoordinates(); + store(event.getFrequency(), State.OPERATIONAL, event.getPayload(), coordinates); + + if (!REVERSE_LOOKUP.containsKey(coordinates)) { + REVERSE_LOOKUP.put(coordinates, new HashSet<>()); + } + REVERSE_LOOKUP.get(coordinates) + .add(event.getFrequency()); + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void receiveHostDeconstructed(MetricsCoverHostDeconstructedEvent event) { + cullReverseLookupEntry(event.getFrequency()); + store(event.getFrequency(), State.HOST_DECONSTRUCTED); + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void receiveSelfDestruct(MetricsCoverSelfDestructEvent event) { + cullReverseLookupEntry(event.getFrequency()); + store(event.getFrequency(), State.SELF_DESTRUCTED); + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void onWorldLoad(WorldEvent.Load event) { + if (event.world.isRemote || event.world.provider.dimensionId != 0) { + return; + } + + DATABASE.clear(); + + final MapStorage storage = event.world.mapStorage; + INSTANCE = (GlobalMetricsCoverDatabase) storage.loadData(GlobalMetricsCoverDatabase.class, DATA_NAME); + if (INSTANCE == null) { + INSTANCE = new GlobalMetricsCoverDatabase(); + storage.setData(DATA_NAME, INSTANCE); + } + + INSTANCE.markDirty(); + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void onBlockBreak(BlockEvent.BreakEvent event) { + final Coordinates coords = new Coordinates(event.world.provider.getDimensionName(), event.x, event.y, event.z); + // In case someone else wants to listen to these, go the roundabout way. + final Set uuids = REVERSE_LOOKUP.get(coords); + if (uuids != null) { + uuids.forEach( + uuid -> MinecraftForge.EVENT_BUS.post( + ForgeHooks.canHarvestBlock(event.block, event.getPlayer(), event.blockMetadata) + && !event.getPlayer().capabilities.isCreativeMode ? new MetricsCoverHostDeconstructedEvent(uuid) + : new MetricsCoverSelfDestructEvent(uuid))); + } + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void onExplosion(Detonate event) { + final String dimensionName = event.world.provider.getDimensionName(); + + event.getAffectedBlocks() + .forEach(chunkPosition -> { + final Set uuids = REVERSE_LOOKUP.get( + new Coordinates( + dimensionName, + chunkPosition.chunkPosX, + chunkPosition.chunkPosY, + chunkPosition.chunkPosZ)); + + if (uuids != null) { + uuids.forEach(uuid -> MinecraftForge.EVENT_BUS.post(new MetricsCoverSelfDestructEvent(uuid))); + } + }); + + event.getAffectedEntities().forEach(entity -> { + if (entity instanceof final EntityItem entityItem) { + getCoverUUIDsFromItemStack(entityItem.getEntityItem()) + .forEach(uuid -> MinecraftForge.EVENT_BUS.post(new MetricsCoverSelfDestructEvent(uuid))); + } + }); + } + + @SuppressWarnings("unused") + @SubscribeEvent + public void onItemExpiration(ItemExpireEvent event) { + getCoverUUIDsFromItemStack(event.entityItem.getEntityItem()) + .forEach(uuid -> MinecraftForge.EVENT_BUS.post(new MetricsCoverSelfDestructEvent(uuid))); + } + + /** + * Get the data for a frequency, if it exists. + * + * @param frequency The UUID corresponding to the frequency to retrieve. + * @return An Optional with the frequency's data, or an empty Optional if it doesn't exist. + */ + @NotNull + public static Optional getData(UUID frequency) { + return Optional.ofNullable(DATABASE.get(frequency)); + } + + /** + * Once a card has received the fact that it has self-destructed, this method can be called to free up its spot + * in the database. Does nothing if the frequency is missing or is not in a self-destructed state. + * + * @param frequency The UUID corresponding to the frequency to possibly cull. + */ + public static void clearSelfDestructedFrequency(UUID frequency) { + getData(frequency).ifPresent(data -> { + if (data.getState() == State.SELF_DESTRUCTED) { + DATABASE.remove(frequency); + tryMarkDirty(); + } + }); + } + + @Override + public void readFromNBT(NBTTagCompound nbtTagCompound) { + final NBTTagList deconstructed = nbtTagCompound.getTagList(DECONSTRUCTED_KEY, TAG_BYTE_ARRAY); + final NBTTagList selfDestructed = nbtTagCompound.getTagList(SELF_DESTRUCTED_KEY, TAG_BYTE_ARRAY); + + for (int i = 0; i < deconstructed.tagCount(); i++) { + final NBTTagByteArray byteArray = (NBTTagByteArray) deconstructed.removeTag(0); + reconstituteUUID(byteArray.func_150292_c()) + .ifPresent(uuid -> DATABASE.put(uuid, new Data(State.HOST_DECONSTRUCTED))); + } + + for (int i = 0; i < selfDestructed.tagCount(); i++) { + final NBTTagByteArray byteArray = (NBTTagByteArray) selfDestructed.removeTag(0); + reconstituteUUID(byteArray.func_150292_c()) + .ifPresent(uuid -> DATABASE.put(uuid, new Data(State.SELF_DESTRUCTED))); + } + } + + @Override + public void writeToNBT(NBTTagCompound nbtTagCompound) { + // We only care about persisting frequencies that aren't operational. + final NBTTagList deconstructed = new NBTTagList(); + final NBTTagList selfDestructed = new NBTTagList(); + DATABASE.forEach((uuid, data) -> { + switch (data.getState()) { + case HOST_DECONSTRUCTED -> deconstructed.appendTag(new NBTTagByteArray(dumpUUID(uuid))); + case SELF_DESTRUCTED -> selfDestructed.appendTag(new NBTTagByteArray(dumpUUID(uuid))); + } + }); + + if (deconstructed.tagCount() > 0) { + nbtTagCompound.setTag(DECONSTRUCTED_KEY, deconstructed); + } + if (selfDestructed.tagCount() > 0) { + nbtTagCompound.setTag(SELF_DESTRUCTED_KEY, selfDestructed); + } + } + + /** + * Stores the new result and flag the static {@link MapStorage} instance as dirty if the information updated. Will + * not flag dirty for any data in the {@link State#OPERATIONAL OPERATIONAL} state since they aren't stored. + * + * @param frequency Maps to a unique deployed cover. + * @param state The new cover state. + */ + private static void store(@NotNull UUID frequency, @NotNull State state) { + store(frequency, state, null, null); + } + + /** + * Stores the new result and flag the static {@link MapStorage} instance as dirty if the information updated. Will + * not flag dirty for any data in the {@link State#OPERATIONAL OPERATIONAL} state since they aren't stored. + * + * @param frequency Maps to a unique deployed cover. + * @param state The new cover state. + * @param payload A list of strings to display on the information panel, if the card is slotted properly. + * @param coordinates Coordinates of the active machine (including dimension.) + */ + private static void store(@NotNull UUID frequency, @NotNull State state, @Nullable List payload, + @Nullable Coordinates coordinates) { + final Data newData = new Data(state, payload, coordinates); + final Data oldData = DATABASE.put(frequency, newData); + + if (state != State.OPERATIONAL && (oldData == null || oldData != newData)) { + tryMarkDirty(); + } + } + + private static void tryMarkDirty() { + if (INSTANCE != null) { + INSTANCE.markDirty(); + } + } + + private static byte[] dumpUUID(UUID uuid) { + ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + return buffer.array(); + } + + @NotNull + private static Optional reconstituteUUID(byte[] bytes) throws IllegalArgumentException { + if (bytes.length != 16) { + return Optional.empty(); + } + + ByteBuffer buffer = ByteBuffer.wrap(bytes); + return Optional.of(new UUID(buffer.getLong(), buffer.getLong())); + } + + private static void cullReverseLookupEntry(UUID frequency) { + getData(frequency).ifPresent(data -> { + if (data.state == State.OPERATIONAL && REVERSE_LOOKUP.containsKey(data.coordinates)) { + final Set set = REVERSE_LOOKUP.get(data.coordinates); + set.remove(frequency); + if (set.isEmpty()) { + REVERSE_LOOKUP.remove(data.coordinates); + } + } + }); + } + + private static Stream getCoverUUIDsFromItemStack(final ItemStack stack) { + if (stack.hasTagCompound() && stack.getTagCompound() + .hasKey(GT_Values.NBT.COVERS, TAG_COMPOUND)) { + final NBTTagList tagList = stack.getTagCompound() + .getTagList(GT_Values.NBT.COVERS, TAG_COMPOUND); + return IntStream.range(0, tagList.tagCount()) + .mapToObj(tagList::getCompoundTagAt) + .map(nbt -> new CoverInfo(null, nbt).getCoverData()) + .filter( + serializableObject -> serializableObject instanceof GT_Cover_Metrics_Transmitter.MetricsTransmitterData) + .map(data -> ((GT_Cover_Metrics_Transmitter.MetricsTransmitterData) data).getFrequency()); + } + return Stream.empty(); + } + + /** + * Data transmitted by a Metrics Transmitter cover. + *

+ * Since only negative states ({@link State#HOST_DECONSTRUCTED HOST_DECONSTRUCTED} and + * {@link State#SELF_DESTRUCTED SELF DESTRUCTED}) are persisted, additional fields can be added to this data with + * little consequence. Ensure that any new fields are nullable, and make any getter for these fields return an + * {@link Optional}. + */ + public static class Data { + + @NotNull + private final State state; + @Nullable + private final List payload; + @Nullable + private final Coordinates coordinates; + + public Data(@NotNull State state) { + this.state = state; + this.payload = null; + this.coordinates = null; + } + + public Data(@NotNull State state, @Nullable List payload) { + this.state = state; + this.payload = payload; + this.coordinates = null; + } + + public Data(@NotNull State state, @Nullable List payload, @Nullable Coordinates coordinates) { + this.state = state; + this.payload = payload; + this.coordinates = coordinates; + + } + + /** + * Retrieves the payload for this data. Only present if the frequency is in an + * {@link State#OPERATIONAL operational} state. Will be cleared if the frequency goes into a + * {@link State#HOST_DECONSTRUCTED host-deconstructed} or {@link State#SELF_DESTRUCTED self-destructed} state. + * + * @return The data if present, or an empty Optional otherwise. + */ + @NotNull + public Optional> getPayload() { + return Optional.ofNullable(payload); + } + + /** + * Gets the state of the frequency. + * + * @return The state + */ + @NotNull + public State getState() { + return state; + } + + /** + * Gets the last known coordinates for the machine broadcasting metrics. Will only be present in an + * {@link State#OPERATIONAL operational} state. + * + * @return The coordinates + */ + @NotNull + public Optional getCoordinates() { + return Optional.ofNullable(coordinates); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Data data = (Data) o; + return state == data.state && Objects.equals(payload, data.payload) + && Objects.equals(coordinates, data.coordinates); + } + + @Override + public int hashCode() { + return Objects.hash(state, payload, coordinates); + } + } + + @SuppressWarnings("ClassCanBeRecord") + public static class Coordinates { + + private final String dimension; + private final int x; + private final int y; + private final int z; + + public Coordinates(final String dimension, final int x, final int y, final int z) { + this.dimension = dimension; + this.x = x; + this.y = y; + this.z = z; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getZ() { + return z; + } + + public String getDimension() { + return dimension; + } + + public String getLocalizedCoordinates() { + return StatCollector.translateToLocalFormatted( + "gt.db.metrics_cover.coords", + GT_Utility.formatNumbers(x), + GT_Utility.formatNumbers(y), + GT_Utility.formatNumbers(z)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Coordinates that = (Coordinates) o; + return x == that.x && y == that.y && z == that.z && Objects.equals(dimension, that.dimension); + } + + @Override + public int hashCode() { + return Objects.hash(dimension, x, y, z); + } + } + + public enum State { + // NOTE: type cannot be 0, as NuclearControl returns a 0 when querying for an integer from an item stack's NBT + // data when it really means null. + + /** The machine is online and broadcasting metrics. */ + OPERATIONAL(1), + /** + * The machine was picked up, but the cover is still attached. Will transition to operational state if the + * machine is placed back down and started up again. + */ + HOST_DECONSTRUCTED(2), + /** + * Cover was removed from its host machine, or machine was destroyed (in the limited number of ways we can + * detect.) Any frequency in this state will no longer get updates nor leave this state. + */ + SELF_DESTRUCTED(3); + + private static final Map VALID_TYPE_INTEGERS = Arrays.stream(State.values()) + .collect(Collectors.toMap(State::getType, Function.identity())); + private final int type; + + State(final int type) { + if (type <= 0) { + throw new IllegalArgumentException("A state must have a positive, nonzero type parameter."); + } + this.type = type; + } + + @NotNull + public static Optional find(int candidate) { + return Optional.ofNullable(VALID_TYPE_INTEGERS.get(candidate)); + } + + public int getType() { + return type; + } + } +} diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_DrillerBase.java b/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_DrillerBase.java index f40c70d2a4..41e0712627 100644 --- a/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_DrillerBase.java +++ b/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_DrillerBase.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; @@ -161,6 +162,7 @@ public abstract class GT_MetaTileEntity_DrillerBase /** Allows inheritors to supply custom runtime failure messages. */ private CheckRecipeResult runtimeFailure = null; + private CheckRecipeResult lastRuntimeFailure = null; /** Allows inheritors to supply custom shutdown failure messages. */ private @NotNull String shutdownReason = ""; @@ -563,12 +565,16 @@ public abstract class GT_MetaTileEntity_DrillerBase } if (runtimeFailure == null) { + if (wasSuccessful) { + lastRuntimeFailure = null; + } + return resultRegistry.getOrDefault( new ResultRegistryKey(workState, wasSuccessful), SimpleCheckRecipeResult.ofFailure("no_mining_pipe")); } else { final CheckRecipeResult result; - result = runtimeFailure; + result = lastRuntimeFailure = runtimeFailure; runtimeFailure = null; return result; } @@ -584,6 +590,25 @@ public abstract class GT_MetaTileEntity_DrillerBase runtimeFailure = newFailureReason; } + /** + * Gets a reason for why the drill turned off, for use in UIs and such. + * + * @return A reason, or empty if the machine is active or there is no message set yet. + */ + @NotNull + protected Optional getFailureReason() { + if (getBaseMetaTileEntity().isActive()) { + return Optional.empty(); + } + + if (!shutdownReason.isEmpty()) { + return Optional.of(shutdownReason); + } + + return Optional.ofNullable(lastRuntimeFailure) + .map(CheckRecipeResult::getDisplayString); + } + /** * Sets a line in the UI to explain why the drill shut down. E.g.: operation finished. * Should be used when the machine has been turned off due to an operating issue or completion. diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_OilDrillBase.java b/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_OilDrillBase.java index 9a6f2b5e80..0a47bdb268 100644 --- a/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_OilDrillBase.java +++ b/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_OilDrillBase.java @@ -45,6 +45,7 @@ import com.gtnewhorizons.modularui.common.widget.TextWidget; import gregtech.api.enums.SoundResource; import gregtech.api.interfaces.IHatchElement; import gregtech.api.interfaces.ITexture; +import gregtech.api.interfaces.metatileentity.IMetricsExporter; import gregtech.api.interfaces.tileentity.IGregTechTileEntity; import gregtech.api.objects.GT_ChunkManager; import gregtech.api.recipe.check.CheckRecipeResultRegistry; @@ -55,7 +56,7 @@ import gregtech.api.util.GT_Utility; import gregtech.api.util.ValidationResult; import gregtech.api.util.ValidationType; -public abstract class GT_MetaTileEntity_OilDrillBase extends GT_MetaTileEntity_DrillerBase { +public abstract class GT_MetaTileEntity_OilDrillBase extends GT_MetaTileEntity_DrillerBase implements IMetricsExporter { private final ArrayList mOilFieldChunks = new ArrayList<>(); private int mOilId = 0; @@ -385,6 +386,31 @@ public abstract class GT_MetaTileEntity_OilDrillBase extends GT_MetaTileEntity_D return l.toArray(new String[0]); } + @Override + public @NotNull List reportMetrics() { + if (getBaseMetaTileEntity().isActive()) { + return switch (workState) { + case STATE_AT_BOTTOM -> ImmutableList.of( + StatCollector.translateToLocalFormatted("GT5U.gui.text.pump_fluid_type", getFluidName()), + StatCollector.translateToLocalFormatted( + "GT5U.gui.text.pump_rate.1", + EnumChatFormatting.AQUA + getFlowRatePerTick()) + + StatCollector.translateToLocal("GT5U.gui.text.pump_rate.2"), + getReservoirContents() + StatCollector.translateToLocal("GT5U.gui.text.pump_recovery.2")); + case STATE_DOWNWARD -> ImmutableList.of(StatCollector.translateToLocal("GT5U.gui.text.deploying_pipe")); + case STATE_UPWARD, STATE_ABORT -> ImmutableList + .of(StatCollector.translateToLocal("GT5U.gui.text.retracting_pipe")); + + default -> ImmutableList.of(); + }; + } + + return ImmutableList.of( + getFailureReason() + .map(reason -> StatCollector.translateToLocalFormatted("GT5U.gui.text.drill_offline_reason", reason)) + .orElseGet(() -> StatCollector.translateToLocalFormatted("GT5U.gui.text.drill_offline_generic"))); + } + @NotNull protected String getFlowRatePerTick() { return GT_Utility.formatNumbers(this.mMaxProgresstime > 0 ? (mOilFlow / this.mMaxProgresstime) : 0); @@ -413,7 +439,8 @@ public abstract class GT_MetaTileEntity_OilDrillBase extends GT_MetaTileEntity_D } } - return StatCollector.translateToLocalFormatted("GT5U.gui.text.pump_recovery", GT_Utility.formatNumbers(amount)); + return StatCollector + .translateToLocalFormatted("GT5U.gui.text.pump_recovery.1", GT_Utility.formatNumbers(amount)); } @Override @@ -423,19 +450,29 @@ public abstract class GT_MetaTileEntity_OilDrillBase extends GT_MetaTileEntity_D .widget( TextWidget .dynamicString( - () -> StatCollector.translateToLocalFormatted("GT5U.gui.text.pump_fluid_type", clientFluidType)) + () -> EnumChatFormatting.GRAY + + StatCollector.translateToLocalFormatted("GT5U.gui.text.pump_fluid_type", clientFluidType)) .setSynced(false) .setTextAlignment(Alignment.CenterLeft) .setEnabled(widget -> getBaseMetaTileEntity().isActive() && workState == STATE_AT_BOTTOM)) .widget( - TextWidget.dynamicString( - () -> StatCollector - .translateToLocalFormatted("GT5U.gui.text.pump_rate", EnumChatFormatting.AQUA + clientPumpRate)) + TextWidget + .dynamicString( + () -> EnumChatFormatting.GRAY + + StatCollector.translateToLocalFormatted( + "GT5U.gui.text.pump_rate.1", + EnumChatFormatting.AQUA + clientPumpRate) + + EnumChatFormatting.GRAY + + StatCollector.translateToLocal("GT5U.gui.text.pump_rate.2")) .setSynced(false) .setTextAlignment(Alignment.CenterLeft) .setEnabled(widget -> getBaseMetaTileEntity().isActive() && workState == STATE_AT_BOTTOM)) .widget( - TextWidget.dynamicString(() -> clientReservoirContents) + TextWidget + .dynamicString( + () -> EnumChatFormatting.GRAY + clientReservoirContents + + EnumChatFormatting.GRAY + + StatCollector.translateToLocal("GT5U.gui.text.pump_recovery.2")) .setSynced(false) .setTextAlignment(Alignment.CenterLeft) .setEnabled(widget -> getBaseMetaTileEntity().isActive() && workState == STATE_AT_BOTTOM)) diff --git a/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_OreDrillingPlantBase.java b/src/main/java/gregtech/common/tileentities/machines/multi/GT_MetaTileEntity_OreDrillingPlantBase.jav