From d6fd32994f40a40f9f9f0c55ef63db8a446b789d Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Thu, 24 Sep 2026 04:38:05 -0400 Subject: [PATCH] Add writing ink and baseline link panel preparation --- NOTICE | 2 + README.md | 9 +- docs/WRITING_DATA.md | 17 ++++ gradle.properties | 2 +- .../rivencraft/config/RivencraftConfig.java | 16 +++ .../gametest/WritingDataGameTests.java | 91 ++++++++++++++++++ .../dnorberg/rivencraft/item/InkVialItem.java | 47 +++++++++ .../rivencraft/registry/ModCreativeTabs.java | 1 + .../registry/ModDataComponents.java | 7 ++ .../rivencraft/registry/ModItems.java | 6 ++ .../rivencraft/writing/WritingInkService.java | 77 +++++++++++++++ .../assets/rivencraft/lang/en_us.json | 3 + .../rivencraft/models/item/ink_vial.json | 6 ++ .../rivencraft/textures/item/ink_vial.png | Bin 0 -> 1727 bytes .../textures/item/ink_vial.png.mcmeta | 3 + .../data/rivencraft/recipe/ink_vial.json | 14 +++ 16 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/dnorberg/rivencraft/item/InkVialItem.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/WritingInkService.java create mode 100644 src/main/resources/assets/rivencraft/models/item/ink_vial.json create mode 100644 src/main/resources/assets/rivencraft/textures/item/ink_vial.png create mode 100644 src/main/resources/assets/rivencraft/textures/item/ink_vial.png.mcmeta create mode 100644 src/main/resources/data/rivencraft/recipe/ink_vial.json diff --git a/NOTICE b/NOTICE index 2218b01..f0c5c7d 100644 --- a/NOTICE +++ b/NOTICE @@ -6,6 +6,8 @@ It is not an official release of Mystcraft. Mystcraft Legacy source: https://github.com/Mystcraft/Mystcraft-Legacy Mystcraft Legacy copyright: XCompWiz and contributors Mystcraft Legacy license: GNU Lesser General Public License version 3 +The animated ink-vial item texture in this project is copied from Mystcraft +Legacy's assets/mystcraft/textures/items/ink_vial.png under that license. The initial build layout is derived from the official NeoForge 1.21.1 ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle diff --git a/README.md b/README.md index 8afd39e..8a8d153 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,13 @@ have not been ported yet. Milestone 4 now includes stackable blank Pages plus server-created symbol, unresolved-symbol, and baseline link-panel states. Page visuals follow the -legacy runtime-composition rules; written-page creation will become available -through the Writing Desk work that follows. +legacy runtime-composition rules. Craft an Ink Vial from two black dye, a glass +bottle, and a water bucket; the bucket is returned. Hold a blank Page in one +hand and use the Ink Vial in the other to prepare a baseline Link Panel Page. +Each vial holds 16 ink units, and the default link-panel cost is two units. +The spent vial returns a glass bottle. Server owners can adjust the symbol-copy +and link-panel costs in the `writing` section of `rivencraft-server.toml`. +Symbol-page transcription will become available through the Writing Desk. ## Linking Book diff --git a/docs/WRITING_DATA.md b/docs/WRITING_DATA.md index bc061c6..883627a 100644 --- a/docs/WRITING_DATA.md +++ b/docs/WRITING_DATA.md @@ -64,3 +64,20 @@ identifier. Built-in pages reproduce Mystcraft Legacy's four-word poem composition from its page background and symbol-component sheet. Data-pack symbols without a bundled resource-pack model use the legacy question-mark fallback while keeping their real identifier and display metadata. + +## Ink and baseline link panels + +An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket. +The bucket remains after crafting. Each vial holds 16 ink units, stored in a +persistent, network-synchronized component; only vials with identical remaining +ink stack together. When depleted, a vial returns a glass bottle that can be +used to craft another. The animated item texture comes from Mystcraft Legacy. + +The server config sets ink costs independently for symbol copying (default one +unit) and baseline link-panel preparation (default two units). To prepare a +panel, hold a blank Page in one hand and use an Ink Vial in the other. The +server checks the held items, blank state, and sufficient ink before consuming +anything. A stack of blank Pages yields exactly one Link Panel Page per use; +if the inventory is full, the result is dropped instead of deleted. Symbol +copying will be implemented by the Writing Desk. Link-property modifiers are +deferred beyond this baseline flow. diff --git a/gradle.properties b/gradle.properties index e48cb67..2be7fdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.3.3.4 +mod_version=0.3.4.0 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/config/RivencraftConfig.java b/src/main/java/net/dnorberg/rivencraft/config/RivencraftConfig.java index c4f6aee..0aa2af2 100644 --- a/src/main/java/net/dnorberg/rivencraft/config/RivencraftConfig.java +++ b/src/main/java/net/dnorberg/rivencraft/config/RivencraftConfig.java @@ -15,6 +15,8 @@ public final class RivencraftConfig { public static final ModConfigSpec SPEC; private static final ModConfigSpec.EnumValue RETURN_LINK_MODE; + private static final ModConfigSpec.IntValue SYMBOL_INK_COST; + private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST; static { ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); @@ -25,6 +27,12 @@ public final class RivencraftConfig { ) .defineEnum("return_link_mode", ReturnLinkMode.ON_CREATION); builder.pop(); + builder.push("writing"); + SYMBOL_INK_COST = builder.comment("Ink units consumed when the Writing Desk copies one symbol page.") + .defineInRange("symbol_ink_cost", 1, 1, 16); + LINK_PANEL_INK_COST = builder.comment("Ink units consumed when preparing one baseline link-panel page.") + .defineInRange("link_panel_ink_cost", 2, 1, 16); + builder.pop(); SPEC = builder.build(); } @@ -34,4 +42,12 @@ public final class RivencraftConfig { public static ReturnLinkMode returnLinkMode() { return RETURN_LINK_MODE.get(); } + + public static int symbolInkCost() { + return SYMBOL_INK_COST.get(); + } + + public static int linkPanelInkCost() { + return LINK_PANEL_INK_COST.get(); + } } diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java index fed17ee..a2935af 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java @@ -11,6 +11,8 @@ import net.dnorberg.rivencraft.writing.SymbolReference; import net.dnorberg.rivencraft.writing.WritingEntry; import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.item.InkVialItem; +import net.dnorberg.rivencraft.writing.WritingInkService; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.menu.NotebookMenu; @@ -200,6 +202,95 @@ public final class WritingDataGameTests { helper.succeed(); } + @GameTest(template = "empty") + public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) { + ItemStack vial = new ItemStack(ModItems.INK_VIAL.get()); + vial.set(ModDataComponents.INK_REMAINING.get(), 7); + CompoundTag serialized = (CompoundTag) vial.saveOptional(helper.getLevel().registryAccess()); + ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), serialized); + helper.assertValueEqual(InkVialItem.remaining(restored), 7, + "Remaining ink must survive ItemStack persistence"); + helper.assertTrue(ItemStack.isSameItemSameComponents(vial, restored), + "Vials with equal ink contents must be eligible to stack"); + helper.assertFalse(ItemStack.isSameItemSameComponents(vial, new ItemStack(ModItems.INK_VIAL.get())), + "Vials with different ink contents must not stack"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void linkPanelPreparationConsumesOnePageAndConfiguredInkOnlyOnSuccess(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + ItemStack pages = new ItemStack(ModItems.PAGE.get(), 2); + ItemStack vial = new ItemStack(ModItems.INK_VIAL.get()); + player.setItemInHand(InteractionHand.MAIN_HAND, vial); + player.setItemInHand(InteractionHand.OFF_HAND, pages); + + helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), + "A blank Page and sufficient ink must prepare one link panel"); + helper.assertValueEqual(pages.getCount(), 1, "Preparation must consume exactly one blank Page"); + helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), + InkVialItem.CAPACITY - WritingInkService.linkPanelCost(), + "Preparation must consume exactly the server-configured ink cost"); + long panels = player.getInventory().items.stream() + .filter(stack -> stack.is(ModItems.PAGE.get()) + && PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL) + .mapToInt(ItemStack::getCount).sum(); + helper.assertValueEqual(panels, 1L, "Preparation must output exactly one link panel"); + + ItemStack written = PageItem.createSymbolPage(reference("rivencraft:terrain/standard")); + player.setItemInHand(InteractionHand.OFF_HAND, written); + int inkBefore = InkVialItem.remaining(player.getMainHandItem()); + helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), + "A written Page must not be overwritten as a link panel"); + helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), inkBefore, + "Rejected writing must not consume ink"); + helper.assertValueEqual(PageItem.content(written).kind(), PageContent.Kind.SYMBOL, + "Rejected writing must not modify the input Page"); + + ItemStack lowInk = new ItemStack(ModItems.INK_VIAL.get()); + lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost() - 1); + player.setItemInHand(InteractionHand.MAIN_HAND, lowInk); + player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get())); + helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), + "Insufficient ink must reject link-panel preparation"); + helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), + WritingInkService.linkPanelCost() - 1, + "Insufficient ink must remain untouched"); + helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), PageContent.Kind.BLANK, + "Insufficient ink must leave the Page blank"); + + lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost()); + helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), + "Exact remaining ink must be usable"); + helper.assertTrue(player.getMainHandItem().is(Items.GLASS_BOTTLE), + "A depleted Ink Vial must return a reusable glass bottle"); + helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), + PageContent.Kind.LINK_PANEL, + "A single blank Page must be replaced by one Link Panel Page"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void stackedInkVialsSplitWithoutChangingTheOtherVials(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.INK_VIAL.get(), 2)); + player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get())); + + helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), + "A stack of full vials must support preparing one panel"); + helper.assertValueEqual(player.getMainHandItem().getCount(), 1, + "Using one vial must leave the other full vial in hand"); + helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), InkVialItem.CAPACITY, + "The unconsumed vial must remain full"); + long partialVials = player.getInventory().items.stream() + .filter(stack -> stack.is(ModItems.INK_VIAL.get()) + && InkVialItem.remaining(stack) == InkVialItem.CAPACITY - WritingInkService.linkPanelCost()) + .mapToInt(ItemStack::getCount).sum(); + helper.assertValueEqual(partialVials, 1L, + "The used vial must reappear with only its own ink reduced"); + helper.succeed(); + } + private static SymbolReference reference(String id) { return new SymbolReference(ResourceLocation.parse(id)); } diff --git a/src/main/java/net/dnorberg/rivencraft/item/InkVialItem.java b/src/main/java/net/dnorberg/rivencraft/item/InkVialItem.java new file mode 100644 index 0000000..ef35cd9 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/item/InkVialItem.java @@ -0,0 +1,47 @@ +package net.dnorberg.rivencraft.item; + +import java.util.List; +import net.dnorberg.rivencraft.registry.ModDataComponents; +import net.dnorberg.rivencraft.writing.WritingInkService; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; + +/** A stackable vial; the ink component is per vial, not shared across a stack. */ +public final class InkVialItem extends Item { + public static final int CAPACITY = 16; + + public InkVialItem(Properties properties) { + super(properties); + } + + public static int remaining(ItemStack vial) { + return vial.getOrDefault(ModDataComponents.INK_REMAINING.get(), CAPACITY); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + if (!WritingInkService.canPrepareLinkPanel(player, hand)) { + return InteractionResultHolder.pass(player.getItemInHand(hand)); + } + if (!level.isClientSide) { + WritingInkService.prepareLinkPanel(player, hand); + } + return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide); + } + + @Override + public void appendHoverText(ItemStack stack, TooltipContext context, + List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.rivencraft.ink_vial.remaining", + remaining(stack), CAPACITY).withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.translatable("item.rivencraft.ink_vial.usage") + .withStyle(ChatFormatting.GRAY)); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java b/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java index 257b688..071f725 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java @@ -21,6 +21,7 @@ public final class ModCreativeTabs { output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.PAGE.get()); output.accept(ModItems.NOTEBOOK.get()); + output.accept(ModItems.INK_VIAL.get()); }) .build()); diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java b/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java index bb022d9..e41f4f4 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java @@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.book.AgeReference; import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; +import com.mojang.serialization.Codec; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; @@ -60,6 +61,12 @@ public final class ModDataComponents { .networkSynchronized(NotebookContents.STREAM_CODEC) .cacheEncoding() .build()); + public static final DeferredHolder, DataComponentType> INK_REMAINING = + DATA_COMPONENTS.register("ink_remaining", () -> DataComponentType.builder() + .persistent(Codec.intRange(0, 16)) + .networkSynchronized(ByteBufCodecs.VAR_INT) + .cacheEncoding() + .build()); private ModDataComponents() { } diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java b/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java index 7e04ee2..6497924 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java @@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.NotebookItem; +import net.dnorberg.rivencraft.item.InkVialItem; import net.minecraft.world.item.Item; import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.registries.DeferredItem; @@ -33,6 +34,11 @@ public final class ModItems { NotebookItem::new, new Item.Properties().stacksTo(1) ); + public static final DeferredItem INK_VIAL = ITEMS.registerItem( + "ink_vial", + InkVialItem::new, + new Item.Properties().stacksTo(16) + ); private ModItems() { } diff --git a/src/main/java/net/dnorberg/rivencraft/writing/WritingInkService.java b/src/main/java/net/dnorberg/rivencraft/writing/WritingInkService.java new file mode 100644 index 0000000..e81a6ed --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/WritingInkService.java @@ -0,0 +1,77 @@ +package net.dnorberg.rivencraft.writing; + +import net.dnorberg.rivencraft.config.RivencraftConfig; +import net.dnorberg.rivencraft.item.InkVialItem; +import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.registry.ModDataComponents; +import net.dnorberg.rivencraft.registry.ModItems; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Server-owned costs and the baseline blank-page-to-link-panel transaction. */ +public final class WritingInkService { + private WritingInkService() { + } + + public static int symbolCopyCost() { + return RivencraftConfig.symbolInkCost(); + } + + public static int linkPanelCost() { + return RivencraftConfig.linkPanelInkCost(); + } + + public static boolean canPrepareLinkPanel(Player player, InteractionHand vialHand) { + ItemStack vial = player.getItemInHand(vialHand); + ItemStack page = player.getItemInHand(other(vialHand)); + int remaining = InkVialItem.remaining(vial); + return vial.is(ModItems.INK_VIAL.get()) && remaining >= linkPanelCost() + && remaining <= InkVialItem.CAPACITY + && page.is(ModItems.PAGE.get()) && PageItem.content(page).kind() == PageContent.Kind.BLANK; + } + + public static boolean prepareLinkPanel(Player player, InteractionHand vialHand) { + if (player.level().isClientSide || !canPrepareLinkPanel(player, vialHand)) { + return false; + } + + InteractionHand pageHand = other(vialHand); + ItemStack page = player.getItemInHand(pageHand); + ItemStack vial = player.getItemInHand(vialHand); + int newInk = InkVialItem.remaining(vial) - linkPanelCost(); + ItemStack panel = PageItem.createLinkPanelPage(); + ItemStack spentVial = newInk == 0 + ? new ItemStack(Items.GLASS_BOTTLE) + : new ItemStack(ModItems.INK_VIAL.get()); + if (newInk > 0) { + spentVial.set(ModDataComponents.INK_REMAINING.get(), newInk); + } + + // All validation and output construction is complete before changing either input. + if (page.getCount() == 1) { + player.setItemInHand(pageHand, panel); + } else { + page.shrink(1); + giveOrDrop(player, panel); + } + if (vial.getCount() == 1) { + player.setItemInHand(vialHand, spentVial); + } else { + vial.shrink(1); + giveOrDrop(player, spentVial); + } + return true; + } + + private static InteractionHand other(InteractionHand hand) { + return hand == InteractionHand.MAIN_HAND ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; + } + + private static void giveOrDrop(Player player, ItemStack stack) { + if (!player.getInventory().add(stack)) { + player.drop(stack, false); + } + } +} diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index 427418d..cff6eb9 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -15,6 +15,9 @@ "item.rivencraft.notebook": "Notebook", "item.rivencraft.notebook.pages": "Pages: %s/%s", "item.rivencraft.notebook.unresolved": "Unresolved pages: %s", + "item.rivencraft.ink_vial": "Ink Vial", + "item.rivencraft.ink_vial.remaining": "Ink: %s/%s", + "item.rivencraft.ink_vial.usage": "Use with a blank Page in the other hand to prepare a Link Panel.", "screen.rivencraft.notebook.rename": "Rename selected page", "screen.rivencraft.notebook.search": "Search pages", "screen.rivencraft.notebook.rename_toggle": "Rename selected page (R); use left/right arrows to reorder", diff --git a/src/main/resources/assets/rivencraft/models/item/ink_vial.json b/src/main/resources/assets/rivencraft/models/item/ink_vial.json new file mode 100644 index 0000000..51d7bf4 --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/item/ink_vial.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "rivencraft:item/ink_vial" + } +} diff --git a/src/main/resources/assets/rivencraft/textures/item/ink_vial.png b/src/main/resources/assets/rivencraft/textures/item/ink_vial.png new file mode 100644 index 0000000000000000000000000000000000000000..5397688cbc98e060f291c1a55366c19701f3a73a GIT binary patch literal 1727 zcmeAS@N?(olHy`uVBq!ia0vp^0t^gH3>?fr)}9oZhd_$4ILO_JVcj{Imp~3nx}&cn z1H;CC?mvmF3=GT+0X`wFKyg)7RW&s=b#-+O4Gm3AO)V`gZEfx8=3=W{6<4~e?g=$K z7-M-Q*K^mdU1!!WJ$LThlP6Drx<S|Sg|1f6fmDKlmz(&{~y_aA^1DHDlkep z3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33J>#Bd(*uA- zkA$a-V@QVc(_6QTniT}t9-Pv>+Lt{3*jc9og` z`1$KfvuVfj%bKSyn*R7#VSoDKSMBGg7qNe5`nOL_>WJkccTbfLJ-10#UkCI;*a%vj3E3A2*3aU(Wu=TO*XxD6C z#(q6`x!l6H3mG&g^zUTfdeHXM%i`t7r!akdQS$d;{KLkpe;W5zC6w-ey4&+Ka}Rr7 z_n$>T#xpOF2t_g2v+j4)st%M4z zw48A+=jQbj*m>SMDrg4u=j>$s{LuEyr*4})UmwUzTt6JiKi6-K`-jC9AAbCQXDEN_ zu_Zs<^AZ-Pmz|o6QcpuAj|TJdYh8>)KY-Y`*!ZHiZ=&;N58)K@prVrd+pb!o0)YNnojs! z$^YQF{7&}iG27*Ozi)q~7<