2011-07-04 03:45:36 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using Substrate.Core;
|
|
|
|
|
using Substrate.Nbt;
|
2011-11-10 02:46:19 +00:00
|
|
|
|
using Substrate.Data;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
|
|
|
|
|
//TODO: Exceptions (+ Alpha)
|
|
|
|
|
|
|
|
|
|
namespace Substrate
|
|
|
|
|
{
|
|
|
|
|
using IO = System.IO;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Represents a Beta-compatible (Beta 1.3 or higher) Minecraft world.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class BetaWorld : NbtWorld
|
|
|
|
|
{
|
|
|
|
|
private const string _REGION_DIR = "region";
|
|
|
|
|
private const string _PLAYER_DIR = "players";
|
|
|
|
|
private string _levelFile = "level.dat";
|
|
|
|
|
|
|
|
|
|
private Level _level;
|
|
|
|
|
|
2012-04-28 19:52:40 +00:00
|
|
|
|
private Dictionary<int, BetaRegionManager> _regionMgrs;
|
|
|
|
|
private Dictionary<int, RegionChunkManager> _chunkMgrs;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
private Dictionary<int, BlockManager> _blockMgrs;
|
|
|
|
|
|
2011-12-17 06:43:41 +00:00
|
|
|
|
private Dictionary<int, ChunkCache> _caches;
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
private PlayerManager _playerMan;
|
2011-11-10 02:46:19 +00:00
|
|
|
|
private BetaDataManager _dataMan;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
|
2011-11-05 19:12:48 +00:00
|
|
|
|
private int _prefCacheSize = 256;
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
private BetaWorld ()
|
|
|
|
|
{
|
2012-04-28 19:52:40 +00:00
|
|
|
|
_regionMgrs = new Dictionary<int, BetaRegionManager>();
|
|
|
|
|
_chunkMgrs = new Dictionary<int, RegionChunkManager>();
|
2011-07-04 03:45:36 +00:00
|
|
|
|
_blockMgrs = new Dictionary<int, BlockManager>();
|
2011-12-17 06:43:41 +00:00
|
|
|
|
|
|
|
|
|
_caches = new Dictionary<int, ChunkCache>();
|
2011-07-04 03:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a reference to this world's <see cref="Level"/> object.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public override Level Level
|
|
|
|
|
{
|
|
|
|
|
get { return _level; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a <see cref="BlockManager"/> for the default dimension.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A <see cref="BlockManager"/> tied to the default dimension in this world.</returns>
|
|
|
|
|
/// <remarks>Get a <see cref="BlockManager"/> if you need to manage blocks as a global, unbounded matrix. This abstracts away
|
|
|
|
|
/// any higher-level organizational divisions. If your task is going to be heavily performance-bound, consider getting a
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// <see cref="RegionChunkManager"/> instead and working with blocks on a chunk-local level.</remarks>
|
2011-07-04 03:45:36 +00:00
|
|
|
|
public new BlockManager GetBlockManager ()
|
|
|
|
|
{
|
|
|
|
|
return GetBlockManagerVirt(Dimension.DEFAULT) as BlockManager;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a <see cref="BlockManager"/> for the given dimension.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="dim">The id of the dimension to look up.</param>
|
|
|
|
|
/// <returns>A <see cref="BlockManager"/> tied to the given dimension in this world.</returns>
|
|
|
|
|
/// <remarks>Get a <see cref="BlockManager"/> if you need to manage blocks as a global, unbounded matrix. This abstracts away
|
|
|
|
|
/// any higher-level organizational divisions. If your task is going to be heavily performance-bound, consider getting a
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// <see cref="RegionChunkManager"/> instead and working with blocks on a chunk-local level.</remarks>
|
2011-07-04 03:45:36 +00:00
|
|
|
|
public new BlockManager GetBlockManager (int dim)
|
|
|
|
|
{
|
|
|
|
|
return GetBlockManagerVirt(dim) as BlockManager;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// Gets a <see cref="RegionChunkManager"/> for the default dimension.
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// </summary>
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// <returns>A <see cref="RegionChunkManager"/> tied to the default dimension in this world.</returns>
|
|
|
|
|
/// <remarks>Get a <see cref="RegionChunkManager"/> if you you need to work with easily-digestible, bounded chunks of blocks.</remarks>
|
|
|
|
|
public new RegionChunkManager GetChunkManager ()
|
2011-07-04 03:45:36 +00:00
|
|
|
|
{
|
2012-04-28 19:52:40 +00:00
|
|
|
|
return GetChunkManagerVirt(Dimension.DEFAULT) as RegionChunkManager;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// Gets a <see cref="RegionChunkManager"/> for the given dimension.
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="dim">The id of the dimension to look up.</param>
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// <returns>A <see cref="RegionChunkManager"/> tied to the given dimension in this world.</returns>
|
|
|
|
|
/// <remarks>Get a <see cref="RegionChunkManager"/> if you you need to work with easily-digestible, bounded chunks of blocks.</remarks>
|
|
|
|
|
public new RegionChunkManager GetChunkManager (int dim)
|
2011-07-04 03:45:36 +00:00
|
|
|
|
{
|
2012-04-28 19:52:40 +00:00
|
|
|
|
return GetChunkManagerVirt(dim) as RegionChunkManager;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a <see cref="RegionManager"/> for the default dimension.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A <see cref="RegionManager"/> tied to the defaul dimension in this world.</returns>
|
|
|
|
|
/// <remarks>Regions are a higher-level unit of organization for blocks unique to worlds created in Beta 1.3 and beyond.
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// Consider using the <see cref="RegionChunkManager"/> if you are interested in working with blocks.</remarks>
|
|
|
|
|
public BetaRegionManager GetRegionManager ()
|
2011-07-04 03:45:36 +00:00
|
|
|
|
{
|
|
|
|
|
return GetRegionManager(Dimension.DEFAULT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a <see cref="RegionManager"/> for the given dimension.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="dim">The id of the dimension to look up.</param>
|
|
|
|
|
/// <returns>A <see cref="RegionManager"/> tied to the given dimension in this world.</returns>
|
|
|
|
|
/// <remarks>Regions are a higher-level unit of organization for blocks unique to worlds created in Beta 1.3 and beyond.
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// Consider using the <see cref="RegionChunkManager"/> if you are interested in working with blocks.</remarks>
|
|
|
|
|
public BetaRegionManager GetRegionManager (int dim)
|
2011-07-04 03:45:36 +00:00
|
|
|
|
{
|
2012-04-28 19:52:40 +00:00
|
|
|
|
BetaRegionManager rm;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
if (_regionMgrs.TryGetValue(dim, out rm)) {
|
|
|
|
|
return rm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OpenDimension(dim);
|
|
|
|
|
return _regionMgrs[dim];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a <see cref="PlayerManager"/> for maanging players on multiplayer worlds.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A <see cref="PlayerManager"/> for this world.</returns>
|
|
|
|
|
/// <remarks>To manage the player of a single-player world, get a <see cref="Level"/> object for the world instead.</remarks>
|
|
|
|
|
public new PlayerManager GetPlayerManager ()
|
|
|
|
|
{
|
|
|
|
|
return GetPlayerManagerVirt() as PlayerManager;
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-10 02:46:19 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a <see cref="BetaDataManager"/> for managing data resources, such as maps.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A <see cref="BetaDataManager"/> for this world.</returns>
|
|
|
|
|
public new BetaDataManager GetDataManager ()
|
|
|
|
|
{
|
|
|
|
|
return GetDataManagerVirt() as BetaDataManager;
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// <summary>
|
2012-04-28 19:52:40 +00:00
|
|
|
|
/// Saves the world's <see cref="Level"/> data, and any <see cref="IChunk"/> objects known to have unsaved changes.
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
public void Save ()
|
|
|
|
|
{
|
|
|
|
|
_level.Save();
|
|
|
|
|
|
2012-04-28 19:52:40 +00:00
|
|
|
|
foreach (KeyValuePair<int, RegionChunkManager> cm in _chunkMgrs) {
|
2011-07-04 03:45:36 +00:00
|
|
|
|
cm.Value.Save();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-17 06:43:41 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the <see cref="ChunkCache"/> currently managing chunks in the default dimension.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>The <see cref="ChunkCache"/> for the default dimension, or null if the dimension was not found.</returns>
|
|
|
|
|
public ChunkCache GetChunkCache ()
|
|
|
|
|
{
|
|
|
|
|
return GetChunkCache(Dimension.DEFAULT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the <see cref="ChunkCache"/> currently managing chunks in the given dimension.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="dim">The id of a dimension to look up.</param>
|
|
|
|
|
/// <returns>The <see cref="ChunkCache"/> for the given dimension, or null if the dimension was not found.</returns>
|
|
|
|
|
public ChunkCache GetChunkCache (int dim)
|
|
|
|
|
{
|
|
|
|
|
if (_caches.ContainsKey(dim)) {
|
|
|
|
|
return _caches[dim];
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Opens an existing Beta-compatible Minecraft world and returns a new <see cref="BetaWorld"/> to represent it.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The path to the directory containing the world's level.dat, or the path to level.dat itself.</param>
|
|
|
|
|
/// <returns>A new <see cref="BetaWorld"/> object representing an existing world on disk.</returns>
|
2011-07-07 04:27:48 +00:00
|
|
|
|
public static new BetaWorld Open (string path)
|
2011-07-04 03:45:36 +00:00
|
|
|
|
{
|
|
|
|
|
return new BetaWorld().OpenWorld(path) as BetaWorld;
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-05 19:12:48 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Opens an existing Beta-compatible Minecraft world and returns a new <see cref="BetaWorld"/> to represent it.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The path to the directory containing the world's level.dat, or the path to level.dat itself.</param>
|
|
|
|
|
/// <param name="cacheSize">The preferred cache size in chunks for each opened dimension in this world.</param>
|
|
|
|
|
/// <returns>A new <see cref="BetaWorld"/> object representing an existing world on disk.</returns>
|
2011-11-10 02:46:19 +00:00
|
|
|
|
public static BetaWorld Open (string path, int cacheSize)
|
2011-11-05 19:12:48 +00:00
|
|
|
|
{
|
|
|
|
|
BetaWorld world = new BetaWorld().OpenWorld(path);
|
|
|
|
|
world._prefCacheSize = cacheSize;
|
|
|
|
|
|
|
|
|
|
return world;
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new Beta-compatible Minecraft world and returns a new <see cref="BetaWorld"/> to represent it.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The path to the directory where the new world should be stored.</param>
|
|
|
|
|
/// <returns>A new <see cref="BetaWorld"/> object representing a new world.</returns>
|
|
|
|
|
/// <remarks>This method will attempt to create the specified directory immediately if it does not exist, but will not
|
|
|
|
|
/// write out any world data unless it is explicitly saved at a later time.</remarks>
|
|
|
|
|
public static BetaWorld Create (string path)
|
|
|
|
|
{
|
|
|
|
|
return new BetaWorld().CreateWorld(path) as BetaWorld;
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-05 19:12:48 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new Beta-compatible Minecraft world and returns a new <see cref="BetaWorld"/> to represent it.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The path to the directory where the new world should be stored.</param>
|
|
|
|
|
/// <param name="cacheSize">The preferred cache size in chunks for each opened dimension in this world.</param>
|
|
|
|
|
/// <returns>A new <see cref="BetaWorld"/> object representing a new world.</returns>
|
|
|
|
|
/// <remarks>This method will attempt to create the specified directory immediately if it does not exist, but will not
|
|
|
|
|
/// write out any world data unless it is explicitly saved at a later time.</remarks>
|
|
|
|
|
public static BetaWorld Create (string path, int cacheSize)
|
|
|
|
|
{
|
|
|
|
|
BetaWorld world = new BetaWorld().CreateWorld(path);
|
|
|
|
|
world._prefCacheSize = cacheSize;
|
|
|
|
|
|
|
|
|
|
return world;
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
/// <exclude/>
|
|
|
|
|
protected override IBlockManager GetBlockManagerVirt (int dim)
|
|
|
|
|
{
|
|
|
|
|
BlockManager rm;
|
|
|
|
|
if (_blockMgrs.TryGetValue(dim, out rm)) {
|
|
|
|
|
return rm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OpenDimension(dim);
|
|
|
|
|
return _blockMgrs[dim];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <exclude/>
|
|
|
|
|
protected override IChunkManager GetChunkManagerVirt (int dim)
|
|
|
|
|
{
|
2012-04-28 19:52:40 +00:00
|
|
|
|
RegionChunkManager rm;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
if (_chunkMgrs.TryGetValue(dim, out rm)) {
|
|
|
|
|
return rm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OpenDimension(dim);
|
|
|
|
|
return _chunkMgrs[dim];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <exclude/>
|
|
|
|
|
protected override IPlayerManager GetPlayerManagerVirt ()
|
|
|
|
|
{
|
|
|
|
|
if (_playerMan != null) {
|
|
|
|
|
return _playerMan;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string path = IO.Path.Combine(Path, _PLAYER_DIR);
|
|
|
|
|
|
|
|
|
|
_playerMan = new PlayerManager(path);
|
|
|
|
|
return _playerMan;
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-10 02:46:19 +00:00
|
|
|
|
/// <exclude/>
|
|
|
|
|
protected override Data.DataManager GetDataManagerVirt ()
|
|
|
|
|
{
|
|
|
|
|
if (_dataMan != null) {
|
|
|
|
|
return _dataMan;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dataMan = new BetaDataManager(this);
|
|
|
|
|
return _dataMan;
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-04 03:45:36 +00:00
|
|
|
|
private void OpenDimension (int dim)
|
|
|
|
|
{
|
|
|
|
|
string path = Path;
|
|
|
|
|
if (dim == Dimension.DEFAULT) {
|
|
|
|
|
path = IO.Path.Combine(path, _REGION_DIR);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
path = IO.Path.Combine(path, "DIM" + dim);
|
|
|
|
|
path = IO.Path.Combine(path, _REGION_DIR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Directory.Exists(path)) {
|
|
|
|
|
Directory.CreateDirectory(path);
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-05 19:12:48 +00:00
|
|
|
|
ChunkCache cc = new ChunkCache(_prefCacheSize);
|
2011-07-04 03:45:36 +00:00
|
|
|
|
|
2012-04-28 19:52:40 +00:00
|
|
|
|
BetaRegionManager rm = new BetaRegionManager(path, cc);
|
|
|
|
|
RegionChunkManager cm = new RegionChunkManager(rm, cc);
|
2011-07-04 03:45:36 +00:00
|
|
|
|
BlockManager bm = new BlockManager(cm);
|
|
|
|
|
|
|
|
|
|
_regionMgrs[dim] = rm;
|
|
|
|
|
_chunkMgrs[dim] = cm;
|
|
|
|
|
_blockMgrs[dim] = bm;
|
2011-12-17 06:43:41 +00:00
|
|
|
|
|
|
|
|
|
_caches[dim] = cc;
|
2011-07-04 03:45:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private BetaWorld OpenWorld (string path)
|
|
|
|
|
{
|
|
|
|
|
if (!Directory.Exists(path)) {
|
|
|
|
|
if (File.Exists(path)) {
|
|
|
|
|
_levelFile = IO.Path.GetFileName(path);
|
|
|
|
|
path = IO.Path.GetDirectoryName(path);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
throw new DirectoryNotFoundException("Directory '" + path + "' not found");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Path = path;
|
|
|
|
|
|
|
|
|
|
string ldat = IO.Path.Combine(path, _levelFile);
|
|
|
|
|
if (!File.Exists(ldat)) {
|
|
|
|
|
throw new FileNotFoundException("Data file '" + _levelFile + "' not found in '" + path + "'", ldat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!LoadLevel()) {
|
|
|
|
|
throw new Exception("Failed to load '" + _levelFile + "'");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private BetaWorld CreateWorld (string path)
|
|
|
|
|
{
|
|
|
|
|
if (!Directory.Exists(path)) {
|
|
|
|
|
throw new DirectoryNotFoundException("Directory '" + path + "' not found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string regpath = IO.Path.Combine(path, _REGION_DIR);
|
|
|
|
|
if (!Directory.Exists(regpath)) {
|
|
|
|
|
Directory.CreateDirectory(regpath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Path = path;
|
|
|
|
|
_level = new Level(this);
|
|
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool LoadLevel ()
|
|
|
|
|
{
|
|
|
|
|
NBTFile nf = new NBTFile(IO.Path.Combine(Path, _levelFile));
|
|
|
|
|
Stream nbtstr = nf.GetDataInputStream();
|
|
|
|
|
if (nbtstr == null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NbtTree tree = new NbtTree(nbtstr);
|
|
|
|
|
|
|
|
|
|
_level = new Level(this);
|
|
|
|
|
_level = _level.LoadTreeSafe(tree.Root);
|
|
|
|
|
|
|
|
|
|
return _level != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static void OnResolveOpen (object sender, OpenWorldEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
BetaWorld world = new BetaWorld().OpenWorld(e.Path);
|
|
|
|
|
if (world == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string regPath = IO.Path.Combine(e.Path, _REGION_DIR);
|
|
|
|
|
if (!Directory.Exists(regPath)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2012-04-28 19:52:40 +00:00
|
|
|
|
if (world.Level.Version != 19132) {
|
2011-07-04 03:45:36 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.AddHandler(Open);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|