From 18e9c71cada35858afa3cc002bf2f91378088c58 Mon Sep 17 00:00:00 2001 From: rainer Date: Fri, 22 Aug 2025 01:59:14 +0200 Subject: [PATCH] init --- LICENSE | 21 + README.md | 89 ++ custom.recipes.lua | 12 + default/README.txt | 2 + default/recipes.lua | 12 + init.lua | 938 ++++++++++++++++ init.lua.bak.1 | 869 +++++++++++++++ init.lua.bak.2 | 901 ++++++++++++++++ init.lua.bak.3 | 925 ++++++++++++++++ init.lua.bak.4 | 931 ++++++++++++++++ init.lua.bak.5 | 938 ++++++++++++++++ lib/smartfs.lua | 1440 +++++++++++++++++++++++++ mod.conf | 5 + models/tpad-mesh.obj | 60 ++ notify.lua | 79 ++ screenshots/admin-settings.png | Bin 0 -> 306798 bytes screenshots/crafting.png | Bin 0 -> 22125 bytes screenshots/global-network-admin.png | Bin 0 -> 294699 bytes screenshots/global-network.png | Bin 0 -> 243104 bytes screenshots/local-network-visitor.png | Bin 0 -> 265064 bytes screenshots/local-network.png | Bin 0 -> 274598 bytes screenshots/pads.png | Bin 0 -> 345635 bytes sounds/tpad_teleport.ogg | Bin 0 -> 12352 bytes storage.lua | 113 ++ textures/tpad-texture.png | Bin 0 -> 4015 bytes textures/tpad_bg_transparent.png | Bin 0 -> 141 bytes textures/tpad_bg_white.png | Bin 0 -> 142 bytes textures/tpad_particle.png | Bin 0 -> 498 bytes 28 files changed, 7335 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 custom.recipes.lua create mode 100644 default/README.txt create mode 100644 default/recipes.lua create mode 100644 init.lua create mode 100644 init.lua.bak.1 create mode 100644 init.lua.bak.2 create mode 100644 init.lua.bak.3 create mode 100644 init.lua.bak.4 create mode 100644 init.lua.bak.5 create mode 100644 lib/smartfs.lua create mode 100644 mod.conf create mode 100644 models/tpad-mesh.obj create mode 100644 notify.lua create mode 100644 screenshots/admin-settings.png create mode 100644 screenshots/crafting.png create mode 100644 screenshots/global-network-admin.png create mode 100644 screenshots/global-network.png create mode 100644 screenshots/local-network-visitor.png create mode 100644 screenshots/local-network.png create mode 100644 screenshots/pads.png create mode 100644 sounds/tpad_teleport.ogg create mode 100644 storage.lua create mode 100644 textures/tpad-texture.png create mode 100644 textures/tpad_bg_transparent.png create mode 100644 textures/tpad_bg_white.png create mode 100644 textures/tpad_particle.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bac5f3d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 entuland + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2574c31 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# tpad +A teleporter-pads mod for Minetest + +Developed and tested on Minetest 0.4.16 - try in other versions at your own risk :) + +WIP mod forum discussion: https://forum.minetest.net/viewtopic.php?f=9&t=20081 + +**Table of Contents** +- [Recipe](#recipe) +- [Features](#features) +- [Appearance](#appearance) +- [Pad types](#pad-types) +- [Pad interaction](#pad-interaction) +- [Closest pad waypoint](#closest-pad-waypoint) +- [Pad admin](#pad-admin) +- [Screenshots](#screenshots) + +## Recipe +The recipe can be customized altering the file `custom.recipes.lua`, created in the mod's folder on first run and never overwritten. + + W = any wood planks + B = bronze ingot + + WBW + BWB + WBW + +![Crafting](/screenshots/crafting.png) + +## Features + +With these pads players can build their own Local Network and collaborate to build a Global Network shared among all players. + +Pads are sorted by name in the lists, the Global Network list groups them by owner name first. + +## Appearance + +This is how a pad looks like when placed against a wall or on the floor (they can be placed under the ceiling as well): + +![Pads](/screenshots/pads.png) + +## Pad types + +Each pad can be set as one of these three types: +- `Private` (default): only accessible to its owner or by an admin +- `Public`: accessible to anyone from any Public pad of the owner's Local Network +- `Global`: accessible to anyone from any Public pad + +A pad can be edited and destroyed only by its owner or by an admin. + +## Pad interaction + +- place a pad down, it will be immediately active and set as "Private" +- right click a pad edit its name/type and to access the Networks +- select a pad from a list and hit "Teleport", or doubleclick on the list item +- delete any remote pad by selecting it on the Local Network list and clicking "Delete" + +## Closest pad waypoint + +Issue `/tpad` on the chat to get a waypoint to the closest of your pads. +Issue `/tpad off` to remove the waypoint from the HUD - it will also be removed when you teleport from any pad. + +## Pad admin + +A `tpad_admin` privilege is available, players with such privilege can access, alter and destroy any pad, they can set the max number of total / global pads a player can create and they can also place any amount of pads regardless of those limits. + +Limits can be edited by admins directly in the admin interface (reachable from the "Global Network" dialog of any pad); limits get stored on a per-world basis in the file `/mod_storage/tpad.custom.conf`. By default a player can place up to 100 pads, and of these, only 4 can appear in the Global Network. + +## Screenshots + +A public pad's interface seen by a visitor: + +![Local Network Visitor](/screenshots/local-network-visitor.png) + +The same interface seen by its owner or by an admin (notice the highlighted "private" pad, which was hidden in the previous interface): + +![Local Network](/screenshots/local-network.png) + +The Global Network interface seen by a non-admin: + +![Global Network](/screenshots/global-network.png) + +Same as above, but the admins can see an "Admin" button: + +![Global Network Admin](/screenshots/global-network-admin.png) + +Admin settings interface: + +![Admin Settings](/screenshots/admin-settings.png) diff --git a/custom.recipes.lua b/custom.recipes.lua new file mode 100644 index 0000000..0857e5e --- /dev/null +++ b/custom.recipes.lua @@ -0,0 +1,12 @@ +-- only alter this file if it's named "custom.recipes.lua" +-- alter the recipes as you please and delete / comment out +-- the recipes you don't want to be available in the game +-- the original versions are in "default/recipes.lua" + +return { + ["tpad:tpad"] = { + {'group:wood', 'default:bronze_ingot', 'group:wood'}, + {'default:bronze_ingot', 'group:wood', 'default:bronze_ingot'}, + {'group:wood', 'default:bronze_ingot', 'group:wood'}, + }, +} diff --git a/default/README.txt b/default/README.txt new file mode 100644 index 0000000..cb168bb --- /dev/null +++ b/default/README.txt @@ -0,0 +1,2 @@ +please do not edit any file in this folder, +corresponding custom.* files get created in the main mod's folder for you to customize diff --git a/default/recipes.lua b/default/recipes.lua new file mode 100644 index 0000000..0857e5e --- /dev/null +++ b/default/recipes.lua @@ -0,0 +1,12 @@ +-- only alter this file if it's named "custom.recipes.lua" +-- alter the recipes as you please and delete / comment out +-- the recipes you don't want to be available in the game +-- the original versions are in "default/recipes.lua" + +return { + ["tpad:tpad"] = { + {'group:wood', 'default:bronze_ingot', 'group:wood'}, + {'default:bronze_ingot', 'group:wood', 'default:bronze_ingot'}, + {'group:wood', 'default:bronze_ingot', 'group:wood'}, + }, +} diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..737a289 --- /dev/null +++ b/init.lua @@ -0,0 +1,938 @@ +-- ======================================================================== +-- TPAD MOD v1.2 (Final, Reworked Delete Logic) +-- ======================================================================== + +tpad = {} +tpad.version = "1.2" -- As requested, version is not incremented +tpad.mod_name = minetest.get_current_modname() +tpad.texture = "tpad-texture.png" +tpad.mesh = "tpad-mesh.obj" +tpad.nodename = "tpad:tpad" +tpad.mod_path = minetest.get_modpath(tpad.mod_name) +tpad.sound_teleport = tpad.mod_name .. "_teleport" +tpad.particle_texture = tpad.mod_name .. "_particle.png" + +-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog +tpad.pending_deletion = {} + +-- ======================================================================== +-- Constants +-- ======================================================================== + +local PRIVATE_PAD_STRING = "Privat (nur Besitzer)" +local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)" +local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)" + +local PRIVATE_PAD = 1 +local PUBLIC_PAD = 2 +local GLOBAL_PAD = 4 + +local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") +local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00") +local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF") +local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF") +local OWNER_ESCAPE_COLOR = CYAN_ESCAPE + +local padtype_flag_to_string = { + [PRIVATE_PAD] = PRIVATE_PAD_STRING, + [PUBLIC_PAD] = PUBLIC_PAD_STRING, + [GLOBAL_PAD] = GLOBAL_PAD_STRING, +} +local padtype_string_to_flag = { + [PRIVATE_PAD_STRING] = PRIVATE_PAD, + [PUBLIC_PAD_STRING] = PUBLIC_PAD, + [GLOBAL_PAD_STRING] = GLOBAL_PAD, +} +local short_padtype_string = { + [PRIVATE_PAD] = "private", + [PUBLIC_PAD] = "public", + [GLOBAL_PAD] = "global", +} + +-- ======================================================================== +-- Dependencies and Libs +-- ======================================================================== + +local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua") +local notify = dofile(tpad.mod_path .. "/notify.lua") + +local waypoint_hud_ids = {} + +minetest.register_privilege("tpad_admin", { + description = "Can edit and destroy any tpad", + give_to_singleplayer = true, +}) + +-- ======================================================================== +-- Original Helper Functions +-- ======================================================================== + +local function copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then return false, "copy_file() unable to open source for reading" end + local src_data = src_file:read("*all") + src_file:close() + local dest_file = io.open(dest, "wb") + if not dest_file then return false, "copy_file() unable to open dest for writing" end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +local function custom_or_default(modname, path, filename) + local default_filename = "default/" .. filename + local full_filename = path .. "/custom." .. filename + local full_default_filename = path .. "/" .. default_filename + local file_exists_at_path = io.open(path .. "/" .. filename, "r") + if file_exists_at_path then + file_exists_at_path:close() + os.rename(path .. "/" .. filename, full_filename) + end + local file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")") + local success, err = copy_file(full_default_filename, full_filename) + if not success then + minetest.debug("[" .. modname .. "] " .. err) + return false + end + file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path) + return false + end + end + file:close() + return full_filename +end + +dofile(tpad.mod_path .. "/storage.lua") + +-- ======================================================================== +-- Load Custom Recipe +-- ======================================================================== + +local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua") +if recipes_filename then + local recipes = dofile(recipes_filename) + if type(recipes) == "table" and recipes[tpad.nodename] then + minetest.register_craft({ + output = tpad.nodename, + recipe = recipes[tpad.nodename], + }) + end +end + +-- ======================================================================== +-- Chat Command +-- ======================================================================== + +function tpad.command(playername, param) + tpad.hud_off(playername) + if(param == "off") then return end + local player = minetest.get_player_by_name(playername) + local pads = tpad._get_stored_pads(playername) + local shortest_distance = nil + local closest_pad = nil + local playerpos = player:getpos() + for strpos, pad in pairs(pads) do + local pos = minetest.string_to_pos(strpos) + local distance = vector.distance(pos, playerpos) + if not shortest_distance or distance < shortest_distance then + closest_pad = { + pos = pos, + name = pad.name .. " " .. strpos, + } + shortest_distance = distance + end + end + if closest_pad then + waypoint_hud_ids[playername] = player:hud_add({ + hud_elem_type = "waypoint", + name = closest_pad.name, + world_pos = closest_pad.pos, + number = 0xFF0000, + }) + notify(playername, "Waypoint to " .. closest_pad.name .. " displayed") + end +end + +function tpad.hud_off(playername) + local player = minetest.get_player_by_name(playername) + local hud_id = waypoint_hud_ids[playername] + if hud_id then + player:hud_remove(hud_id) + end +end + +-- ======================================================================== +-- Teleport Logic +-- ======================================================================== + +function tpad.do_teleport(player, destination_pos, destination_name, form_context) + local playername = player:get_player_name() + + minetest.after(0.1, function() + if not player or not player:is_player() then return end + local start_pos = player:getpos() + minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5), + minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + + player:move_to(destination_pos) + + minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}), + maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}), + minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + tpad.hud_off(playername) + + -- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren + minetest.after(0.2, function() + if not player or not player:is_player() then return end + + local dest_pad_data = tpad.get_pad_data(destination_pos) + if not dest_pad_data then return end + + local new_context = { + playername = playername, + clicker = player, + ownername = dest_pad_data.owner, + clicked_pos = destination_pos, + node = minetest.get_node(destination_pos), + page = 1, + -- Behalte den Netzwerk-Typ (lokal/global) bei + network_type = form_context.network_type + } + tpad.show_network_view(new_context, new_context.network_type) + end) + end) +end + +-- ======================================================================== +-- Node Placement and Data Helpers +-- ======================================================================== + +function tpad.after_place_node(pos, placer, itemstack) + local playername = placer:get_player_name() + if tpad.max_total_pads_reached(placer) then + notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.") + minetest.remove_node(pos) + minetest.add_item(placer:get_pos(), itemstack:get_name()) + return + end + + local meta = minetest.get_meta(pos) + meta:set_string("owner", playername) + meta:set_string("infotext", "TPAD Station von " .. playername) + tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING) +end + +local submit = {} + +function tpad.max_total_pads_reached(placer) + local placername = placer:get_player_name() + if minetest.get_player_privs(placername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(placername) + local count = 0 + for _ in pairs(pads) do + count = count + 1 + end + return count >= tpad.get_max_total_pads() +end + +function tpad.max_global_pads_reached(playername) + if minetest.get_player_privs(playername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(playername) + local count = 0 + for _, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + count = count + 1 + end + end + return count >= tpad.get_max_global_pads() +end + +-- ======================================================================== +-- GUI DATA HELPERS +-- ======================================================================== + +function submit.global_helper() + local allpads = tpad._get_all_pads() + local result = {} + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + -- Der 'viewername' ist hier nicht bekannt, also wird 'nil' übergeben, was die Funktion korrekt behandelt. + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, nil)) + end + end + end + table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end) + return result +end + +function submit.local_helper(ownername, viewername) + local result = {} + local added_pads = {} + + local owner_pads = tpad._get_stored_pads(ownername) + for strpos, pad in pairs(owner_pads) do + local is_viewer_the_owner = (ownername == viewername) + if (pad.type == PUBLIC_PAD) or (is_viewer_the_owner and pad.type == PRIVATE_PAD) then + if pad.type ~= GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, viewername)) + added_pads[strpos] = true + end + end + end + + if ownername ~= viewername then + local viewer_pads = tpad._get_stored_pads(viewername) + for strpos, pad in pairs(viewer_pads) do + if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then + table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername, viewername)) + added_pads[strpos] = true + end + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +function submit.management_helper(playername) + local is_admin = minetest.get_player_privs(playername).tpad_admin + local result = {} + + if is_admin then + local allpads = tpad._get_all_pads() + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, playername)) + end + end + else + local pads = tpad._get_stored_pads(playername) + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, playername, playername)) + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +-- ======================================================================== +-- GUI LOGIC (REFACTORED) +-- ======================================================================== + +function tpad.show_network_view(form, network_type) + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local form_key = is_admin and "network_view_admin" or "network_view" + form.state = tpad.forms[form_key]:show(form.playername) + form.formname = "tpad.forms." .. form_key + + local pad_list + local title_text + + local function create_clean_context() + return { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = form.page or 1 + } + end + + local current_pad_data = tpad.get_pad_data(form.clicked_pos) + local current_pad_name = current_pad_data.name or "Unnamed" + + local current_pad_type_str + if current_pad_data.type == GLOBAL_PAD then + current_pad_type_str = "GLOBALE" + else + current_pad_type_str = "LOKALE" + end + + local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or "" + title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:" + + if network_type == "global" then + form.network_type = "global" + pad_list = submit.global_helper() + form.state:get("toggle_network_button"):setText("Lokales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + -- KORREKTUR: Der 'ownername' wird nicht mehr überschrieben. + -- Er bleibt der des ursprünglichen Pads, was korrekt ist. + clean_form.page = 1 + tpad.show_network_view(clean_form, "local") + end) + end) + else -- "local" + form.network_type = "local" + -- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean). + pad_list = submit.local_helper(form.ownername, form.playername) + form.state:get("toggle_network_button"):setText("Globales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = 1 + tpad.show_network_view(clean_form, "global") + end) + end) + end + + form.state:get("title_label"):setText(title_text) + form.state:get("management_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.preselect_pos = clean_form.clicked_pos + submit.management(clean_form) + end) + end) + + if is_admin then + form.state:get("admin_button"):onClick(function() + minetest.after(0, function() submit.admin_settings(form) end) + end) + end + + local max_rows, num_columns = 10, 3 + local buttons_per_page = max_rows * num_columns + + local destination_pads = {} + for _, pad in ipairs(pad_list) do + if not vector.equals(pad.pos, form.clicked_pos) then + table.insert(destination_pads, pad) + end + end + + local total_pages = math.ceil(#destination_pads / buttons_per_page) + if total_pages == 0 then total_pages = 1 end + + local current_page = form.page or 1 + if current_page > total_pages then current_page = total_pages end + if current_page < 1 then current_page = 1 end + form.page = current_page + + for i = 1, buttons_per_page do + local index = ((current_page - 1) * buttons_per_page) + i + local pad_data = destination_pads[index] + local button_name = "tpad_btn_" .. i + local button = form.state:get(button_name) + + if button then + if pad_data then + local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname + button:setText(display_name) + button:setVisible(true) + button:onClick(function() + tpad.do_teleport(form.clicker, pad_data.pos, display_name, form) + end) + else + button:setVisible(false) + end + end + end + + if total_pages > 1 then + form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages) + local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button") + prev_button:setVisible(current_page > 1) + next_button:setVisible(current_page < total_pages) + + prev_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page - 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + next_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page + 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + else + form.state:get("page_label"):setText("") + form.state:get("prev_button"):setVisible(false) + form.state:get("next_button"):setVisible(false) + end +end + +function submit.management(form) + form.formname = "tpad_management" + form.state = tpad.forms.management:show(form.playername) + local pad_list = submit.management_helper(form.playername) + local listbox = form.state:get("pads_listbox") + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local selected_pad_data = nil + + listbox:clearItems() + for _, pad in ipairs(pad_list) do + local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")" + if is_admin then + label = label .. " [" .. pad.owner .. "]" + end + listbox:addItem(label) + end + + local padname_field = form.state:get("padname_field") + local padtype_dropdown = form.state:get("padtype_dropdown") + + local function update_fields_for_index(index) + if not index or index <= 0 then return end + selected_pad_data = pad_list[index] + if selected_pad_data then + padname_field:setText(selected_pad_data.name) + padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type]) + end + end + + if form.preselect_pos then + for i, pad in ipairs(pad_list) do + if vector.equals(pad.pos, form.preselect_pos) then + listbox:setSelected(i) + update_fields_for_index(i) + break + end + end + end + + listbox:onClick(function() + local index = listbox:getSelected() + update_fields_for_index(index) + end) + + form.state:get("save_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + local new_name = padname_field:getText() + local new_type_str + local value_from_dropdown = padtype_dropdown:getSelectedItem() + if value_from_dropdown then + local index = tonumber(value_from_dropdown) + if index then + new_type_str = padtype_dropdown:getItem(index) + else + new_type_str = value_from_dropdown + end + end + if not new_type_str or new_type_str == "" then + notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.") + return + end + if not minetest.get_player_privs(form.playername).tpad_admin then + if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then + notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.") + return + end + end + tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str) + local meta = minetest.get_meta(selected_pad_data.pos) + if new_name and new_name ~= "" then + meta:set_string("infotext", "TPAD Station " .. new_name) + else + meta:set_string("infotext", "Unbenannte TPAD Station") + end + notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.") + minetest.after(0, function() + form.preselect_pos = nil + submit.management(form) + end) + end) + + form.state:get("delete_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + if vector.equals(selected_pad_data.pos, form.clicked_pos) then + notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.") + return + end + tpad.pending_deletion[form.playername] = { + pad_data = selected_pad_data, + original_form_context = form, + } + minetest.close_formspec(form.playername, form.formname) + minetest.after(0.1, function() + tpad.forms.confirm_pad_deletion:show(form.playername) + end) + end) + + form.state:get("back_button"):onClick(function() + minetest.after(0, function() + local clean_form_context = { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = 1 + } + tpad.show_network_view(clean_form_context, form.network_type) + end) + end) + + -- NEU: Logik für den Admin-Teleport-Button + if is_admin then + local admin_teleport_button = form.state:get("admin_teleport_button") + admin_teleport_button:setVisible(true) + admin_teleport_button:onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.") + return + end + + local dest_pad = selected_pad_data + + -- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll + local new_context_after_teleport = { + playername = form.playername, + clicker = form.clicker, + ownername = dest_pad.owner, + clicked_pos = dest_pad.pos, + node = minetest.get_node(dest_pad.pos), + page = 1, + network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local" + } + + tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport) + end) + end +end + +function submit.admin_settings(form) + form.state = tpad.forms.admin:show(form.playername) + form.formname = "tpad_admin_settings" + local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field") + max_total_field:setText(tpad.get_max_total_pads()) + max_global_field:setText(tpad.get_max_global_pads()) + + form.state:get("save_button"):onClick(function() + tpad.set_max_total_pads(tonumber(max_total_field:getText())) + tpad.set_max_global_pads(tonumber(max_global_field:getText())) + minetest.close_formspec(form.playername, form.formname) + end) +end + +-- ======================================================================== +-- Main Node Callbacks +-- ======================================================================== + +function tpad.on_rightclick(clicked_pos, node, clicker) + local playername = clicker:get_player_name() + local pad = tpad.get_pad_data(clicked_pos, playername) + + if not pad or not pad.owner then + notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.") + return + end + + -- Zugriffsschutz-Prüfung für private Pads + if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then + local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or "" + notify.warn(playername, YELLOW_ESCAPE .. "PRIVATE" .. WHITE_ESCAPE .. " TPAD Station von " .. pad.owner .. ". " .. RED_ESCAPE .. "Zugriff verweigert.") + return + end + + -- Erstelle das Kontext-Objekt für die Formulare + local form = { + playername = playername, + clicker = clicker, + ownername = pad.owner, + clicked_pos = clicked_pos, + node = node, + page = 1, + } + + -- Wenn das Pad neu ist (keinen Namen hat), direkt zur Verwaltung + if pad.name == "" then + form.preselect_pos = clicked_pos + submit.management(form) + else + -- Ansonsten zeige die entsprechende Netzwerk-Ansicht + if pad.type == GLOBAL_PAD then + tpad.show_network_view(form, "global") + else + tpad.show_network_view(form, "local") + end + end +end + +function tpad.can_dig(pos, player) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local playername = player:get_player_name() + if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then + return true + end + notify.warn(playername, "Dieses TPAD gehört dir nicht.") + return false +end + +function tpad.on_destruct(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if ownername and ownername ~= "" then + tpad.del_pad(ownername, pos) + end +end + +-- ======================================================================== +-- FORMS (Rebuilt) +-- ======================================================================== + +tpad.forms = {} + +local function create_network_view_form(state) + state:size(16, 11) + state:label(0.5, 0.2, "title_label", "") + local bottom_y = 10.4 + state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "") + state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung") + local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) + state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]") + state:label(7.1, bottom_y, "page_label", "") + state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]") + + local max_rows, num_columns = 10, 3 + local start_x, start_y = 0.5, 1.0 + local button_width, button_height = 4.8, 0.8 + local column_width = 5.0 + for i = 1, max_rows * num_columns do + local column = math.floor((i - 1) / max_rows) + local row = (i - 1) % max_rows + local current_x = start_x + (column * column_width) + local current_y = start_y + (row * button_height) + state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false) + end +end + +tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state) + state:size(8, 2) + local destination_name = state.param.destination_name or "???" + state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name) + + -- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt. + local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form) + +tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state) + create_network_view_form(state) + state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin") +end) + +tpad.forms.management = smartfs.create("tpad.forms.management", function(state) + state:size(12, 9) + state:label(0.2, 0.2, "management_title", "TPAD Verwaltung") + state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {}) + state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "") + + local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown") + padtype_dropdown:addItem(PRIVATE_PAD_STRING) + padtype_dropdown:addItem(PUBLIC_PAD_STRING) + padtype_dropdown:addItem(GLOBAL_PAD_STRING) + + state:button(7, 6.3, 2, 0, "save_button", "Speichern") + state:button(7, 7.1, 2, 0, "delete_button", "Löschen") + state:button(0.2, 8.4, 2, 0, "back_button", "Zurück") + + -- NEU: Teleport-Button für Admins, standardmäßig unsichtbar + state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false) + + local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- This form now defines its OWN behavior, making it independent and robust. +tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state) + state:size(8, 2.5) + state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?") + state:label(0, 0.5, "padname_label", "") + state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)") + + local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen") + local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen") + + local playername = state.location.player + local pending_data = tpad.pending_deletion[playername] + + if not pending_data then + state:close() + return + end + + local pad_to_delete = pending_data.pad_data + local original_form_context = pending_data.original_form_context + + local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")" + state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name) + + -- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht + local function return_to_management_with_fresh_state() + tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen + minetest.after(0, function() + -- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden + local fresh_context = { + playername = original_form_context.playername, + clicker = original_form_context.clicker, + ownername = original_form_context.ownername, + clicked_pos = original_form_context.clicked_pos, + node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat + network_type = original_form_context.network_type, + page = 1 + } + submit.management(fresh_context) + end) + end + + confirm_button:onClick(function() + tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos) + minetest.remove_node(pad_to_delete.pos) + notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.") + return_to_management_with_fresh_state() + end) + + deny_button:onClick(return_to_management_with_fresh_state) +end) + +tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state) + state:size(8, 8) + state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen") + state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)") + state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)") + state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern") + local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- ======================================================================== +-- Data Helper Functions +-- ======================================================================== + +function tpad.decorate_pad_data(pos, pad, ownername, viewername) + pad = table.copy(pad) + if type(pos) == "string" then + pad.strpos = pos + pad.pos = minetest.string_to_pos(pos) + else + pad.pos = pos + pad.strpos = minetest.pos_to_string(pos) + end + pad.owner = ownername + pad.name = pad.name or "" + pad.type = pad.type or PUBLIC_PAD + + -- NEUE LOGIK: Erstellt den Anzeigenamen für das lokale Netzwerk + local is_own_pad = (viewername and ownername == viewername) + if is_own_pad then + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (meins, privat)" + elseif pad.type == PUBLIC_PAD then + pad.local_fullname = pad.name .. " (meins)" + else + -- Fallback, falls es weitere Typen gäbe + pad.local_fullname = pad.name + end + else + -- Bisherige Logik für fremde Pads + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (privat)" + else + pad.local_fullname = pad.name + end + end + + pad.global_fullname = pad.name + return pad +end + +function tpad.get_pad_data(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if not ownername or ownername == "" then return end + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] + if not pad then return end + return tpad.decorate_pad_data(pos, pad, ownername) +end + +function tpad.set_pad_data(pos, padname, padtype_str) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] or {} + pad.name = padname + pad.type = padtype_string_to_flag[padtype_str] + pads[strpos] = pad + tpad._set_stored_pads(ownername, pads) +end + +function tpad.del_pad(ownername, pos) + local pads = tpad._get_stored_pads(ownername) + pads[minetest.pos_to_string(pos)] = nil + tpad._set_stored_pads(ownername, pads) +end + +-- ======================================================================== +-- Register Node and Bind Callbacks +-- ======================================================================== + +local collision_box = { + type = "fixed", + fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 }, +} + +minetest.register_node(tpad.nodename, { + drawtype = "mesh", + tiles = { tpad.texture }, + mesh = tpad.mesh, + paramtype = "light", + paramtype2 = "facedir", + on_place = minetest.rotate_and_place, + after_place_node = tpad.after_place_node, + collision_box = collision_box, + selection_box = collision_box, + description = "Teleporter Pad", + groups = {choppy = 2, dig_immediate = 2}, + on_rightclick = tpad.on_rightclick, + can_dig = tpad.can_dig, + on_destruct = tpad.on_destruct, +}) + +minetest.register_chatcommand("tpad", {func = tpad.command}) diff --git a/init.lua.bak.1 b/init.lua.bak.1 new file mode 100644 index 0000000..40222e1 --- /dev/null +++ b/init.lua.bak.1 @@ -0,0 +1,869 @@ +-- ======================================================================== +-- TPAD MOD v1.2 (Final, Reworked Delete Logic) +-- ======================================================================== + +tpad = {} +tpad.version = "1.2" -- As requested, version is not incremented +tpad.mod_name = minetest.get_current_modname() +tpad.texture = "tpad-texture.png" +tpad.mesh = "tpad-mesh.obj" +tpad.nodename = "tpad:tpad" +tpad.mod_path = minetest.get_modpath(tpad.mod_name) +tpad.sound_teleport = tpad.mod_name .. "_teleport" +tpad.particle_texture = tpad.mod_name .. "_particle.png" + +-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog +tpad.pending_deletion = {} + +-- ======================================================================== +-- Constants +-- ======================================================================== + +local PRIVATE_PAD_STRING = "Privat (nur Besitzer)" +local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)" +local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)" + +local PRIVATE_PAD = 1 +local PUBLIC_PAD = 2 +local GLOBAL_PAD = 4 + +local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00") +local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF") +local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF") +local OWNER_ESCAPE_COLOR = CYAN_ESCAPE + +local padtype_flag_to_string = { + [PRIVATE_PAD] = PRIVATE_PAD_STRING, + [PUBLIC_PAD] = PUBLIC_PAD_STRING, + [GLOBAL_PAD] = GLOBAL_PAD_STRING, +} +local padtype_string_to_flag = { + [PRIVATE_PAD_STRING] = PRIVATE_PAD, + [PUBLIC_PAD_STRING] = PUBLIC_PAD, + [GLOBAL_PAD_STRING] = GLOBAL_PAD, +} +local short_padtype_string = { + [PRIVATE_PAD] = "private", + [PUBLIC_PAD] = "public", + [GLOBAL_PAD] = "global", +} + +-- ======================================================================== +-- Dependencies and Libs +-- ======================================================================== + +local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua") +local notify = dofile(tpad.mod_path .. "/notify.lua") + +local waypoint_hud_ids = {} + +minetest.register_privilege("tpad_admin", { + description = "Can edit and destroy any tpad", + give_to_singleplayer = true, +}) + +-- ======================================================================== +-- Original Helper Functions +-- ======================================================================== + +local function copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then return false, "copy_file() unable to open source for reading" end + local src_data = src_file:read("*all") + src_file:close() + local dest_file = io.open(dest, "wb") + if not dest_file then return false, "copy_file() unable to open dest for writing" end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +local function custom_or_default(modname, path, filename) + local default_filename = "default/" .. filename + local full_filename = path .. "/custom." .. filename + local full_default_filename = path .. "/" .. default_filename + local file_exists_at_path = io.open(path .. "/" .. filename, "r") + if file_exists_at_path then + file_exists_at_path:close() + os.rename(path .. "/" .. filename, full_filename) + end + local file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")") + local success, err = copy_file(full_default_filename, full_filename) + if not success then + minetest.debug("[" .. modname .. "] " .. err) + return false + end + file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path) + return false + end + end + file:close() + return full_filename +end + +dofile(tpad.mod_path .. "/storage.lua") + +-- ======================================================================== +-- Load Custom Recipe +-- ======================================================================== + +local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua") +if recipes_filename then + local recipes = dofile(recipes_filename) + if type(recipes) == "table" and recipes[tpad.nodename] then + minetest.register_craft({ + output = tpad.nodename, + recipe = recipes[tpad.nodename], + }) + end +end + +-- ======================================================================== +-- Chat Command +-- ======================================================================== + +function tpad.command(playername, param) + tpad.hud_off(playername) + if(param == "off") then return end + local player = minetest.get_player_by_name(playername) + local pads = tpad._get_stored_pads(playername) + local shortest_distance = nil + local closest_pad = nil + local playerpos = player:getpos() + for strpos, pad in pairs(pads) do + local pos = minetest.string_to_pos(strpos) + local distance = vector.distance(pos, playerpos) + if not shortest_distance or distance < shortest_distance then + closest_pad = { + pos = pos, + name = pad.name .. " " .. strpos, + } + shortest_distance = distance + end + end + if closest_pad then + waypoint_hud_ids[playername] = player:hud_add({ + hud_elem_type = "waypoint", + name = closest_pad.name, + world_pos = closest_pad.pos, + number = 0xFF0000, + }) + notify(playername, "Waypoint to " .. closest_pad.name .. " displayed") + end +end + +function tpad.hud_off(playername) + local player = minetest.get_player_by_name(playername) + local hud_id = waypoint_hud_ids[playername] + if hud_id then + player:hud_remove(hud_id) + end +end + +-- ======================================================================== +-- Teleport Logic +-- ======================================================================== + +function tpad.do_teleport(player, destination_pos, destination_name, form_context) + local playername = player:get_player_name() + + minetest.after(0.1, function() + if not player or not player:is_player() then return end + local start_pos = player:getpos() + minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5), + minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + + player:move_to(destination_pos) + + minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}), + maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}), + minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + tpad.hud_off(playername) + + -- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren + minetest.after(0.2, function() + if not player or not player:is_player() then return end + + local dest_pad_data = tpad.get_pad_data(destination_pos) + if not dest_pad_data then return end + + local new_context = { + playername = playername, + clicker = player, + ownername = dest_pad_data.owner, + clicked_pos = destination_pos, + node = minetest.get_node(destination_pos), + page = 1, + -- Behalte den Netzwerk-Typ (lokal/global) bei + network_type = form_context.network_type + } + tpad.show_network_view(new_context, new_context.network_type) + end) + end) +end + +-- ======================================================================== +-- Node Placement and Data Helpers +-- ======================================================================== + +function tpad.after_place_node(pos, placer, itemstack) + local playername = placer:get_player_name() + if tpad.max_total_pads_reached(placer) then + notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.") + minetest.remove_node(pos) + minetest.add_item(placer:get_pos(), itemstack:get_name()) + return + end + + local meta = minetest.get_meta(pos) + meta:set_string("owner", playername) + meta:set_string("infotext", "TPAD Station von " .. playername) + tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING) +end + +local submit = {} + +function tpad.max_total_pads_reached(placer) + local placername = placer:get_player_name() + if minetest.get_player_privs(placername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(placername) + local count = 0 + for _ in pairs(pads) do + count = count + 1 + end + return count >= tpad.get_max_total_pads() +end + +function tpad.max_global_pads_reached(playername) + if minetest.get_player_privs(playername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(playername) + local count = 0 + for _, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + count = count + 1 + end + end + return count >= tpad.get_max_global_pads() +end + +-- ======================================================================== +-- GUI DATA HELPERS +-- ======================================================================== + +function submit.global_helper() + local allpads = tpad._get_all_pads() + local result = {} + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + end + table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end) + return result +end + +function submit.local_helper(ownername) + local pads = tpad._get_stored_pads(ownername) + local result = {} + for strpos, pad in pairs(pads) do + if pad.type ~= GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +function submit.management_helper(playername) + local is_admin = minetest.get_player_privs(playername).tpad_admin + local result = {} + + if is_admin then + local allpads = tpad._get_all_pads() + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + else + local pads = tpad._get_stored_pads(playername) + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, playername)) + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +-- ======================================================================== +-- GUI LOGIC (REFACTORED) +-- ======================================================================== + +function tpad.show_network_view(form, network_type) + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local form_key = is_admin and "network_view_admin" or "network_view" + form.state = tpad.forms[form_key]:show(form.playername) + form.formname = "tpad.forms." .. form_key + + local pad_list + local title_text + + local function create_clean_context() + return { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = form.page or 1 + } + end + + local current_pad_data = tpad.get_pad_data(form.clicked_pos) + local current_pad_name = current_pad_data.name or "Unnamed" + + if network_type == "global" then + form.network_type = "global" + pad_list = submit.global_helper() + title_text = "Du bist hier: " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein globales Ziel:" + form.state:get("toggle_network_button"):setText("Lokales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.ownername = clean_form.playername + clean_form.page = 1 + tpad.show_network_view(clean_form, "local") + end) + end) + else -- "local" + form.network_type = "local" + pad_list = submit.local_helper(form.ownername) + title_text = "Du bist hier: " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein lokales Ziel:" + form.state:get("toggle_network_button"):setText("Globales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = 1 + tpad.show_network_view(clean_form, "global") + end) + end) + end + + form.state:get("title_label"):setText(title_text) + form.state:get("management_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.preselect_pos = clean_form.clicked_pos + submit.management(clean_form) + end) + end) + + if is_admin then + form.state:get("admin_button"):onClick(function() + minetest.after(0, function() submit.admin_settings(form) end) + end) + end + + local max_rows, num_columns = 10, 3 + local buttons_per_page = max_rows * num_columns + + local destination_pads = {} + for _, pad in ipairs(pad_list) do + if not vector.equals(pad.pos, form.clicked_pos) then + table.insert(destination_pads, pad) + end + end + + local total_pages = math.ceil(#destination_pads / buttons_per_page) + if total_pages == 0 then total_pages = 1 end + + local current_page = form.page or 1 + if current_page > total_pages then current_page = total_pages end + if current_page < 1 then current_page = 1 end + form.page = current_page + + for i = 1, buttons_per_page do + local index = ((current_page - 1) * buttons_per_page) + i + local pad_data = destination_pads[index] + local button_name = "tpad_btn_" .. i + local button = form.state:get(button_name) + + if button then + if pad_data then + local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname + button:setText(display_name) + button:setVisible(true) + button:onClick(function() + tpad.do_teleport(form.clicker, pad_data.pos, display_name, form) + end) + else + button:setVisible(false) + end + end + end + + if total_pages > 1 then + form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages) + local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button") + prev_button:setVisible(current_page > 1) + next_button:setVisible(current_page < total_pages) + + -- KORREKTUR: Paginierung verwendet jetzt ebenfalls einen sauberen Kontext + prev_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page - 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + next_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page + 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + else + form.state:get("page_label"):setText("") + form.state:get("prev_button"):setVisible(false) + form.state:get("next_button"):setVisible(false) + end +end + +function submit.management(form) + form.formname = "tpad_management" + form.state = tpad.forms.management:show(form.playername) + local pad_list = submit.management_helper(form.playername) + local listbox = form.state:get("pads_listbox") + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local selected_pad_data = nil + + listbox:clearItems() + for _, pad in ipairs(pad_list) do + local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")" + if is_admin then + label = label .. " [" .. pad.owner .. "]" + end + listbox:addItem(label) + end + + local padname_field = form.state:get("padname_field") + local padtype_dropdown = form.state:get("padtype_dropdown") + + -- NEU: Funktion zur Aktualisierung der Felder, um Code-Dopplung zu vermeiden + local function update_fields_for_index(index) + if not index or index <= 0 then return end + selected_pad_data = pad_list[index] + if selected_pad_data then + padname_field:setText(selected_pad_data.name) + padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type]) + end + end + + -- NEU: Prüfen, ob ein Pad vorausgewählt werden soll + if form.preselect_pos then + for i, pad in ipairs(pad_list) do + if vector.equals(pad.pos, form.preselect_pos) then + listbox:setSelected(i) + update_fields_for_index(i) -- Felder für den vorausgewählten Eintrag aktualisieren + break + end + end + end + + listbox:onClick(function() + local index = listbox:getSelected() + update_fields_for_index(index) -- Felder bei Klick aktualisieren + end) + + form.state:get("save_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + + local new_name = padname_field:getText() + local new_type_str + local value_from_dropdown = padtype_dropdown:getSelectedItem() + if value_from_dropdown then + local index = tonumber(value_from_dropdown) + if index then + new_type_str = padtype_dropdown:getItem(index) + else + new_type_str = value_from_dropdown + end + end + + if not new_type_str or new_type_str == "" then + notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.") + return + end + + if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then + notify.warn(form.playername, "Besitzer kann keine TPADs (mehr) zum globalen Netzwerk hinzufügen.") + return + end + + tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str) + notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.") + minetest.after(0, function() + -- WICHTIG: preselect_pos nach dem Speichern entfernen, damit es nicht "hängen bleibt" + form.preselect_pos = nil + submit.management(form) + end) + end) + + form.state:get("delete_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + + if vector.equals(selected_pad_data.pos, form.clicked_pos) then + notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.") + return + end + + tpad.pending_deletion[form.playername] = { + pad_data = selected_pad_data, + original_form_context = form, + } + + minetest.close_formspec(form.playername, form.formname) + minetest.after(0.1, function() + tpad.forms.confirm_pad_deletion:show(form.playername) + end) + end) + + form.state:get("back_button"):onClick(function() + minetest.after(0, function() + local clean_form_context = { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = 1 + } + tpad.show_network_view(clean_form_context, form.network_type) + end) + end) +end + +function submit.admin_settings(form) + form.state = tpad.forms.admin:show(form.playername) + form.formname = "tpad_admin_settings" + local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field") + max_total_field:setText(tpad.get_max_total_pads()) + max_global_field:setText(tpad.get_max_global_pads()) + + form.state:get("save_button"):onClick(function() + tpad.set_max_total_pads(tonumber(max_total_field:getText())) + tpad.set_max_global_pads(tonumber(max_global_field:getText())) + minetest.close_formspec(form.playername, form.formname) + end) +end + +-- ======================================================================== +-- Main Node Callbacks +-- ======================================================================== + +function tpad.on_rightclick(clicked_pos, node, clicker) + local playername = clicker:get_player_name() + local pad = tpad.get_pad_data(clicked_pos) + if not pad or not pad.owner then + notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.") + return + end + + -- Zugriffsschutz-Prüfung zuerst + if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then + notify.warn(playername, "Dieses TPAD ist privat.") + return + end + + -- Erstelle das Kontext-Objekt für alle Fälle + local form = { + playername = playername, + clicker = clicker, + ownername = pad.owner, + clicked_pos = clicked_pos, + node = node, + page = 1, + } + + -- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat) + if pad.name == "" then + -- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü + form.preselect_pos = clicked_pos + -- Rufe direkt die Verwaltung auf + submit.management(form) + else + -- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist + if pad.type == GLOBAL_PAD then + tpad.show_network_view(form, "global") + else + tpad.show_network_view(form, "local") + end + end +end + +function tpad.can_dig(pos, player) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local playername = player:get_player_name() + if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then + return true + end + notify.warn(playername, "Dieses TPAD gehört dir nicht.") + return false +end + +function tpad.on_destruct(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if ownername and ownername ~= "" then + tpad.del_pad(ownername, pos) + end +end + +-- ======================================================================== +-- FORMS (Rebuilt) +-- ======================================================================== + +tpad.forms = {} + +local function create_network_view_form(state) + state:size(16, 11) + state:label(0.5, 0.2, "title_label", "") + local bottom_y = 10.4 + state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "") + state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung") + local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) + state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]") + state:label(7.1, bottom_y, "page_label", "") + state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]") + + local max_rows, num_columns = 10, 3 + local start_x, start_y = 0.5, 1.0 + local button_width, button_height = 4.8, 0.8 + local column_width = 5.0 + for i = 1, max_rows * num_columns do + local column = math.floor((i - 1) / max_rows) + local row = (i - 1) % max_rows + local current_x = start_x + (column * column_width) + local current_y = start_y + (row * button_height) + state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false) + end +end + +tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state) + state:size(8, 2) + local destination_name = state.param.destination_name or "???" + state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name) + + -- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt. + local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form) + +tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state) + create_network_view_form(state) + state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin") +end) + +tpad.forms.management = smartfs.create("tpad.forms.management", function(state) + state:size(12, 9) + state:label(0.2, 0.2, "management_title", "TPAD Verwaltung") + state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {}) + state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "") + + local padtype_dropdown = state:dropdown(0.5, 6.8, 6, 0, "padtype_dropdown") + padtype_dropdown:addItem(PRIVATE_PAD_STRING) + padtype_dropdown:addItem(PUBLIC_PAD_STRING) + padtype_dropdown:addItem(GLOBAL_PAD_STRING) + + state:button(7, 6.2, 2, 0, "save_button", "Speichern") + state:button(7, 7.0, 2, 0, "delete_button", "Löschen") + state:button(0.2, 8.4, 2, 0, "back_button", "Zurück") + local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- This form now defines its OWN behavior, making it independent and robust. +tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state) + state:size(8, 2.5) + state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?") + state:label(0, 0.5, "padname_label", "") + state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)") + + local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen") + local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen") + + local playername = state.location.player + local pending_data = tpad.pending_deletion[playername] + + if not pending_data then + state:close() + return + end + + local pad_to_delete = pending_data.pad_data + local original_form_context = pending_data.original_form_context + + local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")" + state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name) + + -- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht + local function return_to_management_with_fresh_state() + tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen + minetest.after(0, function() + -- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden + local fresh_context = { + playername = original_form_context.playername, + clicker = original_form_context.clicker, + ownername = original_form_context.ownername, + clicked_pos = original_form_context.clicked_pos, + node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat + network_type = original_form_context.network_type, + page = 1 + } + submit.management(fresh_context) + end) + end + + confirm_button:onClick(function() + tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos) + minetest.remove_node(pad_to_delete.pos) + notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.") + return_to_management_with_fresh_state() + end) + + deny_button:onClick(return_to_management_with_fresh_state) +end) + +tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state) + state:size(8, 8) + state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen") + state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)") + state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)") + state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern") + local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- ======================================================================== +-- Data Helper Functions +-- ======================================================================== + +function tpad.decorate_pad_data(pos, pad, ownername) + pad = table.copy(pad) + if type(pos) == "string" then + pad.strpos = pos + pad.pos = minetest.string_to_pos(pos) + else + pad.pos = pos + pad.strpos = minetest.pos_to_string(pos) + end + pad.owner = ownername + pad.name = pad.name or "" + pad.type = pad.type or PUBLIC_PAD + + -- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu. + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (privat)" + else + -- Bei Public Pads wird kein Suffix mehr angehängt. + pad.local_fullname = pad.name + end + + pad.global_fullname = pad.name + return pad +end + +function tpad.get_pad_data(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if not ownername or ownername == "" then return end + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] + if not pad then return end + return tpad.decorate_pad_data(pos, pad, ownername) +end + +function tpad.set_pad_data(pos, padname, padtype_str) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] or {} + pad.name = padname + pad.type = padtype_string_to_flag[padtype_str] + pads[strpos] = pad + tpad._set_stored_pads(ownername, pads) +end + +function tpad.del_pad(ownername, pos) + local pads = tpad._get_stored_pads(ownername) + pads[minetest.pos_to_string(pos)] = nil + tpad._set_stored_pads(ownername, pads) +end + +-- ======================================================================== +-- Register Node and Bind Callbacks +-- ======================================================================== + +local collision_box = { + type = "fixed", + fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 }, +} + +minetest.register_node(tpad.nodename, { + drawtype = "mesh", + tiles = { tpad.texture }, + mesh = tpad.mesh, + paramtype = "light", + paramtype2 = "facedir", + on_place = minetest.rotate_and_place, + after_place_node = tpad.after_place_node, + collision_box = collision_box, + selection_box = collision_box, + description = "Teleporter Pad", + groups = {choppy = 2, dig_immediate = 2}, + on_rightclick = tpad.on_rightclick, + can_dig = tpad.can_dig, + on_destruct = tpad.on_destruct, +}) + +minetest.register_chatcommand("tpad", {func = tpad.command}) diff --git a/init.lua.bak.2 b/init.lua.bak.2 new file mode 100644 index 0000000..6130adc --- /dev/null +++ b/init.lua.bak.2 @@ -0,0 +1,901 @@ +-- ======================================================================== +-- TPAD MOD v1.2 (Final, Reworked Delete Logic) +-- ======================================================================== + +tpad = {} +tpad.version = "1.2" -- As requested, version is not incremented +tpad.mod_name = minetest.get_current_modname() +tpad.texture = "tpad-texture.png" +tpad.mesh = "tpad-mesh.obj" +tpad.nodename = "tpad:tpad" +tpad.mod_path = minetest.get_modpath(tpad.mod_name) +tpad.sound_teleport = tpad.mod_name .. "_teleport" +tpad.particle_texture = tpad.mod_name .. "_particle.png" + +-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog +tpad.pending_deletion = {} + +-- ======================================================================== +-- Constants +-- ======================================================================== + +local PRIVATE_PAD_STRING = "Privat (nur Besitzer)" +local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)" +local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)" + +local PRIVATE_PAD = 1 +local PUBLIC_PAD = 2 +local GLOBAL_PAD = 4 + +local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") +local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00") +local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF") +local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF") +local OWNER_ESCAPE_COLOR = CYAN_ESCAPE + +local padtype_flag_to_string = { + [PRIVATE_PAD] = PRIVATE_PAD_STRING, + [PUBLIC_PAD] = PUBLIC_PAD_STRING, + [GLOBAL_PAD] = GLOBAL_PAD_STRING, +} +local padtype_string_to_flag = { + [PRIVATE_PAD_STRING] = PRIVATE_PAD, + [PUBLIC_PAD_STRING] = PUBLIC_PAD, + [GLOBAL_PAD_STRING] = GLOBAL_PAD, +} +local short_padtype_string = { + [PRIVATE_PAD] = "private", + [PUBLIC_PAD] = "public", + [GLOBAL_PAD] = "global", +} + +-- ======================================================================== +-- Dependencies and Libs +-- ======================================================================== + +local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua") +local notify = dofile(tpad.mod_path .. "/notify.lua") + +local waypoint_hud_ids = {} + +minetest.register_privilege("tpad_admin", { + description = "Can edit and destroy any tpad", + give_to_singleplayer = true, +}) + +-- ======================================================================== +-- Original Helper Functions +-- ======================================================================== + +local function copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then return false, "copy_file() unable to open source for reading" end + local src_data = src_file:read("*all") + src_file:close() + local dest_file = io.open(dest, "wb") + if not dest_file then return false, "copy_file() unable to open dest for writing" end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +local function custom_or_default(modname, path, filename) + local default_filename = "default/" .. filename + local full_filename = path .. "/custom." .. filename + local full_default_filename = path .. "/" .. default_filename + local file_exists_at_path = io.open(path .. "/" .. filename, "r") + if file_exists_at_path then + file_exists_at_path:close() + os.rename(path .. "/" .. filename, full_filename) + end + local file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")") + local success, err = copy_file(full_default_filename, full_filename) + if not success then + minetest.debug("[" .. modname .. "] " .. err) + return false + end + file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path) + return false + end + end + file:close() + return full_filename +end + +dofile(tpad.mod_path .. "/storage.lua") + +-- ======================================================================== +-- Load Custom Recipe +-- ======================================================================== + +local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua") +if recipes_filename then + local recipes = dofile(recipes_filename) + if type(recipes) == "table" and recipes[tpad.nodename] then + minetest.register_craft({ + output = tpad.nodename, + recipe = recipes[tpad.nodename], + }) + end +end + +-- ======================================================================== +-- Chat Command +-- ======================================================================== + +function tpad.command(playername, param) + tpad.hud_off(playername) + if(param == "off") then return end + local player = minetest.get_player_by_name(playername) + local pads = tpad._get_stored_pads(playername) + local shortest_distance = nil + local closest_pad = nil + local playerpos = player:getpos() + for strpos, pad in pairs(pads) do + local pos = minetest.string_to_pos(strpos) + local distance = vector.distance(pos, playerpos) + if not shortest_distance or distance < shortest_distance then + closest_pad = { + pos = pos, + name = pad.name .. " " .. strpos, + } + shortest_distance = distance + end + end + if closest_pad then + waypoint_hud_ids[playername] = player:hud_add({ + hud_elem_type = "waypoint", + name = closest_pad.name, + world_pos = closest_pad.pos, + number = 0xFF0000, + }) + notify(playername, "Waypoint to " .. closest_pad.name .. " displayed") + end +end + +function tpad.hud_off(playername) + local player = minetest.get_player_by_name(playername) + local hud_id = waypoint_hud_ids[playername] + if hud_id then + player:hud_remove(hud_id) + end +end + +-- ======================================================================== +-- Teleport Logic +-- ======================================================================== + +function tpad.do_teleport(player, destination_pos, destination_name, form_context) + local playername = player:get_player_name() + + minetest.after(0.1, function() + if not player or not player:is_player() then return end + local start_pos = player:getpos() + minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5), + minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + + player:move_to(destination_pos) + + minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}), + maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}), + minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + tpad.hud_off(playername) + + -- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren + minetest.after(0.2, function() + if not player or not player:is_player() then return end + + local dest_pad_data = tpad.get_pad_data(destination_pos) + if not dest_pad_data then return end + + local new_context = { + playername = playername, + clicker = player, + ownername = dest_pad_data.owner, + clicked_pos = destination_pos, + node = minetest.get_node(destination_pos), + page = 1, + -- Behalte den Netzwerk-Typ (lokal/global) bei + network_type = form_context.network_type + } + tpad.show_network_view(new_context, new_context.network_type) + end) + end) +end + +-- ======================================================================== +-- Node Placement and Data Helpers +-- ======================================================================== + +function tpad.after_place_node(pos, placer, itemstack) + local playername = placer:get_player_name() + if tpad.max_total_pads_reached(placer) then + notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.") + minetest.remove_node(pos) + minetest.add_item(placer:get_pos(), itemstack:get_name()) + return + end + + local meta = minetest.get_meta(pos) + meta:set_string("owner", playername) + meta:set_string("infotext", "TPAD Station von " .. playername) + tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING) +end + +local submit = {} + +function tpad.max_total_pads_reached(placer) + local placername = placer:get_player_name() + if minetest.get_player_privs(placername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(placername) + local count = 0 + for _ in pairs(pads) do + count = count + 1 + end + return count >= tpad.get_max_total_pads() +end + +function tpad.max_global_pads_reached(playername) + if minetest.get_player_privs(playername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(playername) + local count = 0 + for _, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + count = count + 1 + end + end + return count >= tpad.get_max_global_pads() +end + +-- ======================================================================== +-- GUI DATA HELPERS +-- ======================================================================== + +function submit.global_helper() + local allpads = tpad._get_all_pads() + local result = {} + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + end + table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end) + return result +end + +function submit.local_helper(ownername, viewername) + local result = {} + local added_pads = {} -- Verhindert, dass Pads doppelt hinzugefügt werden + + -- Schritt 1: Füge die öffentlichen Pads des Besitzers des angeklickten TPADs hinzu + local owner_pads = tpad._get_stored_pads(ownername) + for strpos, pad in pairs(owner_pads) do + -- Vom Besitzer werden nur die öffentlichen (public) Pads angezeigt + if pad.type == PUBLIC_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + added_pads[strpos] = true + end + end + + -- Schritt 2: Füge die eigenen Pads hinzu, falls der Betrachter eine andere Person ist + if ownername ~= viewername then + local viewer_pads = tpad._get_stored_pads(viewername) + for strpos, pad in pairs(viewer_pads) do + -- Vom Betrachter werden seine privaten und öffentlichen Pads angezeigt + if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then + table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername)) + added_pads[strpos] = true + end + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +function submit.management_helper(playername) + local is_admin = minetest.get_player_privs(playername).tpad_admin + local result = {} + + if is_admin then + local allpads = tpad._get_all_pads() + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + else + local pads = tpad._get_stored_pads(playername) + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, playername)) + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +-- ======================================================================== +-- GUI LOGIC (REFACTORED) +-- ======================================================================== + +function tpad.show_network_view(form, network_type) + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local form_key = is_admin and "network_view_admin" or "network_view" + form.state = tpad.forms[form_key]:show(form.playername) + form.formname = "tpad.forms." .. form_key + + local pad_list + local title_text + + local function create_clean_context() + return { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = form.page or 1 + } + end + + local current_pad_data = tpad.get_pad_data(form.clicked_pos) + local current_pad_name = current_pad_data.name or "Unnamed" + + if network_type == "global" then + form.network_type = "global" + pad_list = submit.global_helper() + title_text = RED_ESCAPE .. "GLOBALE " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:" + form.state:get("toggle_network_button"):setText("Lokales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.ownername = clean_form.playername + clean_form.page = 1 + tpad.show_network_view(clean_form, "local") + end) + end) + else -- "local" + form.network_type = "local" + pad_list = submit.local_helper(form.ownername, form.playername) + title_text = RED_ESCAPE .. "LOKALE " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:" + form.state:get("toggle_network_button"):setText("Globales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = 1 + tpad.show_network_view(clean_form, "global") + end) + end) + end + + form.state:get("title_label"):setText(title_text) + form.state:get("management_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.preselect_pos = clean_form.clicked_pos + submit.management(clean_form) + end) + end) + + if is_admin then + form.state:get("admin_button"):onClick(function() + minetest.after(0, function() submit.admin_settings(form) end) + end) + end + + local max_rows, num_columns = 10, 3 + local buttons_per_page = max_rows * num_columns + + local destination_pads = {} + for _, pad in ipairs(pad_list) do + if not vector.equals(pad.pos, form.clicked_pos) then + table.insert(destination_pads, pad) + end + end + + local total_pages = math.ceil(#destination_pads / buttons_per_page) + if total_pages == 0 then total_pages = 1 end + + local current_page = form.page or 1 + if current_page > total_pages then current_page = total_pages end + if current_page < 1 then current_page = 1 end + form.page = current_page + + for i = 1, buttons_per_page do + local index = ((current_page - 1) * buttons_per_page) + i + local pad_data = destination_pads[index] + local button_name = "tpad_btn_" .. i + local button = form.state:get(button_name) + + if button then + if pad_data then + local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname + button:setText(display_name) + button:setVisible(true) + button:onClick(function() + tpad.do_teleport(form.clicker, pad_data.pos, display_name, form) + end) + else + button:setVisible(false) + end + end + end + + if total_pages > 1 then + form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages) + local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button") + prev_button:setVisible(current_page > 1) + next_button:setVisible(current_page < total_pages) + + -- KORREKTUR: Paginierung verwendet jetzt ebenfalls einen sauberen Kontext + prev_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page - 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + next_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page + 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + else + form.state:get("page_label"):setText("") + form.state:get("prev_button"):setVisible(false) + form.state:get("next_button"):setVisible(false) + end +end + +function submit.management(form) + form.formname = "tpad_management" + form.state = tpad.forms.management:show(form.playername) + local pad_list = submit.management_helper(form.playername) + local listbox = form.state:get("pads_listbox") + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local selected_pad_data = nil + + listbox:clearItems() + for _, pad in ipairs(pad_list) do + local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")" + if is_admin then + label = label .. " [" .. pad.owner .. "]" + end + listbox:addItem(label) + end + + local padname_field = form.state:get("padname_field") + local padtype_dropdown = form.state:get("padtype_dropdown") + + -- NEU: Funktion zur Aktualisierung der Felder, um Code-Dopplung zu vermeiden + local function update_fields_for_index(index) + if not index or index <= 0 then return end + selected_pad_data = pad_list[index] + if selected_pad_data then + padname_field:setText(selected_pad_data.name) + padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type]) + end + end + + -- NEU: Prüfen, ob ein Pad vorausgewählt werden soll + if form.preselect_pos then + for i, pad in ipairs(pad_list) do + if vector.equals(pad.pos, form.preselect_pos) then + listbox:setSelected(i) + update_fields_for_index(i) -- Felder für den vorausgewählten Eintrag aktualisieren + break + end + end + end + + listbox:onClick(function() + local index = listbox:getSelected() + update_fields_for_index(index) -- Felder bei Klick aktualisieren + end) + + form.state:get("save_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + + local new_name = padname_field:getText() + local new_type_str + local value_from_dropdown = padtype_dropdown:getSelectedItem() + if value_from_dropdown then + local index = tonumber(value_from_dropdown) + if index then + new_type_str = padtype_dropdown:getItem(index) + else + new_type_str = value_from_dropdown + end + end + + if not new_type_str or new_type_str == "" then + notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.") + return + end + + -- NEU: Prüfe zuerst, ob der ausführende Spieler Admin-Rechte hat. + local is_acting_admin = minetest.get_player_privs(form.playername).tpad_admin + + -- Wende die Limits nur an, wenn der ausführende Spieler KEIN Admin ist. + if not is_acting_admin then + if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then + notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.") + return + end + end + + tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str) + + local meta = minetest.get_meta(selected_pad_data.pos) + if new_name and new_name ~= "" then + meta:set_string("infotext", "TPAD Station " .. new_name) + else + meta:set_string("infotext", "Unbenannte TPAD Station") + end + + notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.") + minetest.after(0, function() + form.preselect_pos = nil + submit.management(form) + end) + end) + + form.state:get("delete_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + + if vector.equals(selected_pad_data.pos, form.clicked_pos) then + notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.") + return + end + + tpad.pending_deletion[form.playername] = { + pad_data = selected_pad_data, + original_form_context = form, + } + + minetest.close_formspec(form.playername, form.formname) + minetest.after(0.1, function() + tpad.forms.confirm_pad_deletion:show(form.playername) + end) + end) + + form.state:get("back_button"):onClick(function() + minetest.after(0, function() + local clean_form_context = { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = 1 + } + tpad.show_network_view(clean_form_context, form.network_type) + end) + end) +end + +function submit.admin_settings(form) + form.state = tpad.forms.admin:show(form.playername) + form.formname = "tpad_admin_settings" + local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field") + max_total_field:setText(tpad.get_max_total_pads()) + max_global_field:setText(tpad.get_max_global_pads()) + + form.state:get("save_button"):onClick(function() + tpad.set_max_total_pads(tonumber(max_total_field:getText())) + tpad.set_max_global_pads(tonumber(max_global_field:getText())) + minetest.close_formspec(form.playername, form.formname) + end) +end + +-- ======================================================================== +-- Main Node Callbacks +-- ======================================================================== + +function tpad.on_rightclick(clicked_pos, node, clicker) + local playername = clicker:get_player_name() + local pad = tpad.get_pad_data(clicked_pos) + if not pad or not pad.owner then + notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.") + return + end + + -- Zugriffsschutz-Prüfung zuerst + if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then + notify.warn(playername, "Dieses TPAD ist privat.") + return + end + + -- Erstelle das Kontext-Objekt für alle Fälle + local form = { + playername = playername, + clicker = clicker, + ownername = pad.owner, + clicked_pos = clicked_pos, + node = node, + page = 1, + } + + -- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat) + if pad.name == "" then + -- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü + form.preselect_pos = clicked_pos + -- Rufe direkt die Verwaltung auf + submit.management(form) + else + -- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist + if pad.type == GLOBAL_PAD then + tpad.show_network_view(form, "global") + else + tpad.show_network_view(form, "local") + end + end +end + +function tpad.can_dig(pos, player) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local playername = player:get_player_name() + if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then + return true + end + notify.warn(playername, "Dieses TPAD gehört dir nicht.") + return false +end + +function tpad.on_destruct(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if ownername and ownername ~= "" then + tpad.del_pad(ownername, pos) + end +end + +-- ======================================================================== +-- FORMS (Rebuilt) +-- ======================================================================== + +tpad.forms = {} + +local function create_network_view_form(state) + state:size(16, 11) + state:label(0.5, 0.2, "title_label", "") + local bottom_y = 10.4 + state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "") + state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung") + local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) + state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]") + state:label(7.1, bottom_y, "page_label", "") + state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]") + + local max_rows, num_columns = 10, 3 + local start_x, start_y = 0.5, 1.0 + local button_width, button_height = 4.8, 0.8 + local column_width = 5.0 + for i = 1, max_rows * num_columns do + local column = math.floor((i - 1) / max_rows) + local row = (i - 1) % max_rows + local current_x = start_x + (column * column_width) + local current_y = start_y + (row * button_height) + state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false) + end +end + +tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state) + state:size(8, 2) + local destination_name = state.param.destination_name or "???" + state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name) + + -- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt. + local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form) + +tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state) + create_network_view_form(state) + state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin") +end) + +tpad.forms.management = smartfs.create("tpad.forms.management", function(state) + state:size(12, 9) + state:label(0.2, 0.2, "management_title", "TPAD Verwaltung") + state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {}) + state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "") + + local padtype_dropdown = state:dropdown(0.5, 6.8, 6, 0, "padtype_dropdown") + padtype_dropdown:addItem(PRIVATE_PAD_STRING) + padtype_dropdown:addItem(PUBLIC_PAD_STRING) + padtype_dropdown:addItem(GLOBAL_PAD_STRING) + + state:button(7, 6.2, 2, 0, "save_button", "Speichern") + state:button(7, 7.0, 2, 0, "delete_button", "Löschen") + state:button(0.2, 8.4, 2, 0, "back_button", "Zurück") + local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- This form now defines its OWN behavior, making it independent and robust. +tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state) + state:size(8, 2.5) + state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?") + state:label(0, 0.5, "padname_label", "") + state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)") + + local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen") + local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen") + + local playername = state.location.player + local pending_data = tpad.pending_deletion[playername] + + if not pending_data then + state:close() + return + end + + local pad_to_delete = pending_data.pad_data + local original_form_context = pending_data.original_form_context + + local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")" + state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name) + + -- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht + local function return_to_management_with_fresh_state() + tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen + minetest.after(0, function() + -- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden + local fresh_context = { + playername = original_form_context.playername, + clicker = original_form_context.clicker, + ownername = original_form_context.ownername, + clicked_pos = original_form_context.clicked_pos, + node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat + network_type = original_form_context.network_type, + page = 1 + } + submit.management(fresh_context) + end) + end + + confirm_button:onClick(function() + tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos) + minetest.remove_node(pad_to_delete.pos) + notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.") + return_to_management_with_fresh_state() + end) + + deny_button:onClick(return_to_management_with_fresh_state) +end) + +tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state) + state:size(8, 8) + state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen") + state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)") + state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)") + state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern") + local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- ======================================================================== +-- Data Helper Functions +-- ======================================================================== + +function tpad.decorate_pad_data(pos, pad, ownername) + pad = table.copy(pad) + if type(pos) == "string" then + pad.strpos = pos + pad.pos = minetest.string_to_pos(pos) + else + pad.pos = pos + pad.strpos = minetest.pos_to_string(pos) + end + pad.owner = ownername + pad.name = pad.name or "" + pad.type = pad.type or PUBLIC_PAD + + -- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu. + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (privat)" + else + -- Bei Public Pads wird kein Suffix mehr angehängt. + pad.local_fullname = pad.name + end + + pad.global_fullname = pad.name + return pad +end + +function tpad.get_pad_data(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if not ownername or ownername == "" then return end + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] + if not pad then return end + return tpad.decorate_pad_data(pos, pad, ownername) +end + +function tpad.set_pad_data(pos, padname, padtype_str) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] or {} + pad.name = padname + pad.type = padtype_string_to_flag[padtype_str] + pads[strpos] = pad + tpad._set_stored_pads(ownername, pads) +end + +function tpad.del_pad(ownername, pos) + local pads = tpad._get_stored_pads(ownername) + pads[minetest.pos_to_string(pos)] = nil + tpad._set_stored_pads(ownername, pads) +end + +-- ======================================================================== +-- Register Node and Bind Callbacks +-- ======================================================================== + +local collision_box = { + type = "fixed", + fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 }, +} + +minetest.register_node(tpad.nodename, { + drawtype = "mesh", + tiles = { tpad.texture }, + mesh = tpad.mesh, + paramtype = "light", + paramtype2 = "facedir", + on_place = minetest.rotate_and_place, + after_place_node = tpad.after_place_node, + collision_box = collision_box, + selection_box = collision_box, + description = "Teleporter Pad", + groups = {choppy = 2, dig_immediate = 2}, + on_rightclick = tpad.on_rightclick, + can_dig = tpad.can_dig, + on_destruct = tpad.on_destruct, +}) + +minetest.register_chatcommand("tpad", {func = tpad.command}) diff --git a/init.lua.bak.3 b/init.lua.bak.3 new file mode 100644 index 0000000..2bdc3b7 --- /dev/null +++ b/init.lua.bak.3 @@ -0,0 +1,925 @@ +-- ======================================================================== +-- TPAD MOD v1.2 (Final, Reworked Delete Logic) +-- ======================================================================== + +tpad = {} +tpad.version = "1.2" -- As requested, version is not incremented +tpad.mod_name = minetest.get_current_modname() +tpad.texture = "tpad-texture.png" +tpad.mesh = "tpad-mesh.obj" +tpad.nodename = "tpad:tpad" +tpad.mod_path = minetest.get_modpath(tpad.mod_name) +tpad.sound_teleport = tpad.mod_name .. "_teleport" +tpad.particle_texture = tpad.mod_name .. "_particle.png" + +-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog +tpad.pending_deletion = {} + +-- ======================================================================== +-- Constants +-- ======================================================================== + +local PRIVATE_PAD_STRING = "Privat (nur Besitzer)" +local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)" +local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)" + +local PRIVATE_PAD = 1 +local PUBLIC_PAD = 2 +local GLOBAL_PAD = 4 + +local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") +local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00") +local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF") +local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF") +local OWNER_ESCAPE_COLOR = CYAN_ESCAPE + +local padtype_flag_to_string = { + [PRIVATE_PAD] = PRIVATE_PAD_STRING, + [PUBLIC_PAD] = PUBLIC_PAD_STRING, + [GLOBAL_PAD] = GLOBAL_PAD_STRING, +} +local padtype_string_to_flag = { + [PRIVATE_PAD_STRING] = PRIVATE_PAD, + [PUBLIC_PAD_STRING] = PUBLIC_PAD, + [GLOBAL_PAD_STRING] = GLOBAL_PAD, +} +local short_padtype_string = { + [PRIVATE_PAD] = "private", + [PUBLIC_PAD] = "public", + [GLOBAL_PAD] = "global", +} + +-- ======================================================================== +-- Dependencies and Libs +-- ======================================================================== + +local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua") +local notify = dofile(tpad.mod_path .. "/notify.lua") + +local waypoint_hud_ids = {} + +minetest.register_privilege("tpad_admin", { + description = "Can edit and destroy any tpad", + give_to_singleplayer = true, +}) + +-- ======================================================================== +-- Original Helper Functions +-- ======================================================================== + +local function copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then return false, "copy_file() unable to open source for reading" end + local src_data = src_file:read("*all") + src_file:close() + local dest_file = io.open(dest, "wb") + if not dest_file then return false, "copy_file() unable to open dest for writing" end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +local function custom_or_default(modname, path, filename) + local default_filename = "default/" .. filename + local full_filename = path .. "/custom." .. filename + local full_default_filename = path .. "/" .. default_filename + local file_exists_at_path = io.open(path .. "/" .. filename, "r") + if file_exists_at_path then + file_exists_at_path:close() + os.rename(path .. "/" .. filename, full_filename) + end + local file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")") + local success, err = copy_file(full_default_filename, full_filename) + if not success then + minetest.debug("[" .. modname .. "] " .. err) + return false + end + file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path) + return false + end + end + file:close() + return full_filename +end + +dofile(tpad.mod_path .. "/storage.lua") + +-- ======================================================================== +-- Load Custom Recipe +-- ======================================================================== + +local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua") +if recipes_filename then + local recipes = dofile(recipes_filename) + if type(recipes) == "table" and recipes[tpad.nodename] then + minetest.register_craft({ + output = tpad.nodename, + recipe = recipes[tpad.nodename], + }) + end +end + +-- ======================================================================== +-- Chat Command +-- ======================================================================== + +function tpad.command(playername, param) + tpad.hud_off(playername) + if(param == "off") then return end + local player = minetest.get_player_by_name(playername) + local pads = tpad._get_stored_pads(playername) + local shortest_distance = nil + local closest_pad = nil + local playerpos = player:getpos() + for strpos, pad in pairs(pads) do + local pos = minetest.string_to_pos(strpos) + local distance = vector.distance(pos, playerpos) + if not shortest_distance or distance < shortest_distance then + closest_pad = { + pos = pos, + name = pad.name .. " " .. strpos, + } + shortest_distance = distance + end + end + if closest_pad then + waypoint_hud_ids[playername] = player:hud_add({ + hud_elem_type = "waypoint", + name = closest_pad.name, + world_pos = closest_pad.pos, + number = 0xFF0000, + }) + notify(playername, "Waypoint to " .. closest_pad.name .. " displayed") + end +end + +function tpad.hud_off(playername) + local player = minetest.get_player_by_name(playername) + local hud_id = waypoint_hud_ids[playername] + if hud_id then + player:hud_remove(hud_id) + end +end + +-- ======================================================================== +-- Teleport Logic +-- ======================================================================== + +function tpad.do_teleport(player, destination_pos, destination_name, form_context) + local playername = player:get_player_name() + + minetest.after(0.1, function() + if not player or not player:is_player() then return end + local start_pos = player:getpos() + minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5), + minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + + player:move_to(destination_pos) + + minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}), + maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}), + minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + tpad.hud_off(playername) + + -- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren + minetest.after(0.2, function() + if not player or not player:is_player() then return end + + local dest_pad_data = tpad.get_pad_data(destination_pos) + if not dest_pad_data then return end + + local new_context = { + playername = playername, + clicker = player, + ownername = dest_pad_data.owner, + clicked_pos = destination_pos, + node = minetest.get_node(destination_pos), + page = 1, + -- Behalte den Netzwerk-Typ (lokal/global) bei + network_type = form_context.network_type + } + tpad.show_network_view(new_context, new_context.network_type) + end) + end) +end + +-- ======================================================================== +-- Node Placement and Data Helpers +-- ======================================================================== + +function tpad.after_place_node(pos, placer, itemstack) + local playername = placer:get_player_name() + if tpad.max_total_pads_reached(placer) then + notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.") + minetest.remove_node(pos) + minetest.add_item(placer:get_pos(), itemstack:get_name()) + return + end + + local meta = minetest.get_meta(pos) + meta:set_string("owner", playername) + meta:set_string("infotext", "TPAD Station von " .. playername) + tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING) +end + +local submit = {} + +function tpad.max_total_pads_reached(placer) + local placername = placer:get_player_name() + if minetest.get_player_privs(placername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(placername) + local count = 0 + for _ in pairs(pads) do + count = count + 1 + end + return count >= tpad.get_max_total_pads() +end + +function tpad.max_global_pads_reached(playername) + if minetest.get_player_privs(playername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(playername) + local count = 0 + for _, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + count = count + 1 + end + end + return count >= tpad.get_max_global_pads() +end + +-- ======================================================================== +-- GUI DATA HELPERS +-- ======================================================================== + +function submit.global_helper() + local allpads = tpad._get_all_pads() + local result = {} + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + end + table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end) + return result +end + +function submit.local_helper(ownername, viewername) + local result = {} + local added_pads = {} -- Verhindert, dass Pads doppelt hinzugefügt werden + + -- Schritt 1: Füge die öffentlichen Pads des Besitzers des angeklickten TPADs hinzu + local owner_pads = tpad._get_stored_pads(ownername) + for strpos, pad in pairs(owner_pads) do + -- Vom Besitzer werden nur die öffentlichen (public) Pads angezeigt + if pad.type == PUBLIC_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + added_pads[strpos] = true + end + end + + -- Schritt 2: Füge die eigenen Pads hinzu, falls der Betrachter eine andere Person ist + if ownername ~= viewername then + local viewer_pads = tpad._get_stored_pads(viewername) + for strpos, pad in pairs(viewer_pads) do + -- Vom Betrachter werden seine privaten und öffentlichen Pads angezeigt + if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then + table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername)) + added_pads[strpos] = true + end + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +function submit.management_helper(playername) + local is_admin = minetest.get_player_privs(playername).tpad_admin + local result = {} + + if is_admin then + local allpads = tpad._get_all_pads() + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + else + local pads = tpad._get_stored_pads(playername) + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, playername)) + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +-- ======================================================================== +-- GUI LOGIC (REFACTORED) +-- ======================================================================== + +function tpad.show_network_view(form, network_type) + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local form_key = is_admin and "network_view_admin" or "network_view" + form.state = tpad.forms[form_key]:show(form.playername) + form.formname = "tpad.forms." .. form_key + + local pad_list + local title_text + + local function create_clean_context() + return { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = form.page or 1 + } + end + + local current_pad_data = tpad.get_pad_data(form.clicked_pos) + local current_pad_name = current_pad_data.name or "Unnamed" + + local current_pad_type_str + if current_pad_data.type == GLOBAL_PAD then + current_pad_type_str = "GLOBALE" + else + current_pad_type_str = "LOKALE" + end + + local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or "" + title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:" + + if network_type == "global" then + form.network_type = "global" + pad_list = submit.global_helper() + form.state:get("toggle_network_button"):setText("Lokales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.ownername = clean_form.playername + clean_form.page = 1 + tpad.show_network_view(clean_form, "local") + end) + end) + else -- "local" + form.network_type = "local" + -- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean). + pad_list = submit.local_helper(form.ownername, form.playername) + form.state:get("toggle_network_button"):setText("Globales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = 1 + tpad.show_network_view(clean_form, "global") + end) + end) + end + + form.state:get("title_label"):setText(title_text) + form.state:get("management_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.preselect_pos = clean_form.clicked_pos + submit.management(clean_form) + end) + end) + + if is_admin then + form.state:get("admin_button"):onClick(function() + minetest.after(0, function() submit.admin_settings(form) end) + end) + end + + local max_rows, num_columns = 10, 3 + local buttons_per_page = max_rows * num_columns + + local destination_pads = {} + for _, pad in ipairs(pad_list) do + if not vector.equals(pad.pos, form.clicked_pos) then + table.insert(destination_pads, pad) + end + end + + local total_pages = math.ceil(#destination_pads / buttons_per_page) + if total_pages == 0 then total_pages = 1 end + + local current_page = form.page or 1 + if current_page > total_pages then current_page = total_pages end + if current_page < 1 then current_page = 1 end + form.page = current_page + + for i = 1, buttons_per_page do + local index = ((current_page - 1) * buttons_per_page) + i + local pad_data = destination_pads[index] + local button_name = "tpad_btn_" .. i + local button = form.state:get(button_name) + + if button then + if pad_data then + local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname + button:setText(display_name) + button:setVisible(true) + button:onClick(function() + tpad.do_teleport(form.clicker, pad_data.pos, display_name, form) + end) + else + button:setVisible(false) + end + end + end + + if total_pages > 1 then + form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages) + local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button") + prev_button:setVisible(current_page > 1) + next_button:setVisible(current_page < total_pages) + + prev_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page - 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + next_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page + 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + else + form.state:get("page_label"):setText("") + form.state:get("prev_button"):setVisible(false) + form.state:get("next_button"):setVisible(false) + end +end + +function submit.management(form) + form.formname = "tpad_management" + form.state = tpad.forms.management:show(form.playername) + local pad_list = submit.management_helper(form.playername) + local listbox = form.state:get("pads_listbox") + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local selected_pad_data = nil + + listbox:clearItems() + for _, pad in ipairs(pad_list) do + local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")" + if is_admin then + label = label .. " [" .. pad.owner .. "]" + end + listbox:addItem(label) + end + + local padname_field = form.state:get("padname_field") + local padtype_dropdown = form.state:get("padtype_dropdown") + + local function update_fields_for_index(index) + if not index or index <= 0 then return end + selected_pad_data = pad_list[index] + if selected_pad_data then + padname_field:setText(selected_pad_data.name) + padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type]) + end + end + + if form.preselect_pos then + for i, pad in ipairs(pad_list) do + if vector.equals(pad.pos, form.preselect_pos) then + listbox:setSelected(i) + update_fields_for_index(i) + break + end + end + end + + listbox:onClick(function() + local index = listbox:getSelected() + update_fields_for_index(index) + end) + + form.state:get("save_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + local new_name = padname_field:getText() + local new_type_str + local value_from_dropdown = padtype_dropdown:getSelectedItem() + if value_from_dropdown then + local index = tonumber(value_from_dropdown) + if index then + new_type_str = padtype_dropdown:getItem(index) + else + new_type_str = value_from_dropdown + end + end + if not new_type_str or new_type_str == "" then + notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.") + return + end + if not minetest.get_player_privs(form.playername).tpad_admin then + if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then + notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.") + return + end + end + tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str) + local meta = minetest.get_meta(selected_pad_data.pos) + if new_name and new_name ~= "" then + meta:set_string("infotext", "TPAD Station " .. new_name) + else + meta:set_string("infotext", "Unbenannte TPAD Station") + end + notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.") + minetest.after(0, function() + form.preselect_pos = nil + submit.management(form) + end) + end) + + form.state:get("delete_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + if vector.equals(selected_pad_data.pos, form.clicked_pos) then + notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.") + return + end + tpad.pending_deletion[form.playername] = { + pad_data = selected_pad_data, + original_form_context = form, + } + minetest.close_formspec(form.playername, form.formname) + minetest.after(0.1, function() + tpad.forms.confirm_pad_deletion:show(form.playername) + end) + end) + + form.state:get("back_button"):onClick(function() + minetest.after(0, function() + local clean_form_context = { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = 1 + } + tpad.show_network_view(clean_form_context, form.network_type) + end) + end) + + -- NEU: Logik für den Admin-Teleport-Button + if is_admin then + local admin_teleport_button = form.state:get("admin_teleport_button") + admin_teleport_button:setVisible(true) + admin_teleport_button:onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.") + return + end + + local dest_pad = selected_pad_data + + -- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll + local new_context_after_teleport = { + playername = form.playername, + clicker = form.clicker, + ownername = dest_pad.owner, + clicked_pos = dest_pad.pos, + node = minetest.get_node(dest_pad.pos), + page = 1, + network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local" + } + + tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport) + end) + end +end + +function submit.admin_settings(form) + form.state = tpad.forms.admin:show(form.playername) + form.formname = "tpad_admin_settings" + local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field") + max_total_field:setText(tpad.get_max_total_pads()) + max_global_field:setText(tpad.get_max_global_pads()) + + form.state:get("save_button"):onClick(function() + tpad.set_max_total_pads(tonumber(max_total_field:getText())) + tpad.set_max_global_pads(tonumber(max_global_field:getText())) + minetest.close_formspec(form.playername, form.formname) + end) +end + +-- ======================================================================== +-- Main Node Callbacks +-- ======================================================================== + +function tpad.on_rightclick(clicked_pos, node, clicker) + local playername = clicker:get_player_name() + local pad = tpad.get_pad_data(clicked_pos) + if not pad or not pad.owner then + notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.") + return + end + + -- Zugriffsschutz-Prüfung zuerst + if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then + notify.warn(playername, "Dieses TPAD ist privat.") + return + end + + -- Erstelle das Kontext-Objekt für alle Fälle + local form = { + playername = playername, + clicker = clicker, + ownername = pad.owner, + clicked_pos = clicked_pos, + node = node, + page = 1, + } + + -- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat) + if pad.name == "" then + -- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü + form.preselect_pos = clicked_pos + -- Rufe direkt die Verwaltung auf + submit.management(form) + else + -- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist + if pad.type == GLOBAL_PAD then + tpad.show_network_view(form, "global") + else + tpad.show_network_view(form, "local") + end + end +end + +function tpad.can_dig(pos, player) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local playername = player:get_player_name() + if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then + return true + end + notify.warn(playername, "Dieses TPAD gehört dir nicht.") + return false +end + +function tpad.on_destruct(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if ownername and ownername ~= "" then + tpad.del_pad(ownername, pos) + end +end + +-- ======================================================================== +-- FORMS (Rebuilt) +-- ======================================================================== + +tpad.forms = {} + +local function create_network_view_form(state) + state:size(16, 11) + state:label(0.5, 0.2, "title_label", "") + local bottom_y = 10.4 + state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "") + state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung") + local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) + state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]") + state:label(7.1, bottom_y, "page_label", "") + state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]") + + local max_rows, num_columns = 10, 3 + local start_x, start_y = 0.5, 1.0 + local button_width, button_height = 4.8, 0.8 + local column_width = 5.0 + for i = 1, max_rows * num_columns do + local column = math.floor((i - 1) / max_rows) + local row = (i - 1) % max_rows + local current_x = start_x + (column * column_width) + local current_y = start_y + (row * button_height) + state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false) + end +end + +tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state) + state:size(8, 2) + local destination_name = state.param.destination_name or "???" + state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name) + + -- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt. + local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form) + +tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state) + create_network_view_form(state) + state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin") +end) + +tpad.forms.management = smartfs.create("tpad.forms.management", function(state) + state:size(12, 9) + state:label(0.2, 0.2, "management_title", "TPAD Verwaltung") + state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {}) + state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "") + + local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown") + padtype_dropdown:addItem(PRIVATE_PAD_STRING) + padtype_dropdown:addItem(PUBLIC_PAD_STRING) + padtype_dropdown:addItem(GLOBAL_PAD_STRING) + + state:button(7, 6.3, 2, 0, "save_button", "Speichern") + state:button(7, 7.1, 2, 0, "delete_button", "Löschen") + state:button(0.2, 8.4, 2, 0, "back_button", "Zurück") + + -- NEU: Teleport-Button für Admins, standardmäßig unsichtbar + state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false) + + local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- This form now defines its OWN behavior, making it independent and robust. +tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state) + state:size(8, 2.5) + state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?") + state:label(0, 0.5, "padname_label", "") + state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)") + + local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen") + local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen") + + local playername = state.location.player + local pending_data = tpad.pending_deletion[playername] + + if not pending_data then + state:close() + return + end + + local pad_to_delete = pending_data.pad_data + local original_form_context = pending_data.original_form_context + + local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")" + state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name) + + -- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht + local function return_to_management_with_fresh_state() + tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen + minetest.after(0, function() + -- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden + local fresh_context = { + playername = original_form_context.playername, + clicker = original_form_context.clicker, + ownername = original_form_context.ownername, + clicked_pos = original_form_context.clicked_pos, + node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat + network_type = original_form_context.network_type, + page = 1 + } + submit.management(fresh_context) + end) + end + + confirm_button:onClick(function() + tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos) + minetest.remove_node(pad_to_delete.pos) + notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.") + return_to_management_with_fresh_state() + end) + + deny_button:onClick(return_to_management_with_fresh_state) +end) + +tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state) + state:size(8, 8) + state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen") + state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)") + state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)") + state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern") + local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- ======================================================================== +-- Data Helper Functions +-- ======================================================================== + +function tpad.decorate_pad_data(pos, pad, ownername) + pad = table.copy(pad) + if type(pos) == "string" then + pad.strpos = pos + pad.pos = minetest.string_to_pos(pos) + else + pad.pos = pos + pad.strpos = minetest.pos_to_string(pos) + end + pad.owner = ownername + pad.name = pad.name or "" + pad.type = pad.type or PUBLIC_PAD + + -- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu. + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (privat)" + else + -- Bei Public Pads wird kein Suffix mehr angehängt. + pad.local_fullname = pad.name + end + + pad.global_fullname = pad.name + return pad +end + +function tpad.get_pad_data(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if not ownername or ownername == "" then return end + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] + if not pad then return end + return tpad.decorate_pad_data(pos, pad, ownername) +end + +function tpad.set_pad_data(pos, padname, padtype_str) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] or {} + pad.name = padname + pad.type = padtype_string_to_flag[padtype_str] + pads[strpos] = pad + tpad._set_stored_pads(ownername, pads) +end + +function tpad.del_pad(ownername, pos) + local pads = tpad._get_stored_pads(ownername) + pads[minetest.pos_to_string(pos)] = nil + tpad._set_stored_pads(ownername, pads) +end + +-- ======================================================================== +-- Register Node and Bind Callbacks +-- ======================================================================== + +local collision_box = { + type = "fixed", + fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 }, +} + +minetest.register_node(tpad.nodename, { + drawtype = "mesh", + tiles = { tpad.texture }, + mesh = tpad.mesh, + paramtype = "light", + paramtype2 = "facedir", + on_place = minetest.rotate_and_place, + after_place_node = tpad.after_place_node, + collision_box = collision_box, + selection_box = collision_box, + description = "Teleporter Pad", + groups = {choppy = 2, dig_immediate = 2}, + on_rightclick = tpad.on_rightclick, + can_dig = tpad.can_dig, + on_destruct = tpad.on_destruct, +}) + +minetest.register_chatcommand("tpad", {func = tpad.command}) diff --git a/init.lua.bak.4 b/init.lua.bak.4 new file mode 100644 index 0000000..729e8e1 --- /dev/null +++ b/init.lua.bak.4 @@ -0,0 +1,931 @@ +-- ======================================================================== +-- TPAD MOD v1.2 (Final, Reworked Delete Logic) +-- ======================================================================== + +tpad = {} +tpad.version = "1.2" -- As requested, version is not incremented +tpad.mod_name = minetest.get_current_modname() +tpad.texture = "tpad-texture.png" +tpad.mesh = "tpad-mesh.obj" +tpad.nodename = "tpad:tpad" +tpad.mod_path = minetest.get_modpath(tpad.mod_name) +tpad.sound_teleport = tpad.mod_name .. "_teleport" +tpad.particle_texture = tpad.mod_name .. "_particle.png" + +-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog +tpad.pending_deletion = {} + +-- ======================================================================== +-- Constants +-- ======================================================================== + +local PRIVATE_PAD_STRING = "Privat (nur Besitzer)" +local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)" +local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)" + +local PRIVATE_PAD = 1 +local PUBLIC_PAD = 2 +local GLOBAL_PAD = 4 + +local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") +local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00") +local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF") +local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF") +local OWNER_ESCAPE_COLOR = CYAN_ESCAPE + +local padtype_flag_to_string = { + [PRIVATE_PAD] = PRIVATE_PAD_STRING, + [PUBLIC_PAD] = PUBLIC_PAD_STRING, + [GLOBAL_PAD] = GLOBAL_PAD_STRING, +} +local padtype_string_to_flag = { + [PRIVATE_PAD_STRING] = PRIVATE_PAD, + [PUBLIC_PAD_STRING] = PUBLIC_PAD, + [GLOBAL_PAD_STRING] = GLOBAL_PAD, +} +local short_padtype_string = { + [PRIVATE_PAD] = "private", + [PUBLIC_PAD] = "public", + [GLOBAL_PAD] = "global", +} + +-- ======================================================================== +-- Dependencies and Libs +-- ======================================================================== + +local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua") +local notify = dofile(tpad.mod_path .. "/notify.lua") + +local waypoint_hud_ids = {} + +minetest.register_privilege("tpad_admin", { + description = "Can edit and destroy any tpad", + give_to_singleplayer = true, +}) + +-- ======================================================================== +-- Original Helper Functions +-- ======================================================================== + +local function copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then return false, "copy_file() unable to open source for reading" end + local src_data = src_file:read("*all") + src_file:close() + local dest_file = io.open(dest, "wb") + if not dest_file then return false, "copy_file() unable to open dest for writing" end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +local function custom_or_default(modname, path, filename) + local default_filename = "default/" .. filename + local full_filename = path .. "/custom." .. filename + local full_default_filename = path .. "/" .. default_filename + local file_exists_at_path = io.open(path .. "/" .. filename, "r") + if file_exists_at_path then + file_exists_at_path:close() + os.rename(path .. "/" .. filename, full_filename) + end + local file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")") + local success, err = copy_file(full_default_filename, full_filename) + if not success then + minetest.debug("[" .. modname .. "] " .. err) + return false + end + file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path) + return false + end + end + file:close() + return full_filename +end + +dofile(tpad.mod_path .. "/storage.lua") + +-- ======================================================================== +-- Load Custom Recipe +-- ======================================================================== + +local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua") +if recipes_filename then + local recipes = dofile(recipes_filename) + if type(recipes) == "table" and recipes[tpad.nodename] then + minetest.register_craft({ + output = tpad.nodename, + recipe = recipes[tpad.nodename], + }) + end +end + +-- ======================================================================== +-- Chat Command +-- ======================================================================== + +function tpad.command(playername, param) + tpad.hud_off(playername) + if(param == "off") then return end + local player = minetest.get_player_by_name(playername) + local pads = tpad._get_stored_pads(playername) + local shortest_distance = nil + local closest_pad = nil + local playerpos = player:getpos() + for strpos, pad in pairs(pads) do + local pos = minetest.string_to_pos(strpos) + local distance = vector.distance(pos, playerpos) + if not shortest_distance or distance < shortest_distance then + closest_pad = { + pos = pos, + name = pad.name .. " " .. strpos, + } + shortest_distance = distance + end + end + if closest_pad then + waypoint_hud_ids[playername] = player:hud_add({ + hud_elem_type = "waypoint", + name = closest_pad.name, + world_pos = closest_pad.pos, + number = 0xFF0000, + }) + notify(playername, "Waypoint to " .. closest_pad.name .. " displayed") + end +end + +function tpad.hud_off(playername) + local player = minetest.get_player_by_name(playername) + local hud_id = waypoint_hud_ids[playername] + if hud_id then + player:hud_remove(hud_id) + end +end + +-- ======================================================================== +-- Teleport Logic +-- ======================================================================== + +function tpad.do_teleport(player, destination_pos, destination_name, form_context) + local playername = player:get_player_name() + + minetest.after(0.1, function() + if not player or not player:is_player() then return end + local start_pos = player:getpos() + minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5), + minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + + player:move_to(destination_pos) + + minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}), + maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}), + minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + tpad.hud_off(playername) + + -- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren + minetest.after(0.2, function() + if not player or not player:is_player() then return end + + local dest_pad_data = tpad.get_pad_data(destination_pos) + if not dest_pad_data then return end + + local new_context = { + playername = playername, + clicker = player, + ownername = dest_pad_data.owner, + clicked_pos = destination_pos, + node = minetest.get_node(destination_pos), + page = 1, + -- Behalte den Netzwerk-Typ (lokal/global) bei + network_type = form_context.network_type + } + tpad.show_network_view(new_context, new_context.network_type) + end) + end) +end + +-- ======================================================================== +-- Node Placement and Data Helpers +-- ======================================================================== + +function tpad.after_place_node(pos, placer, itemstack) + local playername = placer:get_player_name() + if tpad.max_total_pads_reached(placer) then + notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.") + minetest.remove_node(pos) + minetest.add_item(placer:get_pos(), itemstack:get_name()) + return + end + + local meta = minetest.get_meta(pos) + meta:set_string("owner", playername) + meta:set_string("infotext", "TPAD Station von " .. playername) + tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING) +end + +local submit = {} + +function tpad.max_total_pads_reached(placer) + local placername = placer:get_player_name() + if minetest.get_player_privs(placername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(placername) + local count = 0 + for _ in pairs(pads) do + count = count + 1 + end + return count >= tpad.get_max_total_pads() +end + +function tpad.max_global_pads_reached(playername) + if minetest.get_player_privs(playername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(playername) + local count = 0 + for _, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + count = count + 1 + end + end + return count >= tpad.get_max_global_pads() +end + +-- ======================================================================== +-- GUI DATA HELPERS +-- ======================================================================== + +function submit.global_helper() + local allpads = tpad._get_all_pads() + local result = {} + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + end + table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end) + return result +end + +function submit.local_helper(ownername, viewername) + local result = {} + local added_pads = {} -- Verhindert, dass Pads doppelt hinzugefügt werden + + -- Schritt 1: Hole die Pads des Besitzers, auf dessen Pad man steht. + local owner_pads = tpad._get_stored_pads(ownername) + for strpos, pad in pairs(owner_pads) do + -- KORREKTUR: Wenn der Betrachter der Besitzer ist, zeige auch seine privaten Pads an. + -- Ansonsten zeige nur die öffentlichen Pads des Besitzers. + local is_viewer_the_owner = (ownername == viewername) + if (pad.type == PUBLIC_PAD) or (is_viewer_the_owner and pad.type == PRIVATE_PAD) then + -- Füge nur Pads hinzu, die nicht global sind. + if pad.type ~= GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + added_pads[strpos] = true + end + end + end + + -- Schritt 2: Füge die eigenen Pads des Betrachters hinzu, falls er eine andere Person ist. + if ownername ~= viewername then + local viewer_pads = tpad._get_stored_pads(viewername) + for strpos, pad in pairs(viewer_pads) do + -- Der Betrachter sieht immer seine eigenen privaten und öffentlichen Pads. + if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then + table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername)) + added_pads[strpos] = true + end + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +function submit.management_helper(playername) + local is_admin = minetest.get_player_privs(playername).tpad_admin + local result = {} + + if is_admin then + local allpads = tpad._get_all_pads() + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername)) + end + end + else + local pads = tpad._get_stored_pads(playername) + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, playername)) + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +-- ======================================================================== +-- GUI LOGIC (REFACTORED) +-- ======================================================================== + +function tpad.show_network_view(form, network_type) + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local form_key = is_admin and "network_view_admin" or "network_view" + form.state = tpad.forms[form_key]:show(form.playername) + form.formname = "tpad.forms." .. form_key + + local pad_list + local title_text + + local function create_clean_context() + return { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = form.page or 1 + } + end + + local current_pad_data = tpad.get_pad_data(form.clicked_pos) + local current_pad_name = current_pad_data.name or "Unnamed" + + local current_pad_type_str + if current_pad_data.type == GLOBAL_PAD then + current_pad_type_str = "GLOBALE" + else + current_pad_type_str = "LOKALE" + end + + local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or "" + title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:" + + if network_type == "global" then + form.network_type = "global" + pad_list = submit.global_helper() + form.state:get("toggle_network_button"):setText("Lokales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + -- KORREKTUR: Der 'ownername' wird nicht mehr überschrieben. + -- Er bleibt der des ursprünglichen Pads, was korrekt ist. + clean_form.page = 1 + tpad.show_network_view(clean_form, "local") + end) + end) + else -- "local" + form.network_type = "local" + -- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean). + pad_list = submit.local_helper(form.ownername, form.playername) + form.state:get("toggle_network_button"):setText("Globales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = 1 + tpad.show_network_view(clean_form, "global") + end) + end) + end + + form.state:get("title_label"):setText(title_text) + form.state:get("management_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.preselect_pos = clean_form.clicked_pos + submit.management(clean_form) + end) + end) + + if is_admin then + form.state:get("admin_button"):onClick(function() + minetest.after(0, function() submit.admin_settings(form) end) + end) + end + + local max_rows, num_columns = 10, 3 + local buttons_per_page = max_rows * num_columns + + local destination_pads = {} + for _, pad in ipairs(pad_list) do + if not vector.equals(pad.pos, form.clicked_pos) then + table.insert(destination_pads, pad) + end + end + + local total_pages = math.ceil(#destination_pads / buttons_per_page) + if total_pages == 0 then total_pages = 1 end + + local current_page = form.page or 1 + if current_page > total_pages then current_page = total_pages end + if current_page < 1 then current_page = 1 end + form.page = current_page + + for i = 1, buttons_per_page do + local index = ((current_page - 1) * buttons_per_page) + i + local pad_data = destination_pads[index] + local button_name = "tpad_btn_" .. i + local button = form.state:get(button_name) + + if button then + if pad_data then + local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname + button:setText(display_name) + button:setVisible(true) + button:onClick(function() + tpad.do_teleport(form.clicker, pad_data.pos, display_name, form) + end) + else + button:setVisible(false) + end + end + end + + if total_pages > 1 then + form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages) + local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button") + prev_button:setVisible(current_page > 1) + next_button:setVisible(current_page < total_pages) + + prev_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page - 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + next_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page + 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + else + form.state:get("page_label"):setText("") + form.state:get("prev_button"):setVisible(false) + form.state:get("next_button"):setVisible(false) + end +end + +function submit.management(form) + form.formname = "tpad_management" + form.state = tpad.forms.management:show(form.playername) + local pad_list = submit.management_helper(form.playername) + local listbox = form.state:get("pads_listbox") + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local selected_pad_data = nil + + listbox:clearItems() + for _, pad in ipairs(pad_list) do + local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")" + if is_admin then + label = label .. " [" .. pad.owner .. "]" + end + listbox:addItem(label) + end + + local padname_field = form.state:get("padname_field") + local padtype_dropdown = form.state:get("padtype_dropdown") + + local function update_fields_for_index(index) + if not index or index <= 0 then return end + selected_pad_data = pad_list[index] + if selected_pad_data then + padname_field:setText(selected_pad_data.name) + padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type]) + end + end + + if form.preselect_pos then + for i, pad in ipairs(pad_list) do + if vector.equals(pad.pos, form.preselect_pos) then + listbox:setSelected(i) + update_fields_for_index(i) + break + end + end + end + + listbox:onClick(function() + local index = listbox:getSelected() + update_fields_for_index(index) + end) + + form.state:get("save_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + local new_name = padname_field:getText() + local new_type_str + local value_from_dropdown = padtype_dropdown:getSelectedItem() + if value_from_dropdown then + local index = tonumber(value_from_dropdown) + if index then + new_type_str = padtype_dropdown:getItem(index) + else + new_type_str = value_from_dropdown + end + end + if not new_type_str or new_type_str == "" then + notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.") + return + end + if not minetest.get_player_privs(form.playername).tpad_admin then + if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then + notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.") + return + end + end + tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str) + local meta = minetest.get_meta(selected_pad_data.pos) + if new_name and new_name ~= "" then + meta:set_string("infotext", "TPAD Station " .. new_name) + else + meta:set_string("infotext", "Unbenannte TPAD Station") + end + notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.") + minetest.after(0, function() + form.preselect_pos = nil + submit.management(form) + end) + end) + + form.state:get("delete_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + if vector.equals(selected_pad_data.pos, form.clicked_pos) then + notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.") + return + end + tpad.pending_deletion[form.playername] = { + pad_data = selected_pad_data, + original_form_context = form, + } + minetest.close_formspec(form.playername, form.formname) + minetest.after(0.1, function() + tpad.forms.confirm_pad_deletion:show(form.playername) + end) + end) + + form.state:get("back_button"):onClick(function() + minetest.after(0, function() + local clean_form_context = { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = 1 + } + tpad.show_network_view(clean_form_context, form.network_type) + end) + end) + + -- NEU: Logik für den Admin-Teleport-Button + if is_admin then + local admin_teleport_button = form.state:get("admin_teleport_button") + admin_teleport_button:setVisible(true) + admin_teleport_button:onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.") + return + end + + local dest_pad = selected_pad_data + + -- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll + local new_context_after_teleport = { + playername = form.playername, + clicker = form.clicker, + ownername = dest_pad.owner, + clicked_pos = dest_pad.pos, + node = minetest.get_node(dest_pad.pos), + page = 1, + network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local" + } + + tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport) + end) + end +end + +function submit.admin_settings(form) + form.state = tpad.forms.admin:show(form.playername) + form.formname = "tpad_admin_settings" + local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field") + max_total_field:setText(tpad.get_max_total_pads()) + max_global_field:setText(tpad.get_max_global_pads()) + + form.state:get("save_button"):onClick(function() + tpad.set_max_total_pads(tonumber(max_total_field:getText())) + tpad.set_max_global_pads(tonumber(max_global_field:getText())) + minetest.close_formspec(form.playername, form.formname) + end) +end + +-- ======================================================================== +-- Main Node Callbacks +-- ======================================================================== + +function tpad.on_rightclick(clicked_pos, node, clicker) + local playername = clicker:get_player_name() + local pad = tpad.get_pad_data(clicked_pos) + if not pad or not pad.owner then + notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.") + return + end + + -- Zugriffsschutz-Prüfung zuerst + if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then + notify.warn(playername, "Dieses TPAD ist privat.") + return + end + + -- Erstelle das Kontext-Objekt für alle Fälle + local form = { + playername = playername, + clicker = clicker, + ownername = pad.owner, + clicked_pos = clicked_pos, + node = node, + page = 1, + } + + -- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat) + if pad.name == "" then + -- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü + form.preselect_pos = clicked_pos + -- Rufe direkt die Verwaltung auf + submit.management(form) + else + -- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist + if pad.type == GLOBAL_PAD then + tpad.show_network_view(form, "global") + else + tpad.show_network_view(form, "local") + end + end +end + +function tpad.can_dig(pos, player) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local playername = player:get_player_name() + if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then + return true + end + notify.warn(playername, "Dieses TPAD gehört dir nicht.") + return false +end + +function tpad.on_destruct(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if ownername and ownername ~= "" then + tpad.del_pad(ownername, pos) + end +end + +-- ======================================================================== +-- FORMS (Rebuilt) +-- ======================================================================== + +tpad.forms = {} + +local function create_network_view_form(state) + state:size(16, 11) + state:label(0.5, 0.2, "title_label", "") + local bottom_y = 10.4 + state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "") + state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung") + local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) + state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]") + state:label(7.1, bottom_y, "page_label", "") + state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]") + + local max_rows, num_columns = 10, 3 + local start_x, start_y = 0.5, 1.0 + local button_width, button_height = 4.8, 0.8 + local column_width = 5.0 + for i = 1, max_rows * num_columns do + local column = math.floor((i - 1) / max_rows) + local row = (i - 1) % max_rows + local current_x = start_x + (column * column_width) + local current_y = start_y + (row * button_height) + state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false) + end +end + +tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state) + state:size(8, 2) + local destination_name = state.param.destination_name or "???" + state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name) + + -- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt. + local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form) + +tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state) + create_network_view_form(state) + state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin") +end) + +tpad.forms.management = smartfs.create("tpad.forms.management", function(state) + state:size(12, 9) + state:label(0.2, 0.2, "management_title", "TPAD Verwaltung") + state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {}) + state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "") + + local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown") + padtype_dropdown:addItem(PRIVATE_PAD_STRING) + padtype_dropdown:addItem(PUBLIC_PAD_STRING) + padtype_dropdown:addItem(GLOBAL_PAD_STRING) + + state:button(7, 6.3, 2, 0, "save_button", "Speichern") + state:button(7, 7.1, 2, 0, "delete_button", "Löschen") + state:button(0.2, 8.4, 2, 0, "back_button", "Zurück") + + -- NEU: Teleport-Button für Admins, standardmäßig unsichtbar + state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false) + + local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- This form now defines its OWN behavior, making it independent and robust. +tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state) + state:size(8, 2.5) + state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?") + state:label(0, 0.5, "padname_label", "") + state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)") + + local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen") + local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen") + + local playername = state.location.player + local pending_data = tpad.pending_deletion[playername] + + if not pending_data then + state:close() + return + end + + local pad_to_delete = pending_data.pad_data + local original_form_context = pending_data.original_form_context + + local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")" + state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name) + + -- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht + local function return_to_management_with_fresh_state() + tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen + minetest.after(0, function() + -- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden + local fresh_context = { + playername = original_form_context.playername, + clicker = original_form_context.clicker, + ownername = original_form_context.ownername, + clicked_pos = original_form_context.clicked_pos, + node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat + network_type = original_form_context.network_type, + page = 1 + } + submit.management(fresh_context) + end) + end + + confirm_button:onClick(function() + tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos) + minetest.remove_node(pad_to_delete.pos) + notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.") + return_to_management_with_fresh_state() + end) + + deny_button:onClick(return_to_management_with_fresh_state) +end) + +tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state) + state:size(8, 8) + state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen") + state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)") + state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)") + state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern") + local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- ======================================================================== +-- Data Helper Functions +-- ======================================================================== + +function tpad.decorate_pad_data(pos, pad, ownername) + pad = table.copy(pad) + if type(pos) == "string" then + pad.strpos = pos + pad.pos = minetest.string_to_pos(pos) + else + pad.pos = pos + pad.strpos = minetest.pos_to_string(pos) + end + pad.owner = ownername + pad.name = pad.name or "" + pad.type = pad.type or PUBLIC_PAD + + -- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu. + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (privat)" + else + -- Bei Public Pads wird kein Suffix mehr angehängt. + pad.local_fullname = pad.name + end + + pad.global_fullname = pad.name + return pad +end + +function tpad.get_pad_data(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if not ownername or ownername == "" then return end + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] + if not pad then return end + return tpad.decorate_pad_data(pos, pad, ownername) +end + +function tpad.set_pad_data(pos, padname, padtype_str) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] or {} + pad.name = padname + pad.type = padtype_string_to_flag[padtype_str] + pads[strpos] = pad + tpad._set_stored_pads(ownername, pads) +end + +function tpad.del_pad(ownername, pos) + local pads = tpad._get_stored_pads(ownername) + pads[minetest.pos_to_string(pos)] = nil + tpad._set_stored_pads(ownername, pads) +end + +-- ======================================================================== +-- Register Node and Bind Callbacks +-- ======================================================================== + +local collision_box = { + type = "fixed", + fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 }, +} + +minetest.register_node(tpad.nodename, { + drawtype = "mesh", + tiles = { tpad.texture }, + mesh = tpad.mesh, + paramtype = "light", + paramtype2 = "facedir", + on_place = minetest.rotate_and_place, + after_place_node = tpad.after_place_node, + collision_box = collision_box, + selection_box = collision_box, + description = "Teleporter Pad", + groups = {choppy = 2, dig_immediate = 2}, + on_rightclick = tpad.on_rightclick, + can_dig = tpad.can_dig, + on_destruct = tpad.on_destruct, +}) + +minetest.register_chatcommand("tpad", {func = tpad.command}) diff --git a/init.lua.bak.5 b/init.lua.bak.5 new file mode 100644 index 0000000..49f33ec --- /dev/null +++ b/init.lua.bak.5 @@ -0,0 +1,938 @@ +-- ======================================================================== +-- TPAD MOD v1.2 (Final, Reworked Delete Logic) +-- ======================================================================== + +tpad = {} +tpad.version = "1.2" -- As requested, version is not incremented +tpad.mod_name = minetest.get_current_modname() +tpad.texture = "tpad-texture.png" +tpad.mesh = "tpad-mesh.obj" +tpad.nodename = "tpad:tpad" +tpad.mod_path = minetest.get_modpath(tpad.mod_name) +tpad.sound_teleport = tpad.mod_name .. "_teleport" +tpad.particle_texture = tpad.mod_name .. "_particle.png" + +-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog +tpad.pending_deletion = {} + +-- ======================================================================== +-- Constants +-- ======================================================================== + +local PRIVATE_PAD_STRING = "Privat (nur Besitzer)" +local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)" +local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)" + +local PRIVATE_PAD = 1 +local PUBLIC_PAD = 2 +local GLOBAL_PAD = 4 + +local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") +local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00") +local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF") +local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF") +local OWNER_ESCAPE_COLOR = CYAN_ESCAPE + +local padtype_flag_to_string = { + [PRIVATE_PAD] = PRIVATE_PAD_STRING, + [PUBLIC_PAD] = PUBLIC_PAD_STRING, + [GLOBAL_PAD] = GLOBAL_PAD_STRING, +} +local padtype_string_to_flag = { + [PRIVATE_PAD_STRING] = PRIVATE_PAD, + [PUBLIC_PAD_STRING] = PUBLIC_PAD, + [GLOBAL_PAD_STRING] = GLOBAL_PAD, +} +local short_padtype_string = { + [PRIVATE_PAD] = "private", + [PUBLIC_PAD] = "public", + [GLOBAL_PAD] = "global", +} + +-- ======================================================================== +-- Dependencies and Libs +-- ======================================================================== + +local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua") +local notify = dofile(tpad.mod_path .. "/notify.lua") + +local waypoint_hud_ids = {} + +minetest.register_privilege("tpad_admin", { + description = "Can edit and destroy any tpad", + give_to_singleplayer = true, +}) + +-- ======================================================================== +-- Original Helper Functions +-- ======================================================================== + +local function copy_file(source, dest) + local src_file = io.open(source, "rb") + if not src_file then return false, "copy_file() unable to open source for reading" end + local src_data = src_file:read("*all") + src_file:close() + local dest_file = io.open(dest, "wb") + if not dest_file then return false, "copy_file() unable to open dest for writing" end + dest_file:write(src_data) + dest_file:close() + return true, "files copied successfully" +end + +local function custom_or_default(modname, path, filename) + local default_filename = "default/" .. filename + local full_filename = path .. "/custom." .. filename + local full_default_filename = path .. "/" .. default_filename + local file_exists_at_path = io.open(path .. "/" .. filename, "r") + if file_exists_at_path then + file_exists_at_path:close() + os.rename(path .. "/" .. filename, full_filename) + end + local file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")") + local success, err = copy_file(full_default_filename, full_filename) + if not success then + minetest.debug("[" .. modname .. "] " .. err) + return false + end + file = io.open(full_filename, "rb") + if not file then + minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path) + return false + end + end + file:close() + return full_filename +end + +dofile(tpad.mod_path .. "/storage.lua") + +-- ======================================================================== +-- Load Custom Recipe +-- ======================================================================== + +local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua") +if recipes_filename then + local recipes = dofile(recipes_filename) + if type(recipes) == "table" and recipes[tpad.nodename] then + minetest.register_craft({ + output = tpad.nodename, + recipe = recipes[tpad.nodename], + }) + end +end + +-- ======================================================================== +-- Chat Command +-- ======================================================================== + +function tpad.command(playername, param) + tpad.hud_off(playername) + if(param == "off") then return end + local player = minetest.get_player_by_name(playername) + local pads = tpad._get_stored_pads(playername) + local shortest_distance = nil + local closest_pad = nil + local playerpos = player:getpos() + for strpos, pad in pairs(pads) do + local pos = minetest.string_to_pos(strpos) + local distance = vector.distance(pos, playerpos) + if not shortest_distance or distance < shortest_distance then + closest_pad = { + pos = pos, + name = pad.name .. " " .. strpos, + } + shortest_distance = distance + end + end + if closest_pad then + waypoint_hud_ids[playername] = player:hud_add({ + hud_elem_type = "waypoint", + name = closest_pad.name, + world_pos = closest_pad.pos, + number = 0xFF0000, + }) + notify(playername, "Waypoint to " .. closest_pad.name .. " displayed") + end +end + +function tpad.hud_off(playername) + local player = minetest.get_player_by_name(playername) + local hud_id = waypoint_hud_ids[playername] + if hud_id then + player:hud_remove(hud_id) + end +end + +-- ======================================================================== +-- Teleport Logic +-- ======================================================================== + +function tpad.do_teleport(player, destination_pos, destination_name, form_context) + local playername = player:get_player_name() + + minetest.after(0.1, function() + if not player or not player:is_player() then return end + local start_pos = player:getpos() + minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5), + minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + + player:move_to(destination_pos) + + minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0}) + minetest.add_particlespawner({ + amount = 60, time = 0.5, + minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}), + maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}), + minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, + minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0}, + minexptime = 0.5, maxexptime = 2, + minsize = 1, maxsize = 3, + texture = tpad.particle_texture, + }) + tpad.hud_off(playername) + + -- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren + minetest.after(0.2, function() + if not player or not player:is_player() then return end + + local dest_pad_data = tpad.get_pad_data(destination_pos) + if not dest_pad_data then return end + + local new_context = { + playername = playername, + clicker = player, + ownername = dest_pad_data.owner, + clicked_pos = destination_pos, + node = minetest.get_node(destination_pos), + page = 1, + -- Behalte den Netzwerk-Typ (lokal/global) bei + network_type = form_context.network_type + } + tpad.show_network_view(new_context, new_context.network_type) + end) + end) +end + +-- ======================================================================== +-- Node Placement and Data Helpers +-- ======================================================================== + +function tpad.after_place_node(pos, placer, itemstack) + local playername = placer:get_player_name() + if tpad.max_total_pads_reached(placer) then + notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.") + minetest.remove_node(pos) + minetest.add_item(placer:get_pos(), itemstack:get_name()) + return + end + + local meta = minetest.get_meta(pos) + meta:set_string("owner", playername) + meta:set_string("infotext", "TPAD Station von " .. playername) + tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING) +end + +local submit = {} + +function tpad.max_total_pads_reached(placer) + local placername = placer:get_player_name() + if minetest.get_player_privs(placername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(placername) + local count = 0 + for _ in pairs(pads) do + count = count + 1 + end + return count >= tpad.get_max_total_pads() +end + +function tpad.max_global_pads_reached(playername) + if minetest.get_player_privs(playername).tpad_admin then + return false + end + local pads = tpad._get_stored_pads(playername) + local count = 0 + for _, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + count = count + 1 + end + end + return count >= tpad.get_max_global_pads() +end + +-- ======================================================================== +-- GUI DATA HELPERS +-- ======================================================================== + +function submit.global_helper() + local allpads = tpad._get_all_pads() + local result = {} + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + if pad.type == GLOBAL_PAD then + -- Der 'viewername' ist hier nicht bekannt, also wird 'nil' übergeben, was die Funktion korrekt behandelt. + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, nil)) + end + end + end + table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end) + return result +end + +function submit.local_helper(ownername, viewername) + local result = {} + local added_pads = {} + + local owner_pads = tpad._get_stored_pads(ownername) + for strpos, pad in pairs(owner_pads) do + local is_viewer_the_owner = (ownername == viewername) + if (pad.type == PUBLIC_PAD) or (is_viewer_the_owner and pad.type == PRIVATE_PAD) then + if pad.type ~= GLOBAL_PAD then + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, viewername)) + added_pads[strpos] = true + end + end + end + + if ownername ~= viewername then + local viewer_pads = tpad._get_stored_pads(viewername) + for strpos, pad in pairs(viewer_pads) do + if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then + table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername, viewername)) + added_pads[strpos] = true + end + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +function submit.management_helper(playername) + local is_admin = minetest.get_player_privs(playername).tpad_admin + local result = {} + + if is_admin then + local allpads = tpad._get_all_pads() + for ownername, pads in pairs(allpads) do + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, playername)) + end + end + else + local pads = tpad._get_stored_pads(playername) + for strpos, pad in pairs(pads) do + table.insert(result, tpad.decorate_pad_data(strpos, pad, playername, playername)) + end + end + + table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end) + return result +end + +-- ======================================================================== +-- GUI LOGIC (REFACTORED) +-- ======================================================================== + +function tpad.show_network_view(form, network_type) + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local form_key = is_admin and "network_view_admin" or "network_view" + form.state = tpad.forms[form_key]:show(form.playername) + form.formname = "tpad.forms." .. form_key + + local pad_list + local title_text + + local function create_clean_context() + return { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = form.page or 1 + } + end + + local current_pad_data = tpad.get_pad_data(form.clicked_pos) + local current_pad_name = current_pad_data.name or "Unnamed" + + local current_pad_type_str + if current_pad_data.type == GLOBAL_PAD then + current_pad_type_str = "GLOBALE" + else + current_pad_type_str = "LOKALE" + end + + local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or "" + title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:" + + if network_type == "global" then + form.network_type = "global" + pad_list = submit.global_helper() + form.state:get("toggle_network_button"):setText("Lokales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + -- KORREKTUR: Der 'ownername' wird nicht mehr überschrieben. + -- Er bleibt der des ursprünglichen Pads, was korrekt ist. + clean_form.page = 1 + tpad.show_network_view(clean_form, "local") + end) + end) + else -- "local" + form.network_type = "local" + -- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean). + pad_list = submit.local_helper(form.ownername, form.playername) + form.state:get("toggle_network_button"):setText("Globales Netzwerk") + form.state:get("toggle_network_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = 1 + tpad.show_network_view(clean_form, "global") + end) + end) + end + + form.state:get("title_label"):setText(title_text) + form.state:get("management_button"):onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.preselect_pos = clean_form.clicked_pos + submit.management(clean_form) + end) + end) + + if is_admin then + form.state:get("admin_button"):onClick(function() + minetest.after(0, function() submit.admin_settings(form) end) + end) + end + + local max_rows, num_columns = 10, 3 + local buttons_per_page = max_rows * num_columns + + local destination_pads = {} + for _, pad in ipairs(pad_list) do + if not vector.equals(pad.pos, form.clicked_pos) then + table.insert(destination_pads, pad) + end + end + + local total_pages = math.ceil(#destination_pads / buttons_per_page) + if total_pages == 0 then total_pages = 1 end + + local current_page = form.page or 1 + if current_page > total_pages then current_page = total_pages end + if current_page < 1 then current_page = 1 end + form.page = current_page + + for i = 1, buttons_per_page do + local index = ((current_page - 1) * buttons_per_page) + i + local pad_data = destination_pads[index] + local button_name = "tpad_btn_" .. i + local button = form.state:get(button_name) + + if button then + if pad_data then + local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname + button:setText(display_name) + button:setVisible(true) + button:onClick(function() + tpad.do_teleport(form.clicker, pad_data.pos, display_name, form) + end) + else + button:setVisible(false) + end + end + end + + if total_pages > 1 then + form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages) + local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button") + prev_button:setVisible(current_page > 1) + next_button:setVisible(current_page < total_pages) + + prev_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page - 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + next_button:onClick(function() + minetest.after(0, function() + local clean_form = create_clean_context() + clean_form.page = current_page + 1 + tpad.show_network_view(clean_form, network_type) + end) + end) + else + form.state:get("page_label"):setText("") + form.state:get("prev_button"):setVisible(false) + form.state:get("next_button"):setVisible(false) + end +end + +function submit.management(form) + form.formname = "tpad_management" + form.state = tpad.forms.management:show(form.playername) + local pad_list = submit.management_helper(form.playername) + local listbox = form.state:get("pads_listbox") + local is_admin = minetest.get_player_privs(form.playername).tpad_admin + local selected_pad_data = nil + + listbox:clearItems() + for _, pad in ipairs(pad_list) do + local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")" + if is_admin then + label = label .. " [" .. pad.owner .. "]" + end + listbox:addItem(label) + end + + local padname_field = form.state:get("padname_field") + local padtype_dropdown = form.state:get("padtype_dropdown") + + local function update_fields_for_index(index) + if not index or index <= 0 then return end + selected_pad_data = pad_list[index] + if selected_pad_data then + padname_field:setText(selected_pad_data.name) + padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type]) + end + end + + if form.preselect_pos then + for i, pad in ipairs(pad_list) do + if vector.equals(pad.pos, form.preselect_pos) then + listbox:setSelected(i) + update_fields_for_index(i) + break + end + end + end + + listbox:onClick(function() + local index = listbox:getSelected() + update_fields_for_index(index) + end) + + form.state:get("save_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + local new_name = padname_field:getText() + local new_type_str + local value_from_dropdown = padtype_dropdown:getSelectedItem() + if value_from_dropdown then + local index = tonumber(value_from_dropdown) + if index then + new_type_str = padtype_dropdown:getItem(index) + else + new_type_str = value_from_dropdown + end + end + if not new_type_str or new_type_str == "" then + notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.") + return + end + if not minetest.get_player_privs(form.playername).tpad_admin then + if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then + notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.") + return + end + end + tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str) + local meta = minetest.get_meta(selected_pad_data.pos) + if new_name and new_name ~= "" then + meta:set_string("infotext", "TPAD Station " .. new_name) + else + meta:set_string("infotext", "Unbenannte TPAD Station") + end + notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.") + minetest.after(0, function() + form.preselect_pos = nil + submit.management(form) + end) + end) + + form.state:get("delete_button"):onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.") + return + end + if vector.equals(selected_pad_data.pos, form.clicked_pos) then + notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.") + return + end + tpad.pending_deletion[form.playername] = { + pad_data = selected_pad_data, + original_form_context = form, + } + minetest.close_formspec(form.playername, form.formname) + minetest.after(0.1, function() + tpad.forms.confirm_pad_deletion:show(form.playername) + end) + end) + + form.state:get("back_button"):onClick(function() + minetest.after(0, function() + local clean_form_context = { + playername = form.playername, + clicker = form.clicker, + ownername = form.ownername, + clicked_pos = form.clicked_pos, + node = form.node, + page = 1 + } + tpad.show_network_view(clean_form_context, form.network_type) + end) + end) + + -- NEU: Logik für den Admin-Teleport-Button + if is_admin then + local admin_teleport_button = form.state:get("admin_teleport_button") + admin_teleport_button:setVisible(true) + admin_teleport_button:onClick(function() + if not selected_pad_data then + notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.") + return + end + + local dest_pad = selected_pad_data + + -- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll + local new_context_after_teleport = { + playername = form.playername, + clicker = form.clicker, + ownername = dest_pad.owner, + clicked_pos = dest_pad.pos, + node = minetest.get_node(dest_pad.pos), + page = 1, + network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local" + } + + tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport) + end) + end +end + +function submit.admin_settings(form) + form.state = tpad.forms.admin:show(form.playername) + form.formname = "tpad_admin_settings" + local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field") + max_total_field:setText(tpad.get_max_total_pads()) + max_global_field:setText(tpad.get_max_global_pads()) + + form.state:get("save_button"):onClick(function() + tpad.set_max_total_pads(tonumber(max_total_field:getText())) + tpad.set_max_global_pads(tonumber(max_global_field:getText())) + minetest.close_formspec(form.playername, form.formname) + end) +end + +-- ======================================================================== +-- Main Node Callbacks +-- ======================================================================== + +function tpad.on_rightclick(clicked_pos, node, clicker) + local playername = clicker:get_player_name() + local pad = tpad.get_pad_data(clicked_pos) + if not pad or not pad.owner then + notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.") + return + end + + -- Zugriffsschutz-Prüfung zuerst + if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then + notify.warn(playername, "Dieses TPAD ist privat.") + return + end + + -- Erstelle das Kontext-Objekt für alle Fälle + local form = { + playername = playername, + clicker = clicker, + ownername = pad.owner, + clicked_pos = clicked_pos, + node = node, + page = 1, + } + + -- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat) + if pad.name == "" then + -- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü + form.preselect_pos = clicked_pos + -- Rufe direkt die Verwaltung auf + submit.management(form) + else + -- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist + if pad.type == GLOBAL_PAD then + tpad.show_network_view(form, "global") + else + tpad.show_network_view(form, "local") + end + end +end + +function tpad.can_dig(pos, player) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local playername = player:get_player_name() + if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then + return true + end + notify.warn(playername, "Dieses TPAD gehört dir nicht.") + return false +end + +function tpad.on_destruct(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if ownername and ownername ~= "" then + tpad.del_pad(ownername, pos) + end +end + +-- ======================================================================== +-- FORMS (Rebuilt) +-- ======================================================================== + +tpad.forms = {} + +local function create_network_view_form(state) + state:size(16, 11) + state:label(0.5, 0.2, "title_label", "") + local bottom_y = 10.4 + state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "") + state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung") + local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) + state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]") + state:label(7.1, bottom_y, "page_label", "") + state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]") + + local max_rows, num_columns = 10, 3 + local start_x, start_y = 0.5, 1.0 + local button_width, button_height = 4.8, 0.8 + local column_width = 5.0 + for i = 1, max_rows * num_columns do + local column = math.floor((i - 1) / max_rows) + local row = (i - 1) % max_rows + local current_x = start_x + (column * column_width) + local current_y = start_y + (row * button_height) + state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false) + end +end + +tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state) + state:size(8, 2) + local destination_name = state.param.destination_name or "???" + state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name) + + -- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt. + local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form) + +tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state) + create_network_view_form(state) + state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin") +end) + +tpad.forms.management = smartfs.create("tpad.forms.management", function(state) + state:size(12, 9) + state:label(0.2, 0.2, "management_title", "TPAD Verwaltung") + state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {}) + state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "") + + local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown") + padtype_dropdown:addItem(PRIVATE_PAD_STRING) + padtype_dropdown:addItem(PUBLIC_PAD_STRING) + padtype_dropdown:addItem(GLOBAL_PAD_STRING) + + state:button(7, 6.3, 2, 0, "save_button", "Speichern") + state:button(7, 7.1, 2, 0, "delete_button", "Löschen") + state:button(0.2, 8.4, 2, 0, "back_button", "Zurück") + + -- NEU: Teleport-Button für Admins, standardmäßig unsichtbar + state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false) + + local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- This form now defines its OWN behavior, making it independent and robust. +tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state) + state:size(8, 2.5) + state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?") + state:label(0, 0.5, "padname_label", "") + state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)") + + local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen") + local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen") + + local playername = state.location.player + local pending_data = tpad.pending_deletion[playername] + + if not pending_data then + state:close() + return + end + + local pad_to_delete = pending_data.pad_data + local original_form_context = pending_data.original_form_context + + local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")" + state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name) + + -- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht + local function return_to_management_with_fresh_state() + tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen + minetest.after(0, function() + -- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden + local fresh_context = { + playername = original_form_context.playername, + clicker = original_form_context.clicker, + ownername = original_form_context.ownername, + clicked_pos = original_form_context.clicked_pos, + node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat + network_type = original_form_context.network_type, + page = 1 + } + submit.management(fresh_context) + end) + end + + confirm_button:onClick(function() + tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos) + minetest.remove_node(pad_to_delete.pos) + notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.") + return_to_management_with_fresh_state() + end) + + deny_button:onClick(return_to_management_with_fresh_state) +end) + +tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state) + state:size(8, 8) + state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen") + state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)") + state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)") + state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern") + local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen") + close_button:setClose(true) +end) + +-- ======================================================================== +-- Data Helper Functions +-- ======================================================================== + +function tpad.decorate_pad_data(pos, pad, ownername, viewername) + pad = table.copy(pad) + if type(pos) == "string" then + pad.strpos = pos + pad.pos = minetest.string_to_pos(pos) + else + pad.pos = pos + pad.strpos = minetest.pos_to_string(pos) + end + pad.owner = ownername + pad.name = pad.name or "" + pad.type = pad.type or PUBLIC_PAD + + -- NEUE LOGIK: Erstellt den Anzeigenamen für das lokale Netzwerk + local is_own_pad = (viewername and ownername == viewername) + if is_own_pad then + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (meins, privat)" + elseif pad.type == PUBLIC_PAD then + pad.local_fullname = pad.name .. " (meins)" + else + -- Fallback, falls es weitere Typen gäbe + pad.local_fullname = pad.name + end + else + -- Bisherige Logik für fremde Pads + if pad.type == PRIVATE_PAD then + pad.local_fullname = pad.name .. " (privat)" + else + pad.local_fullname = pad.name + end + end + + pad.global_fullname = pad.name + return pad +end + +function tpad.get_pad_data(pos) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + if not ownername or ownername == "" then return end + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] + if not pad then return end + return tpad.decorate_pad_data(pos, pad, ownername) +end + +function tpad.set_pad_data(pos, padname, padtype_str) + local meta = minetest.get_meta(pos) + local ownername = meta:get_string("owner") + local pads = tpad._get_stored_pads(ownername) + local strpos = minetest.pos_to_string(pos) + local pad = pads[strpos] or {} + pad.name = padname + pad.type = padtype_string_to_flag[padtype_str] + pads[strpos] = pad + tpad._set_stored_pads(ownername, pads) +end + +function tpad.del_pad(ownername, pos) + local pads = tpad._get_stored_pads(ownername) + pads[minetest.pos_to_string(pos)] = nil + tpad._set_stored_pads(ownername, pads) +end + +-- ======================================================================== +-- Register Node and Bind Callbacks +-- ======================================================================== + +local collision_box = { + type = "fixed", + fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 }, +} + +minetest.register_node(tpad.nodename, { + drawtype = "mesh", + tiles = { tpad.texture }, + mesh = tpad.mesh, + paramtype = "light", + paramtype2 = "facedir", + on_place = minetest.rotate_and_place, + after_place_node = tpad.after_place_node, + collision_box = collision_box, + selection_box = collision_box, + description = "Teleporter Pad", + groups = {choppy = 2, dig_immediate = 2}, + on_rightclick = tpad.on_rightclick, + can_dig = tpad.can_dig, + on_destruct = tpad.on_destruct, +}) + +minetest.register_chatcommand("tpad", {func = tpad.command}) diff --git a/lib/smartfs.lua b/lib/smartfs.lua new file mode 100644 index 0000000..9c502bb --- /dev/null +++ b/lib/smartfs.lua @@ -0,0 +1,1440 @@ +--------------------------- +-- SmartFS: Smart Formspecs +-- License: CC0 or WTFPL +-- by Rubenwardy +--------------------------- + +local smartfs = { + _fdef = {}, + _edef = {}, + _ldef = {}, + opened = {}, + inv = {} +} + +local function boolToStr(v) + return v and "true" or "false" +end + +-- the smartfs() function +function smartfs.__call(self, name) + return smartfs.get(name) +end + +function smartfs.get(name) + return smartfs._fdef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a new form and adds elements to it by running the function. Use before Minetest loads. (like minetest.register_node) +------------------------------------------------------ +-- Register forms and elements +function smartfs.create(name, onload) + assert(not smartfs._fdef[name], + "SmartFS - (Error) Form "..name.." already exists!") + assert(not smartfs.loaded or smartfs._loaded_override, + "SmartFS - (Error) Forms should be declared while the game loads.") + + smartfs._fdef[name] = { + form_setup_callback = onload, + name = name, + show = smartfs._show_, + attach_to_node = smartfs._attach_to_node_ + } + + return smartfs._fdef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a new element type +------------------------------------------------------ +function smartfs.element(name, data) + assert(not smartfs._edef[name], + "SmartFS - (Error) Element type "..name.." already exists!") + + assert(data.onCreate, "element requires onCreate method") + smartfs._edef[name] = data + return smartfs._edef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a dynamic form. Returns state +------------------------------------------------------ +function smartfs.dynamic(name,player) + if not smartfs._dynamic_warned then + smartfs._dynamic_warned = true + minetest.log("warning", "SmartFS - (Warning) On the fly forms are being used. May cause bad things to happen") + end + local statelocation = smartfs._ldef.player._make_state_location_(player) + local state = smartfs._makeState_({name=name}, nil, statelocation, player) + smartfs.opened[player] = state + return state +end + +------------------------------------------------------ +-- Smartfs Interface - Returns the name of an installed and supported inventory mod that will be used above, or nil +------------------------------------------------------ +function smartfs.inventory_mod() + if minetest.global_exists("unified_inventory") then + return "unified_inventory" + elseif minetest.global_exists("inventory_plus") then + return "inventory_plus" + else + return nil + end +end + +------------------------------------------------------ +-- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success. +------------------------------------------------------ +function smartfs.add_to_inventory(form, icon, title) + local ldef + local invmod = smartfs.inventory_mod() + if invmod then + ldef = smartfs._ldef[invmod] + else + return false + end + return ldef.add_to_inventory(form, icon, title) +end + +------------------------------------------------------ +-- Smartfs Interface - Set the form as players inventory +------------------------------------------------------ +function smartfs.set_player_inventory(form) + smartfs._ldef.inventory.set_inventory(form) +end +------------------------------------------------------ +-- Smartfs Interface - Allows you to use smartfs.create after the game loads. Not recommended! +------------------------------------------------------ +function smartfs.override_load_checks() + smartfs._loaded_override = true +end + +------------------------------------------------------ +-- Smartfs formspec locations +------------------------------------------------------ +-- Unified inventory plugin +smartfs._ldef.unified_inventory = { + add_to_inventory = function(form, icon, title) + unified_inventory.register_button(form.name, { + type = "image", + image = icon, + }) + unified_inventory.register_page(form.name, { + get_formspec = function(player, formspec) + local name = player:get_player_name() + local statelocation = smartfs._ldef.unified_inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + return {formspec = state:_buildFormspec_(false)} + else + return nil + end + end + }) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + unified_inventory.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state.def.name) + end + } + end +} + +-- Inventory plus plugin +smartfs._ldef.inventory_plus = { + add_to_inventory = function(form, icon, title) + minetest.register_on_joinplayer(function(player) + inventory_plus.register_button(player, form.name, title) + end) + minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "" and fields[form.name] then + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory_plus._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end + end) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + inventory_plus.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state:_buildFormspec_(true)) + end + } + end +} + +-- Show to player +smartfs._ldef.player = { + _make_state_location_ = function(player) + return { + type = "player", + player = player, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + if (not state.closed) and (not state.obsolete) then + minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true)) + end + end + end, state) -- state given as reference. Maybe additional updates are done in the meantime or the form is obsolete + end + end + } + end +} + +-- Standalone inventory +smartfs._ldef.inventory = { + set_inventory = function(form) + if sfinv and sfinv.enabled then + sfinv.enabled = nil + end + minetest.register_on_joinplayer(function(player) + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end) + minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + smartfs.inv[name].obsolete = true + smartfs.inv[name] = nil + end) + end, + _make_state_location_ = function(name) + return { + type = "inventory", + player = name, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + local player = minetest.get_player_by_name(state.location.player) + --print("smartfs formspec:", state:_buildFormspec_(true)) + player:set_inventory_formspec(state:_buildFormspec_(true)) + end + end, state) + end + end + } + end +} + +-- Node metadata +smartfs._ldef.nodemeta = { + _make_state_location_ = function(nodepos) + return { + type = "nodemeta", + pos = nodepos, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + local meta = minetest.get_meta(state.location.pos) + meta:set_string("formspec", state:_buildFormspec_(true)) + meta:set_string("smartfs_name", state.def.name) + meta:mark_as_private("smartfs_name") + end + end, state) + end + end, + } + end +} + +-- Sub-container (internally used) +smartfs._ldef.container = { + _make_state_location_ = function(element) + local self = { + type = "container", + containerElement = element, + parentState = element.root + } + if self.parentState.location.type == "container" then + self.rootState = self.parentState.location.rootState + else + self.rootState = self.parentState + end + return self + end +} + +------------------------------------------------------ +-- Minetest Interface - on_receive_fields callback can be used in minetest.register_node for nodemeta forms +------------------------------------------------------ +function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, params) + local meta = minetest.get_meta(nodepos) + local nodeform = meta:get_string("smartfs_name") + if not nodeform then + print("SmartFS - (Warning) smartfs.nodemeta_on_receive_fields for node without smarfs data") + return false + end + + -- get the currentsmartfs state + local opened_id = minetest.pos_to_string(nodepos) + local state + local form = smartfs.get(nodeform) + if not smartfs.opened[opened_id] or -- If opened first time + smartfs.opened[opened_id].def.name ~= nodeform or -- Or form is changed + smartfs.opened[opened_id].obsolete then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + state = smartfs._makeState_(form, params, statelocation) + if smartfs.opened[opened_id] then + smartfs.opened[opened_id].obsolete = true + end + smartfs.opened[opened_id] = state + form.form_setup_callback(state) + else + state = smartfs.opened[opened_id] + end + + -- Set current sender check for multiple users on node + local name + if sender then + name = sender:get_player_name() + state.players:connect(name) + end + + -- take the input + state:_sfs_on_receive_fields_(name, fields) + + -- Reset form if all players disconnected + if sender and not state.players:get_first() and not state.obsolete then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local resetstate = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(resetstate) ~= false then + resetstate:show() + end + smartfs.opened[opened_id] = nil + end +end + +------------------------------------------------------ +-- Minetest Interface - on_player_receive_fields callback in case of inventory or player +------------------------------------------------------ +minetest.register_on_player_receive_fields(function(player, formname, fields) + local name = player:get_player_name() + if smartfs.opened[name] and smartfs.opened[name].location.type == "player" then + if smartfs.opened[name].def.name == formname then + local state = smartfs.opened[name] + state:_sfs_on_receive_fields_(name, fields) + + -- disconnect player if form closed + if not state.players:get_first() then + smartfs.opened[name].obsolete = true + smartfs.opened[name] = nil + end + end + elseif smartfs.inv[name] and smartfs.inv[name].location.type == "inventory" then + local state = smartfs.inv[name] + state:_sfs_on_receive_fields_(name, fields) + end + return false +end) + +------------------------------------------------------ +-- Minetest Interface - Notify loading of smartfs is done +------------------------------------------------------ +minetest.after(0, function() + smartfs.loaded = true +end) + +------------------------------------------------------ +-- Form Interface [linked to form:show()] - Shows the form to a player +------------------------------------------------------ +function smartfs._show_(form, name, params) + assert(form) + assert(type(name) == "string", "smartfs: name needs to be a string") + assert(minetest.get_player_by_name(name), "player does not exist") + local statelocation = smartfs._ldef.player._make_state_location_(name) + local state = smartfs._makeState_(form, params, statelocation, name) + if form.form_setup_callback(state) ~= false then + if smartfs.opened[name] then -- set maybe previous form to obsolete + smartfs.opened[name].obsolete = true + end + smartfs.opened[name] = state + state:show() + end + return state +end + +------------------------------------------------------ +-- Form Interface [linked to form:attach_to_node()] - Attach a formspec to a node meta +------------------------------------------------------ +function smartfs._attach_to_node_(form, nodepos, params) + assert(form) + assert(nodepos and nodepos.x) + + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local state = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(state) ~= false then + local opened_id = minetest.pos_to_string(nodepos) + if smartfs.opened[opened_id] then -- set maybe previous form to obsolete + smartfs.opened[opened_id].obsolete = true + end + state:show() + end + return state +end + +------------------------------------------------------ +-- Smartfs Framework - create a form object (state) +------------------------------------------------------ +function smartfs._makeState_(form, params, location, newplayer) + ------------------------------------------------------ + -- State - -- Object to manage players + ------------------------------------------------------ + local function _make_players_(newplayer) + local self = { + _list = {} + } + function self.connect(self, player) + self._list[player] = true + end + function self.disconnect(self, player) + self._list[player] = nil + end + function self.get_first(self) + return next(self._list) + end + if newplayer then + self:connect(newplayer) + end + return self + end + + + ------------------------------------------------------ + -- State - create returning state object + ------------------------------------------------------ + return { + _ele = {}, + def = form, + players = _make_players_(newplayer), + location = location, + is_inv = (location.type == "inventory"), -- obsolete. Please use location.type="inventory" instead + player = newplayer, -- obsolete. Please use location.player + param = params or {}, + get = function(self,name) + return self._ele[name] + end, + close = function(self) + self.closed = true + end, + getSize = function(self) + return self._size + end, + size = function(self,w,h) + self._size = {w=w,h=h} + end, + setSize = function(self,w,h) + self._size = {w=w,h=h} + end, + getNamespace = function(self) + local ref = self + local namespace = "" + while ref.location.type == "container" do + namespace = ref.location.containerElement.name.."#"..namespace + ref = ref.location.parentState -- step near to the root + end + return namespace + end, + _buildFormspec_ = function(self,size) + local res = "" + if self._size and size then + res = "size["..self._size.w..","..self._size.h.."]" + end + for key,val in pairs(self._ele) do + if val:getVisible() then + res = res .. val:getBackgroundString() .. val:build() .. val:getTooltipString() + end + end + return res + end, + show = location._show_, + _get_element_recursive_ = function(self, field) + local topfield + for z in field:gmatch("[^#]+") do + topfield = z + break + end + local element = self._ele[topfield] + if element and field == topfield then + return element + elseif element then + if element._getSubElement_ then + local rel_field = string.sub(field, string.len(topfield)+2) + return element:_getSubElement_(rel_field) + else + return element + end + else + return nil + end + end, + -- process onInput hook for the state + _sfs_process_oninput_ = function(self, fields, player) + if self._onInput then + self:_onInput(fields, player) + end + -- recursive all onInput hooks on visible containers + for elename, eledef in pairs(self._ele) do + if eledef.getContainerState and eledef:getVisible() then + eledef:getContainerState():_sfs_process_oninput_(fields, player) + end + end + end, + -- Receive fields and actions from formspec + _sfs_on_receive_fields_ = function(self, player, fields) + + local fields_todo = {} + for field, value in pairs(fields) do + local element = self:_get_element_recursive_(field) + if element then + fields_todo[field] = { element = element, value = value } + end + end + + for field, todo in pairs(fields_todo) do + todo.element:setValue(todo.value) + end + + self:_sfs_process_oninput_(fields, player) + + for field, todo in pairs(fields_todo) do + if todo.element.submit then + todo.element:submit(todo.value, player) + end + end + -- handle key_enter + if fields.key_enter and fields.key_enter_field then + local element = self:_get_element_recursive_(fields.key_enter_field) + if element and element.submit_key_enter then + element:submit_key_enter(fields[fields.key_enter_field], player) + end + end + + if not fields.quit and not self.closed and not self.obsolete then + self:show() + else + self.players:disconnect(player) + if not fields.quit and self.closed and not self.obsolete then + --closed by application (without fields.quit). currently not supported, see: https://github.com/minetest/minetest/pull/4675 + minetest.show_formspec(player,"","size[5,1]label[0,0;Formspec closing not yet created!]") + end + end + return true + end, + onInput = function(self, func) + self._onInput = func -- (fields, player) + end, + load = function(self,file) + local file = io.open(file, "r") + if file then + local table = minetest.deserialize(file:read("*all")) + if type(table) == "table" then + if table.size then + self._size = table.size + end + for key,val in pairs(table.ele) do + self:element(val.type,val) + end + return true + end + end + return false + end, + save = function(self,file) + local res = {ele={}} + + if self._size then + res.size = self._size + end + + for key,val in pairs(self._ele) do + res.ele[key] = val.data + end + + local file = io.open(file, "w") + if file then + file:write(minetest.serialize(res)) + file:close() + return true + end + return false + end, + setparam = function(self,key,value) + if not key then return end + self.param[key] = value + return true + end, + getparam = function(self,key,default) + if not key then return end + return self.param[key] or default + end, + element = function(self,typen,data) + local type = smartfs._edef[typen] + assert(type, "Element type "..typen.." does not exist!") + assert(not self._ele[data.name], "Element "..data.name.." already exists") + + data.type = typen + local ele = { + name = data.name, + root = self, + data = data, + remove = function(self) + self.root._ele[self.name] = nil + end, + setPosition = function(self,x,y) + self.data.pos = {x=x,y=y} + end, + getPosition = function(self) + return self.data.pos + end, + setSize = function(self,w,h) + self.data.size = {w=w,h=h} + end, + getSize = function(self) + return self.data.size + end, + setVisible = function(self, visible) + if visible == nil then + self.data.visible = true + else + self.data.visible = visible + end + end, + getVisible = function(self) + return self.data.visible + end, + getAbsName = function(self) + return self.root:getNamespace()..self.name + end, + setBackground = function(self, image) + self.data.background = image + end, + getBackground = function(self) + return self.data.background + end, + getBackgroundString = function(self) + if self.data.background then + local size = self:getSize() + if size then + return "background[".. + self.data.pos.x..","..self.data.pos.y..";".. + size.w..","..size.h..";".. + self.data.background.."]" + else + return "" + end + else + return "" + end + end, + setValue = function(self, value) + self.data.value = value + end, + setTooltip = function(self,text) + self.data.tooltip = minetest.formspec_escape(text) + end, + getTooltip = function(self) + return self.data.tooltip + end, + getTooltipString = function(self) + if self.data.tooltip then + return "tooltip["..self:getAbsName()..";"..self:getTooltip().."]" + else + return "" + end + end, + } + + ele.data.visible = true --visible by default + + for key, val in pairs(type) do + ele[key] = val + end + + self._ele[data.name] = ele + + type.onCreate(ele) + + return self._ele[data.name] + end, + + ------------------------------------------------------ + -- State - Element Constructors + ------------------------------------------------------ + button = function(self, x, y, w, h, name, text, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + closes = exitf or false + }) + end, + image_button = function(self, x, y, w, h, name, text, image, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + image = image, + closes = exitf or false + }) + end, + item_image_button = function(self, x, y, w, h, name, text, item, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + item = item, + closes = exitf or false + }) + end, + label = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = false + }) + end, + vertlabel = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = true + }) + end, + toggle = function(self, x, y, w, h, name, list) + return self:element("toggle", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + id = 1, + list = list + }) + end, + field = function(self, x, y, w, h, name, label) + return self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + end, + pwdfield = function(self, x, y, w, h, name, label) + local res = self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + res:isPassword(true) + return res + end, + textarea = function(self, x, y, w, h, name, label) + local res = self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + res:isMultiline(true) + return res + end, + image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "image" + }) + end, + background = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + background = img, + imgtype = "background" + }) + end, + item_image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "item" + }) + end, + checkbox = function(self, x, y, name, label, selected) + return self:element("checkbox", { + pos = {x=x, y=y}, + name = name, + value = selected, + label = label + }) + end, + listbox = function(self, x, y, w, h, name, selected, transparent) + return self:element("list", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + selected = selected, + transparent = transparent + }) + end, + dropdown = function(self, x, y, w, h, name, selected) + return self:element("dropdown", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + selected = selected + }) + end, + inventory = function(self, x, y, w, h, name) + return self:element("inventory", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name + }) + end, + container = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = false + }) + end, + view = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = true + }) + end, + } +end + +----------------------------------------------------------------- +------------------------- ELEMENTS ---------------------------- +----------------------------------------------------------------- + +smartfs.element("button", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "button needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "button needs valid size") + assert(self.name, "button needs name") + assert(self.data.value, "button needs label") + end, + build = function(self) + local specstring + if self.data.image then + if self.data.closes then + specstring = "image_button_exit[" + else + specstring = "image_button[" + end + elseif self.data.item then + if self.data.closes then + specstring = "item_image_button_exit[" + else + specstring = "item_image_button[" + end + else + if self.data.closes then + specstring = "button_exit[" + else + specstring = "button[" + end + end + + specstring = specstring .. + self.data.pos.x..","..self.data.pos.y..";".. + self.data.size.w..","..self.data.size.h..";" + if self.data.image then + specstring = specstring..self.data.image..";" + elseif self.data.item then + specstring = specstring..self.data.item..";" + end + specstring = specstring..self:getAbsName()..";".. + minetest.formspec_escape(self.data.value).."]" + return specstring + end, + submit = function(self, field, player) + if self._click then + self:_click(self.root, player) + end + end, + onClick = function(self,func) + self._click = func + end, + click = function(self,func) + self._click = func + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end, + setImage = function(self,image) + self.data.image = image + self.data.item = nil + end, + getImage = function(self) + return self.data.image + end, + setItem = function(self,item) + self.data.item = item + self.data.image = nil + end, + getItem = function(self) + return self.data.item + end, + setClose = function(self,bool) + self.data.closes = bool + end, + getClose = function(self) + return self.data.closes or false + end +}) + +smartfs.element("toggle", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "toggle needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "toggle needs valid size") + assert(self.name, "toggle needs name") + assert(self.data.list, "toggle needs data") + end, + build = function(self) + return "button[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.list[self.data.id]).. + "]" + end, + submit = function(self, field, player) + self.data.id = self.data.id + 1 + if self.data.id > #self.data.list then + self.data.id = 1 + end + if self._tog then + self:_tog(self.root, player) + end + end, + onToggle = function(self,func) + self._tog = func + end, + setId = function(self,id) + self.data.id = id + end, + getId = function(self) + return self.data.id + end, + getText = function(self) + return self.data.list[self.data.id] + end +}) + +smartfs.element("label", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "label needs valid pos") + assert(self.data.value, "label needs text") + end, + build = function(self) + local specstring + if self.data.vertical then + specstring = "vertlabel[" + else + specstring = "label[" + end + return specstring.. + self.data.pos.x..","..self.data.pos.y.. + ";".. + minetest.formspec_escape(self.data.value).. + "]" + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end +}) + +smartfs.element("field", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "field needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "field needs valid size") + assert(self.name, "field needs name") + self.data.value = self.data.value or "" + self.data.label = self.data.label or "" + end, + build = function(self) + if self.data.ml then + return "textarea[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]" + elseif self.data.pwd then + return "pwdfield[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + "]".. + self:getCloseOnEnterString() + else + return "field[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]".. + self:getCloseOnEnterString() + end + end, + setLabel = function(self,text) + self.data.label = text + end, + getLabel = function(self) + return self.data.label + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end, + isPassword = function(self,bool) + self.data.pwd = bool + end, + isMultiline = function(self,bool) + self.data.ml = bool + end, + getCloseOnEnterString = function(self) + if self.close_on_enter == nil then + return "" + else + return "field_close_on_enter["..self:getAbsName()..";"..tostring(self.close_on_enter).."]" + end + end, + setCloseOnEnter = function(self, value) + self.close_on_enter = value + end, + getCloseOnEnter = function(self) + return self.close_on_enter + end, + submit_key_enter = function(self, field, player) + if self._key_enter then + self:_key_enter(self.root, player) + end + end, + onKeyEnter = function(self,func) + self._key_enter = func + end, +}) + +smartfs.element("image", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "image needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "image needs valid size") + self.data.value = self.data.value or "" + end, + build = function(self) + if self.data.imgtype == "background" then + return "" -- handled in _buildFormspec_ trough getBackgroundString() + elseif self.data.imgtype == "item" then + return "item_image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + else + return "image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + end + end, + setImage = function(self,text) + if self.data.imgtype == "background" then + self.data.background = text + else + self:setValue(text) + end + end, + getImage = function(self) + if self.data.imgtype == "background" then + return self.data.background + else + return self.data.value + end + end +}) + +smartfs.element("checkbox", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "checkbox needs valid pos") + assert(self.name, "checkbox needs name") + self.data.value = minetest.is_yes(self.data.value) + self.data.label = self.data.label or "" + end, + build = function(self) + return "checkbox[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";" .. boolToStr(self.data.value) .."]" + end, + submit = function(self, field, player) + -- call the toggle function if defined + if self._tog then + self:_tog(self.root, player) + end + end, + setValue = function(self, value) + self.data.value = minetest.is_yes(value) + end, + getValue = function(self) + return self.data.value + end, + onToggle = function(self,func) + self._tog = func + end, +}) + +smartfs.element("list", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") + assert(self.name, "list needs name") + self.data.value = minetest.is_yes(self.data.value) + self.data.items = self.data.items or {} + end, + build = function(self) + if not self.data.items then + self.data.items = {} + end + local escaped = {} + for i, v in ipairs(self.data.items) do + escaped[i] = minetest.formspec_escape(v) + end + return "textlist[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(escaped, ",").. + ";".. + tostring(self.data.selected or "").. + ";".. + tostring(self.data.transparent or "false").."]" + end, + submit = function(self, field, player) + local _type = string.sub(field,1,3) + local index = tonumber(string.sub(field,5)) + self.data.selected = index + if _type == "CHG" and self._click then + self:_click(self.root, index, player) + elseif _type == "DCL" and self._doubleClick then + self:_doubleClick(self.root, index, player) + end + end, + onClick = function(self, func) + self._click = func + end, + click = function(self, func) + self._click = func + end, + onDoubleClick = function(self, func) + self._doubleClick = func + end, + doubleclick = function(self, func) + self._doubleClick = func + end, + addItem = function(self, item) + table.insert(self.data.items, item) + -- return the index of item. It is the last one + return #self.data.items + end, + removeItem = function(self,idx) + table.remove(self.data.items,idx) + end, + getItem = function(self, idx) + return self.data.items[idx] + end, + popItem = function(self) + local item = self.data.items[#self.data.items] + table.remove(self.data.items) + return item + end, + clearItems = function(self) + self.data.items = {} + end, + setSelected = function(self,idx) + self.data.selected = idx + end, + getSelected = function(self) + return self.data.selected + end, + getSelectedItem = function(self) + return self:getItem(self:getSelected()) + end, +}) + +smartfs.element("dropdown", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "dropdown needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "dropdown needs valid size") + assert(self.name, "dropdown needs name") + self.data.items = self.data.items or {} + self.data.selected = self.data.selected or 1 + self.data.value = "" + end, + build = function(self) + return "dropdown[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(self.data.items, ",").. + ";".. + tostring(self:getSelected()).. + "]" + end, + submit = function(self, field, player) + self:getSelected() + if self._select then + self:_select(self.root, field, player) + end + end, + onSelect = function(self, func) + self._select = func + end, + addItem = function(self, item) + table.insert(self.data.items, item) + if #self.data.items == self.data.selected then + self.data.value = item + end + -- return the index of item. It is the last one + return #self.data.items + end, + removeItem = function(self,idx) + table.remove(self.data.items,idx) + end, + getItem = function(self, idx) + return self.data.items[idx] + end, + popItem = function(self) + local item = self.data.items[#self.data.items] + table.remove(self.data.items) + return item + end, + clearItems = function(self) + self.data.items = {} + end, + setSelected = function(self,idx) + self.data.selected = idx + self.data.value = self:getItem(idx) or "" + end, + setSelectedItem = function(self,itm) + for idx, item in ipairs(self.data.items) do + if item == itm then + self.data.selected = idx + self.data.value = item + end + end + end, + getSelected = function(self) + self.data.selected = 1 + if #self.data.items > 1 then + for i = 1, #self.data.items do + if self.data.items[i] == self.data.value then + self.data.selected = i + end + end + end + return self.data.selected + end, + getSelectedItem = function(self) + return self.data.value + end, +}) + +smartfs.element("inventory", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") + assert(self.name, "list needs name") + end, + build = function(self) + return "list[".. + (self.data.invlocation or "current_player") .. + ";".. + self.name.. --no namespacing + ";".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + (self.data.index or "") .. + "]" + end, + -- available inventory locations + -- "current_player": Player to whom the menu is shown + -- "player:": Any player + -- "nodemeta:,,": Any node metadata + -- "detached:": A detached inventory + -- "context" does not apply to smartfs, since there is no node-metadata as context available + setLocation = function(self,invlocation) + self.data.invlocation = invlocation + end, + getLocation = function(self) + return self.data.invlocation or "current_player" + end, + usePosition = function(self, pos) + self.data.invlocation = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z) + end, + usePlayer = function(self, name) + self.data.invlocation = "player:" .. name + end, + useDetached = function(self, name) + self.data.invlocation = "detached:" .. name + end, + setIndex = function(self,index) + self.data.index = index + end, + getIndex = function(self) + return self.data.index + end +}) + +smartfs.element("code", { + onCreate = function(self) + self.data.code = self.data.code or "" + end, + build = function(self) + if self._build then + self:_build() + end + + return self.data.code + end, + submit = function(self, field, player) + if self._sub then + self:_sub(self.root, field, player) + end + end, + onSubmit = function(self,func) + self._sub = func + end, + onBuild = function(self,func) + self._build = func + end, + setCode = function(self,code) + self.data.code = code + end, + getCode = function(self) + return self.data.code + end +}) + +smartfs.element("container", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "container needs valid pos") + assert(self.name, "container needs name") + local statelocation = smartfs._ldef.container._make_state_location_(self) + self._state = smartfs._makeState_(nil, self.root.param, statelocation) + end, + + -- redefinitions. The size is not handled by data.size but by container-state:size + setSize = function(self,w,h) + self:getContainerState():setSize(w,h) + end, + getSize = function(self) + return self:getContainerState():getSize() + end, + + -- element interface methods + build = function(self) + if self.data.relative ~= true then + return "container["..self.data.pos.x..","..self.data.pos.y.."]".. + self:getContainerState():_buildFormspec_(false).. + "container_end[]" + else + return self:getContainerState():_buildFormspec_(false) + end + end, + getContainerState = function(self) + return self._state + end, + _getSubElement_ = function(self, field) + return self:getContainerState():_get_element_recursive_(field) + end, +}) + +return smartfs \ No newline at end of file diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..81736e7 --- /dev/null +++ b/mod.conf @@ -0,0 +1,5 @@ +name = tpad +release = 28854 +author = entuland +description = A simple but powerful pad to create teleporting networks +title = Teleporter Pads diff --git a/models/tpad-mesh.obj b/models/tpad-mesh.obj new file mode 100644 index 0000000..6c958f1 --- /dev/null +++ b/models/tpad-mesh.obj @@ -0,0 +1,60 @@ +# Blender v2.79 (sub 0) OBJ File: 'tpad.blend' +# www.blender.org +mtllib tpad-mesh.mtl +o Cube +v -0.500000 -0.500000 -0.500000 +v 0.500000 -0.500000 -0.500000 +v 0.500000 -0.500000 0.500000 +v -0.500000 -0.500000 0.500000 +v -0.429289 -0.300000 -0.429289 +v 0.429289 -0.300000 -0.429289 +v 0.429289 -0.300000 0.429289 +v -0.429289 -0.300000 0.429289 +v -0.500000 -0.380000 -0.500000 +v 0.500000 -0.380000 -0.499999 +v -0.500000 -0.380000 0.500000 +v 0.500000 -0.380000 0.500000 +vt 0.078125 0.421875 +vt 0.078125 0.000000 +vt 0.484375 0.000000 +vt 0.484375 0.421875 +vt 0.093750 0.515625 +vt 0.468750 0.515625 +vt 0.468750 0.890625 +vt 0.093750 0.890625 +vt -0.000000 0.484375 +vt 0.046875 0.484375 +vt 0.046875 0.921875 +vt -0.000000 0.921875 +vt 0.078125 0.984375 +vt 0.078125 0.937500 +vt 0.484375 0.937500 +vt 0.484375 0.984375 +vt 0.562500 0.921875 +vt 0.515625 0.921875 +vt 0.515625 0.484375 +vt 0.562500 0.484375 +vt 0.078125 0.468750 +vt 0.484375 0.468750 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 1.0000 -0.0000 +vn 0.0000 0.0000 -1.0000 +vn 1.0000 -0.0000 0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 0.0000 -0.0000 +vn 0.0000 0.6623 -0.7493 +vn -0.7493 0.6623 -0.0000 +vn 0.7493 0.6623 0.0000 +vn -0.0000 0.6623 0.7493 +usemtl Material +s off +f 1/1/1 2/2/1 3/3/1 4/4/1 +f 5/5/2 8/6/2 7/7/2 6/8/2 +f 1/9/3 9/10/3 10/11/3 2/12/3 +f 2/13/4 10/14/4 12/15/4 3/16/4 +f 3/17/5 12/18/5 11/19/5 4/20/5 +f 9/21/6 1/1/6 4/4/6 11/22/6 +f 5/5/7 6/8/7 10/11/7 9/10/7 +f 8/6/8 5/5/8 9/21/8 11/22/8 +f 6/8/9 7/7/9 12/15/9 10/14/9 +f 7/7/10 8/6/10 11/19/10 12/18/10 diff --git a/notify.lua b/notify.lua new file mode 100644 index 0000000..945ab92 --- /dev/null +++ b/notify.lua @@ -0,0 +1,79 @@ +local mod_name = minetest.get_current_modname() +local huds = {} +local hud_timeout_seconds = 3 + +-- defaults +local position = { x = 0.1, y = 0.9} +local alignment = { x = 1, y = -1} +local normal_color = 0xFFFFFF +local warning_color = 0xFFFF00 +local error_color = 0xDD0000 +local direction = 0 + +local notify = {} +notify.__index = notify +setmetatable(notify, notify) + +local function hud_remove(player, playername) + local hud = huds[playername] + if not hud then return end + if os.time() < hud_timeout_seconds + hud.time then + return + end + player:hud_remove(hud.id) + huds[playername] = nil +end + +local function hud_create(player, message, params) + local playername = player:get_player_name() + local def = type(params) == "table" and params or {} + def.position = def.position or position + def.alignment = def.alignment or alignment + def.number = def.number or def.color or normal_color + def.color = nil + def.position = def.position or position + def.direction = def.direction or direction + def.text = message or def.text + def.hud_elem_type = def.hud_elem_type or "text" + def.name = mod_name .. "_feedback" + local id = player:hud_add(def) + huds[playername] = { + id = id, + time = os.time(), + } +end + +notify.warn = function(player, message) + notify(player, message, {color = warning_color }) +end + +notify.warning = notify.warn + +notify.err = function(player, message) + notify(player, message, {color = error_color }) +end + +notify.error = notify.err + +notify.__call = function(self, player, message, params) + local playername + if type(player) == "string" then + playername = player + player = minetest.get_player_by_name(playername) + elseif player and player.get_player_name then + playername = player:get_player_name() + else + return + end + message = "[" .. mod_name .. "] " .. message + local hud = huds[playername] + if hud then + player:hud_remove(hud.id) + end + hud_create(player, message, params) + minetest.after(hud_timeout_seconds, function() + hud_remove(player, playername) + end) +end + +return notify diff --git a/screenshots/admin-settings.png b/screenshots/admin-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..8e246d0e8f94673aaf2c62bd1012e8bf2faf3b1a GIT binary patch literal 306798 zcmXuKWmr_-_da|;N^%Bikr<>KX+dg0x`*yTkd_W9$swgnN}8cd8l*dv2Ia|Afy1HktsEnTLk%(%$C8kmntF97c9reg@=>Dfe!ze| zxYO1%M@f@^NXaC7w`cK3_^hDOCk~IATavgen%XX z0vy9@6j?J1ieO_w-~tFFy@eIP)2|1Y#RSj-btDwV{t_MdW%9!NqbXly`%ez|q0tI- zkhY=&Q1ss6vi_>66?ffG#R?{-LfmBI(jc}q`5$aB>6g^x8k34ON%~A!SRn<;3(9S{ z@qkf`dPM$^s|zB7lL z?f-fA?|~qSD(5XUtScyD1@a`6?~6doiN_;0GDCQwPz+2MDTa1(O9@gZ zMN5ri6srB1uRk!T0Sl1x&yPev3p8b`;1R9o(Hf;cpF4gs5)7i|JL4@>DB0v7F+n@#`X~NG9ok`mK^=#xre(Jwl)l9 zBp!)SG*6BPW=N5~j0{c04Au_Dy#{yy0tyA_*r3{|G7~jcOs3{CG7$Sj8Gn57c~Fq6 z;BE-tq27P{^SF?lP&J&cmUm8_3%RtH7UHb1<;C(<`*~4LPYYF1?>gY{;b3#Pm+5uu zBn!TZwp~&wE=!53GG<_N^u05mXsM+UZoGy@pluKB3**4S(HTdWBE2*w<4b7^D@+z6 zXs_NYO%^bPU1+fkqFI}8x4g^{N&=M5qRBGVCLgXh1PV8|M)K&Us}%j-`~OSEySdcm z|BL)oW9=!Sd!Byd-j7D%4HkykL7+f{}GlEcj_QFXHv`M}FSkqPvPiGt;mqHETNpXb@<9Xw;LEMg~m=7Gwbv zo!KA`Iq+e2JCps>liq_M@^}!HRVDS0N#=yq0E-`}2b#-ZKdOP9{lU9FPWv;$D@do63JY4@;b$0rqmRBD}cgb?@f303S zrYbs<-;Q16&k4?%`kfbYqn8k1;4((Jzo+^_7>e)mNyzly#`(m<>yeA%06>WUzoS5n zG5f|e>E>%VoTTUFOO=#AM5s&p=_ii!GZd}ri4w&4cAtr_7Xs?{tT~^RH{?A0mGiA<_RmFIu)okbw70XL7W=Qf`Yl>2AFtX9Dcg z!A?n{``x!$SQczEK6&BSR)v;IRYus#9?7tJlwCs!c7u=J{)nZ4M5(?T^3^QospTmv zCFHwiq^s&k)8kVjYIJ1PmV+)n@GgXYrA(;_Qc(W>*b8+H zNy(R-+uD-U@QoN&k;LB5CoeM3+8GFz=AEi&J3ag^l7%|%KcjueO3DM`K00z6QKsR$ zgpFK1T+cZDyP5dc9sX23^0m6x^_tS~&9=n9nQDIL3AO_&zHAqg!#GUsPaN^|QxJWO zz$W(hYu6i-_u6$2Q|seZ9JCTQ$IBP1`dI-xGR24W2OKl6Ns*Tg@ ziMxNRcQmF&|7!u^)1+tRL{GJtaSPPqDzzd>f2Gu_Hna~_xYxDw)l7M18Wz(-Q)&b~ z`BY1hyhRx)MeoYu6@gs&=CREF8j($WnXHZH{mSLeUa_j+QzVQ9PNpu;`m|2f6A@Tg zO^*TCIVhGI6uGFnC(+}*BH#68Z9Xs@h4bksgV~Q|B%8g(KaeycoJc`-PZN{``-OKS zN2T&LGIr$OH?fj!G+_b&rKFiDOrMkvyhWQAWStOr*-PxdQw2Vth= z*QgRwo6h?MryJyP`=<`M{m#fA4kSuK`^L8Idd_~Zo8C4Mi*SruKm1$$yKnk$qGQvq z(7bWA!~2)9gOKURtBZi5yv#mfgL41Nx`2Bpzqgo<|AR@lK(&kNU4^8d&y0hHq}8zo(1X`7F{dK6&m!WWUPox!rG+vO(Z7G%&jY`Qk|yaN zFjD4e;zF=cQG@b#MO@tml28m(4}~ah?%q**&cmmUx>xu3z2>*AooBRwHaCeg7>oF`q_f|dlRb?ejN&DpK1W1PH&R-()~*`=h)ny5l;ro= zZCL@rb;M)>`h4T`YhBm(0w~?G*D5$m$FRfWOXlb^CR)aF`xB*z4bl8^MqG0JmGgkV zt2eYxU+O(RKa9*Qo2odIUp>5cz4L5bs%pQBS;YdL%fm#-jO#&u*on zEpm$d8HZ2HEAXOlRS6>Cd}NeRjzT@V5Lsw+LpQ#GQql(tUDy;-NZJw<(TsRM?j7l< z8yupA=6ocViHiV+IU4R+az@=g7eDRksJAP|pQfjKY8Uad(pW*PmObQ8Opwm;MA&RR zm<5U1DW?5baw&Ik;Nwyl2j7Uj0}1 zaE@vX6OI~w0CM{om|u7l8XJ<)Kf>xDnA|zW`zmJjZ!89TU)EZ{{U!SY-|hf+z~AJV zg{u4E&b!b4!l=RjCMjPWwO3s~o$e~Ku@;qWo+fVqz($00j3(?DgF2a0o8tAgV}%d9 zMA_oR1=Fs1fo0-LfiS9RYcm(UncN@Fp>)niVZ+ZozADq@(-A6}q7H#7eCPf`lT7IEx`@|%{H-S@wrFsFKM_IZ5t+ucad ze1%R>gK8=CyB@SK@0T;gud|Mzq5 z&U5yFLsuXdH{kSR)zjct+V%YGGFUM4d9vE3hsB!P(jrR{nvRauwmPwoc^bTZ#z&1y zWpj;R9^d<)POT}nFm2N4f#aRfp8KpmmlKEoXxl&_?YkiN*L4S`t`A@G);Ti-B6QT# zNi(VS8OnY>L!-)nX7=ahenlX=tRxl&%XiI@P`;wZximI0{q2G7U!}exF$^L5wr04Y zRmC9E%t10>*37fwH-)A{&`SuIXEvTKl`ku~=m#Phk>O{S2nIh43)-dEuThgtIj~tRG%I4EQfDQfi?<_mo#((&^+BI(DN}4Va zdBp_xhzq!-qEuob#a{78OB|kORm&!Mqyr1h-&5g4nU^3P$@=VC9!ffCIO`QU?zb-L zMWfaKs$r<^^f=S*9FnV{&N=zGMlNA&zOnKQg~9oOrgD?IP-@YmxQ56_tn zN8PcnmN%w46}jDAci|DO+`M45D0=9xXupy<3L^q9@8 zfkAf)ZwmC8Bg|eyof`vcU&ykXy<_iECZ0yId8iQeAcc55537J132;!}<6}B%q-eF{ z{GKR*PSfE=1_N#z_|cd^Qnr^$H&N9&!$6PQ?uZL2ErT33f^KO`YNJ8;`lZDc`QmOX zSEndxF}2+r-e4=;az*)*+9>vj_l!yND|p6 z34I<=tf^0j15?7`nuC(EX>ClW;j4E^y7@b~wm@S$C#xRq@r zF^6oLaR0U`5UT0?a)u`E*&%OAn`X1Vw{mk5JVrBw<66hZ?YA$b@cVvyjVkiv4rDAS zjGX8aZ@Kp3YJ5Tv^+MM2M;K_f_4wlMwdZQyTU5tIK->M#sKz2kB3kX=bW`*OgC`gkFrMF6;AU_nB*a9E1thQiT|O z5m#dE`Jl(8L(e~O4H@znLWaYp*d@yMs4YmoEneMsI7<4=SeQ*#g+c~)F;+1Kn zw9iucwVwNeh?-P@aKtXsIu|Mb!Phx1JXn7*PxmVO*6vL?8eQLu)ITd<0^H`?FK*`i zZ_k1yIvz?*@6%RC{yQ36&KSzgx@;2G8mmbC5$|R?B*b6jYq2`i$}PXFR`W+{>?19o z##JZr-IRmgezhsTS?~3dWA|}y%*mt7PBUQPBSU3_*84{&4gW!bPtm1ZH^`tYpQm;w zNvdd|86F)qC?XF%a@GJ$2Ii9kA3&u^v3fJ{;#nCD?G6v6E%Ct+GApGNkn1>L_9!`N zATPRZ?5YG+R=DUAc>DEsG+~uXI`xzi{L>gA;ZFDQygMt)yB3itqde4$j_(GwkEcM} z{P;a)_+Uy6hqeIUzw=aeZ(6TTQfV`COwi4Kk@|iP!aTn?N-v)|5hp_H<+i)a2)(^; zGRSfg)Xi~O&KHctp(Eu&yf0T`6`3nhT)gUT7H>^-wUM^{hYn~`g+w)s-+nhL@0v06 zxJqZq>(3)`=KT8A%@P1GSr2j??qqT_sdfdx-sA_gZ2fxA`DqmEAYAypcY=wsoVEtx zK}%KCsLE7vlIQpxwVeUqL}@&CzgTr{IQd=#h;*K(n%qyBdJ?$M%-Gg`*AF;Z{ipV3 zywUX$wsPM8d;hE)yt<+Lx3tAmrWRz^e?!42Kw$hvL0Od3y=M`pIC8a#4I=Wkd(?=D z3Z@iZQf&XzHn~bPkykXfoHFr+Tmp8!7*=BtG1IHp)Bh$SB6(w(H8QXy?6NHM7)mT7 z;edE^>p>0779D`&L)a)uzs-W_>!P~|j9@YNxVD`1R1kBGK;BlnHg^GNXcWB?)JQLh z`RS3iB**N0Jhuz_rbOPAXMdwCl}Gq->ecg(j(-PBRsYDS_{o}f8y4@(U9ZOLKGU=} z8a?iHYLw&CK$Z6%w+@YZguHAlsnfgKzMB>MEp;O!Bj>W7cYH!Rj3a+*rSK7xUL@|J z3nIHi_%*u;8CDt+h2fj`&DIj3+%#jm1g^OL>+Rf(1M7NP(qtp9o1-(^*!>N;i$qUS z>kTTwHK?mQ9t5B0R7N5RPN05Ym7XftWYq}P7?Eg4x zzcjy=#QYBAhJ{98dwfzPQ;bHXO}ud(pqMf$>h9y3Kr^e+`$!9XPDOQAd;iHJNXyrI zE1)?pEF(wM>Pc3%fG+XKPib+oaf2>Yu2U|$5~KNvONQ_SnnwpJZ~-$MH5$*fJh?+c zDB?Y!;Wez-#43QaJth^a5TvS>bBHJ*X3E-FLp!LuD6OFqexoj42#cZN_#qngfdd>g z8+RqT)~v~xt_qdc(k7k#j!^7W91wpCX_GZL)MpCbc0I1ASd#qD*wD?Vs(d zvwHaig0zh>UnhZ2eH-OqgZ7WUxuT``S^e=(S1OIf^>A=wz;kVf+>aBLtG@fZ#PYM9 zH$oD$xBvQ`+Q@yEuELA-lQL!A!;e}Wo0pBlCus;@?>-0KFNW5rrj>~`8{|XhQ~y$eJ*}}cYD4+-muj;h}s?T{JFim@NuW_ z=B255iDkP4%i#m`WeN-EtBC%ImAn_@5tn?@hjAf2EA6$g)k#-x z;$z!|!N?9cOaW`_P24g;SaEbO<{S8raDiN@!L`}THi^{}p^y+9s8wsLK?N6%OS0|Ee)gbp^X4&$a>l8mB;s9LpJ!&jlO$#V=y*rZVGI;WHZ{J5wxa%O?$fVSSnznT&)qII?gc z`3PdL?2cKZqtZ#={W3hTm*Saq8JBm9TF-BG%HP?gBb$FfM&q z4hLOA$?WQ6@;oK{%|FQa_1@{u$ODR<`ybV`v(T;}QEkfy+?FPu3Gsl-hr6g$sjpGg z(s+PhfL_HoTr6_%V&~^A)Alscz_vxZg#E&NLZWL|-2QF8b4(3aG6YxTqHJ4VQD&W3 znt|OO3 z+ev6Ym$Wf!_ypwQ2Vo4PV%_3#%Esu#PBN&E^^KKE7_P#SA-1hi5C|^fyA!J)8B8*g zrt^&+OVor1Yp18rg4VC&%2ldZ-Bcozrj~#0ruWYumc?3q9)WO`C|fbey(|FK(dXpT zX2T$+psPgyas+OY<~hsmPO_Eo69vYvK3q6}=@c zWhF{%f6*0Pn!RbO9lVI9JtHz1o+}?RKh?;Wdd_2sWqguu9Ia|rPjhgA(o?+AraKH0v7bUlX zR`+j{WZ}tByC|sC&0k@odO+CLF91FA@^dy0wt=tKKl;B^>T`iDsQJhbf8`k?NFd#* zT0Vlq&QXX+S>KRCA9`^d%q@<;@4ila!t0fN?G#>h(lWtG>gGhm*$@)OD%?p080)#I z;NiIVM$=MzeBS8u5qV$pX>X?I?&84fSLMxR^~e1g@_T14qSb)IoYa?=@iPq&)=D4&oQBUPG$^VbAfDQw2it3pA`jRE^e;KK=+X!%jn&t z=W$SUtE>O6*F4o>7T#?K(O2hL((%FqP{vv?eSH|hK025#Vh=cV7tU`%3;nj7t!ne#jsK{Dj<(0-|-$`Ge&IxYPzssVt@gsF^ zQd-*V=8v?69EBs~w#Ib+{oGvkdlV&x^Q!{DEER!4 zj7EIr_0AU#owQ0)xW6a3W?eHdJC>-w-PUGKS!vo&E(}Dc)OrqpBV=t^f_cN5^>%Sz zxoQII8uAP;OV$m6*?D4Y2+}tm1bzy*4pY7Y*9=nsJoSSREM2YsO~5shxF0s;eqhq) zClrY~R9+vaH}Ht~=;jrpAS6LdooZx-3X%2~*%)zZ&v602qly~80I@$GT`Fp50R3yP zUll3V@(uyN*mLuwq@`JnNHTdzjI?kKcVV*HimH9B7dO7EM9@u)GkU@<5sesDpIzDt zLQE18nl=%)-AZUFrST#mc1J?H+sehAM|+Ez`BdhBFo&-7Y+410Hl0`7NuL&EZ*wO>dy=g|w3Eqh(Y^E~ec&tBP9Shn-Y_i!U4pPuik z_4|JSu-%*DNQ8?@B~`^Wv>5b56y%dsdhjXG646}3HcV0;T7u|)m+>+UE%!sI&B@Ab z(lg>x9*Dl3>kw40ke`fvU~<*{R5JY( zk`!9X*1lI>*W!`cijcEE!YNNfWCw16oHAG)I8Q;Q4MX0ZiWFiz_ zxcszw7h0&=c7_M1{)$>^ijyYay>e9uQ>KpnY6Es|Z1{P)>yQnX8c}K@qsWGD zb`Ha#KhU67CRMYKJX5y}PnONdx3vZ5@0}O`h-{VLR`p((+AF5~j^Y3Hx8MJE+=%f% z8R;szIMz&jUye4I#PJ}w_4p){mA#o(AgJJ}6w7U=PBV z7Zizj&lH0TAfxQ;pS~Pse5R2+cn|}i$CrBYD>$O(j#`w1$>7QY)ls$=b*|=oA(BYV zCJI4-F0Y?roBvfg3JukM!M*CY8Kq8eM4qq8KzT3>W5RO-$zg*gTiiLo}*7q`X0a}9|*<=SvS$;y73E)vTFKFc^ zvhJMDkjo>~S^w~Bxw*;H`|qa)8i4TDc*z6OEvq1~U2jwgtd0NeyRRmqr=x-mMdF5V z+23r;PFyICY_oyTp|471pB+yW8cW#sIP8YVad!ue;i`X1}kqCajBKtLokN?Y*)*&P7-V|5+d5qi@+f!LL+TA-jUD;yyk_kO!Tx#`8ARBJ22ol~ zE_4gw-)lK|!Ez=~?dxh{Q;|5{pqJl;BHwUbiZssinh=XU$>=3Ci)Jj?b}=@5hi5&$ zf8TEHLG-Oi9}h3E-`s2ml#!W;^jOYg39@t8v)4mN&UuLJQeXo{-^MPMT4g^t@Kw~Q zvm;l~z(&bmUyD5~bpCY?u>4QZs^4JMs&=fSZBn4C_*@oj=!9vy?9#-Z>)qXaVYy6{ zJD$VKqbMyLL)FF^ffkLH|4gN{GqWgYv*anPNaq|}#8K(&%Mbaf(19-OU1Q5`@#2kM z)3k_4v6imVnEcl`z>QkHA`*&)P(mxwKMzC*#J#8)FR_BYgxglPx|+Zc%F#azzG!7{ z)aG{3L8sbw&q8JGDoQ{a1e)&Sg(NCznB}Oa7zi_toorZ_pkCfd$FC~?8fT};b(3z- z)4x%&zRuL^b8XLa&ko(?StF4+1Lh@XHa$EA{N;1Ixyz27q@T+-mfWq}RZ>GsY;*wU zJ*}ZLVUAy-7pUcC*9Y6ys>1cC^N*o;^!y`rclT7l`1lFScb-3CqaGidO;+jiMh0lh z=d8mY$DU*N(ch=z0uFN_I|l~|I1`Jp>0x<+AT5&dk?jo7%{?8Y^mO_4uI`D?#W|^9*_Tb5{wdk&Y~^;Jya-2(Y%9zIp%-~@S5wXz6}7qC9~vEx ziLkUIy~cj}m6kA8gcMjxRVx4DXwz9MX|L&dwk2C7OdbklE2N9r?yga1>F%D@+u;-4 zHm8E+YpU!4k@Ed?sd>P6%;K&If!k$+x0X#?H%i)Qc2}nFjQi6xLqa3e_1Ij|)gf98 z3U+Am_^d3iUcGvZQ8ba*Wi@=mvRF8?u=4e!3kJR`kf51P;IH{Jxp5XcA-?(sEOjZO zeW#kkVBd%&vCb;-qV;G!EHb8m(A1e5V;#`bl^6#oI=ufv1}IX;vWi6YOfp!^ux4F- zJr+yp@wY?h6&&gmZY>s4z}>h&niBXT7oI5b+tc-r9R{R;_(!|b+3v7hH{Dssno5a4i8Z>d$%TO6|P;_2AO@EVR&VTt54Mnv{#tAmW#_EG_(K1 z)qXA6YR4Juvsq8(Zg(xaFM?{AT;+wGtWIBD>K^qKcd+UcEIXNv3gWG?zYUj9Him4} zL*V*025)Ipq!jwtQs7l)agC zdBntE-r^s2RgsqE6^?F5vC>$y9Ze=me5M^HTsUr9f(}s%eWEKCCLEtd`!PaMv;zlg zD7>BXgE}34Cew6``{q%5~|@%}lMGp=Q_png2P@?zC_?W|}tzRvssWH}&Gio+1w? z(V2KWEfW=8(liOt81imhBDj}VS~C(q8deyhvv%hska>F5TFW^u3a_AFL_HRz8t%#I zcQbzQ^)j&DB+Gf|Zt{5^{l##oWR{bUl>kT!+DirY^!C;#Ex+*bd%Ol@w9|&a7I0V! zVJh5e6RKp5l$GMmC5W=l<`lX87JA$Ye5f^_q4??5wsPEYf4UrSwm)&#KJhTxdCTh; z_8;oHt=T~I8YyNHbvXWc)a5!ISLF&laa=13WMQLo$E{u2(Jm2=84%&*V>D;&KF*+1 zE+ya@Hh?^HXNc!h)Cmm5({{JR_3h-;%2JHH@QDY#vvkRGX_1AO%k%Z)C~-CGkfP-# zSyViuJWQ5LM&rn%*Tw`h@y(*s3GZqSuY817t{1|R9&vyz6wJE|mB$yefhRAC$b7pj zhkaa#Mv;xJa0DIyLkG(yH}^_a5CNk2eyTmT?u3)amxf>(O)=9Pq8_t$)U63jX3yd% zzGSWI8UT#wp?*VKg zLCfD`3DGq@JmuW8yo9aGs%-`)-0CHVxN371Ggsl!T2^Tc$vdm$d0so4 zG3EhBxG0!W16v6IS(KymuNSTgE8Z|z&^JR+9=$7dEWk3{ucto7T65}BBb}F&i2jHQ z<`ng@Jj~&_B8A1E_7w6qn zXCit=y2GZxdoC>URw~JSvDxDxM$n_Zre^Nc4O*gsFpVkv!u;#cym2=<{V ztPoPzO*(kF!uu{##lWNgVpZtt`8Y%h4PskLMt?$qv+4WI^S*L#+On{Lry=#mF9b_I8uc%+P&BKLTN!9D- z6bCO=cp^=Xx^uh!P{hX>^dK2=A!~;V6sCN%EF*k-Tian*+{S%Vfed*LP_OODzjl4#mQck!SKUYUT?@l@Yvkp_pk zhNvEAlpozo6gB^p6{OW!!+`^<}?Ri{Y0XW#_au-D2~A;;aG9?j}5otcLR9e`)~WTl>t}#aAV) zVzHAsI$yI8s+V7Ipix}e?p*qV(&M-N^6$^E9Gw?0a3rB&@=)aq*<_bUFSC>>KLdEb zuvdZ24dHa<_Zx_IvXa_Za6YMhfhR;@&^YaH68%=@698_n5^Ba3^0J%fLQr%U)YVL9 zrs9dZ^^pn4JninhY0x#fHm!Pdogf~lWnl#`cYSQ0Zv56c0gwCU9#UwRO63XmRV>{SldD5J`HT&G=yIVr`FHeb z*3U`kL^axHa`MW%)nvKLeu3~>CuL;%akeMuS~$O;D)ilHR&!|0hol_J2l;h!GYY9F z$ukzi190fwmNU{{ojZCie=}YWLNM<3S?GU6XnpFqH`jbP(%5eaMGC551H6pj{gz6( zYh)zvx62akc3d<9O0ZQ*k8`IUhs*fvjoAllpK&$?H0{W|uKSrTV~V-7sE+}C;mz^? zC8<}*)C|AwGMW1oaOw?^GIT#XQY3ZkMKWtq76pK9$wgJ4JtdWdhHigR)w!ajR5qr0 zI~(GaQB=DP zD5KD0QFF(cuCnFEWRBQA}5r{8(_6}K zLmuq|tKYdi(fTL(27y{prdcgLqkNRR6%C>XaxarvV`T8b5vt()2J<*Xc@0hpV9C-i zFTf7N05aXXkII?(AV=kJGU`_hxr7I$*35iig9b|SkZvR7-aCZtTk~ym+Kbo!BG7lQ z06M}Y{-YJ2?W<4xMCsJ1*rlXrftPn(28N2n@rPrKKta;YIz`)rjXWzlKq({7T1av` z@Oc##Zi2>_+RaG#5q3teceq%Dr=r!Yp~xEYVRyq(*|~KIeb2U`GAHrl)rr%$NB#X- zVJz!T!tH&UW;AcuWXPn~ZtUIJ2Qe|9KOYc}2Wk{A7Alyi!!Q_GH%_4md;CoRIk{}@ zpdGY`eM~S`r?6Po$QAUvg~T0{sO%c8C;SA#O*aoMs82YSmUE~BA0^rHbJoH~$_bD5 zo2#g(n_V2cWGj61TRfN1<5)Q6xv-p%+qt(g;YWum-n3!nKw#3weG~b*`JQZn0si5t zq;V@w*Y2Yu^8`prIG9MT5e(^XRUgp4y8F>!{X)b?m&)nalP6WI?f$+yLZh`%5?F=-~V+6nqAbYjSJu2W0>E)Us3mug7Hqj za)1v~w1(+$X8KCV<3;Rld4Xb4P=0l0o6&{EK^J`dV^)*0b`|~)km8GyS4^?QDVm6W zbuPI5dbifkYLN>GpAVO$=Qq++wrO}HL4C=4xgyk01pu^UBksyz;AF+ys;(Idtkwh7CRq-R zUnsEE^*D8b>SYJmR~zZmH$~TbXFaz5tM--TQSZj}p%&3Aw;<_fk6oiKk?FjLmMfPf zvioK7@r$Kuo|68gw2?=5#duw_;)I?bv6VH_#YXx;c&8ehO+$O;;}d*0!88hOi}7X~ zQBq2a&w=5a{y(@Z6emnMK5An`*Qk}MHJ6P+Lyh)%1F5f;lhRqz$of0oM33o4%f^QG&jK?QfQ0ev+5Lm(nDx)ir@ zS(}dlf&oK5NsHMQy(Q{DW+>JIk+P~Mzk)!dc#4?%QWIiIW8FcOhLJT`B#J{Tw}T5J z*v#AblCqK2Y?&F@S+14{j0)GsID$^zLDPVNa!6@Xx+aEXu6`H#V-}w4^2^~s)v@5i zxnY&b(=toOa5mc~l%FTW>bxGhPMRkwY1>*i(F>=l9SK=2*gbD%)_>?gTO45$__Lu* z{9ld*fcmjK)YWXX)un9-Uw;ur79K8WGfp#dr8u+gob@f{@ohdYnPhl}ZXjE~whbHe zL3oGFVrEnW(=IgR(T>G;(50P|#E#xnlZ*}~`nvVgBkI`kFtexhqg#z0vBnbrZO@xN zDBds!BQ9NUDy~6;1bJ*gtzr4Z)GjHRIA*uBEnOcD;Lys#iksT88gS4&A=c6&9k=W# z6Y{77D-9us=4$2!zE^ z>PRxuWoV#>ec-Nj`L~wDAEM>QxjLIr>;=Z5Ty#r2X{Sx<64ZXW#`bhC$y7kRkPQ(c zr2nKishn5A?tq{TMgRqYkw25VBiLf@os)frOJ54r@}=AF8!5*8(Z+-#1wnvh-tc-F z9wg?CXD=j=ks+5i3{aE(j?4jrk&g_X!iutZZ~7p+A=f}(L!f*s6LC;)I>^YN@o18j z&T`3malD=#WH*PFv-QOkDkqm?Lb;5rG4AGfqKnMQ!^d`hH;i0$-dwVq{=v&fYDz+* z^C#21SZ1|o^q}5ro-4oR0aS;xg8^Mn`WheYuOJuhZPy6T(}%h%uFl%6AwN^-zwEyy zNY@irE2tuaVh9~xE%1C+dWE98@jmAtw%2y(+={IoS{PXfno^*YWRZMS!QeW)sMy-4 zrTBKmRkfY({3`MkQhSiOB=Z9hw_42>LHl(g`j`XIPu7RUgIBM84@+_L8XP-2bz`zT zr4p|zpBt8qd%yKiRVowd?-B{er2G+H^2{~E*oJGSj|PmNu89ZqQfMg-Y(AoUr{C?e zk7!}0L0O@87R`%+WBwdgPI)Lo$w1NMD-m0d;s}I}==S#U98pgT9UNUP?$MFp8*gw|lDG}8%Qc;xoT!?Gz zcCUS|Ag8dfUXOe*%|>*WgP@3=+LC&}z;Gy{8Fm*3Q9a+vnqYiJOpGgEv3>jnHF|De z;ZPAFIW7_g(DhWG(a*1q_}To*Y=8jYyDcjS=ts{sB}yi}X`TB+W{c``6yJ#p&D0{q8zA{6!1k&>P?17A)aN%Eyq#)3LwIYEUjT zme6jr+OdizEo~dFKK;S3p-roa_B~EY+5(He<*rO7mhgdIvTTMzrdp>O&~dnDbbvte z#WWinu)qS;+Ak;?tq`_`p834NEiA-1fdXVto2z-heX|IZj40|J_d$!OQMyPC2$cBk zb@i9_Vc_9ZBB?p0w;w{BkniSbIq6fb@B4YALp`u@b^p!Xdi!}%5fjHR5Pre4=W2TC z9K>Ipy_!squJ(7BxjzqJ2TwaWVdQRQo|g;zqwHBV4d3;DJw&@{y{)=g4^w$r&&nA{ z$3ZVg>uZpb>B_kvipm;c>{&!3M`gM@b_USp9b2`oqOx3h<)FjfXrG*a%^J82@j@Lw zF-Q;ZYXW%vdUnURx38@^(WiA%>$f~jJ&dc83(m4W?cKEha3ZOwcMuc`oPQWBTHRrC z&}Xtc5_U}gyZ)hbxlJ;s4e4xA?*MZu!AoLH3CGw63vp>T{LE?RqC$VNf7J(nQRcf2j{+ z61O|!Q@dWVyVrUfOMx~`JIcBWuLBh;Piqd`2pCz!)?XR*>6iH&v=B^}ZzMlvth zWBxqyq*5-Nd>q}R^zi#=26IHY$KYI+pYqpP0=^9c<(~%RQ3wQYhcm_QH1 z<4Ys;z2Ck<3qCwFWc-ux6QD{4eT83aQTeg&v5W*1|0)R-`?cMp0Afp1pIs5fCIsOLQE0Wy#}GTVCT>b3{j^! zN#U7yv+bv^+bV4yujk;>G=|RcWtR)>8!gmj#njsD?oVIIs44(06wj3;?9u472KJEq z$2%2GypARmyF1>t6_k128GvLuO7wc6$$K~4{4s-ba^H^dLg|M9SK)p*=K5Bk>RCR# zSKKyOfttgGdZTnduksr_Osukm#Ij?@TNpX%ay`~~dnp+9GT(|AN?RLO7N(HEciT;W zy1ZHLY-u+(5fqmG+)h=9Twv2m*CjhObcH`X^4%NNX7})%JAbFaTwW*7v${0g|=(# z%}b$JMY+4LO51Mxz{b!?8}IZW;?!BwQHVs!!`qaUb*B zxMs(zHfYMlq{%8i&U0XRMLDte>G-}HS)Qnxka;uyOF_839nAA-gh83EMT&-~*al4Q z5nX2wwCKR-RCcMqT?3z`zu0IjA-oa#y#HrrH|HKzfw-^~PYG?_|3ir@$!?ieRR5yM=;)# zd`v$2-q}SlwW+@(t(6UnS5JT=!BzvMRU$8QUkW}M33SBO$}NR7M`2P%R>PDV8Q9$w z9k3gWi>QgBry;~{&ir1{;GHgIMiHhNRTtYlc1{hqu2{l{v!?5VocFLfUZwVq8Xrj` zZhKzDb21X$(Q#bV6=H6bCMU%1-i5|=7jpPLe$J#&im2K$ZiIK%+^JBT@+?9?E%C$6 zK_fxHTD$A`l^-f|%C0C=YuG#8`c4b;x&el2qn9pVWJdWSe!S-HHlF*uDW}yw+cb^* zAPiStC7Sw+4~!m_n}44@h*~W{tyrUpOo#~!kCl?0Y+g16a0RJ`~9=Ef!J#e_4RdIq?q8rzv8yDKyRa9~jNVx!fxZ#i@u)A{7ve{9U99xeFQH z#WY&%ebj4HoNpGW197V05r}z%VCImYBb#L|N|N;#xm#FwEpwqyHAaZ$2#a>a5%0O4 z#u!BqksIt^cx=kC|Er_7eH$Ospl0{6flTE(ME~NV+GW9f`GI}jq?alRN9EA+0Myn& z(&Q#ci4n~^E4phm{1k3y>orYI??Rjc+dpYL=0p5yt?U*b62xvu-V z&ewU~b^L;Axoz;8Hv^)vKlIC;dhf4Ou8TDc=Jr|Eelys&l^OE4ehPx%iqMIG>Ew7|hi3StI((rqeXqd#P%lCFVDT{^i3 z8pU75WtpO;N17cx08iG$rR7L8X?h&=QveB%)iqy5z)h<8E78JQO}HR#3sed_pL5dG zX@{(z4xBj{FecB7FZ`|K_+_)+`h@PPMl{a)WfU;-lK<&#>~c* zhtzY=7yq%pP3>8v`TEI(oz=PJ6IsZ>!Os5nPS-yScIy8cgu>eTjIa%Rl+W4eEVz$> z`z)GCqP$5%rI_^7mTm^*#6 z>lqNxrS(?wYU_`h?4=Mn?dItFoA`q0tkEmrpT<24rfc$#W5-kFJU=gE2R5EHYF?uD zI^LG^M!sr*P+@_0fnw~ z<409~?kuW&tg2(CZ!2>!BHFY#;c;k=k-IWTs^0qIhj2AB$R)vI9sT_D?JYjn>Gw$B zWB%@$unjb)4rDQNT#AVn0Iv`nMm&C{QOYlkq-N8M^0oa|9hfM`Q7xf`G6m|)-3HV| zu%{z5#@N0Nrh?_7^5Q-{Ps}my_pE&U;2y3Jg)6x8YAN)A4CePx4Mxr*wb1;rdO?f~ z+o?#^K;~X&k3?8?;x|U$WedBAdQ2;3goO})ab~l;6dyV#Ltdz++%fA>RFtKp)Ds(9 z?cuMi@ubi4?^q%xeaSCPO(JjzrM;|~_cpq%>S_3nUz1VsT`SzYWc-h3r9>+u$+IYu zTHIPTEf`9OV3_pfim4^E0wZvbo3c=0&=_0zjaTjq*B}9MqR=Y1;NA8{A)=;MJjx7~ zP^-+v0HKV+JKO>6J>o;-e1uF(hJX+^MmdXJ95<2iS_Z5kX zqn@GGZyT!0UaF9BTGG2*{hKpw-16_C@~!lLNvz4&=lPT4!gHcnAy8O>u6Jkay(*@% zp=zv_K0nX?tcZOonq9CW;J@a#H4VZj9vKgc^bA4~jz7xzdle*x5U1sa+Ilo2C&XK3 zWc?7$gJp;%Nj zHE|m0ct@xxO~Qs`?H~XELU9RYZ~_2@=uD$>)UU<|3Qj8Dp1GFGJL9~5 zUwC+7iyHvH1L+q=Ohht=|Dc>cB~MC?^7V+u{HJ@1L>IM8Q98$?jITtvWHe7t2=jtg zx7tr_UyU!Fm+2(lLSZvkEeQ9Q@3SyX za{Ogds-+KVTIm#m;mY=(^!+kvsrv+ALLj~8-sc}`1Dm{TCIt1*!5E$wol0}}+e?|% zdwp)Ml|B56SGe0 zEoX#&k@{qrNY1K`5n@&DUE{I1%gl_t5c8LFEwM2DrDt2)J;M{Yx9vZM-8q^1;zU-1 z7y->_w3=YS=g{+JDG=_Rnk|shSukaY)bbXjSvd3U_im-ZW~i*Zhc1T$PDi5CQDx@H zU~kyrN_P4~zNARr+YEMjzBNotJmMZ;LW+?UG$vQ@fhCi+@vi#I==JPLJ`<>MeEtq}rGkzy4b#HoLqx)S6ErXz%v9KfX}ujfN$+ck$q4A82Ynn_avj$F3P0 z@6{zfm!$j5ch;kE$VU#xxV9DY`yv4>x6!cB;q$2~NqJ8bh9+Dzh#Eq}rj@98TI~0F zi2waQU{4&g#D{@i;?Km|QF75T=azjKWKhEI;*>@DXXhdOpVVCSgF;E@`gaP_sH@(b z#cUq~J!n*5rT~B?c4XB{TZjbd-?kzfh^RVVK>!#qzHFqu8_o<{~AASsr=8(t^hMlj ziFhIrXYFcfDBVt*wyG53zhrI;YEMNc`}~+NxlFl+@aFk37-Q?|)Fou4A-*`0f{)|m zMlZ6bt(>B5->qp(4CrxnJJ6Gxps5dIsmtX-z^t2p zEkXT3QGZ*_Qsoy#K%J-_@;Sr@ySE&=(ZTpO`3_$I_kC+ zbplACY=H**oP20Rl0?yp(R7=!GG!yF!^-h{e1uB zmKwE1b`2>?0QoBp4A2gB9HOd-PYrzBNBnE1gAE~yc$Y5`44BywzhHm=k^&j-jxWVl zGts~oKX6j;3au&OAs3QLoTYAjcyJATC$un(ljlPeU)5!Ex99WF^qrOy)mMq+)=`o2 zd>?{!FGvR@I`a>Y{%_t){b$lZJ@fBXz&;yyp*ieMxZsS#OaopR6Euv%&8If1G(0yJ*3n^h16xEp#tu{~plD|)le&rQno%1Ye?3Y3PF^D%?I`Xa(Hl_}x# zprTcWKF4Mk8tgFq{gzq6?T6do=uL*^y2L`a%(5DKI7wmasS z3Nc8Cd1>Yt&nra)!QvkO#J%hn@XOonmpFX&M&?u6dhbP7z)e4c>RIm_R=jlmM=+ly zOvmL82oNnT>xA<6Fvo(Aghs9Yq&j@y#U9SFEkKoX8LDmG;8k*<6+-&*3t6~KS}R@F zI>X!NorG3vip^mZ8pp7H5MU;|6b|rF;MDpRppp!YN2BioSYjTh+x@9T6K{$jr*flF z#bEq6P2$7#GVx*k5YG>96wYSE=`dQo!)uKCn$*u{NV{?KGWwX0eoY;x7rkVk#_2Ot zaq0YGw6rhu%<2JCP!bd0he@ol`M-*Atul{iP_uU-wsOP(Bg`Gvl^@yBW<2<*=Az_E z;%CDWz!>wc!Xohf0&Fc5mZ2E+DuLvqk?xN{<&oSP+;(#Fda3@R>Bxi~t3gk@Z-B6g@faPpr9)B?jtoTb)f_vlyp=#3^bBqr)pd#~0c2y@1Ku z$Jgp>nN`>&(wu#2;e~#!d@jW|_i_Am0tiM3h{poIi($Q7A4_oYxOb+~u#R&2x zR7>4%iCFjR?}7Z=7Az-JMySaAut)Zs?jS^!L>+pMTwBpw zq_>8*FHp54J3XUtVgwWW@q?4*?@*rjl1Z-Cw?zrybjih=3$={v1N_zglIPE(&=171 zH1WRbE)6c0<)~WiXmzE=QC^VQt!F(_+tAfKG0tWo#7p)=Smqm4muPeGtHf>b@vwrh zd>HR7)355uL#j4-#_6sMi{3MP02JmG*gssk7F5AcVV3|d z=Y2sFLIE4tyVPeme9D}A^Y7;J8~5etz?IFfvgmJ8k+=I==AO3+3O$OI1R{Avw*Bv61elT} zzIGeWsWH`?toH@`5YG|!D;tGIo7lzk`d}^WpfG!CP~w0c^*3nNg3+6h6)GFKk!$MJ zp~;~g6NTf43ms{PfW>ZYjvZ71OtwTZ$B>AOC$&m!WW#@`lEd6rZWrC17J%Au`j~$> z4B;AZr_T9;Ny%Sn_xfJ46<=jY7xN!-oGAUjvzlQbuJuj2U$WaYxADgOB!gF=Q!;Z7 zp&{jHGlB+^H!69eUm6S=m(dR|kFk7IeH5x$Xhc&T$rhrxBgSWDm)6jL5sjy&p71B* zyOUopj`;vf%7htnNzkf`wZw6HMK4uxFh0H?RUT|mJk4RpoL%8DX}8^VlRL0GEgvGR zHbAT!^32@c5yw-!c(s;cOf!SKC|B zf~vTbqmbGGXEc-m$b|l_>LWiO&s{N9m!$qhBpsh1g|+&TlMfT(lSzF!_(`DJa6&xO zy$XOn92YbA04WkxD~tK)jqR_hKo@62n!P8VSeF^U&BZ_&GJx!6 zjhamL$dXm#Hy1cWmJ&A<<`(f$Q{4odaSHBBimUZAzXz)fFHDY_W0`PKb)V*qsNa>z z={N=ZE(hB-;yf_7d3i~bK&h7RgLI;2i?VgGd}IEhLgp8nMRz-1y|zUSL18bjNI{++ zOIM~(Mt_rx%I0`DPy8q9{8dX7`^*kWdGs=uJA*zmr%m+865)kumir)dQko2dRafxE z>ETw#jq*(m_YL;{b25(auFjnMbnTC6g&omSQ|^ceE*$m-ZwMopTTF^&4xzADX=bM8 zpKgnstR{urVv%ZSr6MI}9yAE&fYkSmbcm*WQJz7H+-^S6|LlDgYH>Y_)MdExdwhT% z)GgYimUB{b0;CW6_48pXOvKceQ}X4U@;1Fp&Lg*-?Fvl@R9P(3+stVXcCVaJN>G(pPV#$sE8K@C_44mrp`Am)n>GD*3OAoP)*i`>RGFCnmKJIc9I-7s1@m?z3PEZAg@gk|4V!Rwa7JyAm<`{kKK*YvtlBv!Je0&f17D}6Bpf%X9 z_S7szNf>BdVIN;pfYWNmm2XXbP7}w?5_0CT>V+cX<=`18uqJr|Djfrb@zk9^B&7~- zd=+H+8(`BGs)+%N9KO9@5jnM6TY`{;M>1q_04UK9;Cr>}0 z(Z$-@#WQ*E)h)FH-ip(wWC>K~3B@IXF;}bkjS~nv18F|oe3Zp{>r%T6MuAxad01x# zYfr>PPi_UM=Au860eD`c*kkhdD03XOhe8y%NO+?1`kp(v>$;eyUfFi3{UPBgNuJ;n ztt9{+gQ;B10pany4^k<4Tr#W|J4wQcU(HDsPSD$Ho;iBB0pFS?d3c(H_3>5B67fJ0 zF8F#9^sqsbit3bKeAJ39q-TM`rmUi1u-*3&wvy+se z`3eLc(L|3bra;NNV~Okt_*W84tmv*4APS+TNew2ytK-s$Bh?<dKgG7nKI} zSDj+hpdAbVqrVe~`_z9<8OjEal^qo?69JNsE{6_Ro!*Gx_lR8u9&+~p)I=?N{p0$l z-R=X!dwnH=6X$}ef2+i^+EgB>lPi5AUUrc7G9AwV(_hao&dCiutez9IhnZQ(ECo)f z-vNyeyyt}AZ{taM*}i^_FtyMm78BF0R>!wW2xDgv+; z4b_vv@U0yjN&1Ectfsxl*@Sm3m?TbrDXVBK3323HjR$YDxagWQ?gVDYKF2D~Kq-j& zmdh6?499{RFNaj3l}2C_IM=}viO4v`QSgaQ=s6_fH1tmhvTW*C4$4Z1LVYvP1I_np z9q0ODy@y|nCTT3{;>T}N%Q$pGmVX$_KtA{G-dR@g*`*G(Yzfsqo_=$;gr({G0*46_#)$Bqwsknm3i4gWeA6E|5IeV^r zRAQpX%sXU6GN5E1o260!hzmfekB*2jB93HHZ$G&+Vd&SBb;Q?JU!Q^=}|z zGKg)aAs>f^Xkab*$lmK2yAbJ-n<4M>B}LQWn-jImw%pE(v#!vyldb@PcH(X0GPAGA z>-|34Ux*PxNY^SWAdLey^10!2^ItWI{wy0e4Zet4%8YdOzw5#_BC)9V0TdcYz+t8^ z2@i>+^Zj@6kNIpjO}v-x@fLVl$oou3ziTi-s#psz(d_9xC?Zaqp)%9w-p4r3KTJep93$*GrV)u=KK766hCKHg4aBF|-4sn2gW zn1|{$;pW1IattE6{(QX{fmnYi=`A4&raIJDj*JaPJ;SQLLJwQhK}8KoRhyQLSxZ9o z37rNEZC1dy*)MeKy~A|k{NO0(o!^w1q^$! za?QNIv3H6;RK!%_lJvsf7YBP^82OIEc%=r`^9`I3q`=jD?&(TmdLs{hBC}PO9fl&R zwLm-nFIdG9?1f9_gw#OQ!@hZ82TkO%qEt*Y>hR-)W4$|C*zq-o9DeHoKwiEHf7sIf zd9)P(h+n4!h7g5XYl$`=UDUL9y)5MJEdaHH66d>p$5W$N_y6H|z2(MVGOA5BY`}d;eERF6aB6+I%~&SR>o-Vf5)i zV+tn@X|t^ywv<3f)2oZg5v!EH$eHNp6}kk4JS?wjS@xh7L<07XAXQdEIS7Hx-J1Rk z3&0^@c5i2yAsl*EfDAPP~Axfz1xo3*PFfp0RL7arO8&w#!$Vl`Q&Xy~MgS)J~&k4a~&qUUr)3>m@IdZi*l(FEeyR~@f+)P9ItE`{1b-{JAQ9w5K z9d35S@w1UDikLayIRhSb`QjTf%?4NCQh$&ZqCZDc$c-UdQ9YEn=R-W>^@pM$G0t6Y zU>N%|=P-ZT2=;`}(Qg@AK$ri^%|+~6uLD)GD9y})W@p3p-9>eR+FgKn zoR?YC`B<+3zSMfzihHwaU0PumE7n=XE4+AM}{S{n;|Z!82% zW0=3=rC-Zskx(H2xTb;qWpzU$@ctMXbRq;LrfLE%O#(V|fs&gKXx&@cD ze;3dDkXci6I?Xbph_6xeE(=MY6Sv_z5vg!GsFQH5Dcac!}|cCCs|dylB4w9CYzfEvGBcZbdr z^&H63@whH8Sum@c*5A6>EFFWarW*;pLYJDSw-(wkMP=G-N(0GD9bF*RsPI|93i=;)tiC=-@4Y z;1hdU0(@)i5l-;{`7%s9IiHZ8M;JjblcN<>a=#;+69!VWmYqX?jeR)9cL>~3i z&R|I410X<^o5VZQAXE*hjWySLyBxU3VQHldf@9J4cJ5wPslH#vlvi}CP&kuT4{5^z zk9sSk?Q?KF$qMX6b+XEbETf@=0DBKTQE%8#&k{~|Bh#F`$2rs!8+-MyE^%u!K#4E> zlms+US4wUT!FZGLU|Rz-B2QI)Aumhj5utyS^y7VWhf($>O+M~BWmOf8N=ILL$@oxc zqag*4%|MMpmI!ITFAsB;JGi>MIbbM$Crxqy4bkuvh(|0RUSDHlCH3)3yM6|7^x}mw z$^^SVSp4VOsyOeHuYlHNntEVA3k+2<>AmPTJp7AIscOVrUf+qRV(xea%pQj#8;fHa zf&dMCxUWcLY+%rdUW*Rp<v4=qen0E0OA&=9Zqr8=kuF`V(bbsj=fLn@Jk+E;NoR-?{P6P@4_&AH&f;HR6VELpnmaDNL&^-KicU^T zer>d>A=mv*IOv`xb}*RRjtZbi+)QX7`KA+;?c4{o(o=l6G|7R0T3j}MBOPimh3Jr* zLf!!d;Xi6DDFSNV{+$AJKXKEd?^0X2KFsezOfCfr(nGRO`RKxhN`5{9c=*d7suX2+ zoI-yXuRL+TznLuaRRitsR$2V^*isXjNyjS1m#s$`wd1G<`SC8m`=S6h-`o*&b{?Q2 zxNa>@CU`C|10C=_^qQT)O?hZPHL65^i)~)gKYXeC7$7AOBaT68qwmy50SQ+f=v0de z6a}{rHtJV)R>km;O4_ljE@!bO^`s2w>|j&tt$^sSTL3q84M5LhA#NjUV!dirK^{!!_s^C1>pz(as zZHV!V^@CMY<7NPhW|W!kbLxl>*SyqTFE%9<8Y-7SaQ;yp78mp=I{F? zBn_PoD5g))_u|!7YVcJBp72!iN(3ZI+qHBe{js|AqpNqsOhG!NphLqnX879$5dmTh zaYm(PoAR=mqTl#o?E>_#EpF;-FE1ZX8isi)d+3gzQ(;($F7Qz+j6#s^=YVJG*T9Pa2KFd9r=_|1a>N_B> zbGM8x=gkBZ?d*JHCI@@l!fqwHr$7-1V3EQp!YWU{UHq(M`k0+W{okQ^w7F1`iLPoF z9#<)gLPL`#Zl@~-F=)3WzhpcAq!kbDm88F_Sy7`#%I?zbC47^T=J}cUz8!YbhHdv; zMTzK)(cInRf!Yz9SgI`x8@!Ake^=u>|BR6Z?7j9*t7=i98)&ZsejT9FfH~))>lMH# z9BB>~B}>uYm!2`nh>m8L=cF=EWd&p(YR?7~O}d<f_=k_{z$DHM#7$nfn{u6P8 zt^@oe3RN?4UeqK&y}_BG{Or$R!|`C=J{Kk0a7hs^H>_6hDPRK8VbJq?V<36d|IL|k zhaFt=(j}UzvAXi6^5^eG!^sh~^Xgok+e9#+i~c~Y;?*O$9rZ7`y8Ry(%Q`*oP#O)Oqq_h9>pm zJX4@oks6C#p|S0ga7xlrsQ3dr-9etef*=xbLxiAd|1{%litt#1>>}!;=MkZGpnotoG`?hcP!Ne2N^v<;VZV1W^@lFIu(rI!EohfAct)8e= zc5F)#U$q#;#PS9e;KD>BQKiJZlX^lfd)4}~5edl@^!~&Obgf88(beVA&oxy9;grxF zB;od#zAdh_|=67SkA(nBM+m=K+oL(}}=pW`gLvT;29B z%?2nvBk?t^Aug!r{j$t=shPd|G`&roS^<}T2L=ppBmnWY$Ly5qFgI$Fw=^+JyA%i? z;nOq2x%R%TM4ub~M|t0K@m_Gv5z<$vensZj4LAz!;w_nYqQ9VRt~eQr0*swTic+EV zwp+!cJTeV)G<*g~S? z%$TZktWpzaUt5lCn1N=T=^sQ(@XaWdXrBzfJ{VKh&Yqvlls^5ybkI3U`}^Tm=A-Um zl)r%xZ_obglhgwLFAXTq`L+9iPO0kaqdQ;zGke67G`=-Me_Nd3h9djuoo%Fxn>VQ- z0)+#Iu;}jkGuP(~uTTgp=B*^3J6pJUmRJWXnS!See_$084#qW`8p}Vc>_zz2 z;`m-8Nl2=dFetVrXho-ddT|b~vP4>yGgBY5f}bqBSMryU5BB|6LJq8grris7d%bTM z24#9^i(-F0waP)taq{$!PhbFV$J0Gs9psZBe{2ssayi!K`jktWhMQ1(ee-N@j+|g( zGkf>P$OFN>%(>-!xywEnFo5>qbpG`1(QA{-Dg@%YXM~u3O^e)rq3a*syRpQ&cOp8b z5I_B_1b=VPdC(b>fJPL^cpOg}#v#E@ky%`NfFPLqZT0L!l!**7qMZGO{v&_Rw@by| z0bk7!jveg7w;|nmystenwM*5ihd^#WP?GkgN2K9FO`ljnbx42uXKC41;Yq58gLqdv9R~~OVvs(?1yt%*T7X6_#?6OE7`ITV7b6Vgc z+4Aduwj|?Wv>p=(3LwH{quKfUpgzJ8 zMfL!pu#JSp9nGEd!WUHhw%8(@%XA=Pc}E^7_eB}M{U71#RxjoWT<=8{@v?WC(KI}m zJai+PvcOjLHBij_6TPKx00woo6w9)5aSON6KY&-e*h#*}bPjlhrz8c_cbpbQov zuXYIxm|%!nt4zeQ`MiG0Oj*|;9w$(>xS}?wNK!C=1?awGl|?4gj%&xTz$(6-Wv3?J z18t!=_I6RX%O1#l7>@=9JEdS|i~!?zz)c9NSA;C{w>(AnHj5+00f8_py2yjxqK)7HLf3La30Ut$l|AZY2F+R7hhvyvqGdWUNB9LoWqhyn4O#Z@!6zPT41Nz za*_5+GHIAcp0BM3p|#+#o?My+E-Bwq3babemID)7T#$@kSLh5pRO%}g*Bim9aM0uC zu6Fmmg7&esxSq@Bjjk4-=?WJK=H%=7t%u1hch>$T*WDVH-Cu(G$!8N=o~uV$W+&o+Pl7 z`gsV0Z5@kVRt#GcMIEcw$J16iFn#xTSmKL{=&C6qLI`+ToXqh$D!_AX$?z=YI>R*6 zF!u&~(^~2D`;S%WVr;K;{@a}gww~D!H6xqcLlyq)HOoc{;QK!wV%g-+ZGQ+w15<=* zMB~8ZfZ^LD>On-0APO{?X1bh-u#&l(^c_klJpH+}amn*4h>8$$uc89%XyC$Mr}XGH zxaY~Y{%l6p`#h=6nF4hQNV&(03TE)>OIb^(RDmLm~=HmYTr)e7nge3= z$CIw|y5Y?=vi1*ojTz~UCZ^%}rRS~-lpd^aILE2OPjrabfQ%~_Ik{atKW(_PDBHqY zxAaQxCi+r6SW1g0S1=x+PF6AatOH6xB4?{idbFI81;?^XT;%Vnai-*ttK~k4K7?3= z@Dkj;dtsE^{*>XeV&@;aB=34*wE&VvZ+=vtnm+#!kL+7z$-Pnzy-7IewUM5j^a`Pu zOU4j~i0;fJu)xMKF~gxfetnszI6Hb_D}=N!*#tDVcZ!{7v5N<%CBZ=9L_18$OVm%5tC!tP9shMdH&VOQloHj#gam?3$FmoPgn(3>Crn~?EFzI(553Of4vIP zp@45r6~CI!Vn%Nwta1?KOM!3DV2!lh9EO>U1h{zAV}_RiK$H=oZn((APB5)~W#^bg ziJ&X=V+w&I{T}v4J8;L!Rfxw(w-L8nY(<@)Jdv?3i5&`Q$#x~;)+R6^^Xm7N)Blx2 zx9da?eKOKm0j_1lJ?WzyE?=dJBU1p&n!?TrwxxHq)JKMsN(p~C?N~awjnQBC2fWI_ z8x5)09EECgP;xfj?j@EPoCRK4p5&+X0QWf}tkCum&hBb$=HfwC%ZH3Afo~#lS7MOI z%Q4y0U47qru4S{7LYppJMwoWn#tm4$09hKG;!Vy(YqVEuFK zB%$|OJeL_3-}H^`MN_j!DJm82j)vlWN0H&Ny$8NQ)V$OnBXk8|OyHFst+(Du0Yw)%E`4$j5p`mSZ3Ff^%ykQ?%#J&y2gE2W zbN2T3vVRf>*q8ca75mrgkM=Qv)h|IKLj)F;iO-jT z-nqeKDRbejs6JaNKFvicCs)PWlTU4?A77g#9-FCU1?38uSuAN}tq=n;J4wCVV>$!M zM3{ZyuSJe^DVnU7&3UeoHJnwZ=vodsHm4khWyRR0;Ma7Y_zCUYi(V1}ozwO%@)X@U z3CoY)>hs+nYZg*{>12>O7avmzr+i1p`ryg|^||6UH#BbdH6Rb|h3@!wA4B)+$`(Y_;S{ z_l>Nq!G#IVeUYzbh5O_p5^jC`pKI%IAPJDD&z_ytGvFOOP&r)Ik?Jq#8DA3xo353e z5^owTnKpFBXy~EZWheAfJ=aN%4!oJfHeXsZ>x3%MpCRmOfADk3#T6LyyJM1DJ1{gLfDGVGEL%LG93{Q00iD?|plKUM zx>e}^ku1xB_;#?b!eTIYJA%K&2$wk-=AVdEg>X|wVw6fG<$!~VI!;BOD+xxr2dc?N zkO%mX7Rz=xZ&Y?Ynm$PF55qS1ONUv9&R<6AeacQ9_+<@bc(bTt{g){(ha0Cb`IPlDNNPH{N{0Gdq{kD&)TsgsmgksxGWCMqU;zx+>sAQ`(+j{ zHB|7!md7U_j{1(#)~s4nbjSXzjO(o#t`CyzfTwNr4D-^VenDwj|Ef-foF^r&5qr_V z`8J_r>ebRVZRj+!a=FFs-x2d>cP3A@cYAAW%hnr-R-o=S)3>D)K6c-Mw~)Mgm~MgR zP2p4N`FIiVw{W-O9{neX&rW6V1C)wiTZ?{!!KUzb4>knhatnTlg*({+)jtvb-kv=Ena0e&Zs! zq_YM!(#(8uH!^pZa8X}*n6-C9xH;?!A*Lb}bPamXe$7punorlHZUPJO;p$#nofl^j z#wb8^pKb@Q^8r4B(I)7>DM=;X1B*y!^LwBSOc@&LD%)NhXz|>*|};L0AMPD z65!!J&ppu^!0lszHUVgf{WsZMkELbhRm!^Lt4LrG)lanYQreQ(1A*q#2Z*c(}PeV(lwRS)F zqhCg{tUc2mhm-*|_8*Tz2#dghz#OK^>g7-K_WGW!{p5@Itb$Jo!&PQew-e;A*<^ms)VPpR-V;|I}C$u zC%pu!JYN^UG1QjI)b@`b!|!Kzo9bAxpx{K1e8 z|DDn!E^Fn+s+qi;F?S_?9-@<{1>{in9`I>bi2@kBhnBU|ZWD*B=a3~3wm8R2ap}r& z$j83e_sDt!J6{yqikxK7$Q|v^t2XiP((_m;tOyZwd{DxDZR@`j;D)tjm3_<8zYut-~Dg(BP&u66EMpiwAcFF*fm)|#^%gSF6vq8q%B7! zq9_^qS*{tY7}>nS-J&P%kiN^NL{2`0922e{s$1BZeksU9Al_J~MOv$9rP-wvod}lk zn(^M|_uOAsqH(QPVJk011onSj3%OAvVY>anh z5e{1H9U)Gd7ycr0R?A2?6J}uFY|Z}vvH-rcb5OEAVnzpl-#2K}!k+S}EGoewElh>u zc4k6k(ZaM#nz$$N{BB~!{{8Hbrxubx>D4qoDtb72^R@aU{QtSpwuSMsRl+)oN^Fin zGu9il|5Y|MYVIg^&c0d-`^$-#(Kl!Kmn!gnhJAuTU7b9%mzY0DIz}!BaI)GHQnDr^ zPXJFSYs)v=u9^3ec;8q4eeqL-gs{_gvfRc?HZ$u2FM3z*q?;wJXL4-j+V6vXcZ3sO z^Ajn(%69vpN8!sl&$EX~w@936x(9q$Iu+C*xJA~4d+)nN`u@I#Q^{#S#B@v8jo~$` z-@zOS#S^2i>PSs$67qC*_#tony8Xghw#8)T-vYspfyD0FabSnlAt#cxc?M1Mx@B6u zP5^L8A&a4NS&D;n5_~w^kyaJapoOGlA5zF+v*Ol zja+TNd98M}ODgC`USAP#>a&|Rn}}8P@yL)^Sp85sYB6X|G^7Zj5?b`Sl$Tk*Uh9TR zYa*K~{~3-PZ!ZX~oK3~K&=G=243Q;+-i2mzHEsB{ewQ97i1 z(&3ww(H)})j0Oc|N+X?$0uloiMvoArJBD=U=o-zl&-c2XzhFO{opay!E5%g2K zHGvz_4MF!Mj6(LgqB>iR2os#C!L7c&?>gug=K1Z~b_kID3q(zL70*RJ2=T9y5(ZAT zY;@cHA>euJee%|Ki)2J{1~Q>G0%@!u>_L?1pW4-(rIP>w67;tq%0M^|wP+LrnQvzYjTXL-Q#(;o~?2`(jg!Wr6>HLaDXTzw8t?NWB%r`BI zsDJ!`^1>;hU+vo8?(YTf>Eue+Lts2H>T$n&(w~0rsQeln*e@ls5lV*l)tm_<)bZ-G zmm9tVkPkV@fbUId`e_av&`6w%1wfHBO}N85C(VHbus?V7^VwDFl~6|;y`6Rma-wsd z(eqg$%EBNtgXwLeFrnA%0=Jj6!~pkSdIGK}rSCEu1VFId6SMVov0Y!yC^EdTHaXs~ z5dJq-ppQmbGJgo4P@f|93II?9w?FXwLcHENiiQ_HC&QDLWs{Z4WGtEa=vz!n(3bpn z<4ZoTX5UGTx_vmC`e%Lp-ZH;=vq@L}P9fNVB|MyPG3TLte_wyx$QOMsAC1mo!JDi0 z8$k}j>p_Lj3wIkg*)86ew=LC}*Rdn@h%p1j=Xe+f-be*O*2A~mdSo`SY^XCTfivNw z3p|cO*D(M2K8B9a{q}+1+z}c|mBOED%JbihJ+>=jR{}EeY|^e5VKo^P)GEKVGp90`^%0_jzkuQ=D~1Qp^qd^R}t zVwLulnW@~GI>%RYGA?>ISy%7Ka-fRVAp#EtOzmhXoNdvHVC<)e8 z#K0Z6bHeHu{6Q7^~O&x}_csCD6ey?^J5#W@>;c=ngXt z3?0Mw$-S;;3Ro6~Nh9v?T|er#!g$f|gJ(Lu*%SlAe@l&7zN8S6p$tJH*Zl0S;((t**5GYW`>8X|(9pcA1XiX^q@6EQ4J$`HU0<~#XLZRrI z`};S-8Ac}3Ctl7t&p^OvmmyK%(xR-1grTgaFZEYp-y?z)qGWCDfp!O53}xRB%^J5hP0etd>QiVd~7}~?v>`K_)jtqt*RhKC&ggWhn)Ql=ZwGNjQx43VN{kLje0RTB0ueIy>zOEKV0{rcO= zY>$1|toAKHvElr! zHO}9WU?E3;^(t=_JD|iTm^^bk!f-Iv-I;U+ohX6?HPEnCee&W%H>e)9&v5(u<)1ev zYKre_e#q$UQ;M4X?nerHR_n1CyUp^|&&k=9fAaEtbN?DtV+clcvp-PWCns}FB}|-e z3>tWZD|~`C(&QqmymYqy^G;P&(n=o)k8een#l@4CDCS8RMwt_=X=5P2<)fOOGE0*I z#40@90^-lZ_`YaK-znT3wIp9p3rNN{g5qU@@#Q*cKTy)~pMOuLUlc_78VE{5`vawz z^8o6|zjbd$lE3mNSK9A~Fn^FF@8?g~{s3r&G!~_3Ezh667!&*0dKfV2%FD6s>PRhbNx4W+b0b1IoWj#zwb$>p;`< zV6Lc=fNha)Gp0oeg1J<;l?8ve({3hokAP$bwhpgZ>Ij5$bq%4z1imt10feAvx_v=mpA9zIQlQn>{>I(z19uFY!hJ)Jk*y=lS7f^ztr*y7F6j zOTY?3lkL?jCkARE`;RXh-U^dY7=1i4F*OH$WkC&y$ffF2D@JQ$l9g2KgVJC4R+n&U ze}72k3seOD8Z4pJWKyelJx*mW^(AR~0yr6b{$sMMpqD>)1DN!i+fPB=9tvgrzzK3z zyg6VjpUJ4rprZzl-_KF1I;>O~rqpi#u9DR7iGli8{rLc{ux;5`{`On=G)=pTtHq9& zGcq-WvBlj05QSKECH&$evRY>SPtFPoZ!DISXG+jhA;bUEi~}?Jn=1N#o97(ctJnI> zW_07%UFQ{y=6>PCSbO%|ULs6{$V9r>;Pbxn^;KFJ?r1f@SwXhmK$d)I=acUu!weDc z(?`_78z~s`2eppK$>7H9v$1MUkOx0rj$-fVh>s5-0|L77HYm&|#gYk#jNn5M30sC) z?1$bI?u%Y}s6&5UI>&8V0!Se5)zf_fY9Be+RyO)<3d0akRaa7pp zM>yxo+pX3 zVs#Lta7%nEj?ERoelmW|!(py)^>ODcdq)HL&dQ7Kxj>s+w(1kNIrs(E8WPujV7N_~ z2!Td?`PhQEStlk|#43{#V96o})$5+@CXzEwy@U66!iP)r?eG?LY_4apoM={-2?15)i_Ld1#h5XK~Zad{Ky(e_jJzjQb0d%;Qo4`Dt_zqfoLIpc$f6+Pk6dQ zZ|X)5c@aA}9Gl!7Kt{0rh2pc4nX*kyXp6%4YLb26>%tRWB*!^Dq4{a>e7$*ZpNU|R zHz}SLq$cBtU$Kw9xTR_?o+Bp|b6hvTtNdf+x3A*Nt8ns^b_YvULkjmI8dog#@h$X% zE$2I=VJDAkpAG42d9lU`f57-P_ruZ$Q5?2|Q;^2|GiPnyDF~0n-rl}d(|J3!o!vSf zlEtfiJ7qj`Q|HmgeGNhq`*<*RHMVm#hgx_(+903TdLR2hob^|ZnYiSUn$J6{i-Rew zM7~1k#dq(jt6<^`e>gmP1sA?Q9$v_)R7pei1mNmOeITPuG*C2m-c(c6Xt*BNu;j86yw$k6NT*`*qv++19LoUa|iY(3$Z ze7Y)zVEH8{^(higfVplf>(fIZf;TWH?zO11(iEC~bUbCsoFZ8Ioj%)>q@~V5snZ5% zXS^kbek{|IcXE5RW$y4rW7#+1$<#s@r0bW;PmX+_go{wF*}^N)ot^n7kFL$fdD!l( z^22au`L~N!Ay-y+llh@XLuM`!wu1xogbHzEQ2^wxA|N(v;;OYD@fx3&jlz7_;(aCa zc%!L^V=GPbzk8LIEswUqFI|giqT6j&>`bBc;;0hojsHcjV$I3E@>6vQLq-js1Ox;u zg);v+IQq*2et8$MD^W5?z5QR8NoNM6Pj=|)=52i!^*wvjG)SLAvk(q1|A>-IFZl2p z^c?jpOKFI7KU5Ry{uoGsx1V#6H@bB73#%$xd$22lvkbp62U>U=dyp5^xK@k_PFNS_ z1nuK1HOhPhY?;BALb1oPSo5jid@a`ExvdcWHm9k`p@xOe=bxOijv{!;~ zQI0t1m(}T;>9i7AQ&JkReJnwe03q588}}(LJRS>gIb8q%Cih5O-&1f%?9C#X+{t*6 z&j?2+tIM$>@ti+0dam@L&;bir3{~_uTiLOO=doW>elM_{H0tHaM)K!ARZ348-Maqr zhmTs_g}2bz)_Rb)7;hk^UioP$zcK&SnCKi&?;yp+oRJ%YI0M;mY%-l>Nc#ky!KR|} z^o_m;D14@)S>!!>MQs}^oTp1AwXtF&8cq~KUHxwL`%0vn#xQt?{UM+4vr0S6SC&#oxa$H1vY_n z*#7nPRCIB)1o%5OG26S*d5U)*WLOg5Sq1K~!p^SzD`I%hCI7>XiJC*_|d$T2=E zfel{$tk5DOP)zfyAv!fK)NnW?(x9uDld;=duUwHBO7Gic_r~CxM;;Q^Klx0P?q3+WJ!$!Nw;^SniN9ILtDP$hgA}Xn zm9n<&q~X)yEW1OMNe8&w{X14-rB>=1(CA;#g?%I}9rOU>AirmV~YpT8VWG z-;_%h9>Njejrx&YIkrU%O7FV}XsQ~Nax#NHpMxT|O%wIEM^F3f2$${1|I23!z4DgN zA5H|RBf25MvdpPpzZfRXRck><$=N1`ar2Ya$s861NS6s#3$s9@lr9^u!MBRg)1{DJ zsrwP@@>?!0J&ZA489kY4GU@SAef#CN^{JpvBK=w?#g4!*N!I-S<3L8Z6pzSePUH2L zItllMx)IK2%QeL3k}{BDy9o~2>tna9kwl~ z|L@-S0Ji2dEewvnp%=%2IO*JFvbriX@wANOm>fG97&5=6YVL(C{_pG3yc*QPRE~V# z5R||8u%gXf;Pl&s%t# z2XUfisyeoObr^IbP1DG@h^IIqo^udJ6->SU?;uX#7x)%>WQpk<)&#nqhu>|S@vtNe zXa9>pYutWb{`DDUbLVE7MlmfP+b{&hDaUaXKC(+0?E5_qG0+A5E;5qPikD?_C0uT=*LMv)5*sK z<**29zq7}7yoI8AjWU^-5mS1YX=%pAy?CB=Ot0 z0kA9?s|BI>@8EDsVHFy8WFBn2WOLptnOBn0`dn;Jf5dmIbyb?}R=>*VhB>MtB3>uY6t=wmXP6~H?vq&$J4 z>1iNtaQ5nE4(+3akLe-)+vy5lLykSJ6A`b2;V+2E46PpSVq}bdXE_W5*=_JW+bYlY z54I2?L^Q*AxnsIzt@7jpKOuN|M4k}%=IjtD>R34e79jX zbDxy~>5f(4?jFRe2Z;%TT zPhT&QvN5=HyqCHXk1_cb4iG+SFF@Mluye!+48ZU?n&_w>!8M~ ztk~Y++Dz%1=l}uYW+@zIBJ_V0J7e3O`Oz0*q7b%s<|Y@j!N^y7`HhV{g{w>7j?nu} zjQQcXyBp*khwj4VoM-69X-v4^-Ogm_zi{r2KmSJ|YMjbTyn)pEZ{?nCJ~a%Vv6>sb zFL6I@u`#{jU8UmGM(2-;Zh8@WrgO|zm&duM#VB`+vf|w90$ufr)q4Ya<5t5aEYguR z<^{j`NdO|>`Azom{9)nZhnmr{Ep!rx-Rpa1ac29=6=4AYYmYQ%(FQZ+$id6bxYQP} z-T87qPys_L(&+d187+LXaDVP=MQK726B&6mg`1Q7>K4D;g^;_`jfLir>lv%Y#)ouY zKI0#ynsnJe9nupSplP}(oh3+LlfzD#882`T+>$Gl)iEGbGAVqXKiUbmpwsbR#`p}z znU%b+Px`O3T&T8v$r?H4?`rzQ2wwy|h#xe8XxV>xO;nwU0=FM*oRBg*331qlL1$)q znieRE&}~Tz3PJFJyzC|v-m2r3bi)uN@_P>8JEqiC);+oD*q)`<*euSw&U~K;i~uK7q08703bE32z%3OruE^#Z5R ztFFI{mvLfgN(>V`kAQc{XpIMxzBNx4)})H0B#A3Qy;Dq|52ky&D3^5K<|MipznGnb za1|uO)*w(F-oOpa1?p)uqZq0jKd(J3hB)^2^1{CNDBzEWk(vwyV-?vOs6ycJjn!Qo zRm;tc!g=)F5@`!Amk!a5V#~kVuDj|q33iqJpVU^bc7BBfI`->sO}mV(aoo&1&AIly zN$^GLeAt+$jg3{q)r-T5+lKcY$Hku-MyV5Ws*a|D>A*sDrq{7bNgfq3(vQ_O(kYs~ zC=^CQoWZ_>-cBOpbsv)}T-WAL%tjkw#xL2?gv+%|cw|cU9zo^7dw2S0UiVZK-Cx;Q zBVUUBxHj5V%SEB=o#f)#GV=RKu%zKfEq5dE+Zh{S^sj<{!T5G#@FoZ~@w3shzl}6< z=GIW@8x%6>U@UU$ku#Y7xv0?#3Seh1eB^jGSY}j^qTLy>>HK_qVG*+}wyUp+f8YDe z%}(dBLT=w87@5&4K2ofEKDu#b!YRj)Ue{si0`FNQ)~!e0{y#fLe4+upve%&0hHf$G z2$@rXgDKz(hHzWqG4RPeF#uJBfuz;|$kMuLggwUu9R~}? z^_!@qu|9(?vHGz&SZ{>UbTWek%EQ5%(6J;x8&J-Tpth!ed8@zO4`~7 z2+V6BAOy!a4K^*MFXbSQHOY zC3P+p+{^8Oi2cCT;Fl1Ib%E?5tGjEW2$aV$&4ZJ9Eq;4My%hGE#E}cYev{r!KTW(t zhK2)2*1nJ=Y@WT&H?t!fp+CL9O@Y|lNGtcN5so2)5hYP}cg{#M*nKG!+|sx*GjufxL0 zgfH>cyWx_Awl2HAVUoFQKAOD9!jzPnKFW6boEE7aP5xsa-a@Z^GbX}x!Spn?$RLoD_ z#DK?XC4cGqm5Ba-E&!DTDsy4iAMjc;b3;oY&@^4Yck3h()>}xtpNnf3A_SQ}FLYyg zU+An{Gbqo-YRPt{M(j0Dph#w8fmb?4yKrt1u2%0Ap(Dm2p2d#tr@m5x$F_WDPuts) zUmbN)`|NrM_4)963zf8;YVCA=zno za3E8~E#j>B1&-;_Tv6?Ga~23yM89}%v(ifR(&I;nAWFlZfWnbv)_DiTU4*xZ@Rld$1K!F`y|)Vl$Sk@M+qG;7E17&^=Z2*4W{1LhN;j3a2+YX-GxR&wbn?g1N42pvQzc1m-4w*3EpnAw)B4QHp{u~F#2}P6a^@ZO#awxa1|y&1)ozs z_+9>Pv~mS|tWrbefLs2YVC7KvPgZ<=Z3zZ~q(5X->C73bjJDyK^>196#?$q@p(_&o zm-o@>!CySwL5!eqi+YmC|xB3bOBArkE}YLKpO{AM7%g}L6u-%?esboqBL+u$lPA?n6Cn72cvQSK8SjmT;B|oi zNtia!bEyAj4xnJrs`2jfataf8)kq~6ez!CL?=747 zJpWG#&;ihR^ z1AA!9?v*?|A+*QL$gkFyQd*5rn}mdjgt3)KXi+MFmZXW0Ht#J#zfikS`yfD+T*&61 z6iqVYDO&Uej{nb1V>ybRFX0Ct3No5h`n>Ejnq=m;2>fsS!NI@g>y}$AuGU1*a^TpA z>63a|MN(p-m&vqil#I?XGgth|*5+RHnvkj|TJ4rb>yy-^*g##D#A7M_GD_th{`@aPoJqp8j-) z`hMyS7x}z+h4W>#V!Ybp%|AmG$tW*X0rZMXxnubKEz*u~8|A#_+0m8R%DKGReNtKl z`T0y2n)=4@7aevdYhF5BI;XDnd(#Z{DDm|Sy@Y+!WCBqb5_@>^EN9%%51?!tY^ z{8{VfXi}{=?TMS34(+!$f6CaJZqZk81-i7rEZ~TtR^DuA-qU2+g|J)SkjuzBv6~aC zsceO|?1kgxwc9SQx`sii{F9{s>3XT2_>5dl}*3HeOr8$lI%8cuJO{#1fW`xT?Lgw(APqvwq9=~L z(=6D=HzKME+3Sxv*AnYapU@*yVCBuV^O^pf|0TR(-CS(dz^5n<^k98B|+5y;^xiTh**B0moDMcUY^d{W3`r z7%yyAbk@9uFslC1L^B5C%7O+G4wx8L#2&l4gdGcdg<|LP^H?)B*J32>PI7ufLa$B& zqSXtv;d(P6oWpOUw!aa+qud~tjKlvj_|X#HNXh?^K431s?_{b0X)FP5s{D3+yQ0;6 zA#wNcF8M>2yqsLv;opSvQhVQsFaGY~Mr#Y-wq7lN3+*eugUL<8LMKlf%dA7u@^v}k zH#9hL8I08B1>x`~U!gyueXy|}1>=M@X z*m)~Oz0xYG_ci~pm-Oj{eNmD1e3o5=0QtKUPiU4re1KVg3QcpYFgOq z4nuK_b-SQdkrYjoTWFmWpGlvxHJR_-K|EqZq$L;^<517|aQL&HgZ`LeOZsVWGo^|$D*#WMXM@@=os2An zW#cbmnl?9PRvJT3Gd8Q`*JXi8URi08#bsj+y>yPnw`-03r*tQgR(JDka}}PM84|(z zKc;1MzzTjGo1P{;TLaiT>x*mCwxEiQ%kSH8v$o6KweY_D!T0LH^T+p6{rqL6LLN$f z2!CTEy*QzOyJX!CCBC-@xF4-$`{n14Yyy(rrCF`rI69zIzz)qPE|4(`%Gl<wOl20i2(oqIHrGoHn`sLtpVJtKL)URiyC~0{By7Bd#OiWh;fz|E`03 zC)C(bm~P7uHQY>$fT#-$M{uAkkq-fq+6WYyZ)8KTD|qG`|2mh=)THXy_{Ft$Aw95L zQ3xt39I{sELxM2ZTF$8005En8l3nDG^#o9fnm1oaF?U5$QsVD}U({wk7pF5;YdL%e&Q0;q#)cW4&crK0XH$+91t%$8Op~ z%O(iq!;dfK+F1x^%@;gow)eO7>=U%Yjhk#m% zS~%6$Fr^;ujqCH1fVu|Aq`0j=a|B=^^kQ#)-9KyHStatpNHAvyAljUz0)fcT>oVXq z5c5(h$|w@)bVjP0KNhZ>F{qpw*zId@ue1!SU~E&`O58fEoDrhXY>rp28JWk5u;~^X zFx5fQQ>s)Zb>li;gUr)Z1oA7fR&l~XW|fs5y2V|A@?i*~+`hn2oP zvv&hy_~P8=t+BDlOtyr5PfyFtsfnd0z_hKWJotztt&oRgwfAltE<0}56~aQUXk{q5 zdkuJRU!<45Ebo6+WR5?0+0u%m!i!^0>hrMmVRYEwAdYKQtb|!P!oP@$RJwiZpS%dY zD1W7OVBt)z+%3%sS;QaN&iOGw>+n}c4^0H9N1GEarJ-+N5T`!Z&85_VCkA7e(kk&Nh(!e^V-Q>yqLNB$bANIW@@i1wMn1k7Z6Accr!^0n4N9=FZ zigqwn4KmojjpgM~)|I0F+I6a~J}Dvb4Tr(sEj7q7u}Bs7QxnhdOqhQ(r1o`!W+p7J zW>-w-xV`;mcegL}Ynz-NE%24JA5t6x(c-0|q5@dLT#)zjlgje6SLq>n$NDk-)AFL&bu(*w29ts@P;fK`={fo$Kh}*Z`>AXV`mka(;U}>3O@Xw{UZ?(8Gh9$u=pCOf)mo z$^;&69fmcB5oO~0I>RS5u+ukp7cIKhVX`;ppTh5M5A&49Lf%k!ZR&NA;>uEZ2v^sx zxN-86_~E30mQMvP6<&+b%GT<&co!}dM=jAl7?XS_O?72ioy}z+-SjrhXHMDqzN7Ts zh~tL{;CImvd#$)!+qM22{rEgSR(GCCnue|6K|Y&K5XhcJBk2pxO85CK*Oo1zDBP9- z&7n^}sr==6VTZp`1gf7UDfR_Tz0Dv)qRT~CVfd!A!d?DdbPj$0?1@)hMf7wjsDL6} z`I`)&|9q5jynzP99y?DEVoI6JwV@3z9vc}*YgYn$RCu}~OI5nahQ``gRC3-+i~TG> zO^C8RY(>`eYA<>z)y8zw+ew-KbzC?+8He971I}OHi_VwRM9UavWs4EvY&)YTAqY`H zsFV*6z==+HI3_>vDr$PBo>u`rj|w`17f09Re(0~uf0eB>cI0zwD<^+aQ*X9^>Q(nt zal-pe+R=LcfamSON~k3M`;mvuuH1j6bx48n9MK_!+`hef-v1(g{l{SGkJ}sNcNg;6 zqTmK!EypL8A);}Uy&aSuZ_P>fzZJ_xPQ5!|4SK}kKkQVq(G9(kL-Ztkq$i5I z69JwLPVnbH;RoYOLg?*&xCAT&?RWI^Nq1&oz%kE>wsE_&%F*@RTLB7)HkRk-UOkef zg_*wRXO$9)O&2QaAm00cYE1G@<;vlq%}M;P4Bn&ja{xem17oIJU$ec4-Rf`xhat@r ztCXDx0q}*I)AjWl4DuAi)=0FP!k%-AI2@2F?0eZMCWs$YN#asN$Fc)_yoJ{%t$J`P zpXc1^5EEq@N6_;^?PGJSF?4|U@46ckT5cYPsi>%Mbswu9Zy;3csZY5-dqrQLvgo>= z6Qo8;RkCGkTR!tdN-HqSM_{I=Q?sKfQiFF_`^@{v?sMRp`MN`(G|Paj#-~IVa0u>p z4!>lJ4S4qww9<#-7#Y7OYYLzz=T)uP(hTFfx>Ab2_Ll1D;U*^qL&S`@HIsDbf}9Fw zW+Xg5AzU5z+HFkJ{N(QKXd1x;q*Vn9R|yg5Q+0gdyS^sC5z3~_Ks`}@Dj$Td58Pd% zBD%Zw4-5+Gm#_j9>ubJ#4(b6?du}wnoA{<#x6oO0IvND;NPhp!i~iP7NYpsA{!xApx| z^k;dIe$al}#jGf3q?S;fWnpEHj}Xu{@d#**#WWb_NH3!4Mc$BJY(Vr zUhX6o6+cO4P^6wtLKhGCl94SYBXi>HUvvA?wpZQdQD5!eQ$CcqP2M^Kvxle0#Ew@T zjSZ;O5Btp%ohm{E5UrOX)E%>TEcTOQ&G+g3Bdc>f3_RLc^0F=psIjZY1kYhCk~H6` z>m^Vfhe+n>wkfMbi0CZEXKl2vw4)=tB+pf(uYdDtj5fHF4F{E>KK?9m_lr@@A%o6U zR8~M|GC0eBZf6SDxR*tyo4d|HplGsPJQ_~5vWl3G7MBU7o* zFX5+K5Y?*FA;^W&@~)%YJVyW&yXl@srKbn)&jAE&p%jXhvQ(myj>0 z=c2JPjcs<6^taQkT3t_RLxv2?s2&zttH5J6YY(nE|LXbGMV7p*A{+YnBTK%a1h|Xu z0CA;?v!5ZM{X70K=?zzffF1eM2TqMYsfDF!3b?k=lcjqfz#d^EwUlFzx<82MNPo!6 z+@6}y<+COV#%e0$?j(F{whA0VBGF@pPcy9i^E_AwdR;E`Ras@xz7a0ck^zGbSx?6Z z0Dfjy^k@mqMHfKBTHdAki%v6xA_m*jO4P_q^Yed7DM$}0DmeH4 z>PdEXb{%iP&Gx|B%S9Txy4^R?6u^?<-^_@5+}Yk-W1yw4Gh68MG2<8b9p=X)IBAr- z3~FBGrW7?lKaX)IItaAHoX*+Q``|=$W@-kGLxQi4=F&ZjdD#+p!2ME_?wm!!zOwUH zl-8M!icRQH@9?|%IUVFmU44ChV=x;TMy{%uTe0^aln7L;g@9R}nS*DDy&OQsj1T#MHC9ML8=IU9viY+Ddb4e&Qq8Ftd{M3{uh z?KR@Pfq=uOIu)|FLUf`pOvYBmR*YW|MyL!f6hKgs2PVoWt`0(uDwx0DK`L#O6LrxxSI6V|zz>lbUtTiHYKY)A`!im>fgl~zU?jwj3{d{a1f#WqXcsVBJtWj^rv0XojkaBkA1OXI zGOmO$y2pF)0F~#HX+WdLU@%qln1K8$;AU-Zu5nA$RBwBI8p$`SSNoX*P-hC|PalQI zB&`0a$n)^g?ioOTN*haV~zJ*xp>ds8_q&ebr{qk(-kd;}p2k?`* zf|YHbsxP$!Sk)+{pPb?o7EnhuGbTH5$hqzk=xv7AUM|Z17OOq@DtIiSQx!Scpkq)n z<6n*UY0ov*9yfJl;7?ml_Gc%h5U4gu_*qqcQ_v~nU~swLOzYQE)hOPLk(s6K=gFf9 z*k37IIh2M4BOlv=l$ZDb;W61EM~GRBnrN-Lrfy&v{vH-yElI&cB?)@)=rTGWvmy4( ztJH!k=JUg4Wmv_(J&ElpOZT0Xz+SbyF~PPu;10Q;{+8IMf7a)x zgMuX&ube0`_pJ<@VrADxrIjBhl^6C- zokMMxR{;>^^w!mxjbB(Tg=2r1Pp1z67ALl0R;JU?H_dY4%m~4s(fW!Ou_kf(s z)PQA1HvbCYOEDecS{g_3KHj^8ySZ?tC7z^XH^;?zd4Tn%SM+>DiIj<%t{9F*epx`2bmx7uc7!M{G~zA~Lx?A8Uwg zojFHPfBe(&>?0SqA*I2%gXc;zii^3XzQU0^J)ru_o4+u6wAvm#Hd4@fCPVXK41##% z8!jI3yM>s$01u=RRz7Z>>ZYqje~w7eFE6Q?U(WY`s_AA$O(04066reE_AJtWZP{Mb zP5Fy)0^fate*W%L68ZFHKtk8!QF4Sf*YErZWNiu!5M>=mZu(}(Ln&pks-ly zv7# z-x!X*Jr`}@Js=glx}v>lp*lHU!NFhC&mXJH-+wINwQJ{VtC@l8XEg9~TK`#)pZc7K z3$TeUP63pAVCgwO?zr94=$O3{GLRqwiN_~?5xI?^9Q5~eEImsV{nSkmijXghe;gPUMW1ayl>-1RJ^mdt_uLU z*YNAO(fb)f5OPA2$1J^n%}~m6x4JG z6`Fpr&HsD3=tOlLetY?iLp9ut1NdWW&+F4FGn-@EHyq2p3qjkvs;%uZ1<4<;V2k*k ziHJmyEp0On6GSDuZff3xG!nOs$o-k>vmb}>Z|JV~Mq#1;bTM;OSQ-!|CB4eI2&vwCF`(XErFr`AG!W-pw)$;)Mje=5@=%2B| zV1M_W>byUhbo=6ftZB*>Nr~{UFH2lAFe*2Uj0-JxqkxgYhffZ1*cy_zATbA)NYFTq zP&0o~CK~RCvH=J+RPA^@b~GfljSf8MM0eQGc9sBNr!qsRDr0 zYDlH89BObrA{d?M2Ut*B5(p;+IE5YLg|CF(`VS2>;{iMN$FjAa+mO4n;i@51^Wq&I| zJ#xEwW`=*SM$qS7V{yuga*TSZw4MQ0ZrMAz@q$(4hcslYdz1io-ZkHt`PaxbpNKI- z2KcU#i9J&0nJ>@1M<2)+x&Btu*2g=ZX{Ei;l~_0>z*ox{@U+nvPUR}wurG^cmbmbM zCn}^hGofYRRYWUI*QZSP&8>8n`}Z<476+q%`wdT0n}+l=Gfgwls3H{@uT>JCZ-k0~ zL#$1yZ{8(=Vzn`V`pOsZMde$`r`DbGKH;Dqht1bU?tY1d%F;Ra=Gs1dj-UiYy6;K` zZkv$B0+QlXz(xKLJ#A(I&sMabBS=b3LvYc3VKcR z|7!u5B}9&Y|HzyiU?FEOc&y?t4p;7#;`~o8*OwghNrUZq785|^v-CeN;-3ULucNUC z0i|!op9>uVcH#q@_u4_+qH9q%B}fWq&zsC=e+E@TSnPfxaI9(~=U&3pv8buVgeX<| za|v+=N0^f>(QEOizPOZPVb$LEAN$4DqU{oG>yV%YDDGKl;+xtIE3AivURTjS?PRhpqWMqFnT0yp0$z(4X{E2GIbY zkXe4Wuv?e-W0Cd?9d02q4`Xzkd3NH&te4t}HVhg6({h9#Jt7Eu6Cgte<$XN&5`aJ)Fdt zXz+@uOE#haiax&9BM_Twf4 zooY_#LIObhiQell_kmInyyvzri<#hKb&7opc3gLicm@i%eNcKBh&OdflRi)!l8#3*yW~sY-X+C)W(LZs*aNKah6{?)+{7hNk-Vj%kOIH7E=uq;s zoXwv@Jv2NGOqN_E_ws6#k1!(;8<&|Gz~0aW1u;dwqW*Q7)km{|uVG2byjSlVE8NL$ z3l`ZgWqf3%_-Ae|1JIh7i^!biOSNt6de|)Zg5&@8?|})P32TC!5|eb`*DJm||4-vF zQ&#<#YL%zohA12Y`TMZt$hn(B5BOf5zK8+o@H}U)P8WMrpNMt8d`3NHU}U9&k$ILw z!xj1A!5Srg#}=M|z4&C4o>`>P^=~C#E_RTTvR&1_eX3}dze>I9^+Wbxi^r6klpK(s zKT8^z{Hl*8{5RmHo2_IHS>q}22D!$o6dng3m?J9-neerq&A)|R=S_h8rK0lWTZHKw z9jIBAo|N9+#ujT;>8qhO?GYECH=-E$mjdAJ?XdpE+;X+dtN#7q_b;W5hNN6h2Ni5uuhsX4=`2Nz`pne3?hT;w=C<3;@>iQ>{seA}(TN>hS*0-6j^vklRP0W0xQ%sp7-cOLu zNj-#%fjV%y`!2XofW|06N{Oue7(yuKOmmI> z`FboDtXQg6n2s|a;Z)w=kj>=YxZWHb2#A&@xWB#KKR$u)C4wy4v}>3FL9=BKlvrYp z!<<#m>R!wFJRMPQvHt<5yQcUWIGDh@z6qKr`ORP@PNGTxXz#S8u4RN%KZDbpQTgw- zh|7`ZkN*?$dJ@qf31NZ$RyH0-0Eq3DfAV9f;7b1Q+M#+>5JgwQD?QEd=OJ=XM*%Tb z`+-^cwgQV*YF7@|_|7IF(ib-pgPbPojC??MZcm-iPxQ%3xhwp|Mw zGhT7eENwb&!T%*1Yh_x+%==_P%r2c2Y0FMS`s|IA!cE(j>BvuzX+3`)Mgty0n&xtT zvzygT3IzFFu(V!C3+W=3{Z6&SXLA3;SbdFhvqDZ!ue1VwwY_ek&&Leh=}XPwa_Z&Nk!K?-(Tu*t2Spp`GLDBBNkBGrKuGVf(hK)4 zHo>c25?ZS=XTbI}DCVOik5zy3Sff(rE88OVoqBTjeFkXyFA%+{MrC?tQS!JRqDLFx z6UXmnEgYWSUc;F&PoSY-31d}_DyySDaD~3vQc3euvSi#%v$gUW)|Ht878#{UUbWd-t&Fvv@0xnv6N^RA@d=Wdl zC~!u6t8_sB+9cju{2MNWd$6gr9Js$Dp;gHl-(0|2A%yl+NVUg3mzme@hCmtki51Y6|B8Axhs_m~8Dai6iWg14b?{$EKZc4*8s7hLH)rbKTr zXBM^zZd#joZxu5P%zAjl+njy0CkG7)fQfRcF-Xuw#t_=gW@(jCIz6bjlwG~mO_De9 zY7<)MJ~332sjICu_ftMFj*(E_L2FE2&f5?hU|a29+isGyN(AxiQGb_~5-1KRw-@Q8 zPfA0-hC%GCU~$RbgZ7Rsr_(4OX=#v^!gRfGhKP*swUHPbz2#JIU@+Bf^oOE^#>#U4 z6aHp=14{Zkj0$q6wy@=;MS`nncWRb$uaPFtON{4&fhZZ-RDCvzd;uFB_oJy}*#V_~ zINO<|_om9Shcm+BzUg0_fVaY*7N)uD)1YFkpc=;+RWuhnK6v4UP>doz^bxVUvi>nM zhmY7)I)7Ya2r+{8a3O5k4=h+Lz&c)0vCXga7R6suPbCEbv&3oPqcCjH zb1@?W%ML6L&&B2+P4J?(SlV@@#FEjxY4$4d03^a+ovHJG1RA>2J`tn9gOFCP%Wao2 zWzZ`c4q+m1Fepv{!aA8AQd1+D(2LTHLHX`m@^QvivP;K2l<{B=QjB&8FVC>&`P&$S6 zS&Rfd5s2YesnFMV?{k$2m0`0OKNf*!eTR13&T_t=y9xUwl7<@PD1n>j|6Ed=AWQeKa)30Gas{PUCNcBJ;2Xp%-MeaLe%P ze@8a+-;piI`-6wC>Ng_7R8Soyq(4ue%l-usUVn!|;RKV7xX0O1$lsDS^q#Q^x}>Yt z;*xq+q-pMM-zQ9>4h;}<4Hd7+1&$}mQ9rhZ6VB?ylqz(1W)9 zRy6TkBPkpdBx5{S8(NaASS={%9s4%xdFXL0U$L#db!kK;37|*fmRklQD4}WpIl&<2 zQfv0eh8l;YTE}%J^h2E@<^#9A6FqAmTAfU8IBpb9)Z8PkG#VNOvFXn`(iYZ6*jEw= z{!LrQPJoGdDd#U5Zow8`ydh|GbRrp)WK#E&U`U@bo$Fnh(ZuaFV$dKQvt4AykLV>& zOw9O*?EJ_0+P~)fMKm~%Zc4n|ehy>X*#3~jewdZ>`@A#Z@_x>f@yDBebA?jQ> znMs%y5A(%}gr4FfSkBx{XksE$1)XN<eIY6^0y3`N6u6jNj-p5N|e>>SAJ8|xI=>vZ-4RN6o8Uf=F zzkmb7>Ae)i6$YZvDCs}|mDZf6sI0}cHOwCq#@LP)Nc~d%SSl3NvzhuDRYo$N+=c#? zfO4|KNw|bO1M`1lB~Sk|rOxodYY@29l>B^pv3ECjpMq3iOK>b!pKYJZW2R% zX{^mHb%Ms*aPU~WR?%X{S+?`jQPIm1mc$ec_HzS%;;BAL{a;JVT*f;(9J{C)G3!5s zDJ9RShZJM)COiaKCyhqQsi3NCxz=2>dt>=TuvH&FY!-2-OaO=AzgYFdX5N@wM)zrq zww87r4)4ykwLXJ%!$<0mrgEtCtd=OlcsZW3DU-08HAlIp=RYStt@uEjW}vHinV!Ei<6xi%3V1V|=4jvZLf7-uZtYif3Z~{~5s4A5JonwJ3YF%vvBH=wmJ09PC2& ziOOtDCUUwu!QR<&&vN}Qe755Q?T({n(@-|aGltH(N{O_xQ%#jGFBnrvMEkiujF>nS zB^xzcErVDeuj;11+n?@{GKa=UBC_0F8IstJjjO=A}<%GEhH5eNTzi*=ryc$~xXYeNK9)L6vR zPNm+?5o1Y($w&t$oDo$j;LTvAx9CtqDUDBi4<3+V0%+(mFTM-MIcaZ=yK+jp9%ysPkbX&|k$!131qs3ywU>B`{|oik_or$5w)e&5 z|A4mrd`!;mT^oC0 z;9g16?mn21mxA0YpdhC>ACZ1jFlSPM(yNlCvu)?f;IfiGP*aYw8?0y&XNEUOUVCTa zgai?M32BN^5t0bnECwj^vomlr{=iN5(J-PnEt%dsoy3bp>U;c8t3CT03ZM|8<5srZ^t(n^0(NmomhxQhn-iH>U7gwx3x)tA|hjyaaKu*Cw>urSy&rPA`m zqfL$RGcG%F_qO;$7V6`{21PW+eRVhDv;wruj0hk0$0lKE@uqeY{9f&-)!|WE`2@;L zh?4A5RrW(AvY}QL!t$&)|9W#ktb1MOG{ewe7tFeTKHX*AZ;+PWc*gaIQQqimj9Ji9 zGtqN%Q~6n>%c0`of4$;@(|?I}^*t6##@2rY;7ug=^`lf!ctr^Skf4Yk#-WNpu7YcH z7DShGS>{UV3dz7l0PlU7nWo2^k_Rx${RJQo*aWRpMkEDr1Ew<~>Sz|TY`jS?uz}Mb zVKuq6%mgEq_JJ3Js3u$Z;sxO7qQrG&Q=|+JZ(s45r!S zBQeJk%;=WL`RN-PiodP^T0*9CNPT6f)vqZ z@rnh`qAauud;GN`({_CkwD!ckmhfQK0&}JLOPq2E8Qmi?HoqGuwKKI$1ilNU0!LW< z+oeMWcoA0g)VK8>8M=hw1liMaQ?e8%TWjJ2l_L0bEkpg}^E~VHsc>PX?oFz+S7wIz zWJJxm1dEzG-t*me=`A$=>;6G3-RgJMKHI^%uPw(jrhwtw24*L?TO7i;Fj z+s*VA`+BpW0rlP<9^268R$+vwQK4W({?>vI1o_@lw9xz{T4LeVz9{@p>J)3CRp;`5 zRm%V|_|p(V!1TgsWYn*{_jkGRnGhM}SPUSG98!Bi zm2%c+9Hd|IP7L01k5GN(hZ~DRrZ$pyY9E)m@nctS5sOfpxrnz9RyEC-2j0=m=`&%2e&#-SS=BNQ z<2jo%M0U*U$p3)#6X2zZs-9fGmcp}))Dyr|rU^)ckw)+bEjo=*D0q>fl zT@9N+B$iFF_=90f+j>4m4jdT&RcQ*I1SlUvjeEd0nfSsV8-MM4@|k`ZyZcUd+UD+{ zC7juVY#u?Rq@5O74#8io4dm~$$`=^pk`&+s&OOfdzfWBKoT(8skHgElea5J1AC{=* z^ig)vnuMYF%)>9MSp1bbAftD!O)M1Jl+n@p{+GbZCW|jue~Z7$Jv5n)@)94NEq?NS zo<0lOJJ|XgP26h7La|%nzT*YALgKmVx{vaVP1k>}{gQ{S2}N=d%!D#c^0uM*2YV#i zV>pz%+F>%a6ST|`W<0NuVSnFEVno*A%pr7t`Pp2t^}E07V2JIQnBk8 z)M@%#Y8G^Rq!|9(Nbe)WebAq{(>-1@8gwpm`^|lNWqz*F!4(eCRjt#P6?H=IuKS<- z>gy{S6HGh*X$8gEoS>DK$76NHvf3?3>XM^J{)Unu5R!|O+0TIl6v$iAs4pqKB-yP* zI;g?iHs*hT$?LIw*HHFPK)dyIpSi55IfhePg$Rf=A6zhNt={Yv>VW?;&=}7`4dCG< zAlX#ZPU(4-5%Sa|zVRyMP7ZYu0u3TAE&cV+RDIF~75c^TIfBxjGTGkUe>q=2tT)ro z-hZ{8)an`3gd%s34IzvpPHauVn(`mwk8#bNJ`$jI|`fc|Nc0?MLdj*MhOY zt!t_caEM*FxVu@F4EqG1mz3`cPw4*MBV{M~^(U;Oiy4cy^kJgg5H%X8^MdRjkK|ae zpbQ{|@$GU&(HUjM9AyuKK4!Hj85T{rk>dlbLm>k^lR5EYdQ`t69*^-qn0Ysici=Co zQ&tBd9+fWB*Ao#Do%Vv)#RcJ&6#-VLvWfC?s`8n6Xb|2JHBL?Ier7Q!C_-_;+taZG zWR;L#Co+Jr*BPZuoXNR4p;siw!9tF% z87oXPeWV=HE`~$W@JkN;+$uKagURSOhaCq}r)r>>8`Y|?>E)7YKnaBwJ_KA9;=s(p z1EDyG?TVtUL8~B*0)zl!96rZlVW7*&Ble0CF{}T=K^m@T+(pL4&zeWW^M_4Zjk9NU zC%}I)971)Ay;_Rz84zX%^LbjBF~N=Emx=t1;Z;$SL=_sTtdd2d)W@~cDB#e}`N%(W zqFg}&O5Hp(qeo9LPr=Q;rM@(QDm4U(D zi{}lJVL$AKa*EbQAB%V@f1@b0SrW6QCEwF)2SAAN0W<<9ht;7NF7T8|_A;w;OLY&Q zkaeH~y0rH!mBoCZSzcc?T{AV>wuAs9mSR8lZ8a;y7M4p>S|+LC*Q+@mFQ)+-evcM2 z6bzfdCV_Ix`L^M2xTbB+!UZAa86^yn5Y;l7Uqk|+G{uS=$3`D#hWfo5G~~w7*pxjC z0$>TA`|s~YDYd!E&gl!1Ht)s0gEF`FG^^U1qkqzVNgP=pc`%~k`jSF?C zbeh4Cmee_P^#$v=$IlnMcr--yp+E$Rk%Zn)WV_R0Gv_fSrw&C@nmv`pUKa{@9dGbu z?T=b(1u`bdor2B6BqFcL02C?IBXh114WnxdbG-}E&Oa4Mrhe|LznBjEp% zj<-IhE+R59w-g5!`%~}t9-8pRg%XD7uvj%tXOgA4TrbOP(WCEZ2MipR#2)5nP!ViL z4~wJ1|VIilzCP>nXz8P(f<@=m6!MgYCnGQg^ z8fV|d_4A!z&Kkg?qH)@|F6H+6yz=vBbq=Q)O-;oj&)M)iHxe7X1}_$l4<7D%e=Moi zj`4y_RcF50wkSSO>TU;u)6~cN)NR)rB+9wBPa-Xq1bY6SOY?pOYiLNzrb)?hq{6vZ zUNAxgSS3u<#!+%4;Dj`6i>+u{BMJ!ME5x7XI5e7{ShU`1&KhFelwo%~#b7H(GWM25 zUB7FR+sp|?=0&LuIz7F}QR`Q=VAcI+XC;fK&y^UOSQnthl zP+Teh#7y+OY418Gs@w;MQ!uIj6{%(gpFj^XG6ocvm;7~D9o`Mm>&h#}4Ex-0ZkwFl zs5oAIeV557doWAP0I30ju!(@casbO(I(~GoIO2C*GNB}2h+?G3%vN&YWAj&YX78sC z!uY1!_hyI$pceMDIvs+R;+V8%P+J?QYkvC>v^sNNCkO<+*9s8AN}iETufBH&{@yO% zzuOb_o8*$}#4l`b+%K>DT%CG)>S<`K)-O3-FI^Y;Elcx9X!q;O{Oaw=^Sz27CJ?)*cWxM8pYZ02-D z8l*unT3#tjPTHut>u{~mJ~;G4k)Jw+BEM%e2RsY%H7}E4cisErSDhWT)I4%$pI)b`v|?ugKc?uD-Rr0qFKL?#@N}uYEFL+9x?9%;mZ*-dx&{I zXCnfU3;yXkil7+DI(vqKhwvBP1-sL<-}tp{mOgt-4|l@}O3pyOseCT}u)rQ87KaQ< zHbxx$&Fb@5b}8KQc!s!Yje)6EB5}3TjJ6@lV0__E=|(fep-b&~s+L`k(+qk`>44&5 z`Jk$u8-H>ufNfGo!9+?Yb(S=`)=%bpoIF===;9;zGE__dn;hyQWC z*82I~){=|lAKt0qsH$WqYw32J2}TnlZQ1*CP&;)ImBI}c|7#Y}e|s^Wy$ydVQ^!r% zT*w7sV;Ry{cvAjk_{=|Y@2UHOR+t_C5_*JvK;f)sxi4zxVOcDklUkp6yvV(6D0^7E zN?MbgSjrrNO*cSH5QW#-grSd-+vDmIs1Q*nm;0;^MM;tQe{XDf*QA>!2hdc~l4sEJ z+v@|&HULq<=r1({wJVOHo(k;YNgbb*Aa=&0=>{$LLv3u{eURdimyIpJpprzKLyH3- zAAB0=_`?RDUw9Ktq$r4oOtPn*5}9sm=Lv(Ds)Y0B4w(@^|M1@+{~cRW7in$Y z^kidpCHeN2_I2hqDw*k*DWOXfBN!E#*R;J_L4?c99K=ENHsO7q5w(n8^)x9AXToqS zYA<<+qxzNf#KT&5(zHqZXDFdMa{9W9Fl)hRJ$Myn;g^=7C}I92QX{)qs=nI%NZ;D_1rpYcJ0MtV=in~v;4Y&)1ujAM8|Lb3NSv@T_Iw$SrLX?7TC{b%R*K;1+; zy#kw6Sy8eT9(wSrm21E^RC>`SOGge@$=)#1E$f>bSp1xe-Q3?>ffdroaohvMB{<2L z5Toab-NoWFzo4cNdQ?yUl%BVXT}(f?Ovx_K`o)e1XAUl}eE=B|OVD>u)l$)`#Y$6p zl!#5X!D}e4u6gm<0-_)^%KZ^WbOzDD5rfl|hS=<=Z5(oOi2y0&8?(N;zuBiPw3D`p zJ_3s*N|CP_M-raIOQ`!uhq&&l3`EMn6Yy`M%2%6xYFN6uci*^$ioDB){Ry`! zl0tLIpmUYDi!|YAVyat>aUrM5aXm$Kd*dPqo`hZv3k_E_e3|4I2S>?YUw5J&lBQ%@ zeJFIu#p@L}6dML>Bv-(H45X!F)9ML#r>UK3$mOEpq@w-kyCVVIglN`R1rrjel{8h} zqE%zUG$5oTvjVl!bW@Oz_iNZY{1-Nx1};RWLVh?%o(%*h#}v8uWo7nM&jxJdc5}-F zPs5QhcS)bC$HqgY<)pr8k`hi~*=w*=!XzU9u{9-A z$V}_L{9_zFbK5i;n zMC61g)e{zZ>3BE8MU8LSPqC1=?d3GWVGJnJ;Mn~9d=c1za97&(pxZ}0?&pevHdGF{ zgv?Y5#GfDv^pTZ9`7Fa1PmLvVjCl|d;HG-{>$r(_B_&C zmRK%1@1DOFIp5T+Qbe~&i9mh%+VADr*nzLvQdEZ87`gVI6;Z}4xrPf33NiE&V+I&`y- zh5`x}{{$3IKRyE%!x~W&In0yH#~O$u6Wv1x@@STB2;$|J)OW>$V_W8L$FyGh7ky^5 z!4&-cIPCA(?dwjtrh$`8Snar6PPYlQ)>ofJ>)xN%oPm9-|IPXaF0A-Zo)LW}a$M*P zd%@z^{w8uj;Wb0j!I6t|C4LZfX$%*N|P>7FF81G1gj(%CL zu#04vwps!=EFLmfR9%eC-1^!!b4NomJ(ZNW^CFRi51wGB;+r^B(}BOZ9GafpGPwIG zUQY?(0M@aWXB+-41@7Vy4l*igL}_Uhz~ObTGfcmdf`+L8|J31qB9f~tWk$MEZUYuH#*My8XcS9VWUngKJ`#zl+bPDHzn%FYHPOo`mu&> zkK-8ePB>l=cEATHYA(q(SF^{h6trj3?M~wQI=L-O-t|+FShZ`gpjdO_&Bx z&$WHX=UvdS{ETI-%u~f5DTLag&xp)|-krRtSgA^~{gdG=dKRT1>H0+RKuJX51SP~I zj8d7Ye+v^yzt5Xk{#V8FD_m?tJxtThqc<+F&HT`JqhQ}xSeHSL)PL{x=}nlOJ)Q`H zD|2Q;UaxQCnc`DEI8=nnKV{8cSISWM$D^$rK1DuTX4$ z3=yoRH-4U>nf|Jtj}`g%C#4PwS3YwPxht$=P~uk$W9nI%ule zqnhM3eX494Nlftv67hpE!B~|SL!SMsm@yi^jEv+_E9l0ze2cN@+sP0r<1c+=Yu>B7 z@fswz;7yM;F3|;jXG3Deg-hbTmmVsr5osu|UKkM{QHrqf)(D5pCCOwCV7bp|M&#wu zkmu0|L_(k-st)E&9 z7-^&wzi%ipn=YZNtT$&B%w2&`5%hUC3H$rxjO=BZciWOofa~RtE2#2l>s8{ATmPdY zIWy2yfV2+FtO?!RiaFNU%fCkT@8@caSsV==wtqNS{>f11=2-tJ`g{S%NKX`O}x zKuV7cce8fp$E8R}Cr^9>P$XG);%32)2ncxuk9){i%DDw3k-sP{uq|kQ%L@q3cjKv2 zhOHB3ywojwCCsKifJ3NXCYh3&?b+(^T2*E4sX?s>dye}MgMKMDLI`n@{t`OW>^|7U zL`EUXrEggl3Z|hx0@>9*wb2^K5nf`#> zM=K#6mwR=NB;*_L$byB&=wQ?HO8o(!*jr65dgvG_F_m64H5u8Wa*f1U8S?706(>97T0N=d{&)jRi-IM5;ZsjW3h84rBW$r0 zjf73lGWE0+gmiU_1PY|K;BucfWJ*WLwWh%t?W&?Plod;EwD~f7@+w#r=V-sK(L6^z_v_3Dafo zt&6y_N74(oE%!S=nJhW7utoiTF3j?$f1AJmaXF!7ep&zB z^iKIc8=h~Mc_OT!JyJU~f1Sv5|AkX#$;~OL@OR|>nTz@5QR>3m4LiIX$@YA2TtL$g zCSR~?f(0CWiedDaGjBWYzaL-);mJ_LV&A&2KK0RBcU!~-!V*5LNh`r}t7PmLmsS`H zN6%aO>xy4DRvqW$)9rr>DMj1X%2QbA>q6a@)_lA<&VsAkkn@wl@_$M0_xthhBIMgS z>cr12RTds?s_S^j(KbG%xk}a-e77<Ff4Ej2?XwYsnbQHGY>ajK{M{WET zAfbCv({7t_sU%GrH5keFkQyUdhw(<0781~vQ^I4Sg_i}R_fw{1=mwt2-akqW?4Fc9 zR^#)(F{uvdrMp{==4&}QG=I0X5_r-ZaMR8piq-z1d9#G^u$0s6sFYLs`cl?o{<#|8 zO`eN8W6LYt8atU|llgb!Cb?JNO7fpE+Z>Is-L#+Iw|A2TY^F->CgxsEnF z0rphyb!A;SE!+(pY&|)o&mP8xv_j>Rqf~j}Sa|afk4(Oe3HB!O;4THMZu9HA$iYO1D$7!rhhWI21LU#`4U+Lj!K zV~d!rr{Kr`ylfS2P#zYq+LwbCqo-P4ig*}QVJCV@X7b+L=%s-^X|0lax8Aq2I`cZ!$Q$IgRYc^c>lQgteV!ER^bde8L7A4friM%8(9k zyM8ixZ4S@*i*hbUO7E)KWnz2@5p;B!cK@U&)nCR=oH4?JT!ce{-=b@0R!J-*($D63a37?vmz3sRGIOGT{ROYj&ux(E68S z&ha&7W{NUCi*l~gAEI$lijFtbex*+3G8U(iJ@h^4yV#%AE7H@bKPc++7UKei&RE31 zQ+{gJ%u|WM)x82n{Qe340t4J?U3RtwoMpe)r+3c-xXthFD(@r2-%QjuuASefYLswZ zFV>G?tWe)=FnO$U!x{QL!8w1j=*47$spP!=asc0kh0W#OY|H%CFy7hO5r|mff2~nZ zH!7Efz{mXWo~gV(nBJaO%;v$@XrC$~dd=-myD_!^f6m?as*?BG19=dOY0ZtfSuI~% z39$mGu01+p@HrI=n?9Z4SO;esYpIGN*nZuI7mABb)<62>Cx1sl>lFc4R2u5y^7{G0 zRi&Z1|A6fMs4SC|&w<;A>qfG`3syL}0v3)iD}Pytpvjcff$aT;WbWN+Nz4A+e)l+G zAF~nhp>A&D<=6gz&Eia#>}tk`MJ^e|tGf}K$}-zJvd2ViKYY(~vW|CN1YS}Zwyt>x zT-QA?b?wNUzv!*L99Rgr(#?I-fxFSR_1?v2z#(vlBKM-iyLIK?hl{UjL*g5=-fhQx zfk(1;=$IELofwjz|HAdcePZejo1pAQ!@^zugFAEaRPW|vvky1F)vEpa^<7$*f8QT4 zy*T<-5>P<%86$vj@{;do8&l%j1&UKXbiP7z!rY&W1FKId4 zL3E7zkSyVNL*o3-R-dB-BAn=(}c|$yE_I=rK1ccewaYqJ9iuS6X=*z`(Ki-|Lrz&F!1G^nmy}vO! zeQvQwAP*&0*SqbFB2OzLWYvRG2p|pLgJEK6Wx8LpsEgqnfo?x`Y@cnsl3rYzx+VCk zrfK}~E=aX?Wkocr-O0~qCT`tlCtr<`&fC=1@dK$hhm(=tlL9wCY|Ki_ z3RFzIU11!O_V{y`Bqk@7?uo|QVe#tc;Nk1&*|>`EmAWPS;qdjk6l31B=E48k@|BnA zP2Jnus>=?#+gPf2hL3x0VR+Pvo7tnU29+p9bDHp{ym#k#m~@YlzGJI^fc7iR0iK?* z4N|}MLIu>ZbBa}>u{8LlfF%pOGF)-&FvA>yijP=g7NedW183Lpix|dTMXz=qp9M_T z=A4{)-xPr^ivN!09)8=9Ilt5xy%{%eS$R7YIFB#uqZq5Bfo@lKs1%U1;TB%-+Tl$N zSSu1wT`MYrOo=5Y2A(~*%Qe6MXc({=v2(wKd2Qdiv*5c%<;?=AnfT`p5x-pwE9-fw9w z934cOV*!u6_ZMn?4M&B{kJ!olw{>&Bd=qL}3f=cTDQG>48=AfRo!YiLwQ!b2=D+FE zdVk$^|CQ;kL$rB`N~wLq#`jU5gus@nXa-}#RiAr`Lizj65Q!!mWx&)9*zAxx$G94Ei5*8Jzczy z{-RZv?A2&p-|)iTO+7Rn1k*8ErMMjGw{U!;a<5eW-?>@ljE5vAvX`gExd%Vjx|d(_K4i?~9L6Bxna#&!&F5%^C_(>I}te+n~Ed~ z`6`U99Qi~%NwsYiWA)jksB{oB6wfx-zqBl!C6VlrMy1gyg?=SfLcI0>iO{dAA-$lz zo!axJ>f51W3*b@3&VIl?3E!<2->Lhbq}-cTY<$w2+l4^K$-y~HxjntVU^0B1Z{g}% zaJiMs@o(%oRlvJPMQLyrO!SCK@66<$cblIqa!TDfqcP#_#Q(rg{H=q5&(DZF{Gw*YCAT)VN68-)A>@^-@~OnZf|mfXn?D zmB**LqH`yo@XdTix*sffj1Dl}wBvKA^}D?7*T3I-$wxIVx%b0;ALD6tx@*^R-nirP zV=2(gd+rp7*kru?+2ys*0Z?3T()v!iyqcZrWr&30Wxk znu*mrCdnAaw$2}xYgd#yIJcei$SL84hQr<=Ou$@U3G{IF)|)lEr|9 z{gFKywDOId1!^~Mq&6{uw7cetxPWL(1Z@DONTJX-3=Qy%zUuWU3c6=Hy z&j5Wg8O`b}Di`~-^;#yk>9I!xndStn9;6`0!(I`xK^{l;dp1kv2f4M7$1UOB6TJj` zd#n+scoZCIulYrX>SH;%f*goBe~svK=6OYhHjdXm_o$ghwsjJ%IEf}+?svT_OVcLs zij;1NKf_I}dG6h1#mx}a?w_rriw)}v5`94ITqWCsjQQH@bwNRy*5C%t;o7yLm+*II zwQZ-JM10MMJ%Oj@mjXR5SL-hCHYDfu?Y$}$5qK6z{iP+cz^)RPyKnDsSZd!(i?6%x zFnHf}?B93n1n!@=-s1-@ZUio+-Yp*W56N8b#yoDkC}6rp_0#hI^`2~P9Re&|(>%ge z!h{l4#9eWMn#dT5zu*E?Ymk(b6jDYhRM_Bk>+RVLXwm#`&HTkh=Yzm=Z|Qp~zQC#X zq5<1f)qdlVvNuqj!{Rso|6@B>Ymb?6U~*oD^BfepB&lOhaXo~C>Y2ERZENTD)3lYOAk>z6L5opOS7ux`C4r9!WZrR?V9 zv9-A4xzsqmd$Ekx)cW+CVOy*3Xg}x5^VfM*duHa%Mal3|dQ6NepuCS+rRj3Yu{B(}~rgC^o%jxS1qfEqBVOA{P?Leo4qDO%SBK zeKS~NyY{w6PtA>H$=1e}Y161u-=+o$MSiqj%<)juz_#@tMAKWL*(H@cnwiwOE?bS3 z0n$zbVL|xV&sYr#9q(-j3!F8d^`V9n*iir*2f_+SL{HvijjX;n>It3^b*$Z+uH87q z+;9{*2Zfo=jRX+T=70CzzR#d{(m%i3|KkFfVhUND0>3U`tw4NuLo^h)jOhiNH>5?o zaEcMct7dR_Z(69v^x4Y&!F^Hk6>@LddvX9%%RE2$V!dl6p#apUxx_vM><*;8E*sBv zHJ1xL6l(Udb8&Xwo348lw%ApG3HYFRy;JFZjBK_;HE!tsb?g4+-2Ih_`LXbaOwE)l zOv*Vhu)crr^pVbMq^=rO<(;0W#!4lt?b%I!)qY0(zsAAdPC&%x-@xqG6eI@gq1#~a zVviwV5L^e0AIMY!APn3yP*n7i)Uv~o#1cfyFpRmU-N8DZJEnjBt-_bTaT-cJZoc77 zc4rcU#b?f=z_TeO%Xoa}@{;_HKcKcc@?HEqkg>}5O!Ofhw4}Ry@>ENQ5}+QP?DJzQ zFiVewtIX7`iB(8Q`LbwC5d1e|;(HXzmKr{3uc>qhV}~nran$GA<8g7|*Hv(_bpPeg zV?O^{_C?X{Ays|zgtZV%nyErBd4GW6_Ms&CDo>y zl*%@r`3bx1N&pX)2~XVX#ip+LnEwSxh2D<`G*+a8ra@}=XeA==EM^_eP zY7Xxon3Mf@6FG5rdtD#1@0z@px`<+vIEkfq(?rdC$^}7KgPNW_ELtt+2wOD6?**^U z8bWQcu&GYLP#27FNf1eyA0j7V!^=_jC03dmJ}1UH9x7fka?jKp7_wO=gXjv5g8lrW zZb+3u{$uJe)=uUc6CN9xE-n5;;t#UVKSl0M``%$@gWz77voChpp41rUY8RjGG{)03 z<@@jbe1SVA)mMx7E@|H?L8}2(+?du%QO5PQPV3zt3cp47j?0bv>(u7e>V?}q^ft9g^Aw-Xq|@RU9{x^L%d(SUmpSZkg8chq~pJ z*H02rshcITw$(&6tVm3QD%mV~(XwzoR~>L#;kEa-?pw3pjMI14KikuJt@{9T1T2!6 zsjTRm#!eO+$DTVG7;X6^T}Wf4(?KFT0s?t0VP=n9ajukqk!-B`uklhY;iIv`OAfYZ z77-1K&h{WMOBhU%cytR1&WqMdFn+}Rj+hi^e0s>x*Jnen;8BVX2x;tmih}CP0t%oY zR6<|79%eUHjOhv4^EreCp6U=KF-V}Ae`=e;34>XgG+ER&RvN{NgkE!MR@i4mpsY|i zTdSQqh3lsFA0hd{Ye?!gRZ}E|4!DDu+MyOH9qgvk=W)hEA(*tVH9U73u5IRLav;z4 z0b4cjU4=?zdYZL5UIf^GZ@Rb=j{0P!(i&rdPzjEAn;+0s&yS44E3r!tio(pZWWy5U zL(6@oU-wfk@&lVLcH&r@WV}<}$R>>V)^;1p&0cw0PhL*81a4<}wAoPhxHEbL&h83~ zA9~by3D<`A9LZj^IVxb+eDC;*z0of;qtq)R-RwPgU2q_`a{vJ8uR*}iEG_WGdgjto>Y_J_eyoE8N#isW&bt6WFujbZ>w@!Mr^4viF#Ry_`M&Id zH|9DUdN)yfck$sK2Fzddh)U2cpUt>j*UH|GF8KX(DTdKgFe-B6i3VP!w zoNt>4D8*X7e7VMeX@1*|$_-im<|#(LIM{!Dy_I_%UOOa%Pq1*a{;1k_$hWw^-=%Gx zuloPdbe`dC{_p$0#a1P;YYR1N@2$kB(OR+9tXZ{d6|rNF+NBgRV%6Szebla1r6_7s zYVUu(|Ks>Q%!52gj{APUuj@Kr*Lk-88+-WE)xP=aexJXp^ZLNG0Qk+a?;y}JZxZ-;(yyd1o;=KWT7=sgPtqO+ev-8PD3+2O*N(lH*K@c>`146O zEu|p+t*w<%H#=szx6VwxGA$D?BU8FV|0bu;A-q7anGk}C85+s=8Ao~yOMztke!Qlj zA*6=!T|7q6OZ386+!z8Jmoq{K(D`Z@5K)~MSt_+nNGfPc5;#s`VydkH#u`eb1Cin4 zk~4#fBr1_ISu;}ILCM+!;QT70!^hD^MT0*!btS5!n!PlcqHfxpmP`G{o|W4P2MbM6 z4^6}oyjtu{d`)L@CL7x&XXiI`&wDr3_A1{n24H$^v2xA7=Wm3T-3U4v53Vw?>nxXF ztF>K8SG~{<^L+IW>sR@a_g`blV^iaU5*JQ$+1__^mM^p~{yBxqpcbN6MmqLRXG{mI zYcg5h#NId9ajfT-H1MA&`5nB>T8ZzYZBbpMg^*iS>Us>p-suq0=a7o{E^Cmm#OTXU zKv*IWombU%cIdug{z0|=;JZ0_5z=UE0e~Y=*t2W^xc%^=ABe=bujs2NxE~~t*75siCK5Dkwz=LSETO$=VK?dNKE=fh1QZR}AIHYxX>{X$ zPwuh^3b4)4X6mVbY=rBj(SGl5PBS4^aLRJ2aNYrA`q=i3OwMHy##4UtbeUzS8O@Az zP}IWPx;;BB;VeGh>dLB(&R1G=);TU!f0tT@H0S3HeGa5XZj9>2;)@+ISk`V7dl0Xq-qKPf{qer|L!4uvjjY?z`n-GR&V-8i-zXW! zuKC}=6pEP_4AYh>^!euqCsy}hPR(G?IyYG$|5~a!5(lIy|r9vGA$dm z`kSSle>(#tnbj+A(_QE}X%tGCbuWJXdu4GZf$pI(*QIBTuphh{j2+rx=tA%JdRmAi2(gt ztkb$MORyeAnSEXW79=CZd=N|Z>sQYS7zOZlqD4B>xX3!IO>|k=ngC*U;K#!!>h5DZ z=CsDX-~n%^MiS4X^r$mReC!*sydliSbch#0X)07+OM0yrZHQV_Bq__ z4!(`*dO%wQovF%S*Hqs(S$JaxmzWtT)ARmDmtPHk`_Bi$a>wlYiwdBSk)j@JAJCRaUZ+9}Sz@4rJ zrq0WZTCu=GXiY;4uunNYmPt(1xmf+I{FT;0sjrd7Ehq?Nk^Fl^|Mi6o_<-fK#c(!9 z{4pXLJmpTvw;4^q4X&Hn&5XuP`F8hr`DLsiDBt0XnE|F>c33P_m!y~{&5SKX+>JF> zubbE!uEqjWO^>^h-{hLw{xZ#VT>^}DBe2V(OC654mEe`=p2gPBFW~A{BBv%uGi{hAXk&BXvh4K5tMJ zbhpeC?0w8%RnU)gX{Vw{-7t`ql;wZ7VguqKXz1_=ifQ;tXnKkFIWF;V4FR#EEPBLY$m z-em5E7=8Ftb^$%%@&*?UHHo~1_D^kvL=ZjNUaX!`YC=dd5Uo8ni3s!@8x3H4#L4w<`rhuugm9F52e+lMwS8+{6#g`|JmO(iJ_pP2Ge-g?&|H>XrNgEY_);gc6 z)Pr@LlE+_?fcsHas9^m1I9Bo>S5?U4(Mat|3e&Ks01mkTHe?;C%d45SsewV? z2f0B4o_q__4IO7cd~)$^$}fE2U~>Xpz5(~Ppm0l-!o8O;a)P z-l1oSAva6MMOgjpMbiRY_&mi5y0d;9F`;Uf_KgIu+Ij<O(E`ca0OytD>4|3NzjuL&c};T-T zDtUjuRj;f3zI{J!ns+hyYSzm&AQstb5}lMJqcS$JB57||1vOuJeD4JX{%g6=OGWeX z^G<9E%a>(l1w>oN9iGIqcn}1x@M>jXoCeh^?F`1+qbZfN72$6@ybz%LkSRowVi_gX zv)sbctIb^Q%`r;1L;+uAk?eHSzQmoo;&3eMz z`~ADeCt8b4bA?6o6_K)I7=vm#(_lj^2k##Lp zYCZaoN9$?Q*2JXYl&6%6BGMmI^o&=IXK??rpCr0{BzJ|%zd9y0SrCL)^ z1~X>%POj=(ZEo}DWyOFp6DjU~Q7S!yT4L`r3YL=Gq;U^wZC@XzE>`RNS28eXQq2~4 zt03(*scEVhWLF6i%EKCLh7v~j+V%w#{o;gw$lV_&b{%?GFJfM;bNQ5qF?83l4dz|5 z@^UJnO&(|B*O!|e>++>v&dqPS)2`fNlpJ4dP`00VsY9^()aL8NQ`P&MJKGKUVkw;v z(P}P@AG4zgD&{V{OsGJW^H%<#CtCpyv-rr^nIFn7Z5#h~UNd~sE&0&eG&V5i>mr-; zW$ama{&IhEsZzyV8aT(N9cPw@1!lvFD_Kr6ptfImD?_%c-B- z670OQ_+HnES2QX~paE(vurHi&LJ9!vFUuB|mb&})G0U=_k3oIxw8$9~ZZe_A8ywgi zZ-|SyhHQ#|XF&Xq=mk$MV>VK9M>hfm<;~$`oe!^I5{#*Bzr@AFkZx9aPT9vElgy&= ziUX9+AbZl1mc{hNOhc)nNHK!1R@b}wpfZ7&AnH5&E;oD`j=KwM7?sEH;x5OGt{3SmbJJLA-XLb(yfl*}M) zSP@JQnqu-8p+u7sJXpH9@ZlwlpcJhv93|lPHOaO+ha{e7B@Zq;+F+>1DDN-fK)pdk z=0p(kjq9D&Y%Slw4GG?MyFaZQ*C8LEDB}8$A*%a2I`1-i;q7_OmW=(w{hiAqhAQIs zJuPGkJexJl6T_+|9c?{-^~dLR{pF&;%vLUHwDAY2Tw{y-^49YFV%?j!P2P5fFCEMO zS$!O~AN)EMFz+Y0jL$5$|HJcvenxO6y0eah<(1;=gA&z+p6d$lzgTM&_7Kvhn|++q zbj;<(#5~>M!lj2R0Czw5t?iv?jhE-ltoifBcn6(3Sf@xT8=VS;uC|0As;X|6z&0to zA2gt}P&2Te)16OEo;1WNr23RRZZSv;Cwb6fPSx;EH>>*irI6?kZ5*(H(o<-iAdHm$ z$77sL^(kX5z|!`==;mPnZ0(eRQYYSj{RH5&A&Spn@?2~bLo8@InV0e$rLx#{hoD{} z3uynteo}tIo%Lf;arrz9vR>bu!96SefxgKIlXr*bz8*h_n?eMzf9!KLNtVi+pt|(X z5lq`*8?*Z~y`T^06!bhYTqs(j{U*uYA())~Q^7Yy_nWN{r0IL2k729O5$@nvbFrhL zp`lAreIF-v=96wV$oH>Qv}Ednlq)4+5>{hb4bRV}syGY_)ESZ+H@jbnDl5!dRg!|S zJy(2K2F+rk<4E zk|%V4Gan25xK+Ek{BAM>7nBT5PbT;Q5-X;P1Us`9ce8AL&cymq8!gwT#TCyOY!QTU zz8Ld_J$s666vuVJAUvO)k?ymlixCj6#uL(7&hUntKDc1^v(PX$G@kl0uMA&^_YNwf z^KyXywS`63c0nw1`x{TSnXxeobSO8c%%*Bgw0ZOFXtT%btfKB5)LpsG{x4UxwM)Ly z52xxRZ|kJPw14h%;dsr02?wa+n`zK_4+nVbL-zc+nbZe^k2mf!iWuxP?9B*pjI_V4 zYiw@k5|ws&wpH>~LMvACMeQ?9UKkr_H}+Mr`8;{At&o*9AZ!EI65P!B^?hb54p0(0 zMcgu42Ws%jsBE=XzksRMNi&03rZ2}8y-27_W1jeDFw>t&1Z5W}nr^0no1;;u09c8$ z0s0+mXRmwHdwBS7KwPIOM%W#KrL9OtSz3w?v@k!yB^l44<;Yy+!~wu*3j2JOn0slh zEmut?Pywb;QDE%9w)!jg%ili_-f9;-t9u{UC12<>R1S}ZC?!9Si=6Qa6i1>3dE9Ea zB^;!MyWESxpoZ9Hb{u)5dME@dkvI7-6?bQ&>n&6~1ax z{wn0Ix;R@Sfh&S$(9Dg1UY!^Htv~0-%(q+>`;aH6d1VI?HK#E=-(A&ykkk8WfDzA? zYyH)m1V{-YmG2(*I|7>D*uyua`BpB*TIQD9_{z}w?{L6hC=W_@R9M1I1qky+nh=~Z z{*&vF?ZQ9>mmSrg=gibS#WzPolCOUnJf{d{i)2G2q17Y8-C{X?l;RX4G*G%_d&|ya zl3lKG?)NmFRVHoR(Zf}y2;$w}3(^4xn1**>bclXEFqr?bv4Sd9DAIfp3J9^D@&@r4 zx1}o`iB`!=NU>_u=zGT_h!pX8g}jTKnG{+FbkZ)G=(!h()$)moRoR^F?mqUh9@u6L zLY3j81f1H3sRSV??masQD@iS>APK(*XG#EjXAFOE)D0G5yJZWCLOa3ZYZP%xCLdSz z-Q1|Urx1PV@7A5smzgDsdh6<1;z0AdCtJj*?frrOj)p}mW?L9jWG!|y=+0th=tqC} zgRdM!$dWM=BlCHTHn_NQf4*1Ub&oEu0s*l0+kYdN=~wWDaNgDEMexH#*T1xCGZT|< zl3iP}e-~<(m(qcpX9^>2oQC-qzW}IkxjNcr*tnq(Y-YB$LY6u6Y)}pMq-G>H>neCuGI41A z$%u|}{1~M<3))|57wzlDA)|)$C)vTAvfgd6~vFpmhO1A7=%KpEpt>bE3xv^|%#IH9h zoK3sJ=rg&91EhrD_C@i#$)|k6Kz;{hs`-^e&nbQdjVOuW3>Q@0+>w z=Y-T*t42Y1A^P2g{etZQY@d13Kcm99vpjq(glI!$wx71!%DX}t@>~if6?@tV#-X4b zqm?-A0s~kYTx#J2Yr}E4Jz0++g*XQ%<(C;3LfcL-7AIS^cyzN^`II}8m*ienY9%p{!Jk24Ct}+~uti}`k zFu_H-kz3uS!BuYYaVxJ%DXW}3($NTcK={O(!Stl{^6z-`3^uRuBSEmvVq=x(MVc{k zN^pf`kT!u>t(zIO;hLoev+D#rVChQWY)5kII2I}{RBDwcaUlqiw#G{3^ZA0h4ae>- zts}8+24iPTgY`FfKYK68S`%iwiwx;e=PW1|gaJ0}T1edwuOim6Ob~PG?=E6e${1Ticof*<5RDd_ zrXf$YRu~LNzH4w^6vO!}3wgQdF_Yfns>3D_h2T!9!+xt1Q)-Gu|3Wzjv{hxCpIo8{ z*0sd5)F~s0w3{;Jv>!DgeL?y>W~T6`W?PQNaDSW=BB`y4w0Zw$EBa=hOt|!Vj}VYi zi^}?0EGP_EEE*$Ow)Vl#6qt*SeZQ2B(Be)p2ZTM+M z`~DmLfSbjYpfS<;jU4{A{o3l_yFJs!w+nUluejdGFuE=_9#B^M4mgMi)!ECeKViB* ziW=L0ff1v6!thrvIxk$7+ZRUPbzW_9$nLzzun6*#lH@$Xcf9+Ruo$#=RX5>#Sb3UI z75GEYuA>d#_187{e5viTqroRd33D!~p}D5kLpgl|pOumdM(VlTa8`dy?_PEzjZsQ88{-a|1w+z~h{;S&9|q>r69hQZSlvCbC6&yY4Z-L0Xk9sx^;CQTjm|E9!6i9B z@M!j|IhEc9heImn@={%Wh~6wnB$i!Y4@@YS6lyI%Km%I6OS44z>tricabV-Hhf17> zOV2XStvaXzZb0}VqjC(kfW~k*oAL)zpeiIdKL4smRy=l1B@AYD;uG&fd0 zrQTGEFteEX9#1c5z)=?bE`P5!G6lvs9oGAraDUPN9K0*;49|gk!)T>@DD9^_< zOp(VoxY7Qv$|W|H#wKQ%1Ih9g4gT)izF|O^(cfag0D)S|A<|Qm^m(_;V)|)h4O%wH z6v>#}p8$&4O5IT{K5)tqmd^u!&Npmz!f33;kuPtt=x13m)StsJRDv=I7x%c0MNhb>D9JvR zqHDrbS_0l^1yqnNx5u)_tOSJ?gcE>uF)ozQNKRek%dLb-ej}i}Pb#2}yC8<_BD*(e zSL|8O{E=y}n{rv2yp(~^MTeWg>`fb%7f z5#9;kLE(4JzauePXPG=kpOR)w6o0qW?7cTf_paGWOmVp*;NsuEe?EyZvCB7N3^2n) z3p10f04lp(9^i83x6+n@>gJ0?^pL9i6Dq&21v#B2@-opYM!(MEw>HohQ|5uDGsj{^ z%8uzdp3QV+x$hWylyH;M8`Z^)mjePYKI~rfq|9nuqgcl17v%rP0(`+o)+UOmAM;1h z=xLhI!?~iJYM@Z9X@O*im<0X)-`6Tc%_D)2UKjf79~vtDq);TIu8cG^PZHzG*QPw&YjN;m#n?ziL$ zovLxLv`lbNl@{s&ZT~Tbr`@`jMjA2dV1?@&5*S^pxUt>aVtbkQ(c5SOlkelBj+nnd z4U*eJHiqdxXdrQ6C^fOKV9?fUAGrEXggc8Gmr6LYKv&($j@C3Dh9V-t5=ZgJ-ncRcw;>3>Qn=1Huj?z$ZCS#nn;_>HLz z^F?L$SzMW?iFg^MerymZ`_3 zW#O^I)ZhcK7B6qO@JCWH0Fn9O*Jh^r1EJi@Bu3oNw>_Dj%u3B62rSPyf!cA`qzZU$ofb+zuSx65LK%ltXLjewKj^%=&^Z zd{$5eIAP>NaSyUFay?do;TaTVI(}NLat?fN6mvvR8ix@Ft5B>^*E1Q1rJY-@Vj8_w zu+1|RcDSRkOZKpDd!~lHU5a$UWW8Q;O50qMFMR>lr9!jm3J{c4DF>k7`_c~hb zF>iaH8+g7@9eCgj_qMXa0^VUTxc{L*0pp&ZJuw%yuk(FV@J-VvF_+fMwO9;s*Ztzcj&8CAVGS`uev*rx5|IyE;SAG#2khp0Q zMO_8^Z0zF4rg;Z6u^R!4Y<_4@OIuNKkIB^c<%Ji!m z2k2Ck`x-PHR;0NGHOhXlUu=}kBPX0>dGur^CN-rli)^aiMHeZE!a7$I&b0p*VHwPR zXlS@>z5_Hs*^-59_d>Hn&mc|Q>ZE4u$%}uMmWF?XDZtZ|JUq%cz~mm&Vj^#^H@oJk ztvW;&`)-KyOVT~N9=o5HAt!$4bGTg7CgfYV|GcH*N4<{3#nZjEbH?LmE`|7@n$d8xUH66^SkZ@o!7PZSL!o6JEZzQOrL&+7sJ*KWs zDXh9yoE78nTvb7!Sw1zatnp_yx=ES^hifger#nvV=~gQ`yD=-*YicsXS}uM!8(XfE z46>;m^WJG@{VhT!1$dcLQ5Epaf+FtIhK99eT)7AHH>Fi7 zY;a)M_3NEWvY!>P1$9K&>Iv{bctwGs($K835a-J|MURKA;QOtn_CLJ*{!_89Z+^~4 zf-%ISF9r^3KX7p^-}ApYKcF`En$i;gC6c8X^l%X@k2+1qd|?*D35j=IjU@)|+hmw| z4AU(iMNT;sp`%Ed?$0b9&I~(t;g}kdrGWIqD)#?cny*%UKGAsPDgYv|#dPTI`fo+% zX5AH(jQBKM$;0BBO|p2(j8^JXVYA-B67Pg$VB$$tRhUh_nR~;xAh|n%oSSVuPsad0udGKBA z@kG``pgSHwU$Bwm$up{n1#2)I%}j-kTPP1XobP6(l6@06dZZIc?3ky>V5O-)mGQ|= zW#ek>Bm}_=kXdsvKw8|QB*8V=*^a}Z6m^Z=^hq(zKd7Sri%<{Z*Jne0>+luIzsiz~ z+-CS3h1ZIo60HH*`Z5pKGRLERTVTfpfAk!m<63JV~4WH0Qf3<&uH8cBP%|Dj# zwBeX_8lZmUpRIMeiy*9i+NNL!Ny}k&u7ng#EDtOdDn`; ze@K|*_T!|WwDBr*;_q@a*{l7=oH6m7d(v|l6YP%rvuf9DyOYf^SKmIm*C*NYnB`?m z?>iK{p3!-VvbaWB{Ok(ulJ9-==n)1G-fNk;<6r8szn{idPLC81FWYTx5sy0A^MA+z zZaJ~$7|a6<)cp>hOUn|xNASO52adi5G%+2vGNEq+o-KPE(kAYN&(YFV82jKVQT@Jl z65<`2T}J=j#d~x+9plL+bDKY^c-P1oW0S*cycsEb$Yjf|_lQ0oR3m+sn$_WK#om-C z>O!t)Ysa+~+to8x5&}vh)6FXTbtlC5bZ$>6Fvoy|vgUqtG#-Qi+CiIPebki2R zjfLc^_@692Caf8FE8cQV1i0HsUsug#-Y=Hi8V6hF%rdjl1w0jE1>L3bS|JW?c^mts zi9BX~9oJUnDJ6VIDtn}gnj<6IuRh_ibbq6>)^+{!zrqRN#XB3E{|xY64?2 z*C19lTQs`Wr5Na-AxXYn5CO!oNsb3;6zPG=nyTGK7Z;Kd{aJ6HW*$_yK$spyumZTI zlw+9_vbl7{IIPsKgdF*pCP8~c0UlLECS*}UgySs>$ZWnzVk@R`i_^t8t*(wD7uv(k zCMiJBYa!u`{jn4*Ypagmh^$(OEku9T0?~U>DG5L<)&|BI`g71TV#lU z!CCznlq}2?D@h4F%vG%GQNe1;6&T~`M)ibvUu#0wYpsgEf{yxXEU6t2^bM1LvQ{Yo zyxnW>wDJOwl$@TWAv17XU*@Bd}JN=t2*yp~7G4Mt+J zgf+rDzj-bZ_@B;vUe;|h7G?oYsDDGwkh|O{#kKM>BKo!Z;YO_NUe9rr1yd>Q3BGl zQ4UDc^*F>c%tz^`Enz84i>>SAdUE`fCz0FTqpJwQNURIL5pvv&A{uh5Y$e`ETj+T` z6Lqh+ixU0IYP^vm0(GYdM}4dR%CEY_ zt~pJn*GyS3nPNu4HN(ZvfoEU!$BqvDD$RF3l+RSYlnIz|;AIQBp|PlJIZulNNb&4< zOlT|me*ITsARyi;6{HzP%TPGHw^uqmr-Z4W+OLvluj@{1AG{u_&X3F`nL=XE3Ehji zPE;mTP>N0A|mv=%)z=F@lm#?T(+Gykff1?z+ET>{dU1Oz_YBb*eq<8yjnD zoiOktfSD{MMj)!nSr$!7Kf<-^2?}HE5#V1sr@d9(RA3jzR)U`$YXa}DBu+g_*7`ov z6!?;n{I^7kMN00a=(u>s-Nf3>J^SZRpSLU-*URA;mY1IhAyJta2V{xRFWjpKv|~t& zO{(J@O0wMZJYHxdWW1=DSp}x+fs6(RdfD1}%YGL7xM2GcCI5Kjb1F zz00q1(NpIe6c;AxMCwD1S<;c{7_{H3O(}mspf2^hzHnwXp`b@EYRt{o^|x6VC6_W~ z;MJmcB?_%BO17bk$?{akvJ>OT1~#f5pZ%Z{h4ZGUp8Xgm0X9FW+rIN0QR4I-9*FWo zX71|$geoDEqPdYIYLW#TZy|eieNP~P!^`h^mtCEPUsST_Fbw#ABoa`ZPGm(U30Zd6 z`?mA-Mmdd%^0z|!bMY@GI!_yB2NnKS+2xJ<7+x6FjXT~s4OxhCyp`$*D?J-+H32=O z&BojdZ)TD>_GKM0mo|tPJ*vqH{LoB?OD5s&D0yKJJ))F-Axb@zJ)dV@EpO`Kc~qYF z?3af*TzEZ`LdV5gU6+XbPfkR5l$C;cVr$h|i0ZVM0M0qqX%4R_FNuw<7=F%WcC#AC8bPetgey5>0+o=^4=l^i8Je zam5vnWE4%geuMcVU@11sSA@3D$+}H=McKc0k2v*>9rOtn4#Fl4tuy@xOb*5k<^K1j z4_~jsbV~oJsyiCJw*5A;qQctfY{xY66Ge!^*3BDA5{wl`rl%Zd=C zr`(So?Xge4P=o7eQyMPyb;mNHQrlj_S{ok=wtDeT#5d=^8h50RKD75vTzIaeoHd9& zKtZV8JzD$ozQX~po!~v^w3ey6*cf!tVM+;Sf4^0jH$^;}3muO>;&>F;OJ&qhQqnr=XjMO%w!dzh{8Bv% zQ2v>kE|DXpRnthW=yf1ZQ_4-IgQbL8ZVa_6ozdhV6*Vd;5j(is++QyQ-H=RhRb|^m z#;Q)KvVJ$rX5~0mx`^Iu&ueHrrUdpoC({1(p+(1=Uixq=T3jsE8ohemx%JO4g&#lT zI|X18ebLqS&y_=;+Cz}2g39}sO}wI^JD)mf%cH~+i8eTWsYKQ6jKmN6+n(SGAJ_RZ;rDI~c7uk(W9JKX_$Tqmlg&K% zBnMw}A6j^ffyvLtN+TMba}y~g>dhCUeQoeP``>S~{0m&4B}o=N(c_~La}clNVt-DcLU-2#zay~L%`&gPQqv$)(#Kr0jC%7E zWio{xQgCa$tsWXtnbDJ$(x9r0BdRO_CuH%qI|N*^2RllOiGDPt6{zXe9OdEY|L zC5QnTZQ_={U0!k*t1Az{G-~Af2PF!)nOSi4BMNw*7J?L>DNEoHzlAL%?VUvvrHHyX z+l|`Bn9nPqb=X=3 zSjb{Zs~vK8{(gvIXFAL`zMnI>hXmUUyqce z_Q{W%FOEBvtLF6Kl5&8U)f&ZFzYC5A=wN?wkwkhz2D;r;b7_gPf$ZPI=xUB!)s zeT7O&n%J{lQUj``XWT)TnoT_jqUE*KV zCATI*>-!6eInX_{H3^{y&RuP{(Ld-qzpch8*}I}+9!2L*P_jebB)SNmWagR)fo9H1 z-F}1ZI0*BkB<>-JPYz`Ab%z1w@9I110h1w0Kt37{fAJu<7TL*K{WQ4EM^;)m8twTd z^TljYse{f=p2LbW2*mQqm>?uB=eua#=G8hYclfr|b5Amq@nJ1?65gc4t3FJT(9&D% z@@XMy*as<4K|QoybF$C}oZtZLpEcQqBcw@*w(sZ9?B5ZGJXTPD3I8);xhN*Q$3#xe zF@#p1k5NzCO(04o1bn;J#Pz0kympBKyDXlPim4#Lp-tM#F?&&Hv?9z^O0}>CjR2^; zBJ!d@4d%|4a*5GpWMc$I`aPq#PQ_#r5^^(uB0>K%?j9QgN#BA5i&Z#0@bc)(g3=V7 zB32g7i@MMltWb(SYQ>gB9rWB!7Vz989FmRjakf3g`a>Ose*LX5p@j3oMLlM94o%jO z`5lj7{SoiX=byzad;?XSi=j*!XB0UZPZJ4^?W+%UGWGcGYMMexltcVn5 z3|tgbekAl(GN8t~bv+^M{@;FUAW%}(^Uo&$A9XF64Tca>=!YW%n(@B8^iSO7l5ULp z7Ta}|D9$9++%Mz&bzE!qwtY8RW3N-vzHKqr(D;ac>FZ%gyS$;-=|BHY@$84)`&y@m z=D*AH$*PsiU3GigZBlyJ5|K4gmsre6ESjxOEDSs1_P@T50+~hxbc)h`=ra84kE#{G zSGlwp3SjOj7Gl82uB=0)jQ49m)~C>bPxuiuYeiH8?&7ipc<)+#DpDtc>`urg2KKO> z06?KlEb_lmmcimI|H5hVCal|A7!umsdj6>Bm&HmnfX1FtuPA`fU}h?^30Q=1imA;g zoWOn3b05}?`IFJG?$yS6Je}66EmVO%RCzN#=gEln7LWDS25ecWIF}>q@3ZISRLhm98 zhu%sV{(8F_jjcfNg}D3`6MI_9p1+wjx0djKU*jYN@Xn54Fpi9IlX57^Ws&4(E2B5+ z#KGk+>4)I05CjseYu-Pa&rA!Rg&%ja3T#krTA~t(6=9G!*3{+$shO8a7q9pL{2UZL z)cWgcW6#1@wqgoNq~oDfjDwRKV2OJXX2w;f)We1u-b2D@5YJ7$?uv;pd~e;eT6GH#-4T%h{%y}OC@ur) z%Wm-eu9vmGL-O$#Jj~{XJxtWb6f4zP;apC8{c$S8_rMwS&?CrLO~5z!)7EJc5N6`&Rl6L5$F z4uAh~f6%C7k(6NM%;!>rH1YidE3Z{g7~eAkzU!2DyX<7BAgIe{r=boj-JGbMZT&n~ zKlZpRa-FfKe>9ig$DfFGNVu&zsYO4b-f{8y)osD4^gV$IAU|XHG=6!hUF53|CGEX6 zzS{oJd(CChqECVe9;x~6^Y{^ zl=Bs)8ThHNKjy7I5c>5yTg zsxEBU2-JeaZl=lN3%ME>la^;Mux0Vz;SV1P)Dn zp<4kXV)k;$&^TNu!uvc!r+6CKeIdua8)}X1$-eQEMDwX9Ojtk^CQt*8j))ToqxM>$I!ZD^xAru8hIp0u4g`!#97G=gZm@}<{us}h!Ze)>O?pbI5U3laNn8-6z4K9|*9WO;+ zw(GB{)@Yc_VqS434CO4X;O150xI%EvZ)vx6t7OJGMlqR_Kj&(f)twt>35R=|4{5Ua zisd0f5}R)c%()MV1oUQ^F(_$%3TR7);ng5IM$i{q9IY~qeNdm81O>U#Xchu2rzbM5 zR#`t1Ej}b~F<(VIKG^t*#pza&5u--%1&D~iwZxVN*{*`w0+3&BI13v9oZ{~2qFK4C z1)xBvcBCyg#zP3p=9y=(W{ zT3SmnvV&cP$?&t5c29F7Av}ooNZg0%En@LdpBV4g=n`($4HMxofaFAp7#lx_ zS*Ji1lC0k@My5vbQW8)oCqpCgEUARm4i1W=zBdmLC)|?pp!B)Ypap9p&zQDbQtnFi zrZm0&3<*1bQY4l!qG&PnN`rgsV9o$;xYlB`0YkKmb&J}LJ<+z7F7XH3VF_bnV@sobF1ig}b@RfCIHYfFO=^V(grF!>Erfpet{fQ$Dv?Sh1Q7dqtI>TRw9%yq ziT~59GSbM8bQ*uq?TE0TEP^KRkGiIAOcXxpWiy&}A&efY0s5rrLWUKPlyirbYczr<^FEY=+D1xndrIcTYHnWpy@iUjBsPZF>`g_1H7uM}OQ zj&?#bf7fB|ol-GbFa!@je;6e|hMTFY1Xt0p@sOq8J^c&;1H&I^oh)BQ955h*W@FIn zakwa9<_+LiJ?2H%&TT#XwsPB3ffWZ3{N#iRg_0X#!W2(b13(9u)#Dbu_istl=E+Xd zTfSx&QoV2U{G4Y#JJe41R|Z4MpTys9ThV`E#G7yK6rdiKC zhQDNV^n)W@f7fZ_7hIjI9D3OxTl!?jM6+wYb6uC9U6SwWlJssOw{;cGA{(mgMq&;>WF|7+p&vdN$`rl3d`N#{5 zJ4Ef*?+sbl+*wa@e8&CqmhN|g&#IXgwQwq!IwXc)Xc0zde)xckA~pZ~t1y)tXO+=v z0O~Pt!}n0!O0FavoeCNE`m`U%$%D^QwhqKjQo|B`@@_Tf5GfF&RxVrGy7?vAZgi zL=|^@W1PYskCt7@qH{!LDZ0>(!FuwAnFmW_DHF5+!~B^+$Z?V~yG&*lPpJ;PNKJEU zroHd8Bn^dr3IhVRBcjxbT81KwT6g*>k_3Prr_tvIX&q{ak3j;bO0C88&ajn|J z#n|bVLkQ(4-lR>l;``IPY}`x)9m@|DxQ)(PW|{kvp#BwN%51v+ykcjOyU~fpxgj)) zcIl2mU``S7J6ydVW>(}ovQ~0#oD{3(K5@0yYdtL@H}6$kI54lFnE( z7qW;25Fx)QxboK>65%zsP;lvjsOMu)`d3c1=)E1=-QtnHC=$MB&j!IjO_U`|v?@58 zkC(5dUG`K^RC)Gi@iA5j`kJ31Ud4R=Y8%hL3cll!VO{^kUUZ9Q{ol8L&*YP%6_G3$ zd0x32jaI_UPI*=YxP_m~uYIp_h6I!-x|a7ZhYGu}f3g4c#V4Q;qmydtqSeQ_ilJGY zF##lCEz#ncEo}9&JrjL@vj}81`bcwy!6y5$HAJDFl!7Qk3CZK}HB}QSG;35x_ZFpB zMVF+L2_lOisSqU@?Gbf*5r>p0Hd1G|)_LObkr)gkQpEu*_L;-DahRp;rab=IDFwhbGDk_wOvU}JGBSqZ2}q?-rN1A69dHzu6$*XxP%~GGo{~Ud z37uy}sgxqo^r6{SYp>VR=LUA4|E>>C(tj-}a&vj3x}K$dX9bu2OLg1-I}A7~y;l2J zFBx9=RyC0}zDsKUw8*9+w50PLQ?uEL#>9!9iF{K+{soQ zzt0)JSC6Cuz4HsP8FGZvQRGdW?SEP3YQ#(N8q$4C@ae9_7@XZrQr|7ahmSsQK&alI z-SD~Z_{=x3sKAIvr*~#f{`-(pR19b)CzC}W>IZ#u5(C#hP6QryJ$#f8venWOScTzYK)K`NoZ1aZxPH|E+Jg z(ve)BJZ}q<3QiZV(jkUi;4o!TnP==VBRQ?0{N!?cYYI-nUIhPV1Y@6$9*GN$G%jW# z^Vf_mlf-Az!D0#T4l!pX1peN3Eq+{3BvsO_Y3p{G+E03diJo=0c zbg%-Egd}C}3YaRxUD}F6X(6BNdWpms|5WvbDD4^%A>^KwE*SbU^x+WB0QwR0hAiO& zr2rW3(NxHxEtfs<3`-4P1CGKg$WgItap#-=N7Gq`HQm2$d?4N3NGdJh=$4QMY3ZJH zjglHjgTPI92m&&?yHgk-E!{a%I`{1V;@Ru%x9vE-d0prEISZ|l#8HAjX`PZ0tH9$J z$$6yGtz`MgQJCd{qjtfsDX^oKXynmeF{eb!$o2%Q;q3h4kq>t5*EzH=tJl9Q?r6`N zlU6SUoF7ajI~&0W_$V~^(QG+u0|y_ z!MEFNzJ$-eTzoKh9wvY^c^PW z(I@>U<_tQ-i_rj_Nmi(#h>IrSe6RU=X~=%Q1bjMHVU!edhi&jYs3%^%>b^?8(D_tq z9*>1ML#lBmq#i4}?=F2_{w7GNsin(9>jZ}T z5yiEeRNl@51>UC(%r&H5h~fDJ@nD|w{M<79oXu6`dv-(rzjsO9J?%?V0$vBb^dtW= z#P@y-a;j?n^ppl{R^q_=q!RSEf8ud?`DL>da#XzB^gPa!|1#kEJP%*7#RhO@fqkZC z@?U-snqFRHyS7>G$5VD6yhrQUin)D(jRnyFh#2|SYrv1m?>c1UgElZ<``_lpixjOQ?hSwvZp=wlm#-oWN{Fq$47lu!QkU5 zh9qlW3i4xWAl>-@l1sh;Z2)cD5Dd?vbFyDX&tqR|c;lRaZ4njl-hrQEFYMAqlOJ6V z99yos0(C$Oy`3?SW0EH%#snBllsQafejh4=9<|`4SpBl9kflO4j5}&aY0EoOMzuL{ zFKoKX?RuG>S3eYiIv?->L=S^ihSH13n7bLGDLh$);DW-aoVb^bxJ<~A?uA=(5vSTawcrK6)vhN7%ok!_OgCCw0T zzsE1>2$$j0$zzeHOnS_w?)}rTb?@6<*X8SV^ICX!_tp(%p8F&2k-m< z*XM|uD{EW>LXwj;*RM&VPlyM13=N!*h5XC4-)=EIKi@)D?|C&^kq4Jgj`S|)GIHag zK^5d8N+{{sctu%3<-0ZNI{5t1s1W5uWDzY{Il=R+8QYGHS?%OZ8Zp>bH6e}Y==3L) z*u+ji^ig(uS|CuQ?Jqu?BbE{O(j9x!X5H40gpx0jYX}2_)4#QsG2LAcitqeJH4!_p zRnOVZ)i0?AollfzwLX*0^Oe#?b|H9x^DOhn$;p?MH_{i$yvvP_3-gx~PutCiNrSG3 zRLEs7A#zR|ic1GP|If{4{;Y{K)lLT8Xg&?7o*|JoM}AjB>-XJ|7sz94!m8)YcK%a8 zL(5U!3;;z0kw2*y0#HK-$ z>$-O@DR0XWl*{{cMU*DN9Pxm7c%n49In)e%NK)kuTGT|O#7JF4YqlH|mP+6M)*LT3 z@wiBkcw9_>SzUk5Mc9B`mYd*hDbsu7Uraow231vKk;sO-gE)rgt5TQyWa;~D^T$T> z?uVmthMSs5=k=}=B)Id*MogWk z{%eNk1I6yAU1OBsj3cNY+<5&-wc2<0=0rU2E{x5@|732#>@fsMm^#yZp4EKmWq7jG z)02Mcmqy~$kgLgr`0W$odKhx&*nN|~2DyGj^73+My-$(MyKXlN#E+EewDIh`r=8MO z_u(opxG>2NWA0oEEqS~i-B?X|fJC}$M~wxv)n;8tEKnta_7RsiY@cr_8w@GBoZ+NFVP6Ws_jUde1j>ic@MJGkcM6uiNtjqjn zBlq!eb~vo$EmY~hkG-&VO;(IgGV*-|%*^PCYL8^P%tz^OqdM8g{HUVD$PhooN+x0s z%N2z+%O}i{dh%g!gIldj^|`e<(LoBA^_h1U~#g0-RHB;0(`|0)YYP(@c*y;A3j zc)j?D4`5$!;aR_uLQGfr(=_EuQ|jkp(*HdP&EHT8or^6$trb6{ygXV2^ND!nzVbex2eAIW!MMD<0!?6Dx}vzIUD2`>vkBu;+GQEFqV8!(^t8$Dg(dc*XQ zVxD)k34W6^Hgp|3HkQ2Z_nf_cxexjGlg-ra-rV%QOcUbq90AOMO~6dkb?^YeShT{q z_gAHoC#Qivkc})tp4{4+y4&;klV!-x@5wtDPyVfWXHAU_K>BHb?W`M~{iVYERx=;r zF>${Fo=mMmCoIH90ie1QYw-i_W~BWiGf>a*g;(wdaP!U+;Z^=eGeI_IYm&IQvF0z4 z#^4F0&h-H~W_zoVHyk#kkG}Yt7U=}&UhVX6&N@ZPvd7ZKdHpVI89Fblg&|ML;@&2% zfTF1u7}`$|;C#a8MUgm&&v#1YH8^d=}(^~ z#m`n8t6FcosvpM{C65x3OM`I!z_u9;-_ca%GmS1f%saMMOc`G^f3TdpbW(y?7 zgKGC;_#q9Z1k$g0>BKz$m}1Sx77om2U=iyU+tYNvA!f2#Xv~22*gyw}3n2AG#FVo- zgm&mtQUY0j`6;4;nFeyiZztQjwbdh0Xi;TyNcaZg8BxD+{p|_1&-}{KFqVncUiGHW zPr%Gjlum@~w?ZB9Jq~$KozYAa|J&_gtfato&UevcrnozfTSb_FL{0rr1*O-7JM$ab zIBj4F_WxpwdxR;_Mz9sBQk{+7GqVE}by4ts8mjlebk%5Ip<_icb9^*tXzvLNF!=Q? zCIXFlXND3DO&18n?f6*S>*MXb!MAQ2xOLvEe_gvhxCmLK(wB6(eEd+o+PX|7E0T%R zZT+F-wtlFbwt}}}!h@jPKe@i#AKzws+%ZPxv#3Dpb=tq7bU=@dNYUZNNsGRE4Tyn$ z708suG5&k)%Sgn`kKunW&@;4d!Ac!;%03udV!EEyMR!Aq5muuiyb`XN_<>0qw>$$M`Z5z@e z#SGK|0)6KYFEL-tpVZjobvgLK@4lX`1um?+ZL69EEb=0T4S2b$aGBm?CQf@*{W+5J zRy}R1SP!_(>zw_N5k_Tyf4*7Y#ULwBIFZ;I zw%2~UhMZoyKaZu|HYzH=2suA;bw7@Bc4)G5trwl|GF6oQSZDTcYU5MmEvJuOBeZw3-E?G-r9Ar7ys|6$Mq9S4fZo% z_j4Z?CtoD-h02lU2QkNuiB+F>@3_aS2#}EnJ`G6IEtL+|=0Q4=Ir3G_eHU$!DWj`?dr0B>W$Wa#B=ZBq zgQ?HS#rV?c>N*rI7D2|Max96ame zns^pTZu<}nMY?SoBFjVkUJ^pPWU&g8v^_2a!FJ1KEPAF?Q7_WRnsCmonnxm36t$)K zY7$A;L`eza+7wwe;M4FqRN&RsTTON9sYj+p*#WcxwGgBx!5|G8GGMw;tjlC!LqHdX zYSH{6k)e_OjLC@W^m+0J>LCd+xuB~I%6B5epwaRl|)71Y0turhzyqE$>KigYEMoB0or;#1{pYS(PigQu`nol zbdt^+xB@lZ<1qBS2XO#%!6Q|FNw@$TFXE>vdU^hz`_+H>8Hp0OKXgK$ZCkn37Zx69uGCaIRk~q>9rH6 z%aRs^K@{s8!iP#7MwI`>sUL3}rJ)u}*$zdOr%{Mi=2zT}zG|ngfu4cw?oC?MdZ*7z z3vuF^Ey#8uX>RqloR{m8=VEIxmePF-x!ts$StE9z9-w$Q|FJ1g`rj;c94vQVGDsod z3_%B$n^%hofur<%-P<>%lDCd|m+jrROZm@-NXUBr{nrMHBI2J^A}knVNcckngY?>2 zs_mA&prL4o$LY#qix|I;;B$oQ-2?KDB;I{fo_~4HLdd3^A9&7`IQx||B1q!ur4|~Q zK)3eyuZlvg$HAZQxw-1crRu<`*sjY~^GB~QuLE^ORv`$s=$hIt_q%f(;>@hF*V5h+ zdH!deXMqdtYkCWF$XtPM|2rFgMZy|S)y~a-eRsb)n5vwjVpSwy=<>bTLf*e#`)+{E z@AV+kvPJ;D%ilVsf2fL$w8;3iGPH^DSCAl@_>ZJM=7yq3?PKkuSz!MHWEJUukIlc# z+0T;-6nk_3w9gQg@ZtkGYwW%_GFSyq`oI3Vlb8TGw(j5D$^?FqzHOZh95e_hZdU{l zldk%1tIPb_Z3Tx2H)Hsb{3&KfwOw3ZS&`{k$tlC01|{lV#Z-7zFO_X z;`U5#)4v9MbbZ)Ts3Zj!(X4G9Y!3kFWJ>Q zF#bI+ib~5cZ7Tgce>^jJFD&93lF{*MLYFVr|8FmwEELQWU4K-tKx>e>bju*ua_Xj2 zG7TLKTM29s9H(Xu{*kq#oQs>91JnY4l?#Jry;T-92z}Au*=D@|)%7m@+Yc8(VNE{# zN~{&rr~tgT9^u+Nl zb=3GOPbdw34F|u7*@Iul3|pNT46T4|-j!d6<25_ye>#-Hb%@N}+RSxfkGzv8$+YtN zOMR?FCSdGPJmqd{+fp0I|1aiJPPAW;)-b3pEkA?r@YS!%>jzQ>DCBh6cfx?)=lT({ z+BM#o3D0SdG~nj#>ZtdJxwx~F{T~0IR%xv-KJNc%H~n*gk2J_NYDwq3%k1(q(sBWB z4y_(D@#Y9}>V27ye&BL}=6!AGve$~#{CP62cAXIPq1LUVs=Tdgc}fENjS8P$e=nah z^InC~en)F-qacksC~2AyS~7#n=r<<5b|Z%vBYfGUMYK?{Vu`BhCGgUjw)e@i8a6Kt2?Hb^tdYTepo!~+Ls)Tr*Cg*x*1_f;O2h!Kb+|qw2hCh z>+Jcg6Up!HzEbT*>$&XS%}$^bJ$B&@=*10s*?&O}P6j**jeM!Gfuc%$fat;x8JzET zTW5Z?**rYFf!7Ao`)mH|k88JGXLlD!ptEDX&)rBZ(#VfA$oRtzg60gaM@h!n-w5n8 z{;1#MiwNArHO~M`c^>?!&%ZoI+N0M%%-`*ipj6~SRH`XmSJQHi+yiYU+FDuw8!X5| z&BMdeD&RbnLCpL3@Bf$q?|2y6E{3w#E^eA7ksI)90?I#SC9=K9 z62zuiH>I}ExTvyF|+~bB?q*`Y(p&Ki+o(^Jjeq3;@9Ss+2!i&iG z@wXa+8Ce&;%tZCQjsHx<`BzrvaG9+pQX8X2=J3%bC(uKSSxRs~qtMw$?Y_=hNtxV$3d)Yk=MmBfdozK_DJ3n@Q}mNba?oAN5cl-@=QzUE3YR5myb=&-~dSP8@#KpZm>GV6C z?Z0`>2rk95CzVRJneoeQ-28H(3BJfBy%BcQ;K=^L97k|96v{rDB45Rt^z1RVI==pN z(;Re$+YL`xe;OjXA4)wjUi+8kdKXdsh@3~~dwY9aW*4r=tH?TH&fsaf^X##w=*z?t z1aT-$ZzTFq9dt?wIb83!g1=YO|h0~fnW|<=!md`h?h#SHH#4X}Z z^Xcx#t{n6sCcXP<^|k~4XEa(mXvP@n64AUGG9&A4*)G&dmdDz}DN8<~QJK z{}V^EdwBN?{UI;v_OM z=fwjIzO?N=|F_VAynqGlRtF)m8wgeNhs*A3V?<)q%kkveeb;SgegD8C2F#oxUKrNC zA`{U!d}p%P|5YLmjL+K7in6n5c^|{g16JW?kD~@%{|0$qPBjCMX1Z_vtDkEb9;ept zbJvMEN@k9p?uWbf{a+SU&Hj<{1|D|5Em?S4b6mg7yIlzw`}!3WVWJam*8k`EBo{7nt4YSa9&|e+bd)mXu4)^ov*0QYu4i0(3k};svJGC<9xU*EM~f#4q!P`TH>%Z zwg|d>Skm$S6Da8FpLu%Vd&(%NX-;!uiqm*F-xjbjz7ILU1nf{_xV#|;xH!FeN}rKk zb;=L2ifDtQ8GkYQ!1J7cW)iq!EBUc&qVxZ?0Eu(Ty}weYCs#Is!7(6&ImgLtL94u3 z_5CMH24gP3O5iVy^ZUw%RBV~wW_%P$3~*QAH-#yYPp;jw+f_S)E3tuIj_#TQL<=Py zVw>(#pAQ~H8XwAh^tT)9xYbiLw`temx24IyeZ>Uz-cjnPC_oM+T?0>go3Nzse@Kh@ z-F-IID8HP!zBt;XjX%tqniG9v+gS$GR@>t}G2o$_Q-gg1B*Y;@_g_3Y?LVjP@OkS_U(leI)fxmANw}z_9}J ztowPp2EK!Y62Ajvo};ZH2pMzWxh;A2G?UEc%I^1!tmA<~Uo7mu+FDx&zNZcc%5+QP z{j=UZ-~8=cC%CjTBsLZ%+M^g16J_^@;pZvIjQdeU@>%C?p7pTC&%$3NIz)c95O;g( z9=is?%;0#O@2isk=(g!SRc@eJMj~o3Dl*J1CdTg{6}PqY^!V9`xBbalr{oQr zK0(Vvdt}pc!v*}OzF76K{%%;X34N7>)_X%i&FuJ2xQyH^Zkwi=WX=z^iA3ODw7Ter(*9 ze*)ZWlYD)qstLcXa4Wim35VbygO`E@s2YSjqIZ*I_d+YJUvU{elVF}9?iGV>EX?-) z$z0|o_-(wpUAbl9Jq65193?3MD%5A%F>wK}b2Lyb=rsoeiz<`ut6l&+0-_5`<|nUe zT(bB3L4i^Wd*Jk2d*X@rHVJxj7xWxrz*}U1#ZE>g{kLa&033$bv$UD&2j^OA^xaQ) zL3B1sJq$!rtY6dkuQLFAE2{pe^jcxm2n{3MFiv{P*60#!9^Ku!%qDpsYUz8j0&OsB z@><}bTve)g0!^WFkI{`%P5Kgn9FfP8|LMw;f1tYvwKOS? z(9z8fW-v1ICPeMe`5hcSXshR?2T+ZTt)Y|CbD8H=1@sRFoglrELiJSXU<=EZzwm2u zPboSA%uptjAO8qKgT=mBnVFdE-OI^PMkm9K)ZuMLCMMc*hn^&i93Riq(g_CP5nWJ& zn~47KLr>IT!;%>e;#`rh{hngjp5$eMiDF4$9m0nKP!a3eGAk zAG+D+*5Y1b&mJPT4PXr*y+6jM|Bh8JE} zq*r8t-;GYsdQ9|w4(oIP!WHQp|5C+7B-pZCMb!LcrXJG3d8<>3M)G;=h)73&!HL-} zZlM#(8JuCy_nF4oCBx{Lwvw&8Kl>#&6B`U8m8T*jPF2W^=~FA}g+fm$bmBmHNuT2j zYK9!8igg!sM6{2kEmUTFm}rPsW06x`Byc!Gd}^W{24vq?8KEC~B)A zCS;P9eed9;qJLinFWcy%1H6Mx#hRf*iNhN^@&odQV7`_SK}wrmEq(!GjhzVA_hn5h zGN;-~c_8JVi-JBsj7Q^YSk3q%idR;$TfmX+Ap>tU(0uy!XpT(X>1qEKoCkD@%9)TR z2#P`81yP?(YX$yxbJuW5jiJBv0Z6sf2KhU(yYXD)i5jmj6`$fa?*!cY1=#S~nOo z4o3S~!Ec5C&?e01)MsG}V=qqOV1vI>C^Hl3>XGjqM^*I9AJECj&tLg*{2>qii=E6W zIH%%&@`I^Ihn3J_B0l3_cFK;6In;_}XXVavsKc^)A)y{reT-}#FL((|FjLnoz#~Qb z2odLmog3ln_wZ`J>qx1meOz4C83!7xksL>zWY&==c2;c4J2AUbw-8F@&cBSxKe_!2 zvtpPRyj04d<)3)VK#lXLT?S$uRgNM})=glcmTZM2`rFS_!O+9QmF_aWmfSIs!RybN z+Gv{!Wgo5HX1rNwOp%<-j1K+-rIgoQX#7IN$k7@pQ7#IAa*%n^=lzQ#)K{@pt1Mwv zgB9D6|DGUQ#E|C`_JG21u=5^FYGOnT*~_D%SIWh#kDQ1bdRDwn`|?5U|=UW%fpdEQ$@LLZ-CU@56ANwt>e#& z;Nl8kqsQ0U+3}eS2~Cr!K=8l}&@e0A;s7#?362!FHaCu@>NRkDCw zl$ui&q4d)^z0Vr?%Qh%E2vPtonu?0HZX)29`7$q-vzv~X=0!Y>ndz&ub*=%NmE@-C z)_L+aoJ2F_ajOk(Q29_sMA?)tY~ySrl2s?DPm!rhWeOmUVh6F4Yd@4|liuUKyvFE) zc*<)*Qs9_AnJtZyRQjea*7d^7-G3y>>dUXtw1_ld+#I4rpbKU^0eYCe4G-X`4JYZn zlj;W|D?n(ko00n5EDMb$d5;BOl-p_DLN;KBn2`!A&(9VLRZdEw&q_r{L%SdkNad?1 zQ=8GfQjt+Fcfh$VJm#O-+XE$FPz)KI%Htg}y$O-yanw7W{zGTds@ZADN;lPQ%K^}v ziP-WI8mB94(+w^gC8zAeK4pzIQI3(-i+#s25f^31_n*}mrw(syScHU=6t`=M&-i!- z?d!x0H9Zkg*}5BflOZz$Px1`=x>^j2j-ZRaBXmHZ#q(3^I4}Y58GRz~6iC>4;UoTP z(z;T&050qmbRb;1^;v5k_@XDiqrZRT7r7CbxciD0o>8q>el(F8=`3;mH1Fza;OZQ- z3i5Yr|1jrt4S9l>W=V@JZj6J+yj5#Z z2hXDM_`-!`teIgto;qT(j8;YsZOhQJvAu-7L>C~UIt<{xpT}^9D3u=W#8eN9mc@oG zV>;1S%s%6!sRb92NsC<-$>IKN6xvj5BKSzq=rNWq%GY+j;d`Ri`L!fVAE2=AW?!>z zH`F-WZie7mu>YN4P|AB|=?^EH%$N2LF0X5!U*I*{CV6KT;K0F6Sn=Ag&R_$(Ea7Di zlhDn6@_txa;MNBtQFTwBb$DV+{kSNTmXx$7DdV%C+^;I&EhD3j=9^YgD$)I5IhQEL zTx}X|K|D=-AD<{ z+rP_A?hj9#$YS(gVvKp4y>Xoe0`bEUtL=$+`B7?}B=1%`64|XS+PI1VK(N^ENxk`q<(9O}V2pXEkW`MiYc{RG!S9ndxtW_VE>_Je?`+ zZAt#Y$?Ec1V%v4?A_{2?E14C53GR}=9OO`tA}`oJyoUx1hhd!Tbj#BU?5E{queM{S zVJH4X)hRw;&m1nAjb=5YSCgOph+b4%^BxQ$qEauN*iJ_O53CM5o#3{U9B6ONvdhCz zRH|KWq?HFN06E_X$Jt!4CNb4U3K$D{(|)QTw<2_6-6q=m&r9`a&d^py>}HL57rUM1 z-}?;TuV2f60ttijF=eOjG65@omuHA>aTlhsKG}iI_03baQKQQPrl)&CNj(!X>7->RoE8!Lh%8PHYIXHdYna{eN&Ab zPJ~V7TTu7;y~D`U=hDeclEKV-q$;-j#{KwoHGjP_#z7F#2B*a#E8*D&h8v;fr^NOt{ zXNwnJc`i`Is|HtEnb%y)A2k%JT+pUpf3#C{%5jv$ovkGC-cJuP$*2F>D~eF6nB<5ryg>j8FX#*7Z{la|XYK-jK66#Vr>$z?R*(PY1V$1yUMZ|t38 z9hbw6SbIAc;JYTYZ&;WFMwCTp(k=Fs;+&+r0NS;SxtW!Nq?>l+-+kG`wK+W|)WtLY zNYMlOz(2CSqYxH}2`IOTW)DtpT!yP-i=_Gx_YEewN+V^mIU zq(I*0`lADly*!&{?g9CD8JCb>af4K+*z@kY;5nF=k-%p|5(`*#G(Lt54^T^uJlc)b z_Qyw`RM}TswaT1FCP03P07FTe_*%<;eF1L>o9hEU&l)QDz^tXUs=#~ShndK{dS=FI zlPmNUz>NWdkj%eJq`WqKH|P&D((z7PZ-4G;7Xxa_pO`maKZwiPzxXwX&bQursk*XV zWp;{BO$6F|htE8jOnj+MbM?1!Nb7I+%JcWH$y(Ds)wNQGt#t+ww}Z%~rD*6+uC?1O z37ccW@jGb{vuv*{8d(7+51u|;H8-raO^RArJL<1$;Z-=gOmB{G2Kl?sspuCwaY~<# ziyG=Tui0~{EZ)mP_XM88o&q^jyMF7hEmYIUz=hU0Y^=)Ui^dAvRA=dx0`{fEN z0i~~*SM>*0w&>J?TZv<{W23S{Nuu!NJK|_z0&k)hpl~`ejeZcZ?Kpm1FI|%v&gz8K zQO7^pq}VV+;{+2O(oLnM52~NgMaekCxSgQfBtjZY=Slafb26g!(wTDZgI>;kEB|Sg z5VRPHxO=wntw>pGYI2~E)%!BF#W;^~&;PpP7^%ByrQ6^J@#t^@7ECaGC@PHVlTP!1 zWofqd1DnK4^!Q>{37LZXtO^7^K@`H|%3h{gBC#sCwmA)6(=nCfeQ7MS6AA-rETg(iFXERy7MQm~y@yHMdS-epuo~H2wHb@t*y=g}YRqa&dlF;?%XhkMz6iKGpwIi^Xt2D6|LFnRlE*o$QfnA+Q8jeLw( zSF;oauGn0Rag*qHB!$^iX1Y;$&qGXOfLVE`vZ7S6C3a3+##Eju_FjzR9QxG1% zyQk{-_i$_J9RiQWUs+KeRT;^>TjGoynW04ZT{cw#*ijaWM$BF zy=nOX1j3f*%V7I1EG7KeZwaN#67cMa?dkPqfA;s@kfQk`MEdE>EJ&o7Hxt{aD`x3~ zVXo7C(&=<93rM5xDgz*)tygb$vhTqogD;WR{Kk-4>ha4gq_Y7}hOn@{FFo$x|)0_r|UVc6wC#4LbII(u-c_7n&QVo?wl5RM@~c4K*@f1l6WR zob*jvOt$1qBg&FID>K@A!kVr?f!HC#Pc>^Af0#V+iAytZz+S&+ux02Aedb`6XZ$+! zsd_bMnRwp2yTmT^S_-EVfLeLGkU=jjYBZedZzgmAQFl%&SsgHvJ+`4R;S_8)U zZofVF6WFah`2NftpJ1%Ca-EX)Tqe*x_n6_C%an6fvY}Yq$4lGgx8DYh9B&&rpRa7k zKZMuMqQUddk!5vp2Tjj#iZm3F!+k(bQPJ>|tj`&2)P!X6K;kdzryIY-GojlS`23rd z?*EZxem6Le|Lif2M$4GV2fO z68qJ1qt)=`ISvY22yOOO=lDp&m7RVS=sGg1aD(3UwN}xy|HPs<*875m>9~7JAZ*9g zhSBF$)~W{fA>;5`x~%!oHso3(P?iEjvA%sDnOXl|d9uJ^mBkm!{M$|^P)YSB?GR5Gi$|zvzyA+h zun!-r?j7?M0fjqIo#JCPHy;&U2^v-GFPf#Pn7;;+S)oaFij>kc-%dv&s3@b9Sy5*< zB6>{HB+7PEaCELYbfPh4M?VNo@5iWQsHk%ZU z1m=qw!j`jMF=P*;)autu5n!=Hq6Kl%Z;CEsbHN`o6j{?{4K&MC(x0Ica2qdR}o)>q9kQKK@{`eMwm8&HO83(ZQZWU^UA{_{WV!Bfabzo!FKD zhL%!f`PP@)eJNkZCX5XB9XxvJ!+p>c`Ch!ccy~&}BNNYGnuL$rQ+6j?3dDy!^Q1Fx zR`z?5U4~1|*Oi0ZjM0lH9}9=J2ps*mrl1=mK`w;rL$Cp-kg8xJU1E1qg`BYOCbVV# zcF`H4*)eWdIsNGT$60?ml3rO@GNqYmh9*S6gu84@17wQE@G;(mO;fNDG;1V=mPl`* zK*e_bZ`ZQZitS;~)q6u`?Z?QYq4;3>=%Aog~R1InW4tRzv9)gDIuc=rYUh4`(-_W1qH(l{}veN#Z%fT zRsTuI&gPWiR#Vx-_{20-8GB>JA~o|-MxRAUQcek0!H(lBC$FF_;+kpbNjtte8?7|w1w)}I1IJh8*9{I_huqT=qDn^x|964TdhT?IXISvO5okvh{w1+An^(+O0=-bLCX zE#QA#wq$X{8hCvX0yy$b#S&QG^jGoSgkzP_nd0fES(g4AKw;81V8%f_ZdjoZeUS8d zXE6J{tZ&>ftapUMyDFWB@#~YH$?1`Y+teZ9%4HVh;>6NqF;mZM#9&A^(AT;M{PXyj zopOov{)^FCdA5#t7v(F~_AJNJa{GlRoCnT6u(w_6>nbRIvmB-+wckeLcK_PRhM=*} zcy{;%^qOI8&iu^pw6+kBqyOj$V{w&lwKUwXZI__*&=hIlWGeaX2kT_^JCXP==;wf`qvXqym`ZT!qg2TvE zF9noCL1Ldse#++D8);72L?{IlCvI?O{9P@F#-x@~7gnL*0RA3XIW-mq$9G_?+v^Rc~nUC}DJ&{-$3J)#$N~f5Bq_&c&ddm>jq_ z>kR&n(li&!Exw2E=q{QP|7B{iv=*in&WJeG5HYpYrmg;N6y0scENQDM6wd_5Grmn4 zT9PqVmu>0LZhy_J999mk8_FuIP2-EZeYe3_j>f1~bmpAVw<%ylO^Y9Gm@;7hD6Z>K zKKK7x0N>&rC96&{{%w~D7whfUm}}@G7XQuj4On8JSd!?UnZ3Oz({gy*q}RwjdgtNA zMM?IP8ph2+iiP)mRYhr)Do-Xy1*e%HraBnA9t{mWYZbdMNXF@t^nIe^Dc;&;b~R@Z zd;t*f+l~Kw%4z=hd?~8D^75tUuFAK&R`coagAS!aCjLDdaYkjnzyfw%5aIuY&s`c< z1ILZ5x_ona_`GMofad?~P0ICC(<_{19Tu)OCSE$2N;^~D-W21;0$yjFu6Ppa&y04m z;C`w7>*DDpdc6bu3}keuf>WCMi_mChos`22>9+q4=`Rn@VEd6eh?OV?T%*#jT@*eR zloR+X19^*D1Vmb4TotN`W=LE4_JwapQxoRjwkp%UKq#`Ld0zCYJ+QdAu|yNClAO&8 z2w9qOxsP{6ut6U3&+a!OU%ts$h zO`8g^O}@24frO8i6F9zq2Us#l8aHaKW+gJ9ME1%@)O*D#ad>0jUhMTlqQd(Ve+c4Y zF`(h=rQM^+uyvvm)v)yF%)E12_Yh{GE*Lgalficq0FmTjhq)Su{n$zPN#j~T%e=c# zAEI1|@nx1n=#QaS!EhyEZ zZ*~Xg@9i1b#v6yhUWkJ|&{JK1sHf23*I}%+F9ahHc+3&*L*TKkwlsgcWA(><9^11i zuGxN5kfbtTOuY3A)?2G6JKFcY{pP~r;=!OMT#W;)@>N!fUOxCMiP);N0C4C{#@&In z*oU(gY47-W`rBc#M;i3yFM@QwK>|NV`0_FY8@cd!t-Y*T^RXRJMWo2}o;k*NwmMUG0FwU|1Au?r`cPdExj8eFI| z%y0DGV5s6;lSE2?Yj1@@jY%CxlD-wOGDqx3P&f=~m+atADX8(ektS`+kE(I^^;z!r zRC2R;>cT3rR=JA{CpL9tLd?}^rPV%3TuepKhN2Fa^8qgQfJyuFbxW^`C@( zNpH8#6xT13Gtf*tlBzsDJ=F^+r7O+w!I~;muJso`78;HjJY74HYHM9|&M_>hTz)$T z<60BVxFBhV34_lYUs+r7Vsy0Ef3;cJYd_0x9TYF*`ZLEbji92B%Xjx@gr0I-Jc^3F ztvO&r6**1FEiF>qn>DCkh)TbHxv3cY>Y_i6T!tlvma?M40-qY2hN@E z4SIw958Vi4q}js&KAXR4=%j=jf=77msi&WiAXZA^eKe^@)5o-e${ zhK*LExA3&8Te8&>zveb2<($^AdFPI8#2 zyb+RQ=`WJ4s`@f8kk2P>@_zu8Kx)6;)xbrWv$CYnk>`5MQmP{rVZ_jirQ8w8iMfWt z-I*GHEU=721 z&4K>ot)-%2$moN&>p^= zXj4^zPqc(nD69IQBTPf{ke9`M@u^{c?h?TLlW~lH9^V!8SrBJ<$GWXwb5P@x-y%e# z#_Ep#v$xLNK->REUvPFG@i`?{b6iqLhpqs`?*f}eSF@!v+}8-hd31%@W8Eco&&Rpq z-1oWjFEG1S8ePQk+U0?*>(M#fvCN>gu6?nl^~U;Vv>E}JP99TU6qNV8=Y4ByYfDSZ zoSN*BlP6D{e&lpr)wkSo3lm?saPgUU-~IUqA3pu)>8$ytTW&V>^Q-6Ye#SFC^p}6} zw3`pP8?nHf%jGsk2_nf!vx5Y#aJY?37GgM^RaH^}GjFYsLvPj0<;@_fBq@gY-M0nK zS#yj{WzA;Rn48CBTZ>=XKpbakl;CEHR&Z+Jk7X9;7^c?v3TTW*S**=w3GGoEIownW zj9Sb48853?et1(kk(o(>JZ9$eFzgS)TOro75tTP)N(=x}8jKU%oJ5KQ zQza2F7tGVigo_j{LLy?~@D~7qtRWG(HE?fV0-@`lR@2zE+&p{k?BFkDZW&R~WhzFa zQ3ObL>l~n}NNHMC0k(o`q#0wamR0rHt}ZtaGXsquX~4li!>#+n^{P;m=TS|)7U)zi z(aj9EeE{y7l^SP%1ySsxECNuBi#Cj`IpA(e#KL6cfZLWifFT4_=Q_gLi6q((fj&)@ zQc_iBxPyvp6$qrJB68rzG3+|Vs_Nvj4a$$CVyl|oQuaQ$v3^spe?HfTQ?Wa9aJK5p zP&;*c`A)aq1?F@f2L;iCz)fX2n%_m&a^8WiP8H=N`&v3(FaOqd$=cF6l>@-2%^#e) zFEmz#$c`h+GMLd~kSWnWwZ z%SNm8{=Gi#`$k(aFt{u!AKdwKHP_b*V0FET{p?KIMmG?8&pm1fsM&$I<}ghW848oOn%*R%dPP3&9N{TZftH2hC{>_m`sizJCaVHUSHo_C>P)$#&lzH;^c{~ z)lI)KuCP@dF+>*rn*`KaGlM2>Jzz=%7be?MPGU(3(446QB`qQ3=4#p7Y#cL-J&2ir zravew+L~82Z660(HfTDTpm9b3!P64wO|R-{lA@!X%(R*L2kN?xk3B3O)ii)q(;{q- z*x_Y`_CmjF%mq4W%(_}HD;j3bv~)^VA94UIH_C;FYH2MSh{&|*HinD|?G)iczehwq z&cZ-qVX5m1KoYLk4Fga+szTf_XQ|k>jVMx~YSnZKU^rap_4<;E#RcgwXz)Io4HTa2 z2%H)e8Z*PVX_At}F9Z@1F3L^?X(})8k zxn9R-BJ!yU8RyhcnnIdu2i#OrQP&kfNtlVMdMeE zXD8rdRB%V@@UH$~cO&dwYpVdhbaCl28?GHB99!Td2e1VQ(Gt_d96*+B^65g(kG313 zbHbgNU7VS(eEML>h(r`>Fd}W!a)GE;isrLhDY~E0DN*X22h!G=nBAvc`??Ft^L(%d;2WL-Lr4lnUGzm zuN%PHu1napw-T#}Vt~eX74zCopK1BUQM}Q~G&k(E@NN9F=vzHZ0T`HJo3wb;FKp7{ z6qke}hkSjFmeDHQ+oEpK-CLS@xtT{?vi8JY*Ti^rZNz9U^m=6~_5)j2Cd0ZOJlI~9 z3fev(#7wwT)47OimF8$@p)r>v+#HqN-O3^rxiK*c`7<#qD!@SjtjyVj3$i3Ga1~)p zbCLpJz?G;-$)$lxX!)LPGpEM&s#(if!>aCV8XETDNg6$B)}{)UhbV8r-4{>F_&fnv zxlsVDKAhvcs}__}+gc09%ht5B3quUi4~sAaj-2Z-ZiuxCPpqan$jqED9}CFLDHS$S zB3ga8a`?iDwED0z^YSU)JV$1YUzCLmh_;PJVIBj9MUr55RC7@js>;Md<|DYoWImax zy8_hc=$W%>k(i0fvIHmu^V7*RYi;IBi;W;M{3;3H#sEws;4cJVKI1r5*I?m*Am-3^ zh#__ich6drQb_U4y*u2uaEB;*c?4DMe6RpCHcp|(4@R6D;g+ffqZ31^j>g2FSz@gy ze6ZYEr1QZ_lHA)JP-2)s9ML@_v(|e7K%3rUt>tXM7ro~8#QxRj)riA~J)9SkBn3r| z5Vw{kB1LQ>YZ~PQG*+<@=F`j=5EVsfX2dBZWo96f?Sa9Dd}#A|!g<6K&}4Xb^Gt3= zE!%Nf?}a=}D9W_QM%$QyK%4dUG6kM?9@}?T-2T91vXTn4m;=DN=0z3&&sto1hTDCU z2@b+G=~6YjDW0{sR1kk`yfyoPi&=Nex8HGbr!Ic#J|%ceE4%l&rIdh?+1YlN>$;x< z;fpPw1o!1s#%tu6_N5TT;}s5Xk`@QT1L<{}V`RF}HUt+~J~uPIx02Fw3clZG$SHJr z)f=ka+y7f%|4*L&+;4uymwy$2hu-mrXW#Xvxc%LoIv16>{U3+uGJV?}AtU-$lgZlkXKR22I$Ud?hY6Mzz1iq=?rSx9jSp_-!;?ZF^6{j=*hORtuj7Urx|s9OxQq7|1>lSJg9L5vQyyGBqZ zF;{gJsxn~&ZWc*q8iIYao!MqKiO_T^D6XcO{5Y?_WQGBQoh0$4_$o6X>}e>orpfSAAKi9Ut*5Skxp0C)y2F= z({EzB6G^y#x~e|auJ#RO38E<+Gn9-F4A^WBHGSf2zBnTr2*G}=X8@qth~?!=*lUH> z>TF#Dh_s(r!j@Y1v$MG$S&WrnU31-D)*gS}w>@nkW%-fK4YX4T2USn+4|0ES-2s+2UxPhRQ}eS$S9`SJT0k%(U7uf+ z!^3B$9Vvpll0reuE@e#%;+BwWq|m&t_}XsQhS_4+DyqyqmQ0>-q$=^ zbB@#84W~}N=iYli_t1m4+5%tZ(PifG#)#_ z-CP(bQdOh3jPWV}H?^2+3u+`4$&3L6J2hi8YTUH{wo?dWbdg)5QM9eS?!iv#WmS(d z_n6e&u(gqy0f%U^+L(K0k#Zf)6NVU}kI(0@2NmXcBMmSDV$NSpXVVx>n^qkYbr>Wd0TZPp z#8lUHWPvw?cjtrEp3J&VE-?vGNbFf8OvubUsICZj++LdiO`veWXnnA}NJ-Ti(=}=$ zfI>AdKs0&7I2VNAdlFfIn4jL>q0(kYnbviCiQ# zEy}X&4I?cwppu%6ClhasISMt+T2rh3h+U$a-)ufvY7}xrAQG#!;1t>b5RnJV=#0h| zuB$>0n9_5QLiYDKw5oDorTq?WcpL^pgv&~~zIlemIzBxYmcH20)_}a-bcI(kRblQS z+$Fnzk1qM+<(0?rbqocNPzailBJ%P6P9N^6k9r>9b^7TuAj^5+*Je+1n;Na{3`0MYy{m$#3ef`CkUi!v2zOlWt-Rt_6z^r%0G&A;IgerlUdWpJv zK3Y^lNQlw-4O4djXAInuABmGj0L2(Qi`2`fh&fh*Y@DsqQY;?;YEqOUfm$2^41`ve z{Z*QQSV``JW&uT5FEAfh5M9^uvC$|Wb63Y*nPq$&^MIqA3xL}nXkTh5Cc=SClzak* zdWBclD4=1c+JwH~%iQM4e|M6BRfRZUu)bjPhhlGDn>*gDC zr!TzOvs*3rXUTg|)vXt+!y9>2%P|EI`a%LTZ;f=^jnS1@M9w~m^lfTF2!R6;n1ixn zT$hL7nXDp(Ef7J6mzei)IsWfpa5bz7)r<*&0+>Wa%}ms50!Iay4|ox&FNUfD2>Tz& z&hz2u4u}JXpi(fogLz9OHAN}T!Fz(zOIH=@Dl}S4_CKX?^56Y$JqN%cXfAHf4h0BB zN*JJ0N>$FCsl}=qtVm(j`-u!ai5wVnWHRO=ebXniSaNntFjG-bh>5&&lnIDL3;bFc zTT%jQk|ra-kIPX#AQaS5MT6vHW(}>o7v3!ysrt$C=;voMpT*ry$>lybbDlo~DEx!P z0>I0IPOB!GipB1h;^=A`f4Mk1W#d@@jp>eh?`*0Ecakj*4i4^U2H>*pjY(C(LiL4I zVVD|5Bq3tcKGV|{gqKqSur&4k1Gt{5cPFX9?Lkl&n8p<3?Li>r;h60CulH%&%fl)9|M6&9*{abHYnK-~au8b>qedzw#@8_uu>v|Ms_j>o9EInF!u{cU=02_iG;D;!P9RJ0JWinvdhSL$`St70?| zdyhI*8XC)B=xY?+G9HK_E*1*_v-#Y|W3gNSw6(bf0GpzXLJAXUmAO>)MGc^%d!wqX zM>BnE9SUB1*tU&dpCK>^-ea*>QbYQRJX4-j&%_x0N#b6k-4{MGOA+rLSV|G~KH>~m z3_(qc2oYjTZE42K1ARMW^5m(iF7GQ}W{_<^9~O6A6DUe2lnocJaWOl((^nCS6f+gE z5Cel$ggI!hAAhDn=>4;}NcImk)^@lsi;2-UuY{u|4q<4cWBru|BXYNk-Xzu(LmMbF z6KAT}72oP4EBTtiaXAsOSBe6n#YKcPRx&>0@J?K-&)e|^eY{lF4hx(V2}JIqbTj7% z)SZnN->PBK=$2$F+pNYtDGmqrMOXj3>YH_B_Wvvw_dzHnGF_kCO0v4l|BUZxaDTszRcUA#faEDkA^ofAwGg(I5T+4P{D1bp3@F z`LmzxMr;M7=qkfdj$a zIhkNC0_%H-xcJ$Z0ihxt)_x)aaYZo!NHMBvk!)%`)iE5;!;D6G)Lq5c*C&cdpTt^% z=8&vJkOScz&yhg|FsP~xntd|^9Ljjrq;VJ{}e!;5(%~ zit0-RffQ37*4)~+|29_^^|s?De6V_Tm#CyvF`MP0WFV-Cpoj{*dk>bTrrP&&B%&f^ zj@^{O;NZ}R0O>a+>s9S4V+-dV4VxK+!Gn^d*UJ;JTg&#nLHU`(t+j6;T{Gu!s1ku0^A|QY z-2~xg?nW1-A1#;eEH&CDtt~El!nhLS^;DheE%>A5;>D`!CWpQ-d$W`lKnOd-lLsQB zKx#XL_u3XizR$Jiv%ZOf0bs)BB+2-0+cG3kR}3;~WT^1KBphqiN^IF&p^8Bo#Q8i{3x!fA(Ym>x>G)2izU(rW)uiy@K`Wtiz^@0S zQBOQ^_J}`v=JCul;u^5^jAU4Y#UC;C0^xt~>;F*u!gY%w`Udr^{^~FMqSu#+h^R7I zUDsdv%2!OpTkn~<4h9UE^$R;9&LvZ1reML%8CXr2Ly>HT82Zv98c+!_+p%?LKVaH- zcJBQ-o&3N+Pq;IK(@SQcW1)fPv3xp5-&oh{lmlt@JK8ZjE2^F?MdWaRtl zNAkEEV{gkTZc=JGxaS){rDP}RKyV9&zKtNU|0|blX5PBn?-+iKo#lgdo;cXst#Y-o zGxrOkSVtK~2uLbu4|INo7I$T_>_k!TkTqiJUTD4lEe$-b6shMM1S@$N)6?lvx*AHV zZi^PlOktysS|$zy`KQdRQb6QAK#LYrGpMSlTd>ArICh>lmQdK;!lqZFw?YpbLy6C_q$ZwE43vn2Q-d)gK5gCA1A<`VM4;whZE7_62T06rYIQZGY*m(xL2~D)7eu^_&ys3%jcXP*o-9B$pqi2 z>H7FTj;3rqO=CTiHN4vTTu#5l1aQiEt6vx85>gZZb6GCK9Kc1}+ohcaop5PuQ3LQA zLjx|_?k-*QM_0ZxPScNe>gUOLuy|`7(mZSHhfrVRcs(rhcy{h#5Xc{ct@zBDtu)*2 zmc0T|dyfDNa2Wt((65XyagGE=2K8AsrpnY_c;Wg#{g3}C5%(7l^(Cg?{KhxF|AQZT z6F+A6HUx;WQ}1T3BDrMdzDc54;lL*5#$aNDxnMw_yJU9PeF_#L_r6HIPb`;G#1sV}sA!Sm z)D)

E0z!L~ZB}`558F3O)kvx#fgPj4_uSIC%Y_Q#&Dq7{lE=w?#^~=u(~1EcS$Y z@ESa;Qq{Cg*A<=vKwxxP*aE3A2O?zw5P_6%T*OKQ(!NWkTWA?^@M)3c4d3@$;RqA@Wy<@8w20W`O0vAa2KWR5O>urgdgOYw908s-I5;Z zI9L5=Ka}#ru3MNv23m!I0WJBXql#=cX3%P8cd2lR)mR%iwc@-SUA%8hsEK`Vmtw>q zkiRuLs)Jk}-k6Tu-UWok@OFzqs(1M}zWyKl=GWg?$5%b@d_N>H<6{3SE9UIKzH{ll zwppz=^iSuA^Y)603y+J9rf^%H2Eej2KKaA=j^h>nDVk|@vr*qZZZT%%JXL@b)>Pv* zW}|4)(bZnNF}k?#(heqgYugU?>B54J7Ye&{(c#oCU34@?XP}ewX|T>>u+9V6Prr7R z`Wms?!|qA6R>*n!%^YlX%dT6RfsesfNW(%TGTJA01y-{#=rde(!AzjweRDjKXA`tN zh(*|-3|7?;dmQl5M<4ymfAKGV_~W0#+ob{I>0-0jUVH7i>n}ND5n{|;8)Fs7wL8ft z0}+WSk(rdzcO6#q9NDRPP6tm>^a(W>z(;0kAp}mOhr&i7Ko5<`>KqW^Up;QQa2EsT zeVJFW&ApMgdP5Z=QS~OlAV8cb@ESDo&Acxm0GD1(O&Q!uV3aQT!9rbFY$>v;B2+LC zhnQlZ#8QM!20qMpVoU(3DZ$);y9c;L975tg0aw@aE_cKAWUf)G>)K78T)UHVGuDDJ zlYca0msFX;>bTyFrl}u4@4wGx?#D$jA;u5JyO0v6v>KYZR1=e8xcwi9}ls% zju(m)4(yg9lN_s+l0%4NHtng0Tdi87reM>+OdU!Pi)?Hv%8|?vv?gM<-nj}vVdClT zK35QW3g2W{%#I}Eux3H1l!E7CiQLZAyib4GHX`Nh)$3D(O8_XNbcS;8E z{MY`&FaGA&UwPxJUJ`i5NUhA`BQY>}QY@6C0|56+eswlq>-GATjV%CqzUF=2Y8wD+ zyFM%MC;NM=&%ZisyfYcDx7%Ym{Ak(#+{yMtPXe%pIsE|$;h1Ftdb#c7zsJAYNHtCO zxI%Q0*7G5|+c0QTtufFwDhEW_OK7DFQ> zTG^!?hIXhbtT~tGBe5Pofwu6!W;l<+on#*bDE)V+-IU1nEg{SvvYcGPtbqc%)~`$1n=0RJ+)<~Rs^Hn z6aa(95Zku(G4pqKlg|?u!L&Z=f&FAhcpU<}QmA>-i9$tvuAu@V((~DZtxpGwQ_0n*Zlw6yV0A7o9nYH5T)iVeBIsS4z!+@4DZ2S~7Bn>?q*be!h(RzwOpQ`Ia zm8I$E`>;jf`-74FSQVIvN3X1n-}r5RgY8ef=6dq&t1o={(sS2der+rX6bs`dQ!D@| z2;=XQZBVTL#H$QP_SpCeJFKmDSy3?w&S8ej)~IgfwS^ ztihzpDW;yz$AokNPG!H&;IZr&r%VhxuTl4*acv0<_$dyoa}MiNgU_P3CFT*LL(WfP9Zs-CrNL#}OC3C`XyiK=%PPAOt=6Yz@8oQoEL z_0~1RPjaDH>0#XB@ytcn~5TV;sbpA z<(khonr7MTH?!>vF(i@9%GUW^mS7elsVG1c2m%gNiW0NqD(hjY5@=|v{4~B|>%#ng z>?T!xHc*)XA~hRWNrFLphkB@}!Ri<(51*)|P^VH#(x4>bx@lqIoA1)b!V%#Nvobo z;q+qp(V{c^?&9cu|GsNC(o6RzG}62;bnRD|m^hNVh1Hz;+CbMrxy^(N+pm1>>%QgA zr(Of_*{d)3pM2>#M^3-GbGfs)vAqL8EzGvI0mRK645$dlNQfLE6@(?Q%qI}i(~V!= zy0F%-;daiSUdL2<{Mo_2yS%>kC~b>Y!|5v@_q5vfYrk zIw_wTp0wT-&wLHq+y3?3RG++IJ; z$hG3cMFU8p-jy*-q0*>~CR$Sty>jNBSr8BQmQX=o_6~9$geVxnCA9!XLAR>kwYe1c z@224H;ihKB2A>al<*W*G2A%()98noFO76ss)Kx6dg&@8D2|zFIMqfI|MYPM=h`TQL)>cG|HUm~fF~Ghj%f$7^ z*30rk@P>dy-Wi68DTe5Ed>jIqSZQf+Ij8>RHGmknh-3>0ZiH^7WEZG`z~CYUpuWO! zsvG0be|BOW`Eo+fw3!)DN?!Xh^Ew>{`lgeRu{?gjnyXt9ab3@jjt&nOM;A7?LP$gi zf%^-5@c7V$@Kzv&WQDPVs&TpKjI6u4o-lcQxVMFB27T2#)vxlQYQcW0x4tchPSRJI1!f$+*R=@o zxzMKq?@M_;RRmn;3cxL=)BeHlCSdDRuLIb4?j;81m#&Tb{LD+6xy$wD7Jy=LzICBk zm~UN>3Hs@YB?|!TB!T38Qb*Fc|KuNEDMBA|a6b)|gRAcLFILsK7gH6|;9qh0E z_rtFH%mBy^CY6JC2XZk_II|J9)@SSDpNYtQC8xJ$13I}OfD<{f6Rg&W&D?xt$p68y zSuYbY!>AV-U$?^j?zJ0LDD&81;$N4z0NAM)%TkSxd+kPZ_bw2avULgH)XZnp~tCG>X+SvlF9W+G4@{D%5t#V7?M@pa!Eu;#A4zV zQl89IQBQGtTRL&hDFii%A&Qi$njOpJnRv7aG<3Ney2MP)MUN>fcX)Sk(g>ucb#SG~ zlN7{_kR^oRT6J%hH{|9t#uPZ1ib@BH?%f(V^xgORJeQ};ikmnDW~axPxi>1yy+xGU zE2tEoXjQ$UpTL0!vX~@_0^pDbkDXN(ez!yhM~5NM+Q534sVl!Xa{EpQ)K#_zyQ5^W%5E^SlT8sNwz`7;XBQIB+Y* z1V9ym$cPNZb?Rb;Bdcla#N_jn{M3jh_NX;su-BO%}X;iONHGo(CvwumOYXtiq4%tT9*@45+$BvMm zFm^_4H^i=#SL+!j<2en7Qy0!aLT@z<2IlYS#(4pj4eZfb|DR6zexK`ab>^xJ_kGT; zUrF^mcWta{kus)Zn&mz~9AZ_^hkf>$K%MTM#{Z1K;sjjqw1W*vLt8~yfTOE1oU#I- zMn%K2RBCjTYXG9jU>nzdb?@CjE@JP#`RB3PS{~k0DV+H24}Jh(wmHv@gvdE}2J4Ev zf9vMu?H!nPxd6C&`O*isZ@qN&+OqBDb-iv7A9~Aw&`rhwgQ-}ScGH6|(>shfsRY#@l)&qssdQob1HqKmEOgme|M2`TD0^iF_;%1 zFb8%QRt8MXsu+fBd!SSk$hA$w7m%b-8gw; zM5I;-)b~_I&#@7)g(P5u5xZ*!nL^bkn8DO)IEZ6D=@?Df>IFUC1Ri(j9t~Z2)b*^C z(zQ8-7y`qTGZ|ATPF#)<1R_$hs2wXD`a7o6@G2V@ubtleQwr6-leS3V-V;up?hPUK zC4^?e9TBNmNF*u-b2lr1Je}=KUd}nj|DEl=SHg82$!$CcJKE4SFgW%uy-p}GY&CuU#J{|YgJu4^}-W^@VY`^6uJ*hXgESTr9egX&i&nc2m5B2#mdw^_u8rZ#TrIx z?fE$SRMGe-s?v3B*I{5IQcN*L<`CCat3F0NmD`3$M}!y{0E?%C0uiZ*`wJ4NloMTP zy)`Vn51lueQI}@-sLKn~NYzZ3gNS$+TVggf?Iqp-`WddLwTQ%ey&yABTRx+=5!Z2f z;r^Ty`Uzi)4aZ4l=(-}_xUlD_OA!J3OH_0~9gh_wGt@sCMib6H^~Qk+DWw>anz*7% z^Ri0S;E+kGLQDjTfk7mqT@eRTy)Bccc9;M%?hiMF;IM{?Lu6HRlXp=A85x|4bPh&D zJPYaG4C4nLejBb;ivH7%fzo5QL|L<9Sc|UCNnZ^vA1C5GDO6@osZyZLg$ij~1|2F+ zA7L{t!q*%p5pI9*vum$>_RKZXjoPIL26m;3*x9w=bI9RmX{WV!;LADA%44 z1mMKg$Y43lI`KJ;K78l@4T)ZQ^{emw+j|1nNH?B~KbaWdVmvM00z?@RDp^6a4 zi8*Q827tkew!QcsFzJQ(kNNT4&XmZAJcH)&2!y}^py1GxLyXBk;|co2tT*L1WgU8ZdDfd5kDsbL-t7KMRZ{QVNMxtkn04vd~dc z=D=VTPa3i`;<7M!&EQfg0RRC-N(|A|#O?I~0%A+AJK+4vdPe9spVN^VnCL$ye!YRqA4`-+)ypo6_z97wX=zx{!cQJ}h-HH*g7su^?O z(z0sFZ4+liM5REB-M;x&RabZKz6UXY(u{X@Uf#WXBULpFv8wyTVt)jNV=ereXr)>f zVzN4PF2h=us%;kYc)PU=?c%N$on3r+aqp*+yBKC|bM&(xej5S=eqmD&(H!3W5p!4` z9#)$H5NXT|n#D{*j4?zGVx19aaqHVLrJHa68G#V!;;X;*mv8>@h08B<*{Qh;keXN3 zwGSb*YaBi>3e#TbeC!Q|F6fm{a7$IfhyjktFeqQR!&xIT0+3U~d1 zYp@_voVQ9TscPV;BH+Ycm&ZX>#LR}AqZ-J(pK&N%Zue?p%O!6<$L(G}wMZ$BD%=O* z=;)|zmS=tn06kjj3#ORK%p!;J$WshOdeut|971pldjkwC2yO&G11NEwxOYB|lgV|o zV{kT6yp64tVr&YhK$GzvwrYfAO!UeP6J9HX7<+I zrU03$7>v|lF{I&w(xMWB)pd=&c!gt3Ip?lzQ&m}C$LR}YQWH111rWW;;o~YKJRYC% zQdt}T_@%82V};tMXY;9n`zG@rtaDC!rg6Qh$K>N!wl*Hp^B?okd~)$W-@6Oo^BWs8 zJ~4T{SXFNgS-+Rom7QHn?$`DBZ#~jiubr$?+&$UL=$rs+xZ0+GWk6guo$kp13LFuT zyC$VN#yaQLRF{i=)pl$9`Mo93I}=+`N380CJ3FHfX1DxOWGjt(_}Jci&TM zVPJ0e+a)u=^WCEZzz!m7?%e_M;%yPrtR;|S;Y9UDY>(uFcfZpu)yxouh`J*MFuzcD zOF=MgRW+-Hb^+7OF|JKx9Q8yL4&DUJ0ue^n1j3jQ#gsYR`<0|pz_EOm?R5;Z+`O|Phod|6Mqwb z6Xyt1A&@SN>VEO0t%X1$C4>k=jIqyj58m$7w|piW69v>$ydq_I3?Z@x0aMWez+fRp zZLFX`q^i;&u18&0F0b?=U)R68tC@-tNVKpiz$fAL#~pD3x^L(F;vhx;vxEKD=krJ9 zpT^R&8!NSkS4U^8VPUX8RQZh5%Ogqb|MD1GQvd)U07*naR2s+k4L9%KJN2Ze`R2?c zx;hoT`>5HemX01DblsMx%mEBNf&lDIHy)N2ubu7wFpYizJ8GEDSgnq3=>t17g<3`` zxzFpDqTauPVTx83zM>By!Ay$o-@CE7bE$N>Y4(;!8AMiuh?hr8A_`Tis%m+#Nb^dh z3yDlZZVNMmRf};n4ry_;N0C@#ktQ$UD z_O89~nGfIn{`Hr?_~BdMOLZuXnV0}-hqLXg?ZIsT6xp)G8RunBIha_C35K>Q04?^K zP_bs#*33|Cu;$Polc2GoAq~vPX55N?hNIK;} zIND%v4BY3&K%%ldI=X%P#-+ucun!Kp#c8tvP+KLT*=+Vfs|dUSMj z#(+O$%`=VATU7M1v2*RUFaO>6G@4eK_j2x}PO)TR0*E#A25d}Rv;+%Y_ZLI*N>#^C zePxN6e~*~hEjoun2Qe1(MUg%Cq*l@n}saW@IWLP(ZY*J-TCt7+{)=G{i`Abd68|5krX>RC==j%?7qAZYY(m zS;90_p)3iYZmAR~a#c!4L`H~-fmpikxzBv<{yYDti8g!RH-mQIG>aQo(rkxA&9NR) zSFunKV96m@Bmq!U9`V-6t6-f%_Vg2v48lZBQ~>D)vAn~q7-M)!N_h?mu$Yo$xaFg0 z2{C$QL=4e;+W}A&0ywZ=hfUK2W)(FRw_R|Jkz+y_LJU%*ltRoY_RaDDfJju#P&$ou zkgOq+6(b_)v|={xO#wVHSBF}q08lA0rmZ=)qFo_cw%x&^X_{p<+Z4iKhq?-;B=EI< zH0uol+T6?N!D$2_1kMV8VURoSD*g$~B6+0=S_r4ktO3aR^j#yYn=Rv2sOvKKRK? z8yojaS*-J;88zu^<8CR}Q^FZtpU)Zp?(n!caeJ5$r+sq#JU(cqv^9SjjE08G09@$= z7;~&S~3nP0JS0`kW!9SC0+J9rfOE@wgX7bI8bi03&QSw^nPj8 zRA6RG1|<$A#SE%BPzW2TT`oXfoW+s_76D-nCFj7=l&B&TV-Ee>F_~%%6<1<8`B~E+wmJ6)PqvX+#AG6w-RG z6huKJo1l>lLuZYdGPBFuyRPlprK*@Q44mqnoqBtF8xv4c3;^i^N)#R?&MyyN#S^Ph zCQGY{ufdwPJ?ERQdF9Fhi=3WC&V#O0Wd-N0VZw;`jLlYec+Tm0J-S*4&K?wxuIpd_ zJvy};A{_00^zIM8`*;|u5a@*Ot%mW6_yQcE)nERo(Qh;Tu{HwB_pqG4@DRiuBUY zKg#B(V>E^gq$|(ZWHX)qYy90G?==s?H_s?OH{bZd^2kT{$_fQ=Jyo}J{;=!La#gDc z0N!u9$uhW)DKYk_rfierIHs=F2Cd<*#vBj8;F6oegIfe#*uHkKd&^9T5mF^(Nu(uP zNL1CcX4%5PQD`v07QXywy)p6d*jOUFXz&l+NG;6ee}UkZ@&Md7hZjXIlTAg z_duXvrY2pETU%|rs5h$Iw9HZ3PN@qONhwei$uOhBG1jVG?v_?eQKU#rRkxIljSI$7 zmc?Loyj(r!;KXsB+k=ar`rEtj{9n6of9v9_ zfB(U!Z8fFn;;rh8l%j{4BP4JC#N+S2!{gt<5ASIpPoxkW!-*?5K}e}0fMd+V9nJ9# z6B|Kt4lxXk0ue^%s#1z%8Z7>E*T~SGFXt||jZgDl6WC0aJMQLq{3oC|0SK6BTD4bB z)4u~wS#P#99i7KPqO0?Cwc$#;E%uZ&wpE%t7``=jqZ_tIxBKmVymdbyemx>T4L&^M@7 z)euw3*~@axTXUKj5Qh^@OkLMGr|TWr`=fDDIW^d1u+C~_(;SlMJXcLI4}`K z4n;&OBPJ|Cb1zdpUcWQ5W^afogQwaj*6DeS0Q|+#p{uW6tm|np+7~u9zq?X;c8WoH z>tQ7_=Zw8|%AvIp$Px>Hz^73WCQxaLitvcn=hIh=JXW}kds|<&CWnsk9BJ5L3#_F3qbVtsA40Rnjd1%r<7FGy#>Wa1<)RRFI)09Jokp zV)Z;qQ7xLNzWveLsS0K|*!|EKz_a-l$MpVNf1$--7Hi5&0f+(?dwWFGs93R(T12ZN zo0RR&%I%xsz0-=$n z|0|SsS?u1r0|0m=jKyNH0puahk*q7p*j;S{JU%wi7xPC5{CrxFUm1QbBVfpjo4R5p;Bwz)e> z)#d{~;xmc&MYl(5hNb~>3H#t#LV=CTfSb+bww|MR*pnT8SQInOm&`ii7=I6lv$e^LzI` z5;9`4qNYZXa#ra|wK>xaN<}(rxNKtgxaa0)R-<|w7LA`~)UAnKY- z6vQ$qb!z1KX0jMluc?eE9dS}s?b;7|!1UAjDdYCbm2>JR zW$lSx+eBz>N8$0@9nV!Kci0<$^lh4+N!zitF<>ma{QS%3e2E$~xXe+A$rM3(wBUC( ziU1D5{o;svj!L8ug%Bcv1ZtJROhh=5iJ2jA1d#%qDiA?BsPrr{k@`}}yx35BCHr76 z7ebU$su-0_4UvhMi+CqQr4WowU&_y!_xrzZKC|Q_^ zm}8pfyey4{DupWKMq@ow&00!5+d^p-xvs0+s+mHKSW2h$M$B153MDH6p=J}@djCz3 zabhd+){UPKf!I=2>tYE@)duoX5kcVGw%$>TE4G4ep#+4KjEV``MW#qx*qtNlO_m(m zWv*tNmp0o9r6GdCkmjVh>K1vnl_XcC?Pi-*>9pS1$W1Fnq*$6;X_SaAT>HY|M?bj# z?jKpP%b)&r0K4ye>*CM-{U99Q|{&DLdY-$6ESMQ{6e*GoFb*t5O%ZF&jRHi0yJa)moOf*li9qYCrCtpk)iTAJN<(If zU?|LqN>c!UQK!u!UE4II8t1Vz1fV$-j@-AXD3~IvJNw|`|tl`{PWa&{9tr-j#xuqrD1hwC*846-FZtnQABp?z#fXO zrg1Vk)vId8S$ndlho~1%?QNATSqHFaEvNF(cy{P&d~#?wUB*8hAG@2z;3hw|onCYM zd*zkarhhJ%ORR&gYikvsS)97bJ?3G+3++{(a{!xFI8wpHv20(;eh~l+x|Y(pXzi6@Hvp2mnCjd$&I6ayn3-iW*xtR5hzSM? zD4kLu;;?h&+W!5!U2YguZ`R9$WxZ852Q88iL9=Q`h>2?IT1_?QrGO9wZ zH%Oeoqx0gSvyWul)iLHXTQvxHI?LHrsqZQJYE_PX3SFt{e7>owV}nHiZMy{U$NL8u z$9DW+K6?JRG8$i*pSm6hf5o&)pJ0594TA9Yzx$JOVXR`Qgm^KU*z$gBWKbh^{{rDL@><|WVyJUDgc4-?Sm73`mI@K_(+ulzuA=+ zs>;vM%nZP8DQ<xBc8J$gg-K>`Y>Bv31>kwc zg=z;o<9zq*)$xxtLH|E{?;S45Rb7j(z4tj)-8bJ!nk0dU0t3cmkt89>MERL4TQ-mQ zjE()w1KSvbv5oD=_e^lk9}d{~14K|jC?X^w31xl=B(P)&P@Z__#_p=zGq-0K2$rlIYR5V% zhlPb*DO5!%p-SDkE(z*x?YsxzhBd_yx>gKH)eTS!Ro(){u8KJJ423HwbhM^Gn1yb1zUa($y)Rv%nh1s%s(SkN)EzIzwum1|)gCD)EY&1?yT*QS@V2S*Nqmmq&6=2KPbL29=XU#1E(=F2Mmy@W<+H{EN}m zqHnzlw8{%Fp*apd0hb)WEGoSZRi{<<%|zTX{`-T|?^y~{GP>&TPli_cJLVU69eFg+ z14E_&tmkjfh}mFm^1C zQnVB+FhD6*$*co2LzIG3r6e>zGYx>4LaptwvO8a!S~*n2O4SlG7X`%%8iPjlElR;+ zHxVJlnhUAxxl9fS!8is3G${|bit%R=nT&N`gWeK+EEbc>o&=d3} z=xRysY87;~u}Od_l)!84TvRRWn>K=35adrgU$Q;gySzFJry6U+50 z5~3iPz@D#mgE5Q7Kt#;kC_I=pf9@5!CMO=0F<&BLZltyr-6;n%1n6jLGw+J=iS40& z#-gjK--dQiuf$G3RtDT%*8q$t`#NhQXMs&+E7z;0Q_8FAssh;4W1v31J+z<+pj?*f z_DWZ|Zl%_1;3vu!>vz``fam?r-)}zpn9YxWDh?4H@X$tWwo}s`RrfAc09(WX=nw## zO)1xNXHu-x)(^N||^!w0?7boY>@g040!s_V`)=9w^5q z`qyGPGT!{M4})#DVowb)0JN-`fDF5Y6RztP*ngdDG{;rhDb>|^OY|+bS-J( z5?Vo{S%jLFuAmmzoy3kcBn670(wRh!q~@F~RJArN0+3Kx&_LQA@pTu@YgJna^Lc0u zp(~|J8Z7Bhd$Nvm1ea1M%Asr$lY2uAR!WE|maYPzps63;kdOSngn6>n@!%^<{u~^k zMn)E04XdN8CCF^ayMv=V7Bj?`puJV-K}$BVq{U2RQGsB|dKSO6B#*W9Sgnq(malyO z!weyW!H%|mN8!K`+voFjV7rHkIeN%rf4}&0l=5TvTv;9=R4wciV43BfKFdAm)br}d zK7-9ImPP4Q4Nz6~l6*=jXIS!ujlQ;fJ`G6|082*M5kbh3^_6y0N~#I$gu#1RR%|`Z z(xzY0j97@Erj`uCjnfn%r?{x+PkxYt>S~?$UA4YG!gm(t6_$z2k?5k%LdUFc!!phe zz!1y5q3*<(n+gR`asal)nETGUs%>Ea9ZdjA<}F2uUdbuJ`n`coils*nm`O2XuY2oy z?a6O^`H8PR`dP;>(x!Y+u@v_&b{8IadC9#;gNv(ituCq@1XUm#oeZabpjf|U+OP)z zaMa|6$Ml31{PM`y%14zl*Tgc` zf{`VF#5*z=ktG#R;{4>Aqs-J`EQmFtR%S+wk0sXA7+yv9gGBw%(9QfrMwTyW|jDav7~JId_HV5VFOCtsz?YAf0ty2^zVrD#kG zoys|HVx&rrC}_#PgNAF>@NUFRR}9fk1me~gsz_PjA!p_i-S3&B?*`%lhMHz zLNnZY>v#Kqt`5S7wWLez3v5x2lyN@Z1o1@8`y2=DJ>VkW)&Bl9N=FSjJvI z_;C=)NQ)xIv;_JO#Qx@%cv(kQQ-fXgTZy;>MZ9}J5te5$2Z8{qGJ{%ClBtv=EH2_14_1poJ>=%cm=#BsYAOJ~3K~z+Sd&nsRsXpPo|H}sNF+iXuPQpbkJc9aO1(+i=vTH@+9Z z+Jm{(#r%C6_+9&0b1}hRjofO&SBu|T=>!-QMnLu%j5f9X&XMu8(^ET$C`DzLhbT?K zW~ZjulQ>dEw>x8ih^Am!Lc23R&7RHFyCPJziIQ;&fgNc=sutK05qDgmQbf5X}3mcuAY}jl|>?=mW!lTTe~QjxvoQka>*eE z_Us&SbSl~n$+xAnBE&=vYNkm|0jD8Uh-eLY)neOSHy^R}$teb3v;o}qt;?4m`X%6F z5Z|nhu9l>)n3;L+?a>_&9e&MgetvRtP5;cVTer6|H9I@~na})7|Ia}PwTyhdPjt1! z3vUowVI_361RylnCSrpQ@&mLY{k99-YT%G zzv15E21CPJoLIMUX3u?V)*mrFwVj2AhStvR*$F0BLflYyZXp2*ZH*M6u2TX#)}RJ! zm95049JwIxeCS5h34ld2koTn`SyW;&By!Z5p9dMhW>$AA08X-(908CbA+cFv5(C&9 zBa#X!g;<+rO?AW+VyuaHWO#JGI|sm(lwxGz*fnBecE&+f)uAj~F;uJ)qRw<@iO3Hz zC1RlhqCo)+6-6h75os0P8wafkqz*;Zt;&{A5M!griOk4|JRuP)5s?Wy9U2*rRS4Ar zv)gvZ%|{&dg`^sWj3n3h^6^lfJNv8gDIxm^ioJ&75u(tilH07ONt_c6*0mJV9OWwG!u$(ggjFA@HN6%#|cgDl>s7htNswu~yZok>`kvlD!CbHIR?m zMb=fUA>UJb0G#2_63F7NC6lTnZhiAQFpp50#Hh3+9hI5S`7JEC$N>yE7S<{ zbMpWh49+njc58qP%EDTixu}&oU&h)T#VjSmo?_R8OcK?SjKPGE!muxAy9*>tf$}gE zeHh}l24%)VRr|L0AM8+Jo?d(>2~*KCJxAWy8qDUp>OU-SfOAEDq8XSS!RQHEe9iAoV`Anp}?H!O(`Lz43DN# z1aO2ewuKr%rcMvP=+wM?Nf_GlcmP>SXMEk3{CaD019}%nIleX@9XC3O-ZR(_Pvm!3 zAqfqJnb9J5rzOR{`^%4HyQFH1ao)8g)Io##Hpjs1CB+0Y=L;AO z50A~wOnH{FG!YIlnX)jqM$42m#0Ue-e5*~c7!s3HF&Mn_30g=27(qrxht!OTk}5Gm zjP0T@D1itV!B8>{NaP&BELkUYOd-s!bfx*Z?n1jL0g6#!(RrV$2%s!SyYn+eYq;uG zOvSc4Z-&vGw_LvjT@6;&RzX)w9{CwvnJGX;;Z-fy(Xz~=s^0eUp9PS1ZhO!5S0N@K zKYA^%x4*sq3hVpsU3wc2Bq45?on0oCbWmeI2jm~&{8Kq_RjkO(UoN_{f^Y7R?)QHL z2H}1@r}uTVSEPk4U4C^CUoFh8x@%?&KnXKwM~TSis-KYc*te$9462Q&dP;#nM3hou zk(Al1y-rx?d{uXuxZyaf0!W&e9oQ=qGgH>f4VA%!#i*%tH&DzWsv$6i6zmY?M5oxqnFL00#FwaQ14|4Tw(li&=`j z4$q6ILcQO;=-}%sE<6~AG1G$%w+Kiz9QG_GYmMwaHObrDA(n3rBB*K>6V*z|14NpP zm_k(#O|(I@uxEjV+avAHY!^UXb<0+(;G$z4Q)n<(dQBk(H8p7PLxoukb&7Rl&mdS$ zL$2&{&C2lJs7hVyT6d_`j+V$#p+%>z0QfdjD3WSQ!56-cwI(qQNn`2TK`ToJShrdz z+Cv~48eQ9+pK%^z6B}ZzyWQl6HtW6DF)Uhg#4)9o_slHJnFp!>#0rtp`=q+it(G zN$95AuKvp43F|Yd%~Zto>wPID?~6VQq~Gn|&mna>%}sL4Pd;mKg*-gc{I+K|0GnTA zEI|9Mm31(K=)eG@{z_IC;1-dTQl6b3h>b4g+7HIm+GRq_%Zr=kBgIMS0LG6n(p?t0 zUiNTrWksu^eFa4>y~LoJQsG_HWNHRuak+UgPrV4lEFenR5y3Qs+WR7Bs?-!f2(|M? zj6qmL#0*(;K~;r?SQ1Fp2w7J^m4q`=${Ho4yw}!ED2o#kYj$|fw|1^D5&@)Uc8Hj5 z?2F+w8vu;VOadZbv|2-Bow+G)kA?Y} z(UD<*6m<->YLo$FqsgE~1U6Ie&CCg#kR@}%Ate?vpd9wqeC^7jUZ||C8d%!i6$KfE zki0KLU8_}l_UuLvC$-x{gQq8@LDm#i18qdBG#EbNTh}eYSpWTEf1cI{_KBWSTG1JB z@fIJoG)@jz1`e@Bzm`PVGDt!u8=eB;i({yE9UO%GioF&n+Y-N(v$x9d@&Gb(0^?vez&o zN}5VvE}_6sV>XmxtZ>M zPlfNlFX6{FM3!nn_e)&Y)ut!F5F&=!`IQ>x4ayb~jKD%MB^U%2Q}WIyRS@N(5i@H_IiV$-4RIjOrx?61 z)s%^I2QLvh?@hITn25-+BVkp*z(h?=%W`*H6cG?IxdMc&CYldxo@N{6g_Ve63Pfz) z3?N2T&$h%ZH;g*SA*6@vWrt(`!%?pJS!yz~xv@YMrrA)W(vS>}t#s@>!>m(bp%a>F zRHKoFTwG*g@vNqawcRca>cvSRVp3v=1>@_EAO*~3XQv!{Crr#K1nqU1X@bDA%!(x? zcEfFNAQO$2h&aVqc$ZQlgR(I<7S$m}@0>+Nrv4drY`@c$JTksv$M(C{Z#Z&%Y*YXK zToH_rw209*oTf^dfjP>x?+PSH1%|QTSOhv&V zL=lN8Wy#~&Jv+@zio)t@uG@v0D+|*)Da=fK5xW(L6zW?()t1|*Yns`o4D11mM|8TP z4=#^6=BWcO8Oeq_I_g z7y+qqIy7NXBLj$-$cRbJz}jXsOP1Zf@j-jF~h= zBL?7@RMk0X(xjm4R8=vzgIR7fF*SqcTqap|WsfnEkZDq_ERT%}X|b6FRZEGo zO-ObqF(W1dB~2o1rbL`l^yI)GpezfRd3JyaGe9g{)wRpaJr-gB)+$Fk-MLuVo@(3p zZG`%w< z!km(pzDWw^hGaE!1Qt#)nFSEaR$0w=W2h}zIqWr2s4EZ^!`_k$p_>U?!ajDx(CDG$ zi9h)89?Kie7N>}wIl8EUKR=)P&#)yfIsx zUos&cBwRBuZRvk#jQIUX?)>+KH4i8b)L}4Sd5Ux;^n>NNVMU+#13*T8Fg43ZjwEb< za&_=GJ+*O53{{Dd72Y#YWF~daTTkKJNEOx$^~5HYybeltQ!)(-m}>IAsOpMY6r8l- zMX8ykNT#{fhL}@|s);xR5+@mI-xqG(sLEGa1??njDa@Md?x|cE}b>w z8YPWcdMbD2s;VYK1Qg6s(_R znrn8Hw+JwcY08t_+%_CzXca9Yfj7X0-2yUNVkj$sJ5dAuUkgTr(mv!2*DMB+a zS`(@!!m4I!DMk;2!8B#Slu`gFrC?^nWEne}rKH{oGi5y(CQ`urf&|q*|O!xl+x<7wyj&YUVH5|TelwhfSS4X73ZG&l}~>1lb`?m7xuLP4HiA) zO6k4j;AZ&&^DF09j@rEG*7MF8dit}{p%Ws17-I1IAB&8WRXi0zyntSXi9)U=R@_MTkSn z;##IDF)@3JPyiZ@1(pm%S^Uz}U|ERENMI@Vo`V50uw*5zOkS;A9BtY z%VZvN9GH1>tMv%MSi{4^!^5rl`DSOH^PFc_RkyCI9Xqyf-TJ5lyRA-VVTrmJ05s|( zn4h0(wc5Q`mb|-YRGpun^L|CqgpB)_;*}+3jy-GFfr3c;dl_yoEsIH92;HnMCG)U0hSUmXLflbX~j+ckC@_Ylwr)j1Su!*$V=)u7$=Mr`nQ4k#J2uN68$^I6Bd4XY5EcwW1ch}CtxQRTb`&ib z8j*&EDALbLW=0kcyu|k5I2;EdGWyAKSCrX@)SHT_riS(oq!`UCX#yc3fnYE-1)9_@ zF&m7ji6-TnZ@9Z=D(=EtzMgq(;zgxC>|=l%&MEw?Qi>uJYV5`^abHmr@q4 zijC493o(RURtGaP7S4jj85idKM@p%mS71i~rgq$I$=DH$h^bzvSr`T|nAwu~f>RX% z(xAnV)ZMN<>^%2JN?dv6l`nt!%la~^C!To1b=O_r12hL=fL)p%$O?%2`{AYuzW?*E z?bjnZY}wD@IEb+iyCE}8DZ-G^6^K+79is(U2bj0q>Qs91fqh_CQyHBLdF~8@5lC3F z-y$(P=Tudb5}R`_vp1N?`+@)wCrnIWBJWFKDw9Y>PQ+JSsxO>yP3XijiEpO3)RVap zmi|trEES;-=vF*faac6raNKV&EHNUOi~w-?~6 zo4SiggVMN3apeuBWC>PvLxH(%v(wXYKEZZPeZN&4saEA>&}7{JKwOb0RRj`d5J{#} z5eajwD`uCgftsR7ky0R*>=SHiX6i&#%{!ONn%=w2Ayb%!ruta6i~M^&2uz+GF*wC2 z&W8%F#Rhc+SDgsbwOby4a4v;I9v5HywNp-cWv4R_Kt!JV+~;0)*<~5*R#o=}FL?ex z|MNe7<}?5P=}-UbamT$lgz&0Yz4Fta{*e)-q0yYAX6ue{o?#0osWFvBSdV5m%j96mtA(*_19m2)m2x%{`IfVpE~u_H{N>QIp03# z>_7kA4>YqBqL2Q@d%kz}8Q(qoOW*j9Pd)c(z23;aL3m! z`qTHnFJJl3dD>Gi`}C*2^W`(I|I+EFzVa0Sj@rCw=9|~N>$IPny8fz7>(}*HarD;B zGuK@C*4O-xZ++==cYO8y{u=u6suPa8;R~O=<=nGw{_>YjJLMICUHYki`qiI%V}F4k zoPW;A$37RpbDsK?%l_?OzH`UQST1sJF#t1UC>jV(nZp``Wv+^#${NKqmhk*FqL zwEN!+0MrtUi9?Jah_GW$JrR6%E(HTnv|Zf^;(V(pf@!r-7rszcI7@*_R|rYeZqXj9 zyDN06-)}tbTi5^BPd@X(T@wd5`poymoTuw&W(NO^W*6RY{dNFGmbm%MX!DBtIt3G!gs#& z9l!Q#|L2Z7ez0-l=6hQ!+;qnscLI3qV;}phXFYptZ2TMFxZ>+yzx=GT&iIFa z_=h*Y`7M`Ue))5r^X&7^JMaAS&(G^owLw{V)l?M4rcE24``lwECnv7F@`}qZ|M~?N zT=Zvu_KCN=t|H_Tu{`T(qPFWONC&!-fqT{#Ubtg@%$z7Z&X<7KA zCdQxkvRAGjYF&5Ud6!>t#kK!+TV8+R{onrH@1FMV_g-@G#m7GLnHPWY^m8w~=%2rI z#yem4+Q0ndKheHv6MOVP-AFlc*!d(y3;)R(T`m*m3aH@ixfTXw^4BrV=Yl( z$Z8;~xPKnhV5#Qz?UrMjQWSALH3Z8kB4L+@rkJ8<28&tBB30r%*o*WisQ?w0tTKVt z=K51&ZYeVWaRq>+A&=YWQ7M#EL^1^)Y8h`aGdGRH8Btj;iPi8ca$$uCYA{uiN1~;~ z;W!+tN1nV=e*#UtEYV)^g$4|vfnQ7(EOk`7d($v55I4r2x!W$6(t5Q#63)Ktl&3;j z5<<8qpaKZH+z;GKVi6Mo5N2l5MnMGzG8nT^qY#`YSIrb$HKiD0s8pkBG{6+h{V#dwfnYRJHH2jnVq=YBboK|oD(O~=ru6=qGaD_(3fQ^M$6XMYpN;+ z1(SIak%FAYm{^IZa6ZJ2cMr2K%HqeLR@LASR0n@7g#HgyA#TwR%c39eA823U=gvR> z^{2cFF`jthOTO}zbERiM7D71rbLV;I zo#(xunwmDXozDOOAOJ~3K~%c%!V6D6`Q)5%{ky;WJ5`&XpTFp$ufFhw&+l})8#iwJ z+Sk73eetbt-55gHvSo8`RV$!}Pk!>B%xrdc?!pT%dcg}`*y(hRIp)b<``X3c`x|fk zcK-OW$3E}--~Yk2*Ivt`x*Baq2V9S&VTvhOI~}@ODqNepZvE^Z{M+_ zkFFN2A*G$hc@3zm{CXY2k;k2Q$yYC;(Xp$4@WVQUN3WSY{VV4^?&wE9>By}BUiZ@{ z{rg#Go7qcV^uk?xrq24}7innZhx6SpUvlYdUvh$l0N|6K{`B^pyJ+nOI1d&J^^zZ? zwEXAWozC)~2Xyfqs>m>l8*7(`E0!_bSBP?Gk@N}riCmZgB95}aIw2dUiummRoad-X zHPa%TI8_ZX8c0FO48%HM&O_XgrC3N!bEMvXrphc?<3>bGHKvqe@{x%f{sveZI;^-u;zhV>{3zu%562-27owVKW&-8R5YY6DG-4z@fkdwJ zt*HVMAr^^2$zYby600mTpbVy(oCtuZ31&CMK}(!lf8yzbZ}HgPRzX`> zDOQPy4Sq#eeWM09$nH6ux8DgoCC`X-*Lws0Qv*th7B8b?%bK50RVRI zp4zx^W6n!$+qT_%4`6C)YRi_*X8OSoe(()%ctZ%02t`pa%PPgA9XobpW$>w~sV!T! zsHWfgt=~N5lv6^eiMS{VW}2LwoSNEAL|GPL_w+P~Zoc)_H@y43zx=va|H+3w_@6)c z;ctHI6W{pOjR;H6&i0)1(8?wOgR)~`Qea`LE6oBr#%tEtDJKYjalt1AHa+;<HH@)U( zKkzqy|LT_2MbA7O{_9QGRMG-+GPeMTMEwWr(&lNmw=)8hJ*667K$l|}o zM8cAhlas7`VnoCwrb-ML8Q`2(P1(_kSdt|nNxGLPV_C6NQ%?J@qTQP1lQbK(S`_V94sT}Uab zOIYEHYj>#n<;b2r^|V{UkO+~Xd5{PD+q_OqY+?ce^bKlzi7 zFIn64^vuM>ct5SMb?er_6vOuI+t;sO-@l;Nu3fur+qMjw)~s2xXU{Z%$;ruGyLO*) z$|=A4tH1iZ=N-Fq=S~s6_uhL~KSpcUuAQ3NlRv(D_pVpG;uY_B$J?L({1@DN@4epp zd+xa#z@9zRlarJEt#4enE?3eozWSQ4UV5p7@Jl~`>gPW8|F%B=#f$v54FJ}yS#xhP z0Bgp^cTG**v+cgy@3`Y>FMRPJrrEexbCIFM;qr3L(>C2m) za{P;b<&Cd@#!Fvu-`#hKb9-+2UVcvh(-+S8?8pD)yvwin!QJ=X`n~V*MA zcRcNdFIu$GqaKBvExun($VcD<^8iIoWi4+Y1+XHn+!s<`^0Z#Q*m6s$dCXBGGi-w9?A=Dl4=*}*UpeABvW3hSC{bAQ?lwOqlF<(;jEIvPzZX-;&V{D0qz z`fxnRU<|Sl2A)46F2*dDXOfKq?e5tuD`i+Enu<5D8_s0lkhEW!j z1CVvwn`QI;oJ)xyvuq;@mP|Zv#;9!9V1RQ@&5|lW;(U{ABLa}k8H6)ep4GH0d^X|X z4v8=kd|)T+ol{jOd4=9Fr<7XWc@ZZfOrBY4cFe9Kw?K~6V5OHjq;4msXoVw~IVVJv zD}y;Y}h)wX5;w!%@Z3o506d`jZTiP+dMjXL~D55IiFHeWiwJG=Uh>? zM0~4gjZLiI*U5{In9S1wjL%(g;rsvhubp$wSG@QA2h#ZX*!1-D-FM%8#1Wf5@PXe^ z)#2e`=iDFt(T6|&@jw0D-~G_jpZ@e`KYJucApJ@Ld70c_c_^{0RO7|!UPOh1nn%cg7`^d=X zhd%UsDW&1zr8FP_y|Ojl{N^_UShHsGq?1m%^wP`Lu3ful&z>DSc8raU|JI*;G^+Y_ z8!o!_w#Pp1aZi5Hi{bq1Uir$klam16_VYjgH^28m*J|m|@OSS3tw2)0-A2qdyMhw> z+kW9Rn5`Kff7y#(@YTz&xa7*KHmqCsvXf8Zwd>YA`sho~`SP(Zc?qqwhl|#rwq|_% zWzT>9#aCR}14*`KY;1Lix7dh zORm0V!}@hEJLzPeT)XC|M_+o*mybRE1R7pRd0IQ?U<=$@dfSn36YdiGlipp z^pc3n)C=ku&4$ibFsYh28%Iz=K!b?01=6y z)`W(KNWCnRDp){d4O2b>FjdQGtp+@?sm{hrZb7baxfuuH;X~R|)z7Cq|Mvj7@AUKgmo}M_Q=4jO2g|aCb+JM9<#;17O(Y-~KG^hba6$CqDz`4gY` zgsQ6Ux#!+XFa6rY#Kf6ro^|%wUv9VCZ+OG&Z@u-FnVH!SfB3^=W21ljw}1QVzy9m} z-LobE(8TWDyDq!zl8K3lv(7r}?6c3Fn3#C|>tA>C%{R@?&i?ja{mrM_t$+N}kDv0+ zcfI?MKk~VM_&X!|%B5ew?3!yu`1EtmJ^s1R{m=8hl2oT=W`6NKzp>)5y=(XG8@~9t z$%%>2pMSww=bVG#k)M6n|Nhu}-u>t6*AvsHzI5g{F1d8$qaL-&LEg1{_YG%!esW^s z3m0B=<~ip)>WRnX*S~PlSKn~*Nw;5r?eyII2R`vhb4@7;CJKc8{t zAO70APXGMr)M^7ZGdK6M?|j$Ce)BzlzJ5Isf9gwTeB-i9H$LWZ5Bj|CBL=_!A%4U@ ztQ4z)qyU+!fkY&xz#~ajvljFGtf3%^Sq@Y) zZHq9huouN}M1Wi6N!H z?D`TvhvUaKTCP6SyYOIw{^Fb;_QbBJp{(!CMVqO`6f>d9QdA9`jd2KQ>;j$j(#i&+ zJ~zdZ#gKVdK-25T;)oDqBx36`=raOq7*{b1i4u{fq^iz|?k!Wo%m{tg_h=5reL-Dg zN7;dgvT08;7dWMuWxi6YC+4Y2jS#Up&nXxYQ4KcAh>*Ea7lM4-tw(0zw|d4I3R#bI#1N3~5dfinx^0+}uoAw!HU5D2g(r7-K5SvfJ$r4GlLlbztk( zE!SUv-PqW;bFS|JToeUh-EL*3ov{fT8AV-L)#Zsb7V6B0qoTAFsnw>krPEUg0U$8Z z@CXgK6&2#?yl z>DCL*EjMkkp<(NGAVRG+jg2EE-F_c$+5$7(ejj4WTSr}6UGe1F|MRxDJmoP*zvWln z#p^bJC|4UT2AiA96-FvsG(G`U-MO9DAJO0Fk`oPpR+eU6;R*WG_6lB{W!YNtO2ZIZ zjDPw9F-sQSM;GDJ#d58G@hdAXgBatkT|3LNH9kI(;d=;SA2#hPK(u|~wk7fz%c)&U zN`!kJ^MuD#cH|R!gMb-;`Kfw%vWQh@cH0kfWgw;~?2-myNm+L^rQ9f_Su(U&ZXizq ziFofGS~3@!b|_|!F&Z=#OiV1K$(W%DJ~rHvm;#eD^@>N^B&65GTsV6GQ}Kp-SfrR?p5(@7XRs z*FL?aJH!nER1`xbq85x&QiTx!?_Ekw4^C@%?4h4#8X8`TvGyH2x@e1ojQBy&Mnz$( zOTaH-XWw5(A+PHdL&fpB4f%mfWl3cTfQZ(v-OKkc%NDR`z%9#i)21zZciT`D#b5o^ zpZ~>Q{MC29bJNqG{*#+GZ@%G%Z|2LbZ|TU)tyT*I&p9H>LI7FJ3_z#T0pNYnZnqoB zG?7dX8y+5BK8*LUnjd&}Qr=3Hzd~8k=okQUjvC4My(2x3j<0^8hzRW=TI@zsY|&W!li`WUP~azD?hQ;$=1>fXIdI+Z&!GBPqU;{Vsb*7smw zP6$iZyk2ucF6-XceVa~>N1*H=ICEl+pe5iI2e(tRE*NJS_Y{4Ur;wq!24d zx-=Bk2{&I)1NRW8*De$2SwuAKVJnx~<;u50_h}nV-Kgv)-zM^`fIXW!MJH%QxRs5biHL=95ux z&gf@nKZrQ*y?^+@Zvc~`7kSgodxVgUR`jME=12x)Rm};e0%knV+yQ$SlI96{8_GVt zudPiA?c~e48V!oFDGQHd`At&w_-f40O!EdMhlHujymWoC2WeCDCBvL!XF^yIjDBkS zsZA%q^RrN&x!p(_pPSM8X>pel-Nnslw4q8hSh1T@gNE15Q3q>iBaCONl2of4R#sHF zs8WOnNgM@ANu@5!td~4{Euls?E#sJBDb$gro7MYKL^KPERoHom=I-!d=Ry*@X(E6` zUpw4vvF>B(RUbie>Yko(voq1?i)j*o*avAzO6lH|85!KTKoLQ6xA_&N;Wg3vuv+;hRg{SXszU1Yl5MM*{aNe1j#m^xZ82| zF?;FsU|#0&6&^Rq_^fk32kZGgEK?*@Re zp)8q|k|KkS)>@VqtRTSybo2CR0XP9~t@rNUj-p)VmXW(@%2bbNW;82)&%mV*M_TCE zGPok#I=J!r?8P~OP7IV z>IovLRG@AmOQ)x<|4z0qmE>-3f3}4K0~D&8?rU?sLWp5^RH<%;dxJVMNAo zj5d=(Pw900NJan6|Mye3`9Hsc<9KGAo<2G~ee~(u0RYaP){uSghhNIU#24OA{NVdv zc}4_doa|Z+>BMe*UCfp1!tt_lJKAfA1Nu|G|Id-!Nc_Ed?ZPTjiE? zb4f%O(6uBEi+I|VWBEd$I3HCRVIEYAlJT@$h~`8)VoQ`(=#nG=a@05DBz4xj9HLt&8)r-5&q- z%(V20fZf(S`FbKh`tULg>r0G(=imPip53W@<0{7GTAkpgO3yATaSGrP{ zD_fWFhyV}~>$vor?;ETT-o@x4=HNSwabII z9uJ<{I=uaCe`=}q+Uv{1S6@lofAtLjir&{VICV%rv2`E+)35yvNU^PbNhDK*ygV`x zmYS7O1AMg;U<+t;xY2s7Hq@tCG73G`V}Y#_wG7eRrh6&aQ7*Kvqbsr1;y{b7|={Tq|CkQFIS*A$Rh%DeaaIBcIiom6k#oAcqrmQp;Ul0HU`H=6cOocrn zvsc}#wv0?8D(iChWj%0H&Du&?IVj2*hrYPCgrEW?tYQ<)Ko~nB7=)@orI0xVwb`O0 zyKGez+azTMAoFHsYhTSxEj3PvT4Ra7Q_R#vQ%?Y8i5eNC6&rwL@2h)*xO=PN{q7Ij zbiWHPB%yiP;(o@%-+$HC`r2rDb@cr1)xzFo(pGJFjbnAegxYrV;fL>C-sP>$qnat# zb=~jJr)fSpIl1XrLP8qT`L&t#{I!CsUmCt|NvA&j8QoxzalS#syCI5pbDHN>rF-B8 z5n*ii>50-SH@Ja#YEE-KKDqzA7_3_ptnb`N6K@qyzp2BCt>bnxb$S2&_nx-#ar>tw z@yE;K-o29|@jQh>c>8faEs=VT1nbHM=Wo9KPTFg4e+NMJbvb+lmlEre2IANL#%i5T zG?RoT0a6g4Awv+>CCa2sg_jLUSeC;y&9NbI^NLAgoLks(2*vVb4}Hm>{bu*}%)P7a5CVZ-{m z4)#E6y=A1p8#PPE>yZq``Q5V#e|9$=O(j3TLyofj&U_|z{cXgD6RXJ>+ zAyb8CDwqdR2q!>5$r>69xs_7$s<0z62$RCh%}nxI5LWBZ%!TY32FnbCfu;PcO0x>i z0WuMk@h8w5a-+9VvPef7=*P=wqJRcZz$(5$&bYg;Whny$0JPdc_K z0Z3}@5iv~-KtvP{08l+HS5Xme4j?nxfLeQH5Ej|Z8fd*F?4S}%uc1LI(K{=nLx+Rt zeU%&%_hsWQWoS<`&qz^x$o@FmJgv*Y-Fshrnp>L=hrPEcBH0Q*CU>rnVnt4Gv_iF4 z*LCt1X?^WxUK&fI20})UFoB2=AcL)ko>vor-~5;U$dw=PYEGlW;nGW2m(k0`OFBQZ zONs{h!7a=8Z&VzygU{UP^QIS7+uJCI{LTCDW&Ne@;>RZ1hB8p0kcy^3zfDOi`oMqB2|dkj)z&<0h$`S@YU`4URdhjaoU_FmFlrAsPsKb>nb{L=bo0n6)hB5Y zxoPFZ(G{vBcFW12&R8n$z1Bd@CtS|g^>l(GyFK`pJ^4^>CQF0ee}iWqB|)EqC0@EU zQ_O4>-m;UEP?|moT)VK2S>&booaLyyD%M@RkZ1#cwLxOF$)%0<&A6bCRb>|jGD=FL z788B|03ZNKL_t&(WU5M7CF?+LB^ii=Cz6};X2HFidEwA&y@HF68+$WzDn&N8QCGu& zQ*nUsX|%{bmS<3vf=+gG?;&OLsh3%GRKgZ!wk(J7p|q&fi8j2eiM6jbdF6Hl*JYjO zoz$x?ml>_i>$(VVZ`n&CE6g3r)+RGw)}qM z_-JN5!WNRW4mzjS)<d3jIrR~65|31tR(%L3y$WkZhmwwoxx3Q29l*irCK zKr$m48{DB*aAtIxl)bOz17>DDy3-=M(cK%th%mPjp2+NG-Wl8>R?#C9l}_Sap`NUxvV{wq<3q^*$#)`y#i&JO*lk5dEK&SKL}JUcaP$TrP1SUDooq$XJ||d zSI@>>nsgUmVQeUu@;)exN*41^ip(4ptf9!j>aXRNHkG4aHPAU85`-7KunJlkeYDS& z5WKZ@T{Zz=AK(~evGyW*`7>+4eL z4`ng22^=?+kOaxrpbP;vcL8)LaP%DldSA^NP=&VM+U)MBjL7I+fO25SIL;L!vq1eP z^DdQde)spk|Gj_oMJ|Iccp1H{@e+NnOl?HlXU5G9ZceZ_q|VrG6l1<7Da*TfW{mm1 z%w}HDXFMy@(sqGKTY0DCb&fsmp{O1iZsu)*kkK>c?im3f71Ojk9L~+GmfJ@^LAtGL z`c#-yNQ_Vem*r4?VVRK-R#QT2fDn|c$Xr7{GXi5>x*k@eLUg*%2CudQ2h^RJiGa11 znKUQeZgOC8*BHEuFF#Uy<4N!Dwe}W($W!Bk9`7~#mM(7a+-~+!8-jGH0r({I9;9{& z-#*qj+agS)X*j;d8BC48rfRQ2o(7xQwn8F)4Z;S%))Q=d3UluoR@Xu+SlVS zuviCb<(B3ki2#f#AyMVRn6!xyOct#0H8q_oH;)&=>Uv6$=I%WD>2l zzV_Cph>r0M5Q-om8Vv=oXTVVg_U4;TeXCw8<17huMPCqdQ{ie+}9?BR~}zz5w+I-?4SKlf8#fP^H+cMS3lG7ALAM6 z&eK$izTo2bu0eJeUvnG>`mxLgfQ+aY+#M+YF>|wG3)ScYz^!F=caMy628*%Wg_LDe z;qqN~&g@HMtkX2*dBQh%loZ*`8_s~8Xw;Kbro810EEHw;sVA8*yN^5{N^R{PYq33g z3$0~DwNM89tFJ~b%M+|;_wGO4tZ5rpKij>0mst0sCr>TomXB`nO0SR zM&SzOE@B0%1xif%`Kxx#E}wQ=&MW8UyLZKOCzwZhjM@?t@qWY z37bMhE%JJFZNAs2^_JA5pX_!KoiwEiFglaX!*6@6 z*e);T`V~|SF9C6`nE~Uf{~#{$4eiahKjQ`X!sCzqv45T9d+)vbTfg;NKl3v`^Cj;2 zbEHArBIL8z5wCsLH)JS(o;K!Pe7$j_KgcWEv#QaoARS{wt$Mt&SVZtm60EAZ2^-LL ziJ-SBN|vyIxim9cw?sO9bGd-|Fm0;Oa&HqSwNzK8U`}_lhJ-u;@<28Ah_*~@Ftgr! zMwUR%Xt8hqW`<9*(F)Fc*guBxSqaufUiDe7*E@jwH$S8Vl5AC_pf#y~?i?$n^V8pb z@bD*}oc^v6=G@J1uJHjctZhh*dKARWZjQ*0qbH=bM=Enwv$N8{8V!!hSvY)i2#By_ zO{hGcwW+0BNhFU@qh)DMq zeWjTIZ&PN8i`qP&ESYXoks<+rh1{}|bT2aIk;V+OLS;?UoSV2;Y78#kEmLl$j5fDk zltWWy$T>|@L~N4WFRVhc-{a&qaqX`u-gx7UU-*ST@OS_2-~Cau3HnhhaXr`f*=RGs zH!4xv?M{~4)aqZ}$ixwyyp_mFPx=9ztC2(mcQ$1xC~fXq&7&`3a3QaH#2 z-Q1?3{Ja*eJHkYpe2IgjxqHM?Dc8ELAe*-yeQFbwX3aL}y}K8YJC#N&3RMl)gh5D* zH8WP(?o%(I&z>&vbG_>>Ua}ZnxFH0xKL1dYxi=frUn&UnwbS(Y@%M}M-Xr??50JU? z-C?XEl*b*3R7jCJ0xuN`rtS zkb-G@Ne^&nUuCVF5UowAFf;;fnqyea*oE7)hWv>T=3dJxBi*LxfuV7gMP@7*yPHim zMfAv++@ruxy=R75t6>K4EVYOfIjTqU6=Fug$TZ4Gh?21?`i!@E7)h8b*Ea2zburMY zNVZj>rfH7o)hHB{fZO;PjL3*K%>%t2VeU0lfd-b+eX*Sl#K~?q_Bl_v(F$I9?aj|I zIjgU?7^-{|;!pgEKmIrW=HH5l-}8IE`?PJ^?)`oPhV_itAcJ4V@!iE|iQD87Zevc~ zRv>i0^#Z>B_V<5y3p?+vZyy`fmovQE$K7|{`N?a~kh~`Ee%lc~mC$$V^It{iNVm2c z^N+CF9DM2__~&L-T(2Cf5Fqn33t0O~(RQtS@S<8q8F+@GCAxVzPv|5AsYF1b)lR6r zihC$&tctg5puqKzqI@+*As~ajFe*a&cqBJVP)gE_)|%(4>tFUtTqYH2d*&{FTp|aQ z2xgOX;ZbqH??Xa$^#ynuaZ-~nbQ;-}B^Xu7nle;Y#?}Za)-3P90wpmvC?P>Ady)Qx z6bv08aekgn$|E*hQbE*56|8t7c?BxJ-4c?txvvetFsq4YHrT(>Y16iZ3IY*5qF1#^ zqa}yvOQGpi#uS2L+qid9ZVeHjP1D?aPo-c$H31osq3-6&I4t{l-Yv^Pz&!5`hXZM% z)97u^2q{T#&3o@*t1{`xNRrlwl(|U(Ar4NP(~92+fSjh3rjwnZ@{0%L-rQ{ygBcMU zQeK%bbN3!ynck+-YFA1QAZ#X52!PDgY*R*V7m9YfliqtZ1;46G^y`nG`?;Tc_0?DY z>R=ZI>5WqsI5Xd4bF0q==5Zt`@ z=9-i#YqnL|?v)u~nfRDBNHmkwruo`AfJSA}!L10xz zJ$dv1G~0{JwM{cJ*CYFHIP;ZCNL8mw12pTh7l7Q0Guv)y&dIo@4reof4CFrTgsiE$ zESN${Yf)yVlo_2C3lmaP^w>L)Sq#hkdy;DHYAQ=uI8G2tg-T5J) zXXeYY2Q8iDdkvzmX>K-+rfT1>22#g< zJb3V#6;>}-GH!}bUYoJre)~$%;)4&~yE)1}Kkka8yFa;q?U-*&s5hlnzlOB$vsXjE zDY@y7duaQga?8Pw3D&wUtxaX3isE&bd;^NLH#kcmGCN5(>lt)rh{jCmTqQtySoY1Q zT3Gf+-5+@EHBJ)%CI?X3S_Lv03-FYHdD;n8PEPkJVx>168I6UfC>cd1ScV@aK&G^a z({~>}ybUvYOPBAt8-I7%)i*W-Df`HuQdWYvl9?h(H(^`Ya5?M`yZHg2#MyeblPj0> z6UJU>(vqC1Mcgs>$sM^uIn2m+ujJy3;@-NS7oOg~zk76gxPKD;@vfQ4Jr5$%Koags z#k#0PAfhbP#bWWPppWV9`$M0nO2RgrR{dLc3c%>>6^VD-1|^*`?3vTdpsegFB9&1} zKLZ<%q83s>6aqjhjfTjw?6IN>S93KptxYLJkIK{vbT4Hjs{Xv`e1*XTWolhlZ?05C zX0FX0<^6sxJ|qo%Ox~ALIL*wo`%Yn97kBr8?t-~jUy>N4M1X?K+^oKMiEq`3L}q$x zG%{FXTA)RN)c^u!ET>g-0)p9OQ)aB|T9y21nj-Ap{a3z%D>dK!!MJ2End$)e#nZ=^ z|J3h(uO9rydq2d5y2XfV{h-V%Uc%Rw-Ip6c1+e|aU;Lxr|Nig(`mg``&;IPsUVvRC z+-7BXEpF`-J%7CQ);sULciDvT;)`9j%K37OYTxkqjHQCRcwuq&etgiXFgi*Lq)- zL2o517}32=00{J0Tbp8tFO;xinz_zP)-6%85<9xPs-=U$nA)7&dd(fTq*6Cd$!9!; zcQCAPc9h9WhA9+Ezy<=l3qDf!sxoVpwR@Tu9vbh3_N}>9I)M&ATscimJtKQ_w<&Ys zVL6{>_nidn_jq!4aKmBgv)@;y0Gc?5_T1xaht^|-TN6+;jhZ`1Yd~(I?MCY%f-oMD zQUoJ|=7>HRv1U@1nG9-e*oXky_h*d~X-TZJP zw%Sq-1XK0!toant-CYp?OV7P9n&n92=D?^t`GU!&Y=J%IsbPW^^$fHY5$xz0<(5?# z|ICzz8KO4wv%8xyGkQ<6^4{qhYer7f1ZiCt_i4}@GurBriR^$*b|;4?s}-s}g7za7?G+#XKS6WC~um1VyiWbAA7oKrRfB9vN zZ-4vSKmF7H+^_t~ue9d#JOljBcYf-ZfB7$e=^ylI@%g^mkL>_`muY?d@#g*vyD=1K zjpm+_ETl&akb5Epx;H5=U*olOG*ZqU