Pathfinder: Drop jumper.lua's pathfinder in favor of MarkBu's pathfinder with slight modifications, as the later one supports 3D paths.

Clean up old pathfinder code.
Add path decoration to MarkBu pathfinder's generated paths.
Change actions.lua and places.lua to use the new pathfinder.
Properly modify README.md.
This commit is contained in:
Hector Franqui 2017-06-17 11:47:51 -04:00
parent e1bf931064
commit fb549e7f93
12 changed files with 346 additions and 1548 deletions

View File

@ -4,25 +4,40 @@ advanced_npc
Introduction
------------
Advanced NPC is a mod for Minetest, based on mobs_redo.
The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of Harvest Moon games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention.
Advanced NPC is a mod for Minetest using mobs_redo API.
The goal of this mod is to be able to have live villages in Minetest. These NPCs are highly inspired by the typical NPCs of _Harvest Moon_ games. The general idea is that on almost all buildings of a village there are NPCs that are kind of intelligent: they have daily tasks they perform, can speak to players, can trade with the player, can use their own items (chests and furnaces for example), know where to go around their house and village, can be lumbers, miners or any other Minetest-suitable profession and can ultimately engage into relationships with the player. And while basically only players are mentioned here, the ultimate goal is that they can do all of this also among themselves, so that villages are alive and evolving by themselves, without player intervention.
Installation
------------
__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs will spawn on stone (default:stone) and the mg_villages' plotmarkers (mg_villages:plotmarker). The spawning is not controlled, so you will have several of them walking around. This is not how it is planned and is just for testing purposes. In the future, only a handful of NPCs should spawn at village house's plotmarker and they will know their way around the house and have specific jobs.
__NOTE__: Advanced NPC is still under development. While the mod is largely stable, it lacks one of the most important pieces: spawning. Currently, NPCs can be spawned using eggs (found in creative inventory as 'NPC') and by themselves on villages of the [mg_villages mod](https://forum.minetest.net/viewtopic.php?t=13589). NPCs will spawn automatically on mg_villages villages and over time will populate the entire village. If something goes wrong, you can reset the village by:
- Clearing all objects (in chat, type /clearobjects quick)
- Restore original plotmarkers (in chat, type /restore_plotmarkers radius)
- The radius can be any number, but it is recommended you use a not so large number. 200 is suitable. So stand in the middle of the village and then run that command.
This will actually restore the village and will slowly make NPCs spawn again. Currently there's no way to disable NPCs spawning on village, except by going to `spawner.lua` and commenting out all of `minetest.register_abm()` code.
Download the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version)
__Download__ the mod [here](https://github.com/hkzorman/advanced_npc/archive/master.zip) (link always pointing to latest version)
For this mod to work correctly, you also need to install the [mobs_redo](https://github.com/tenplus1/mobs_redo) mod. After installation, make sure you enable it in your world.
License
-------
__advanced_npc__ is Copyright (C) 2016-2017 Hector Franqui (zorman2000), licensed under the GPLv3 license. See `license.txt` for details.
The `jumper.lua` file contains code based on the [Jumper library](https://github.com/Yonaba/Jumper), which is Copyright (c) 2012-2013 Roland Yonaba, licensed under MIT license. See `actions/jumper.lua` for details.
The `pathfinder.lua` file contains code slighlty modified from the [pathfinder mod](https://github.com/Yonaba/Jumper) by MarkBu, which is licensed as WTFPL. See `actions/pathfinder.lua` for details.
Current NPC textures are from mobs_redo mod.
The following textures are by Zorman2000:
- marriage_ring.png - CC BY-SA
Documentation and API
---------------------
This mod requires a good user manual, and also is planned to have an extensive API, properly documented. Unfortunately, these still aren't ready. A very very very WIP manual can be found in the [wiki](https://github.com/hkzorman/advanced_npc/wiki/Concept%3A-Dialogues)
Roadmap

View File

@ -599,7 +599,7 @@ function npc.actions.use_bed(self, args)
end
local action = args.action
local node = minetest.get_node(pos)
minetest.log(dump(node))
--minetest.log(dump(node))
local dir = minetest.facedir_to_dir(node.param2)
if action == npc.actions.const.beds.LAY then
@ -746,20 +746,9 @@ function npc.actions.walk_to_pos(self, args)
end
-- Find path
--local path = pathfinder.find_path(start_pos, end_pos, 20, walkable_nodes)
local path = pathfinder.find_path(start_pos, end_pos, self)
local path = npc.pathfinder.find_path(start_pos, end_pos, self, true)
if path ~= nil and #path > 1 then
-- Get details from path nodes
-- This might get moved to proper place, pathfinder.lua code
local path_detail = {}
for i = 1, #path do
local node = minetest.get_node(path[i])
table.insert(path_detail, {pos=path[i], type=npc.pathfinder.is_good_node(node, {})})
end
path = path_detail
npc.log("DEBUG", "Detailed path: "..dump(path))
npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos))
-- Store path
self.actions.walking.path = path

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
-- Pathfinding code by Zorman2000
-- Pathfinding code by MarkBu, original can be found here:
-- https://github.com/MarkuBu/pathfinder
--
-- Modifications by Zorman2000
-- This version is slightly modified to use another "walkable" function,
-- plus add a "decorating" path function which allows to know the type
-- of nodes in the path.
---------------------------------------------------------------------------------------
-- Pathfinding functionality
---------------------------------------------------------------------------------------
-- This class contains functions that allows to map the 3D map of Minetest into
-- a 2D array (basically by ignoring the y coordinate for the moment being) in order
-- to use the A* pathfinding algorithm to find the shortest path from one node to
-- another. The A* algorithm implementation is in the 'jumper.lua' file which a
-- reduced and slightly modified version of the Jumper library, by Roland Yonaba
-- (https://github.com/Yonaba/Jumper).
-- Mapping algorithm: transforms a Minetest map surface to a 2d grid.
npc.pathfinder = {}
@ -30,57 +29,15 @@ npc.pathfinder.nodes = {
}
}
-- This function uses the mapping functions and the A* algorithm implementation
-- of the Jumper library to find a path from start_pos to end_pos. The range is
-- an extra amount of nodes to search in both the x and z coordinates.
function npc.pathfinder.find_path(start_pos, end_pos, range, walkable_nodes)
-- Check that start and end position are not the same
if start_pos.x == end_pos.x and start_pos.z == end_pos.z then
return nil
end
-- Set walkable nodes to empty if parameter wasn't used
if walkable_nodes == nil then
walkable_nodes = {}
end
-- Map the Minetest area to a 2D array
local map = pathfinder.create_map(start_pos, end_pos, range, walkable_nodes)
-- Find start and end positions
local pos = pathfinder.find_start_and_end_pos(map)
-- Normalize the map
local normalized_map = pathfinder.normalize_map(map)
-- Create pathfinder object
local grid_object = Grid(normalized_map)
-- Define what is a walkable node
local walkable = 0
-- Pathfinder object using A* algorithm
local finder = Pathfinder(grid_object, "ASTAR", walkable)
-- Set orthogonal mode meaning it will not move in diagonal directions
finder:setMode("ORTHOGONAL")
-- Calculates the path, and its length
local path = finder:getPath(pos.start_pos.x, pos.start_pos.z, pos.end_pos.x, pos.end_pos.z)
--minetest.log("Found path: "..dump(path))
-- Pretty-printing the results
if path then
return pathfinder.get_path(map, path:nodes())
end
end
-- This function is used to determine if a node is walkable
-- or openable, in which case is good to use when finding a path
function npc.pathfinder.is_good_node(node, exceptions)
function pathfinder.is_good_node(node, exceptions)
--local function is_good_node(node, exceptions)
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors, gates
-- and cottages doors are supported.
--minetest.log("Is good node: "..dump(node))
local is_openable = false
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
--for _,node_prefix in pairs(pathfinder.nodes.openable_prefix) do
local start_i,end_i = string.find(node.name, node_prefix)
if start_i ~= nil then
is_openable = true
@ -88,159 +45,346 @@ function npc.pathfinder.is_good_node(node, exceptions)
end
end
if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then
--return pathfinder.node_types.walkable
return npc.pathfinder.node_types.walkable
elseif is_openable then
return npc.pathfinder.node_types.openable
--return pathfinder.node_types.openable
else
for i = 1, #exceptions do
if node.name == exceptions[i] then
return npc.pathfinder.node_types.walkable
--return pathfinder.node_types.walkable
end
end
return npc.pathfinder.node_types.non_walkable
--return pathfinder.node_types.non_walkable
end
end
function pathfinder.create_map(start_pos, end_pos, extra_range, walkables)
minetest.log("Start pos: "..minetest.pos_to_string(start_pos))
minetest.log("End pos: "..minetest.pos_to_string(end_pos))
-- Calculate all signs to ensure:
-- 1. Correct area calculation
-- 2. Iterate in the correct direction
local start_x_sign = (start_pos.x - end_pos.x) / math.abs(start_pos.x - end_pos.x)
local start_z_sign = (start_pos.z - end_pos.z) / math.abs(start_pos.z - end_pos.z)
local end_x_sign = (end_pos.x - start_pos.x) / math.abs(end_pos.x - start_pos.x)
local end_z_sign = (end_pos.z - start_pos.z) / math.abs(end_pos.z - start_pos.z)
-- Correct the signs if they are nan
if math.abs(start_pos.x - end_pos.x) == 0 then
start_x_sign = -1
end_x_sign = 1
end
if math.abs(start_pos.z - end_pos.z) == 0 then
start_z_sign = -1
end_z_sign = 1
end
-- Get starting and ending positions, adding the extra nodes to the area
local pos1 = {x=start_pos.x + (extra_range * start_x_sign), y = start_pos.y - 1, z=start_pos.z + (extra_range * start_z_sign)}
local pos2 = {x=end_pos.x + (extra_range * end_x_sign), y = end_pos.y, z=end_pos.z + (extra_range * end_z_sign)}
local grid = {}
-- Loop through the area and classify nodes
for z = 1, math.abs(pos1.z - pos2.z) do
local current_row = {}
for x = 1, math.abs(pos1.x - pos2.x) do
-- Calculate current position
local current_pos = {x=pos1.x + (x*end_x_sign), y=pos1.y, z=pos1.z + (z*end_z_sign)}
-- Check if this is the starting position
if current_pos.x == start_pos.x and current_pos.z == start_pos.z then
-- Is start position
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.start})
elseif current_pos.x == end_pos.x and current_pos.z == end_pos.z then
-- Is ending position or goal position
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.goal})
else
-- Check if node is walkable
local node = minetest.get_node(current_pos)
-- Check node has air above it
local node_above = minetest.get_node({x=current_pos.x, y=current_pos.y+1, z=current_pos.z})
if node.name == "air" then
-- Check if node above is air
if node.name == "air" then
-- If air do no more checks
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.walkable})
end
else
-- Check if it is of a walkable or openable type
table.insert(current_row, {pos=current_pos, type=is_good_node(node, walkables)})
end
end
function pathfinder.get_decorated_path(path)
-- Get details from path nodes
local path_detail = {}
for i = 1, #path do
local node = minetest.get_node(path[i])
table.insert(path_detail, {pos=path[i], type=pathfinder.is_good_node(node, {})})
end
-- Insert the converted row into the grid
table.insert(grid, current_row)
npc.log("DEBUG", "Detailed path: "..dump(path_detail))
return path_detail
end
function npc.pathfinder.find_path(start_pos, end_pos, entity, decorate_path)
local path = pathfinder.find_path(start_pos, end_pos, entity)
if path then
if decorate_path then
path = pathfinder.get_decorated_path(path)
end
else
npc.log("ERROR", "Couldn't find path from "..minetest.pos_to_string(start_pos)
.." to "..minetest.pos_to_string(end_pos))
end
return grid
return path
end
-- Utility function to print the created map to the console.
-- Used for debug.
local function print_map(map)
for z,row in pairs(map) do
local row_string = "["
for x,node in pairs(row) do
if node.type == 2 then
row_string = row_string.."- "
else
row_string = row_string..node.type.." "
end
-- Use the following if the coordinates are also needed
--row_string = row_string..node.type..": {"..node.pos.x..", "..node.pos.y..", "..node.pos.z.."}, "
end
row_string = row_string.."]"
print(row_string)
end
-- From this point onwards is MarkBu's original pathfinder code,
-- except for the "walkable" function, which is modified by Zorman2000
-- to include doors and other "walkable" nodes.
-- The version here is exactly this:
-- https://github.com/MarkuBu/pathfinder/commit/ca0b433bf5efde5da545b11b2691fa7f7e53dc30
--[[
minetest.get_content_id(name)
minetest.registered_nodes
minetest.get_name_from_content_id(id)
local ivm = a:index(pos.x, pos.y, pos.z)
local ivm = a:indexp(pos)
minetest.hash_node_position({x=,y=,z=})
minetest.get_position_from_hash(hash)
start_index, target_index, current_index
^ Hash of position
current_value
^ {int:hCost, int:gCost, int:fCost, hash:parent, vect:pos}
]]--
local openSet = {}
local closedSet = {}
local function get_distance(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distZ = math.abs(start_pos.z - end_pos.z)
if distX > distZ then
return 14 * distZ + 10 * (distX - distZ)
else
return 14 * distX + 10 * (distZ - distX)
end
end
local function get_distance_to_neighbor(start_pos, end_pos)
local distX = math.abs(start_pos.x - end_pos.x)
local distY = math.abs(start_pos.y - end_pos.y)
local distZ = math.abs(start_pos.z - end_pos.z)
-- This function find the starting and ending points in the
-- map representation, and returns the coordinates in the map
-- for the pathfinding algorithm to use
function pathfinder.find_start_and_end_pos(map)
-- This is for debug
--print_map(map)
local result = {}
for z,row in pairs(map) do
for x,node in pairs(row) do
if node.type == pathfinder.node_types.start then
--minetest.log("Start node: "..dump(node))
result["start_pos"] = {x=x, z=z}
elseif node.type == pathfinder.node_types.goal then
--minetest.log("End node: "..dump(node))
result["end_pos"] = {x=x, z=z}
end
if distX > distZ then
return (14 * distZ + 10 * (distX - distZ)) * (distY + 1)
else
return (14 * distX + 10 * (distZ - distX)) * (distY + 1)
end
end
-- This function is used to determine if a node is walkable
-- or openable, in which case is good to use when finding a path
local function walkable(node, exceptions)
local exceptions = exceptions or {}
-- Is openable is to support doors, fence gates and other
-- doors from other mods. Currently, default doors, gates
-- and cottages doors are supported.
--minetest.log("Is good node: "..dump(node))
local is_openable = false
for _,node_prefix in pairs(npc.pathfinder.nodes.openable_prefix) do
local start_i,end_i = string.find(node.name, node_prefix)
if start_i ~= nil then
is_openable = true
break
end
end
--minetest.log("Found start and end positions: ("..result.start_pos.)..", "..minetest.pos_to_string(result.end_pos))
return result
end
-- This function transforms the grid into binary values
-- (0 walkable, 1 non-walkable) for the pathfinding algorithm.
function pathfinder.normalize_map(map)
local result = {}
for _,row in pairs(map) do
local result_row = {}
for _,node in pairs(row) do
if node.type ~= pathfinder.node_types.non_walkable then
table.insert(result_row, 0)
else
table.insert(result_row, 1)
-- Detect mg_villages ceilings usage of thin wood nodeboxes
-- TODO: Improve
local is_mg_villages_ceiling = false
if node.name == "cottages:wood_flat" then
is_mg_villages_ceiling = true
end
if node ~= nil and node.name ~= nil and not minetest.registered_nodes[node.name].walkable then
return false
elseif is_openable then
return false
elseif is_mg_villages_ceiling then
return false
else
for i = 1, #exceptions do
if node.name == exceptions[i] then
return false
end
end
table.insert(result, result_row)
return true
end
return result
end
-- This function returns an array of tables with two parameters: type and pos.
-- The position parameter is the actual coordinate on the Minetest map. The
-- type is the type of the node at the coordinate defined as pathfinder.node_types.
function pathfinder.get_path(map, path_nodes)
local result = {}
for node, count in path_nodes do
table.insert(result, map[node:getY()][node:getX()])
-- For debug
--minetest.log("Node: "..dump(map[node:getY()][node:getX()]))
--print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY()))
end
return result
local function check_clearance(cpos, x, z, height)
for i = 1, height do
local n_name = minetest.get_node({x = cpos.x + x, y = cpos.y + i, z = cpos.z + z}).name
local c_name = minetest.get_node({x = cpos.x, y = cpos.y + i, z = cpos.z}).name
--~ print(i, n_name, c_name)
if walkable(n_name) or walkable(c_name) then
return false
end
end
return true
end
local function get_neighbor_ground_level(pos, jump_height, fall_height)
local node = minetest.get_node(pos)
local height = 0
if walkable(node) then
repeat
height = height + 1
if height > jump_height then
return nil
end
pos.y = pos.y + 1
node = minetest.get_node(pos)
until not walkable(node)
return pos
else
repeat
height = height + 1
if height > fall_height then
return nil
end
pos.y = pos.y - 1
node = minetest.get_node(pos)
until walkable(node)
return {x = pos.x, y = pos.y + 1, z = pos.z}
end
end
function pathfinder.find_path(pos, endpos, entity)
local start_index = minetest.hash_node_position(pos)
local target_index = minetest.hash_node_position(endpos)
local count = 1
openSet = {}
closedSet = {}
local h_start = get_distance(pos, endpos)
openSet[start_index] = {hCost = h_start, gCost = 0, fCost = h_start, parent = nil, pos = pos}
-- Entity values
local entity_height = math.ceil(entity.collisionbox[5] - entity.collisionbox[2])
local entity_fear_height = entity.fear_height or 2
local entity_jump_height = entity.jump_height or 1
repeat
local current_index
local current_values
-- Get one index as reference from openSet
for i, v in pairs(openSet) do
current_index = i
current_values = v
break
end
-- Search for lowest fCost
for i, v in pairs(openSet) do
if v.fCost < openSet[current_index].fCost or v.fCost == current_values.fCost and v.hCost < current_values.hCost then
current_index = i
current_values = v
end
end
openSet[current_index] = nil
closedSet[current_index] = current_values
count = count - 1
if current_index == target_index then
-- print("Success")
local path = {}
local reverse_path = {}
repeat
if not closedSet[current_index] then
return
end
table.insert(path, closedSet[current_index].pos)
current_index = closedSet[current_index].parent
if #path > 100 then
-- print("path to long")
return
end
until start_index == current_index
repeat
table.insert(reverse_path, table.remove(path))
until #path == 0
-- print("path lenght: "..#reverse_path)
return reverse_path
end
local current_pos = current_values.pos
local neighbors = {}
local neighbors_index = 1
for z = -1, 1 do
for x = -1, 1 do
local neighbor_pos = {x = current_pos.x + x, y = current_pos.y, z = current_pos.z + z}
local neighbor = minetest.get_node(neighbor_pos)
local neighbor_ground_level = get_neighbor_ground_level(neighbor_pos, entity_jump_height, entity_fear_height)
local neighbor_clearance = false
if neighbor_ground_level then
-- print(neighbor_ground_level.y - current_pos.y)
--minetest.set_node(neighbor_ground_level, {name = "default:dry_shrub"})
local node_above_head = minetest.get_node(
{x = current_pos.x, y = current_pos.y + entity_height, z = current_pos.z})
if neighbor_ground_level.y - current_pos.y > 0 and not walkable(node_above_head) then
local height = -1
repeat
height = height + 1
local node = minetest.get_node(
{x = neighbor_ground_level.x,
y = neighbor_ground_level.y + height,
z = neighbor_ground_level.z})
until walkable(node) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
elseif neighbor_ground_level.y - current_pos.y > 0 and walkable(node_above_head) then
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
else
local height = -1
repeat
height = height + 1
local node = minetest.get_node(
{x = neighbor_ground_level.x,
y = current_pos.y + height,
z = neighbor_ground_level.z})
until walkable(node) or height > entity_height
if height >= entity_height then
neighbor_clearance = true
end
end
neighbors[neighbors_index] = {
hash = minetest.hash_node_position(neighbor_ground_level),
pos = neighbor_ground_level,
clear = neighbor_clearance,
walkable = walkable(neighbor),
}
else
neighbors[neighbors_index] = {
hash = nil,
pos = nil,
clear = nil,
walkable = nil,
}
end
neighbors_index = neighbors_index + 1
end
end
for id, neighbor in pairs(neighbors) do
-- don't cut corners
local cut_corner = false
if id == 1 then
if not neighbors[id + 1].clear or not neighbors[id + 3].clear
or neighbors[id + 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 3 then
if not neighbors[id - 1].clear or not neighbors[id + 3].clear
or neighbors[id - 1].walkable or neighbors[id + 3].walkable then
cut_corner = true
end
elseif id == 7 then
if not neighbors[id + 1].clear or not neighbors[id - 3].clear
or neighbors[id + 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
elseif id == 9 then
if not neighbors[id - 1].clear or not neighbors[id - 3].clear
or neighbors[id - 1].walkable or neighbors[id - 3].walkable then
cut_corner = true
end
end
if neighbor.hash ~= current_index and not closedSet[neighbor.hash] and neighbor.clear and not cut_corner then
local move_cost_to_neighbor = current_values.gCost + get_distance_to_neighbor(current_values.pos, neighbor.pos)
local gCost = 0
if openSet[neighbor.hash] then
gCost = openSet[neighbor.hash].gCost
end
if move_cost_to_neighbor < gCost or not openSet[neighbor.hash] then
if not openSet[neighbor.hash] then
count = count + 1
end
local hCost = get_distance(neighbor.pos, endpos)
openSet[neighbor.hash] = {
gCost = move_cost_to_neighbor,
hCost = hCost,
fCost = move_cost_to_neighbor + hCost,
parent = current_index,
pos = neighbor.pos
}
end
end
end
if count > 100 then
-- print("fail")
return
end
until count < 1
-- print("count < 1")
return {pos}
end

View File

@ -196,7 +196,7 @@ function npc.places.find_entrance_from_openable_nodes(all_openable_nodes, marker
entity.collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20}
--minetest.log("Start pos: "..minetest.pos_to_string(start_pos))
--minetest.log("End pos: "..minetest.pos_to_string(end_pos))
local path = pathfinder.find_path(start_pos, end_pos, entity)
local path = npc.pathfinder.find_path(start_pos, end_pos, entity, false)
--minetest.log("Found path: "..dump(path))
if path ~= nil then
--minetest.log("Path distance: "..dump(#path))

View File

@ -32,7 +32,6 @@ dofile(path .. "/trade/prices.lua")
dofile(path .. "/actions/actions.lua")
dofile(path .. "/actions/places.lua")
dofile(path .. "/actions/pathfinder.lua")
dofile(path .. "/actions/jumper.lua")
dofile(path .. "/actions/node_registry.lua")
dofile(path .. "/random_data.lua")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 B