Filter of autopickup for condensation drive

This commit is contained in:
DBotThePony 2021-08-29 16:30:34 +07:00
parent f1f1ce897c
commit b3b1a03136
Signed by: DBot
GPG Key ID: DCC23B5715498507
11 changed files with 583 additions and 48 deletions

View File

@ -1,15 +1,26 @@
package ru.dbotthepony.mc.otm.item;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.stats.Stats;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
@ -18,16 +29,22 @@ import net.minecraftforge.event.entity.player.EntityItemPickupEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fmllegacy.network.NetworkEvent;
import net.minecraftforge.registries.RegistryManager;
import ru.dbotthepony.mc.otm.OverdriveThatMatters;
import ru.dbotthepony.mc.otm.capability.MatteryCapability;
import ru.dbotthepony.mc.otm.capability.drive.DrivePool;
import ru.dbotthepony.mc.otm.capability.drive.IMatteryDrive;
import ru.dbotthepony.mc.otm.capability.drive.MatteryDrive;
import ru.dbotthepony.mc.otm.menu.DriveViewerMenu;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
public class ItemPortableCondensationDrive extends Item {
public ItemPortableCondensationDrive() {
@ -48,9 +65,14 @@ public class ItemPortableCondensationDrive extends Item {
var item = event.getItem().getItem().getItem();
for (var stack : event.getPlayer().getInventory().items) {
if (stack.getItem() instanceof ItemPortableCondensationDrive) {
var cap = stack.getCapability(MatteryCapability.DRIVE).resolve().get();
if (stack.getItem() instanceof ItemPortableCondensationDrive drive) {
var _cap = stack.getCapability(MatteryCapability.DRIVE).resolve();
if (_cap.isPresent()) {
var cap = _cap.get();
var filter = drive.getFilterSettings(stack);
if (filter.matches(event.getItem().getItem())) {
var copy = event.getItem().getItem().copy();
var remaining = cap.insertItem(event.getItem().getItem(), false);
@ -84,6 +106,8 @@ public class ItemPortableCondensationDrive extends Item {
}
}
}
}
}
private static class DriveCapability implements ICapabilityProvider {
private UUID uuid;
@ -140,4 +164,109 @@ public class ItemPortableCondensationDrive extends Item {
public ICapabilityProvider initCapabilities(ItemStack stack, @Nullable CompoundTag nbt) {
return new DriveCapability(stack);
}
public static class FilterSettings {
public static final int MAX_FILTERS = 12;
public final ItemStack[] items = new ItemStack[MAX_FILTERS];
public boolean match_nbt;
public boolean match_tag;
public boolean blacklist;
public FilterSettings() {
Arrays.fill(items, ItemStack.EMPTY);
}
public FilterSettings(CompoundTag tag) {
this();
var list = tag.getList("filter", Tag.TAG_COMPOUND);
var i = 0;
for (var get_tag : list) {
if (get_tag instanceof CompoundTag compound) {
items[i++] = ItemStack.of(compound);
}
}
match_nbt = tag.getBoolean("match_nbt");
match_tag = tag.getBoolean("match_tag");
blacklist = tag.getBoolean("blacklist");
}
public void serializeNBT(CompoundTag compound) {
var list = new ListTag();
for (int i = 0; i < MAX_FILTERS; i++) {
list.add(items[i].serializeNBT());
}
compound.put("filter", list);
compound.putBoolean("match_nbt", match_nbt);
compound.putBoolean("match_tag", match_tag);
compound.putBoolean("blacklist", blacklist);
}
public void serializeNBT(ItemStack drive) {
serializeNBT(drive.getOrCreateTag());
}
public boolean matches(ItemStack stack) {
if (blacklist) {
for (var item : items) {
if (!match_nbt && ItemStack.isSame(item, stack)) {
return false;
}
if (match_tag) {
var this_tags = item.getItem().getTags();
var stack_tatgs = stack.getItem().getTags();
for (var tag1 : this_tags) {
if (stack_tatgs.contains(tag1)) {
return false;
}
}
}
if (match_nbt && ItemStack.isSameItemSameTags(item, stack)) {
return false;
}
}
return true;
} else {
for (var item : items) {
boolean same = ItemStack.isSame(item, stack);
if (!same && match_tag) {
var this_tags = item.getItem().getTags();
var stack_tatgs = stack.getItem().getTags();
for (var tag1 : this_tags) {
if (stack_tatgs.contains(tag1)) {
same = true;
break;
}
}
}
if (match_nbt) {
if (same && ItemStack.tagMatches(item, stack)) {
return true;
}
} else {
if (same) {
return true;
}
}
}
return false;
}
}
}
public FilterSettings getFilterSettings(ItemStack drive) {
return new FilterSettings(drive.getOrCreateTag());
}
}

View File

@ -18,6 +18,7 @@ import ru.dbotthepony.mc.otm.block.entity.BlockEntityDriveViewer;
import ru.dbotthepony.mc.otm.capability.MatteryCapability;
import ru.dbotthepony.mc.otm.capability.drive.IItemViewListener;
import ru.dbotthepony.mc.otm.capability.drive.IMatteryDrive;
import ru.dbotthepony.mc.otm.item.ItemPortableCondensationDrive;
import ru.dbotthepony.mc.otm.menu.slot.MatterySlot;
import ru.dbotthepony.mc.otm.network.MatteryNetworking;
import ru.dbotthepony.mc.otm.network.SetCarriedPacket;
@ -490,4 +491,99 @@ public class DriveViewerMenu extends PoweredMatteryMenu {
});
}
}
public enum FilterSwitch {
MATCH_NBT,
MATCH_TAG,
BLACKLIST
}
public ItemPortableCondensationDrive.FilterSettings getFilter() {
if (drive_slot.getItem().isEmpty())
return null;
if (drive_slot.getItem().getItem() instanceof ItemPortableCondensationDrive item)
return item.getFilterSettings(drive_slot.getItem());
return null;
}
public record FilterSetPacket(int id, int slot, ItemStack value) {
public void write(FriendlyByteBuf buffer) {
buffer.writeInt(id);
buffer.writeInt(slot);
buffer.writeItem(value);
}
public static FilterSetPacket read(FriendlyByteBuf buffer) {
return new FilterSetPacket(buffer.readInt(), buffer.readInt(), buffer.readItem());
}
public void play(Supplier<NetworkEvent.Context> context) {
context.get().setPacketHandled(true);
if (slot < 0 || slot >= ItemPortableCondensationDrive.FilterSettings.MAX_FILTERS)
return;
if (value.getCount() > 1)
value.setCount(1);
context.get().enqueueWork(() -> {
var ply = context.get().getSender();
if (ply.containerMenu instanceof DriveViewerMenu menu && menu.containerId == id) {
if (menu.drive_slot.getItem().isEmpty())
return;
if (menu.drive_slot.getItem().getItem() instanceof ItemPortableCondensationDrive drive) {
var filter = drive.getFilterSettings(menu.drive_slot.getItem());
filter.items[slot] = value;
filter.serializeNBT(menu.drive_slot.getItem());
}
}
});
}
}
public record FilterSwitchPacket(int id, FilterSwitch type, boolean value) {
public void write(FriendlyByteBuf buffer) {
buffer.writeInt(id);
buffer.writeEnum(type);
buffer.writeBoolean(value);
}
public static FilterSwitchPacket read(FriendlyByteBuf buffer) {
return new FilterSwitchPacket(buffer.readInt(), buffer.readEnum(FilterSwitch.class), buffer.readBoolean());
}
public void play(Supplier<NetworkEvent.Context> context) {
context.get().setPacketHandled(true);
context.get().enqueueWork(() -> {
var ply = context.get().getSender();
if (ply.containerMenu instanceof DriveViewerMenu menu && menu.containerId == id) {
var settings = menu.getFilter();
if (settings == null)
return;
switch (type) {
case MATCH_NBT -> {
settings.match_nbt = value;
}
case MATCH_TAG -> {
settings.match_tag = value;
}
case BLACKLIST -> {
settings.blacklist = value;
}
}
settings.serializeNBT(menu.drive_slot.getItem());
}
});
}
}
}

View File

@ -27,6 +27,12 @@ public abstract class MatteryMenu extends AbstractContainerMenu {
public final ArrayList<MatterySlot> inventory_slots = new ArrayList<>();
public final ArrayList<MatterySlot> main_slots = new ArrayList<>();
protected final Set<Integer> locked_inventory_slots = new HashSet<>();
protected boolean isInventorySlotLocked(int index) {
return locked_inventory_slots.contains(index);
}
@Nullable
protected ContainerSynchronizer synchronizer;
@ -73,7 +79,18 @@ public abstract class MatteryMenu extends AbstractContainerMenu {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 9; ++j) {
var slot = new MatterySlot(inventory, j + i * 9 + 9, 8 + j * 18, 14 + i * 18);
var slot = new MatterySlot(inventory, j + i * 9 + 9, 8 + j * 18, 14 + i * 18) {
@Override
public boolean mayPlace(ItemStack p_40231_) {
return !isInventorySlotLocked(index);
}
@Override
public boolean mayPickup(Player p_40228_) {
return !isInventorySlotLocked(index);
}
};
inventory_slots.add(slot);
this.addSlot(slot);
@ -87,7 +104,18 @@ public abstract class MatteryMenu extends AbstractContainerMenu {
MatterySlot last = null;
for (int k = 0; k < 9; ++k) {
last = new MatterySlot(inventory, k, 8 + k * 18, 14 + 58);
last = new MatterySlot(inventory, k, 8 + k * 18, 14 + 58) {
@Override
public boolean mayPlace(ItemStack p_40231_) {
return !isInventorySlotLocked(index);
}
@Override
public boolean mayPickup(Player p_40228_) {
return !isInventorySlotLocked(index);
}
};
this.addSlot(last);
inventory_slots.add(last);
}

View File

@ -7,6 +7,7 @@ import net.minecraftforge.fmllegacy.network.NetworkRegistry;
import net.minecraftforge.fmllegacy.network.PacketDistributor;
import net.minecraftforge.fmllegacy.network.simple.SimpleChannel;
import ru.dbotthepony.mc.otm.OverdriveThatMatters;
import ru.dbotthepony.mc.otm.item.ItemPortableCondensationDrive;
import ru.dbotthepony.mc.otm.menu.DriveViewerMenu;
import ru.dbotthepony.mc.otm.network.android.*;
@ -200,5 +201,23 @@ public class MatteryNetworking {
SetCarriedPacket::play,
Optional.of(NetworkDirection.PLAY_TO_CLIENT)
);
CHANNEL.registerMessage(
next_network_id++,
DriveViewerMenu.FilterSwitchPacket.class,
DriveViewerMenu.FilterSwitchPacket::write,
DriveViewerMenu.FilterSwitchPacket::read,
DriveViewerMenu.FilterSwitchPacket::play,
Optional.of(NetworkDirection.PLAY_TO_SERVER)
);
CHANNEL.registerMessage(
next_network_id++,
DriveViewerMenu.FilterSetPacket.class,
DriveViewerMenu.FilterSetPacket::write,
DriveViewerMenu.FilterSetPacket::read,
DriveViewerMenu.FilterSetPacket::play,
Optional.of(NetworkDirection.PLAY_TO_SERVER)
);
}
}

View File

@ -1,11 +1,16 @@
package ru.dbotthepony.mc.otm.screen;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.gui.components.Button;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ClickAction;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.item.ItemStack;
import ru.dbotthepony.mc.otm.item.ItemPortableCondensationDrive;
import ru.dbotthepony.mc.otm.menu.DriveViewerMenu;
import ru.dbotthepony.mc.otm.menu.slot.MatterySlot;
import ru.dbotthepony.mc.otm.menu.widget.GaugeWidget;
@ -14,6 +19,7 @@ import ru.dbotthepony.mc.otm.screen.panels.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class DriveViewerScreen extends MatteryScreen<DriveViewerMenu> implements MatteryScreen.IMatteryScreenGaugeGetter, MatteryScreen.IMatteryScreenBatteryGetter {
@ -44,15 +50,49 @@ public class DriveViewerScreen extends MatteryScreen<DriveViewerMenu> implements
autoAttachToFrame(frame);
var view_panels = new ArrayList<EditablePanel>();
var settings_panels = new ArrayList<EditablePanel>();
var view_button = frame.addTab(FramePanel.FrameTabPosition.TOP);
var settings_button = frame.addTab(FramePanel.FrameTabPosition.TOP);
view_button.bindOnOpen(() -> {
for (var panel : view_panels) {
panel.setVisible(true);
}
});
view_button.bindOnClose(() -> {
for (var panel : view_panels) {
panel.setVisible(false);
}
});
settings_button.bindOnOpen(() -> {
for (var panel : settings_panels) {
panel.setVisible(true);
}
});
settings_button.bindOnClose(() -> {
for (var panel : settings_panels) {
panel.setVisible(false);
}
});
var grid = new GridPanel(this, frame, 0, 0, 0, 0, GRID_WIDTH, GRID_HEIGHT);
grid.setDock(Dock.FILL);
grid.setDockMargin(2, 2, 2, 2);
var scroll_bar = new ScrollBarPanel(this, frame, 0, 0, 0);
scroll_bar.setDock(Dock.RIGHT);
scroll_bar.setupRowMultiplier(() -> {
return menu.view.getItems().size() / GRID_WIDTH;
});
view_panels.add(grid);
view_panels.add(scroll_bar);
for (int i = 0; i < GRID_WIDTH * GRID_HEIGHT; i++) {
final int index = i;
@ -90,6 +130,146 @@ public class DriveViewerScreen extends MatteryScreen<DriveViewerMenu> implements
};
}
var dock_left = new FlexGridPanel(this, frame, 0, 0, 90, 0);
dock_left.setDock(Dock.LEFT);
dock_left.setDockMargin(4, 0, 4, 0);
var grid_filter = new FlexGridPanel(this, frame);
grid_filter.setDock(Dock.FILL);
settings_panels.add(dock_left);
settings_panels.add(grid_filter);
for (int i = 0; i < ItemPortableCondensationDrive.FilterSettings.MAX_FILTERS; i++) {
final int index = i;
new AbstractSlotPanel(this, grid_filter, 0, 0) {
@Nonnull
@Override
protected ItemStack getItemStack() {
var filter = menu.getFilter();
if (filter == null)
return ItemStack.EMPTY;
return filter.items[index];
}
private boolean clicking = false;
@Override
protected boolean mouseClickedInner(double mouse_x, double mouse_y, int flag) {
clicking = true;
return true;
}
@Override
protected boolean mouseReleasedInner(double mouse_x, double mouse_y, int flag) {
if (clicking)
MatteryNetworking.send(new DriveViewerMenu.FilterSetPacket(menu.containerId, index, menu.getCarried()));
clicking = false;
return true;
}
};
}
final var no = new TranslatableComponent("otm.filter.no");
final var yes = new TranslatableComponent("otm.filter.yes");
var match_nbt = new ButtonPanel(this, dock_left, 0, 0, 90, 20, new TranslatableComponent("otm.filter.match_nbt", no)) {
@Override
public void tick() {
super.tick();
var filter = menu.getFilter();
if (filter != null) {
getOrCreateWidget().setMessage(new TranslatableComponent("otm.filter.match_nbt", filter.match_nbt ? yes : no));
getOrCreateWidget().active = !isWidgetDisabled();
} else {
getOrCreateWidget().active = false;
}
}
@Override
protected void onPress() {
super.onPress();
var filter = menu.getFilter();
if (filter != null) {
MatteryNetworking.send(new DriveViewerMenu.FilterSwitchPacket(menu.containerId, DriveViewerMenu.FilterSwitch.MATCH_NBT, !filter.match_nbt));
disableFor(20);
}
}
};
var match_tag = new ButtonPanel(this, dock_left, 0, 0, 90, 20, new TranslatableComponent("otm.filter.match_tag", no)) {
@Override
public void tick() {
super.tick();
var filter = menu.getFilter();
if (filter != null) {
getOrCreateWidget().setMessage(new TranslatableComponent("otm.filter.match_tag", filter.match_tag ? yes : no));
getOrCreateWidget().active = !isWidgetDisabled();
} else {
getOrCreateWidget().active = false;
}
}
@Override
protected void onPress() {
super.onPress();
var filter = menu.getFilter();
if (filter != null) {
MatteryNetworking.send(new DriveViewerMenu.FilterSwitchPacket(menu.containerId, DriveViewerMenu.FilterSwitch.MATCH_TAG, !filter.match_tag));
disableFor(20);
}
}
};
var blacklist = new ButtonPanel(this, dock_left, 0, 0, 90, 20, new TranslatableComponent("otm.filter.blacklist", no)) {
@Override
public void tick() {
super.tick();
var filter = menu.getFilter();
if (filter != null) {
getOrCreateWidget().setMessage(new TranslatableComponent("otm.filter.blacklist", filter.blacklist ? yes : no));
getOrCreateWidget().active = !isWidgetDisabled();
} else {
getOrCreateWidget().active = false;
}
}
@Override
protected void onPress() {
super.onPress();
var filter = menu.getFilter();
if (filter != null) {
MatteryNetworking.send(new DriveViewerMenu.FilterSwitchPacket(menu.containerId, DriveViewerMenu.FilterSwitch.BLACKLIST, !filter.blacklist));
disableFor(20);
}
}
};
match_nbt.setDockMargin(0, 4, 0, 0);
match_tag.setDockMargin(0, 4, 0, 0);
blacklist.setDockMargin(0, 4, 0, 0);
for (var panel : settings_panels) {
panel.setVisible(false);
}
return frame;
}
}

View File

@ -28,6 +28,8 @@ public class EditBoxPanel extends MinecraftWidgetPanel<EditBox> {
@Override
public void tick() {
super.tick();
if (widget != null)
widget.tick();
}

View File

@ -135,6 +135,10 @@ public class EditablePanel implements GuiEventListener {
this(screen, parent, 0, 0, 10, 10);
}
public EditablePanel(@Nonnull MatteryScreen<?> screen, @Nullable EditablePanel parent, float x, float y) {
this(screen, parent, x, y, 10, 10);
}
public float getRenderX() {
return parent_x;
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.mc.otm.screen.panels;
import ru.dbotthepony.mc.otm.OverdriveThatMatters;
import ru.dbotthepony.mc.otm.screen.MatteryScreen;
import javax.annotation.Nonnull;
@ -20,12 +21,19 @@ public class FlexGridPanel extends EditablePanel {
BOTTOM_RIGHT
}
protected FlexAlign align;
protected FlexAlign align = FlexAlign.MIDDLE_CENTER;
public int panels_per_row = 1;
public FlexGridPanel(@Nonnull MatteryScreen<?> screen, @Nullable EditablePanel parent, float x, float y, float width, float height) {
super(screen, parent, x, y, width, height);
align = FlexAlign.MIDDLE_CENTER;
}
public FlexGridPanel(@Nonnull MatteryScreen<?> screen, @Nullable EditablePanel parent, float x, float y) {
super(screen, parent, x, y);
}
public FlexGridPanel(@Nonnull MatteryScreen<?> screen, @Nullable EditablePanel parent) {
super(screen, parent);
}
public FlexAlign getAlign() {
@ -71,13 +79,17 @@ public class FlexGridPanel extends EditablePanel {
desired_width = min_width;
}
// ширину на середину для позиционирования по центру
this_width /= 2;
int index = 0;
int index;
// определение высоты всех рядов вместе
float total_height = 0;
// утютю никаких goto
// зато код чище некуда!
while (desired_width <= this_width) {
index = 0;
total_height = 0;
panels_per_row = 0;
boolean calculate_row_width = true;
@ -107,8 +119,19 @@ public class FlexGridPanel extends EditablePanel {
total_height += max_height;
}
if (index + 1 < children.size() && desired_width != this_width) {
// не все панели уместились. ну чтож
desired_width = Math.min(desired_width + min_width, this_width);
} else {
break;
}
}
index = 0;
// ширину на середину для позиционирования по центру
this_width /= 2;
// определение точки по середине по высоте
float h_middle = (getHeight() - getDockPadding().bottom() - getDockPadding().top() - total_height) / 2;

View File

@ -58,6 +58,19 @@ public class FramePanel extends EditablePanel implements NarratableEntry {
this.on_close = on_close;
}
public FrameTabPanel(FrameTabPosition position) {
super(FramePanel.this.screen, FramePanel.this, 0, 0, position.width, position.height);
tab_position = position;
}
public void bindOnOpen(Runnable value) {
on_open = value;
}
public void bindOnClose(Runnable value) {
on_close = value;
}
@Override
protected void innerRender(PoseStack stack, float mouse_x, float mouse_y, float flag) {
if (tab_position == FrameTabPosition.TOP) {
@ -150,18 +163,26 @@ public class FramePanel extends EditablePanel implements NarratableEntry {
public FrameTabPanel addTab(FrameTabPosition position, Runnable on_open, Runnable on_close) {
var tab = new FrameTabPanel(position, on_open, on_close);
doAddTab(tab);
return tab;
}
protected void doAddTab(FrameTabPanel tab) {
if (top_tabs.size() == 0 && left_tabs.size() == 0 && right_tabs.size() == 0 && bottom_tabs.size() == 0) {
tab.active = true;
}
switch (position) {
switch (tab.tab_position) {
case TOP -> top_tabs.add(tab);
case LEFT -> left_tabs.add(tab);
case RIGHT -> right_tabs.add(tab);
case BOTTOM -> bottom_tabs.add(tab);
}
}
public FrameTabPanel addTab(FrameTabPosition position) {
var tab = new FrameTabPanel(position);
doAddTab(tab);
return tab;
}

View File

@ -21,6 +21,8 @@ public class MinecraftWidgetPanel<T extends AbstractWidget> extends EditablePane
protected T widget;
private final WidgetFactory<T> factory;
private int disable_ticks = 0;
@Nullable
public T getWidget() {
return widget;
@ -35,6 +37,30 @@ public class MinecraftWidgetPanel<T extends AbstractWidget> extends EditablePane
}
public void disableFor(int ticks) {
disable_ticks = ticks;
if (ticks > 0) {
getOrCreateWidget().active = false;
}
}
public boolean isWidgetDisabled() {
return disable_ticks > 0;
}
@Override
public void tick() {
super.tick();
if (disable_ticks > 0) {
disable_ticks--;
if (disable_ticks <= 0)
getOrCreateWidget().active = true;
}
}
private void recreate() {
if (widget == null) {
return;

View File

@ -48,6 +48,13 @@
"otm.android_station.research.item": "Requires %s x%s",
"otm.android_station.research.missing_predecessors": "%s needs to be researched first",
"otm.filter.yes": "Yes",
"otm.filter.no": "No",
"otm.filter.match_nbt": "Match NBT: %s",
"otm.filter.match_tag": "Match tag: %s",
"otm.filter.blacklist": "Blacklist: %s",
"otm.matter_bottler.switch_mode": "Switch work mode",
"android_feature.overdrive_that_matters.air_bags": "Air bags",