From aeb59081d9fe3a29c14d565a7eb92c2c56d74ff0 Mon Sep 17 00:00:00 2001 From: koolkrafter5 Date: Sat, 16 Aug 2025 07:24:40 -0400 Subject: [PATCH] Information Handler (#695) --- README.md | 53 +++++++ src/main/java/codechicken/nei/ItemList.java | 5 +- .../java/codechicken/nei/NEIClientConfig.java | 2 + .../codechicken/nei/config/IMCHandler.java | 104 ++++++------ .../nei/recipe/InformationHandler.java | 150 ++++++++++++++++++ .../codechicken/nei/recipe/RecipeInfo.java | 2 + .../resources/assets/nei/cfg/hiddenitems.cfg | 14 +- .../assets/nei/cfg/informationpages.cfg | 28 ++++ .../resources/assets/nei/csv/handlers.csv | 1 + src/main/resources/assets/nei/lang/en_US.lang | 2 + .../nei/textures/nei_tabbed_sprites.png | Bin 5191 -> 4662 bytes 11 files changed, 302 insertions(+), 59 deletions(-) create mode 100644 src/main/java/codechicken/nei/recipe/InformationHandler.java create mode 100644 src/main/resources/assets/nei/cfg/informationpages.cfg diff --git a/README.md b/README.md index 06ba32b3..ed4fca1d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,59 @@ If you have issues with NEI outside of the GTNH modpack you may report them in t * Cycle between Recipe, Utility, and Cheat mode by ctrl clicking on the Wrench Icon * GT5u Tools/Items and GT6 tools should now properly work with the Overlay Recipe Transfer +### Information Page Handler +Want to add some information about a block or item without making a massive tooltip for it? You can add information about any block or item by registering it in the Information Handler. + +Information page(s) are displayed when either the uses or recipes of a matching item are searched. All matching items will cycle at the top of the information page. + +Your mod can call: +```java +FMLInterModComms.sendMessage("NotEnoughItems", "addItemInfo", nbt); +``` +Where nbt is an NBTTagCompound formatted as described below. + +| Tag | Type | Description | +|------------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `filter` | String | A string filter to match what items should return this information page.
modname:itemname, metadata, Ore Dictionary tags, and regex can all be used. The full format may be seen in `config/NEI/collapsibleitems.cfg`. | +| `page` / `pages` | String / String NBTTagList | The actual text to display. See below for differences between single and multi-page messages.
Both formats will automatically translate provided strings. | + +#### Single Page Format + +Use the `"page"` string tag when you only want to add one page of information: +```java +NBTTagCompound tag = new NBTTagCompound(); +tag.setString("filter", "minecraft:log 3"); +tag.setString("page", "Exotic wood found in tropical climates."); + +FMLInterModComms.sendMessage("NotEnoughItems", "addItemInfo", tag); +``` + +#### Multiple Page Format + +Use the `"pages"` tag as an `NBTTagList` of strings when you want more than one page: +```java +NBTTagCompound tag = new NBTTagCompound(); +tag.setString("filter", "minecraft:diamond_sword"); + +NBTTagList pages = new NBTTagList(); +pages.appendTag(new NBTTagString("A powerful melee weapon.")); +pages.appendTag(new NBTTagString("Can be enchanted for extra effects.")); + +tag.setTag("pages", pages); + +FMLInterModComms.sendMessage("NotEnoughItems", "addItemInfo", tag); +``` +* Each list entry is one page. +* Pages will appear in NEI with buttons to scroll through them. + +#### Item Matching + +All items that match `filter` will show your information page. See `config/NEI/collapsibleitems.cfg` for the full filter format. + +#### Item Information Config File + +If you're a modpack creator and want to make custom item information pages, they can easily be added in `config/NEI/informationpages.cfg`, which has a few more examples. + ## Other items of note: * Remove TMI style diff --git a/src/main/java/codechicken/nei/ItemList.java b/src/main/java/codechicken/nei/ItemList.java index 88aa744d..91b28296 100644 --- a/src/main/java/codechicken/nei/ItemList.java +++ b/src/main/java/codechicken/nei/ItemList.java @@ -28,6 +28,7 @@ import codechicken.nei.ThreadOperationTimer.TimeoutException; import codechicken.nei.api.ItemFilter; import codechicken.nei.api.ItemFilter.ItemFilterProvider; import codechicken.nei.api.ItemInfo; +import codechicken.nei.recipe.InformationHandler; import codechicken.nei.search.TooltipFilter; public class ItemList { @@ -289,7 +290,7 @@ public class ItemList { .collect(Collectors.toList()); } - // For optimization it generate itemslist, permutations, orders & collapsibleitems + // Generate itemlist, permutations, orders, collapsibleitems, and informationhandler stacks @Override @SuppressWarnings("unchecked") public void execute() { @@ -303,6 +304,7 @@ public class ItemList { ItemSorter.instance.ordering.clear(); CollapsibleItems.clearCache(); TooltipFilter.clearCache(); + InformationHandler.clearCache(); List items = new ArrayList<>(); ListMultimap itemMap = ArrayListMultimap.create(); @@ -329,6 +331,7 @@ public class ItemList { CollapsibleItems.putItem(stack); TooltipFilter.getSearchTooltip(stack); + InformationHandler.populateStacks(stack); } } diff --git a/src/main/java/codechicken/nei/NEIClientConfig.java b/src/main/java/codechicken/nei/NEIClientConfig.java index f36d4bf7..a44ab846 100644 --- a/src/main/java/codechicken/nei/NEIClientConfig.java +++ b/src/main/java/codechicken/nei/NEIClientConfig.java @@ -58,6 +58,7 @@ import codechicken.nei.config.preset.GuiPresetList; import codechicken.nei.event.NEIConfigsLoadedEvent; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.IRecipeHandler; +import codechicken.nei.recipe.InformationHandler; import codechicken.nei.recipe.RecipeCatalysts; import codechicken.nei.recipe.RecipeInfo; import codechicken.nei.util.NEIKeyboardUtils; @@ -899,6 +900,7 @@ public class NEIClientConfig { LayoutManager.load(); NEIController.load(); BookmarkContainerInfo.load(); + InformationHandler.load(); mainNEIConfigLoaded = true; new Thread("NEI Plugin Loader") { diff --git a/src/main/java/codechicken/nei/config/IMCHandler.java b/src/main/java/codechicken/nei/config/IMCHandler.java index 06ab21af..62837a3c 100644 --- a/src/main/java/codechicken/nei/config/IMCHandler.java +++ b/src/main/java/codechicken/nei/config/IMCHandler.java @@ -1,13 +1,13 @@ package codechicken.nei.config; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; import codechicken.core.CommonUtils; import codechicken.nei.NEIClientConfig; @@ -15,6 +15,7 @@ import codechicken.nei.NEIServerUtils; import codechicken.nei.recipe.CatalystInfo; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.HandlerInfo; +import codechicken.nei.recipe.InformationHandler; import codechicken.nei.recipe.RecipeCatalysts; import cpw.mods.fml.common.FMLLog; import cpw.mods.fml.common.Loader; @@ -33,18 +34,11 @@ public class IMCHandler { if (type == null || type.isEmpty()) continue; if (CommonUtils.isClient()) { switch (type) { - case "registerHandlerInfo": - handleRegisterHandlerInfo(message); - break; - case "removeHandlerInfo": - handleRemoveHandlerInfo(message); - break; - case "registerCatalystInfo": - handleRegisterCatalystInfo(message); - break; - case "removeCatalystInfo": - handleRemoveCatalystInfo(message); - break; + case "registerHandlerInfo" -> handleRegisterHandlerInfo(message); + case "removeHandlerInfo" -> handleRemoveHandlerInfo(message); + case "registerCatalystInfo" -> handleRegisterCatalystInfo(message); + case "removeCatalystInfo" -> handleRemoveCatalystInfo(message); + case "addItemInfo" -> handleAddItemInfo(message); } } } @@ -57,7 +51,7 @@ public class IMCHandler { } final NBTTagCompound tag = message.getNBTValue(); final String handler = tag.getString("handler"); - NEIClientConfig.logger.info("Processing registerHandlerInfo `" + handler + "` from " + message.getSender()); + NEIClientConfig.logger.info("Processing registerHandlerInfo `{}` from {}", handler, message.getSender()); final String modName = tag.getString("modName"); final String modId = tag.getString("modId"); @@ -100,7 +94,7 @@ public class IMCHandler { : HandlerInfo.DEFAULT_MAX_PER_PAGE; info.setHandlerDimensions(imageHeight, imageWidth, maxRecipesPerPage); } catch (NumberFormatException ignored) { - NEIClientConfig.logger.info("Error setting handler dimensions for " + handler); + NEIClientConfig.logger.info("Error setting handler dimensions for {}", handler); } // true if not set to false @@ -118,7 +112,7 @@ public class IMCHandler { } final NBTTagCompound tag = message.getNBTValue(); final String handler = tag.getString("handler"); - NEIClientConfig.logger.info("Processing removeHandlerInfo `" + handler + "` from " + message.getSender()); + NEIClientConfig.logger.info("Processing removeHandlerInfo `{}` from {}", handler, message.getSender()); GuiRecipeTab.handlerRemoverFromIMC.add(handler); } @@ -130,7 +124,7 @@ public class IMCHandler { } if (!processedCatalystSenders.contains(message.getSender())) { - NEIClientConfig.logger.info("Processing registerCatalystInfo from " + message.getSender()); + NEIClientConfig.logger.info("Processing registerCatalystInfo from {}", message.getSender()); processedCatalystSenders.add(message.getSender()); } final NBTTagCompound tag = message.getNBTValue(); @@ -139,23 +133,14 @@ public class IMCHandler { NEIClientConfig.logger.warn("Missing handlerID for registerCatalystInfo!"); return; } - final String itemName = tag.getString("itemName"); - final String nbtInfo = tag.hasKey("nbtInfo") ? tag.getString("nbtInfo") : null; - if (itemName.isEmpty()) { - NEIClientConfig.logger.warn(String.format("Missing itemName for registerCatalystInfo in `%s`!", handlerID)); - return; - } - final ItemStack itemStack = NEIServerUtils.getModdedItem(itemName, nbtInfo); + final ItemStack itemStack = getItemStackFromIMC(message, "registerCatalystInfo"); if (itemStack == null) { - NEIClientConfig.logger.warn(String.format("Cannot find item `%s`!", itemName)); return; } final int priority = tag.getInteger("priority"); - RecipeCatalysts .addOrPut(RecipeCatalysts.catalystsAdderFromIMC, handlerID, new CatalystInfo(itemStack, priority)); - NEIClientConfig.logger - .info(String.format("Added catalyst `%s` to handler %s", itemStack.getDisplayName(), handlerID)); + NEIClientConfig.logger.info("Added catalyst `{}` to handler {}", itemStack.getDisplayName(), handlerID); } private static void handleRemoveCatalystInfo(IMCMessage message) { @@ -164,33 +149,62 @@ public class IMCHandler { return; } - NEIClientConfig.logger.info("Processing removeCatalystInfo from " + message.getSender()); final NBTTagCompound tag = message.getNBTValue(); final String handlerID = tag.getString("handlerID"); if (handlerID.isEmpty()) { - NEIClientConfig.logger.warn("Missing handlerID for registerCatalystInfo!"); + NEIClientConfig.logger.warn("Missing handlerID for removeCatalystInfo!"); return; } - final String itemName = tag.getString("itemName"); - final String nbtInfo = tag.hasKey("nbtInfo") ? tag.getString("nbtInfo") : null; - if (itemName.isEmpty()) { - NEIClientConfig.logger.warn(String.format("Missing itemName for registerCatalystInfo in `%s`!", handlerID)); - return; - } - final ItemStack itemStack = NEIServerUtils.getModdedItem(itemName, nbtInfo); + final ItemStack itemStack = getItemStackFromIMC(message, "removeCatalystInfo"); if (itemStack == null) { - NEIClientConfig.logger.warn(String.format("Cannot find item `%s`!", itemName)); return; } - if (RecipeCatalysts.catalystsRemoverFromIMC.containsKey(handlerID)) { - RecipeCatalysts.catalystsRemoverFromIMC.get(handlerID).add(itemStack); - } else { - RecipeCatalysts.catalystsRemoverFromIMC - .put(handlerID, new ArrayList<>(Collections.singletonList(itemStack))); + RecipeCatalysts.catalystsRemoverFromIMC.computeIfAbsent(handlerID, k -> new ArrayList<>()).add(itemStack); + NEIClientConfig.logger.info("Removed catalyst `{}` from handler {}", itemStack.getDisplayName(), handlerID); + } + + private static void handleAddItemInfo(IMCMessage message) { + if (!message.isNBTMessage()) { + logInvalidMessage(message, "NBT"); + return; } - NEIClientConfig.logger - .info(String.format("Removed catalyst `%s` from handler %s", itemStack.getDisplayName(), handlerID)); + + final NBTTagCompound tag = message.getNBTValue(); + if (tag.hasKey("pages")) addMultipleInfoPages(message); + if (tag.hasKey("page")) addSingleInfoPage(message); + } + + private static void addMultipleInfoPages(IMCMessage message) { + final NBTTagCompound tag = message.getNBTValue(); + final String filter = tag.getString("filter"); + final NBTTagList pages = tag.getTagList("pages", 8); // 8 = TAG_String + for (int i = 0; i < pages.tagCount(); i++) { + String page = pages.getStringTagAt(i); + InformationHandler.addInformationPage(filter, page); + } + } + + private static void addSingleInfoPage(IMCMessage message) { + final NBTTagCompound tag = message.getNBTValue(); + String filter = tag.getString("filter"); + String page = tag.getString("page"); + InformationHandler.addInformationPage(filter, page); + } + + private static ItemStack getItemStackFromIMC(IMCMessage message, String logAction) { + final NBTTagCompound tag = message.getNBTValue(); + final String itemName = tag.getString("itemName"); + final String nbtInfo = tag.hasKey("nbtInfo") ? tag.getString("nbtInfo") : null; + + NEIClientConfig.logger.info("Processing {} for item `{}` from {}", logAction, itemName, message.getSender()); + final ItemStack itemStack = NEIServerUtils.getModdedItem(itemName, nbtInfo); + + if (itemStack == null) { + NEIClientConfig.logger.warn("Cannot find item `{}`!", itemName); + } + + return itemStack; } private static void logInvalidMessage(FMLInterModComms.IMCMessage message, String type) { diff --git a/src/main/java/codechicken/nei/recipe/InformationHandler.java b/src/main/java/codechicken/nei/recipe/InformationHandler.java new file mode 100644 index 00000000..d59b7f5b --- /dev/null +++ b/src/main/java/codechicken/nei/recipe/InformationHandler.java @@ -0,0 +1,150 @@ +package codechicken.nei.recipe; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.StatCollector; + +import codechicken.nei.ClientHandler; +import codechicken.nei.NEIClientConfig; +import codechicken.nei.NEIClientUtils; +import codechicken.nei.PositionedStack; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.util.ItemStackFilterParser; + +public class InformationHandler extends TemplateRecipeHandler { + + private static final List ITEM_INFO = new ArrayList<>(); + + public static void addInformationPage(String filter, String description) { + if (filter.isEmpty() || description.isEmpty()) return; + ITEM_INFO.add(new InformationPage(filter, description)); + } + + public static void populateStacks(ItemStack stack) { + for (InformationPage page : ITEM_INFO) { + page.addItem(stack); + } + } + + @Override + public void drawExtras(int recipe) { + CachedInfoPage page = (CachedInfoPage) this.arecipes.get(recipe); + drawWrappedText(StatCollector.translateToLocal(page.getPage().info).replace("\\n", "\n"), 4, 24); + } + + private void drawWrappedText(String text, int x, int y) { + FontRenderer font = Minecraft.getMinecraft().fontRenderer; + List lines = font.listFormattedStringToWidth(text, 156); + for (String line : lines) { + font.drawString(line, x, y, 0); + y += 10; + } + } + + public static void clearCache() { + for (InformationPage page : ITEM_INFO) { + page.items.clear(); + } + } + + @Override + public String getRecipeName() { + return NEIClientUtils.translate("recipe.information"); + } + + @Override + public String getOverlayIdentifier() { + return "information"; + } + + @Override + public void loadCraftingRecipes(ItemStack result) { + for (InformationPage page : ITEM_INFO) { + if (page.filter.matches(result)) { + arecipes.add(new CachedInfoPage(page)); + } + } + } + + @Override + public void loadUsageRecipes(ItemStack ingredient) { + loadCraftingRecipes(ingredient); + } + + @Override + public String getGuiTexture() { + return "nei:textures/gui/recipebg.png"; + } + + private class CachedInfoPage extends CachedRecipe { + + private final InformationPage page; + private final PositionedStack stack; + + public CachedInfoPage(InformationPage page) { + this.page = page; + stack = new PositionedStack(page.items, 75, 2); + } + + @Override + public PositionedStack getResult() { + return null; + } + + @Override + public List getIngredients() { + return getCycledIngredients(cycleticks / 20, Collections.singletonList(this.stack)); + } + + public InformationPage getPage() { + return page; + } + } + + private static class InformationPage { + + final List items = new ArrayList<>(); + final String info; + final ItemFilter filter; + + public InformationPage(String filter, String info) { + this.filter = ItemStackFilterParser.parse(filter.trim()); + this.info = info; + } + + /** + * Adds the item stack if it matches the filter. + */ + public void addItem(ItemStack stack) { + if (filter.matches(stack)) items.add(stack); + } + } + + public static void load() { + ClientHandler.loadSettingsFile( + "informationpages.cfg", + lines -> parseFile(lines.collect(Collectors.toCollection(ArrayList::new)))); + } + + private static void parseFile(List lines) { + for (String rawLine : lines) { + String line = rawLine.trim(); + + int sepIndex = line.indexOf('='); + if (sepIndex == -1) { + NEIClientConfig.logger.warn("[NEI Info] Invalid line (no '=') in config: {}", line); + continue; + } + + String filter = line.substring(0, sepIndex).trim(); + String description = line.substring(sepIndex + 1).trim(); + addInformationPage(filter, description); + } + } +} diff --git a/src/main/java/codechicken/nei/recipe/RecipeInfo.java b/src/main/java/codechicken/nei/recipe/RecipeInfo.java index e5703230..c1c33020 100644 --- a/src/main/java/codechicken/nei/recipe/RecipeInfo.java +++ b/src/main/java/codechicken/nei/recipe/RecipeInfo.java @@ -101,6 +101,8 @@ public class RecipeInfo { API.registerUsageHandler(new BrewingRecipeHandler()); API.registerRecipeHandler(new FuelRecipeHandler()); API.registerUsageHandler(new FuelRecipeHandler()); + API.registerRecipeHandler(new InformationHandler()); + API.registerUsageHandler(new InformationHandler()); API.registerGuiOverlay(GuiCrafting.class, "crafting"); API.registerGuiOverlay(GuiInventory.class, "crafting2x2", 63, 20); diff --git a/src/main/resources/assets/nei/cfg/hiddenitems.cfg b/src/main/resources/assets/nei/cfg/hiddenitems.cfg index 262fa7c5..ce0cd6f8 100644 --- a/src/main/resources/assets/nei/cfg/hiddenitems.cfg +++ b/src/main/resources/assets/nei/cfg/hiddenitems.cfg @@ -1,18 +1,6 @@ # Hidden Items Filters # -# parts: -# modname:itemid - identify -# $orename - ore dictionary -# tag.color=red - tag -# 0 or 0-12 - damage -# -# modifiers: -# ! - logical not. exclude items that match the following expression (!minecraft:portal) -# r/.../ - standard java regex (r/^m\w{6}ft$/ = minecraft) -# , - logical or in token (minecraft:potion 16384-16462,!16386) -# | - logical or multi-item search (wrench|hammer) -# -# example: minecraft:potion 16384-16462,!16386 | $oreiron | tag.color=red +# Uses the same filter format at collapsibleitems.cfg # minecraft:portal minecraft:end_portal diff --git a/src/main/resources/assets/nei/cfg/informationpages.cfg b/src/main/resources/assets/nei/cfg/informationpages.cfg new file mode 100644 index 00000000..7abf966b --- /dev/null +++ b/src/main/resources/assets/nei/cfg/informationpages.cfg @@ -0,0 +1,28 @@ +# NEI Item Information Config +# +# One page per line, in the format: +# filter = description text +# +# The filter uses the same format as collapsibleitems.cfg. +# +# You can repeat the same filter on multiple lines to add extra pages. +# Use \n to insert line breaks inside the description. Use it multiple times for a larger gap between lines. +# Using a translation key for the description will automatically translate it. +# Only the first '=' after the filter is treated as the separator; feel free to put them in your descriptions. +# Spaces before and after '=' are removed from the description and are optional. +# The same item can match filters for multiple pages, unlike collapsibleitems.cfg only putting them in the first matching group. +# +# Empty lines and lines starting with # are ignored. +# +# Examples: +# # Jungle Wood (meta 3) +# minecraft:log 3 = Exotic wood found in tropical climates. +# +# # Diamond Sword (two pages of information) +# minecraft:diamond_sword = A powerful melee weapon. Does 8 damage total. +# minecraft:diamond_sword = Can be enchanted for extra effects. +# +# # Two different ore dictionary tags. Iron ingots match both. +# $iron = Iron is a very versatile material. It can be found below sea level in the Overworld. +# $ingot = Ingots are the refined form of metal or clay. +# diff --git a/src/main/resources/assets/nei/csv/handlers.csv b/src/main/resources/assets/nei/csv/handlers.csv index ba9096d2..10e9d557 100644 --- a/src/main/resources/assets/nei/csv/handlers.csv +++ b/src/main/resources/assets/nei/csv/handlers.csv @@ -5,6 +5,7 @@ codechicken.nei.recipe.FurnaceRecipeHandler,Minecraft,minecraft:furnace,,minecra codechicken.nei.recipe.ShapedRecipeHandler,Minecraft,minecraft:crafting_table,,minecraft,FALSE,,,65,166,5,,,,,, codechicken.nei.recipe.ShapelessRecipeHandler,Minecraft,minecraft:crafting_table,,minecraft,FALSE,,,65,166,5,,,,,, codechicken.nei.recipe.FuelRecipeHandler,Minecraft,,,minecraft,FALSE,,,65,166,5,nei:textures/nei_tabbed_sprites.png,80,0,14,14, +codechicken.nei.recipe.InformationHandler,Not Enough Items,,,nei,FALSE,,,65,166,1,nei:textures/nei_tabbed_sprites.png,96,0,16,16, advsolar.client.nei.MTRecipeHandler,Advanced Solar Panels,AdvancedSolarPanel:BlockMolecularTransformer,,AdvancedSolarPanel,TRUE,,,68,,5,,,,,, appeng.integration.modules.NEIHelpers.NEIAEShapedRecipeHandler,Applied Energistics 2,appliedenergistics2:item.ItemMultiPart:380,,appliedenergistics2,TRUE,,,65,,5,,,,,, appeng.integration.modules.NEIHelpers.NEIAEShapelessRecipeHandler,Applied Energistics 2,appliedenergistics2:item.ItemMultiPart:380,,appliedenergistics2,TRUE,,,65,,5,,,,,, diff --git a/src/main/resources/assets/nei/lang/en_US.lang b/src/main/resources/assets/nei/lang/en_US.lang index 9b81b45e..f5ea555d 100644 --- a/src/main/resources/assets/nei/lang/en_US.lang +++ b/src/main/resources/assets/nei/lang/en_US.lang @@ -513,6 +513,8 @@ nei.recipe.favorite=Favorite nei.recipe.favorite.bookmark_recipe=Save Recipe Tree nei.recipe.favorite.change_result_index=Change Output +nei.recipe.information=Information + nei.itemsort.minecraft=Minecraft nei.itemsort.minecraft.tip=Sorts items from vanilla minecraft first nei.itemsort.mod=Mod diff --git a/src/main/resources/assets/nei/textures/nei_tabbed_sprites.png b/src/main/resources/assets/nei/textures/nei_tabbed_sprites.png index ec3c6e42910222c8bfc6bae5a4e03391ab5ff4cb..e752d943672448d3376dd9f18d2789710009d064 100644 GIT binary patch delta 4367 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 JPDHLkV1nw->3RSF literal 5191 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 KoTuqreegft0?%jw