forked from mirrors/NBTExplorer
Organizational
This commit is contained in:
parent
63e0bc1876
commit
66dfc9da95
8 changed files with 1591 additions and 1601 deletions
82
SubstrateCS/Source/AnvilRegion.cs
Normal file
82
SubstrateCS/Source/AnvilRegion.cs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Substrate.Core;
|
||||||
|
using Substrate.Nbt;
|
||||||
|
|
||||||
|
namespace Substrate
|
||||||
|
{
|
||||||
|
public class AnvilRegion : Region
|
||||||
|
{
|
||||||
|
private static Regex _namePattern = new Regex("r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mcr$");
|
||||||
|
|
||||||
|
public AnvilRegion (AnvilRegionManager rm, ChunkCache cache, int rx, int rz)
|
||||||
|
: base(rm, cache, rx, rz)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inherits />
|
||||||
|
public override string GetFileName ()
|
||||||
|
{
|
||||||
|
return "r." + _rx + "." + _rz + ".mcr";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inherits />
|
||||||
|
public override string GetFilePath ()
|
||||||
|
{
|
||||||
|
return System.IO.Path.Combine(_regionMan.GetRegionPath(), GetFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests if the given filename conforms to the general naming pattern for any region.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename to test.</param>
|
||||||
|
/// <returns>True if the filename is a valid region name; false if it does not conform to the pattern.</returns>
|
||||||
|
public static bool TestFileName (string filename)
|
||||||
|
{
|
||||||
|
Match match = _namePattern.Match(filename);
|
||||||
|
if (!match.Success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ParseFileName (string filename, out int x, out int z)
|
||||||
|
{
|
||||||
|
x = 0;
|
||||||
|
z = 0;
|
||||||
|
|
||||||
|
Match match = _namePattern.Match(filename);
|
||||||
|
if (!match.Success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Convert.ToInt32(match.Groups[1].Value);
|
||||||
|
z = Convert.ToInt32(match.Groups[2].Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the given filename to extract encoded region coordinates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The region filename to parse.</param>
|
||||||
|
/// <param name="x">This parameter will contain the X-coordinate of a region.</param>
|
||||||
|
/// <param name="z">This parameter will contain the Z-coordinate of a region.</param>
|
||||||
|
/// <returns>True if the filename could be correctly parse; false otherwise.</returns>
|
||||||
|
protected override bool ParseFileNameCore (string filename, out int x, out int z)
|
||||||
|
{
|
||||||
|
return ParseFileName(filename, out x, out z);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IChunk CreateChunkCore (int cx, int cz)
|
||||||
|
{
|
||||||
|
return AlphaChunk.Create(cz, cz);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IChunk CreateChunkVerifiedCore (NbtTree tree)
|
||||||
|
{
|
||||||
|
return AlphaChunk.CreateVerified(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
SubstrateCS/Source/AnvilRegionManager.cs
Normal file
43
SubstrateCS/Source/AnvilRegionManager.cs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Substrate.Core;
|
||||||
|
|
||||||
|
namespace Substrate
|
||||||
|
{
|
||||||
|
public class AnvilRegionManager : RegionManager
|
||||||
|
{
|
||||||
|
public AnvilRegionManager (string regionDir, ChunkCache cache)
|
||||||
|
: base(regionDir, cache)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IRegion CreateRegionCore (int rx, int rz)
|
||||||
|
{
|
||||||
|
return new AnvilRegion(this, _chunkCache, rx, rz);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override RegionFile CreateRegionFileCore (int rx, int rz)
|
||||||
|
{
|
||||||
|
string fp = "r." + rx + "." + rz + ".mca";
|
||||||
|
return new RegionFile(Path.Combine(_regionPath, fp));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DeleteRegionCore (IRegion region)
|
||||||
|
{
|
||||||
|
AnvilRegion r = region as AnvilRegion;
|
||||||
|
if (r != null) {
|
||||||
|
r.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IRegion GetRegion (string filename)
|
||||||
|
{
|
||||||
|
int rx, rz;
|
||||||
|
if (!AnvilRegion.ParseFileName(filename, out rx, out rz)) {
|
||||||
|
throw new ArgumentException("Malformed region file name: " + filename, "filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetRegion(rx, rz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
SubstrateCS/Source/BetaRegion.cs
Normal file
82
SubstrateCS/Source/BetaRegion.cs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Substrate.Core;
|
||||||
|
using Substrate.Nbt;
|
||||||
|
|
||||||
|
namespace Substrate
|
||||||
|
{
|
||||||
|
public class BetaRegion : Region
|
||||||
|
{
|
||||||
|
private static Regex _namePattern = new Regex("r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mcr$");
|
||||||
|
|
||||||
|
public BetaRegion (BetaRegionManager rm, ChunkCache cache, int rx, int rz)
|
||||||
|
: base(rm, cache, rx, rz)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inherits />
|
||||||
|
public override string GetFileName ()
|
||||||
|
{
|
||||||
|
return "r." + _rx + "." + _rz + ".mcr";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inherits />
|
||||||
|
public override string GetFilePath ()
|
||||||
|
{
|
||||||
|
return System.IO.Path.Combine(_regionMan.GetRegionPath(), GetFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests if the given filename conforms to the general naming pattern for any region.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The filename to test.</param>
|
||||||
|
/// <returns>True if the filename is a valid region name; false if it does not conform to the pattern.</returns>
|
||||||
|
public static bool TestFileName (string filename)
|
||||||
|
{
|
||||||
|
Match match = _namePattern.Match(filename);
|
||||||
|
if (!match.Success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ParseFileName (string filename, out int x, out int z)
|
||||||
|
{
|
||||||
|
x = 0;
|
||||||
|
z = 0;
|
||||||
|
|
||||||
|
Match match = _namePattern.Match(filename);
|
||||||
|
if (!match.Success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Convert.ToInt32(match.Groups[1].Value);
|
||||||
|
z = Convert.ToInt32(match.Groups[2].Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the given filename to extract encoded region coordinates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">The region filename to parse.</param>
|
||||||
|
/// <param name="x">This parameter will contain the X-coordinate of a region.</param>
|
||||||
|
/// <param name="z">This parameter will contain the Z-coordinate of a region.</param>
|
||||||
|
/// <returns>True if the filename could be correctly parse; false otherwise.</returns>
|
||||||
|
protected override bool ParseFileNameCore (string filename, out int x, out int z)
|
||||||
|
{
|
||||||
|
return ParseFileName(filename, out x, out z);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IChunk CreateChunkCore (int cx, int cz)
|
||||||
|
{
|
||||||
|
return AlphaChunk.Create(cz, cz);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IChunk CreateChunkVerifiedCore (NbtTree tree)
|
||||||
|
{
|
||||||
|
return AlphaChunk.CreateVerified(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
SubstrateCS/Source/BetaRegionManager.cs
Normal file
43
SubstrateCS/Source/BetaRegionManager.cs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Substrate.Core;
|
||||||
|
|
||||||
|
namespace Substrate
|
||||||
|
{
|
||||||
|
public class BetaRegionManager : RegionManager
|
||||||
|
{
|
||||||
|
public BetaRegionManager (string regionDir, ChunkCache cache)
|
||||||
|
: base(regionDir, cache)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IRegion CreateRegionCore (int rx, int rz)
|
||||||
|
{
|
||||||
|
return new BetaRegion(this, _chunkCache, rx, rz);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override RegionFile CreateRegionFileCore (int rx, int rz)
|
||||||
|
{
|
||||||
|
string fp = "r." + rx + "." + rz + ".mcr";
|
||||||
|
return new RegionFile(Path.Combine(_regionPath, fp));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DeleteRegionCore (IRegion region)
|
||||||
|
{
|
||||||
|
BetaRegion r = region as BetaRegion;
|
||||||
|
if (r != null) {
|
||||||
|
r.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IRegion GetRegion (string filename)
|
||||||
|
{
|
||||||
|
int rx, rz;
|
||||||
|
if (!BetaRegion.ParseFileName(filename, out rx, out rz)) {
|
||||||
|
throw new ArgumentException("Malformed region file name: " + filename, "filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetRegion(rx, rz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,187 +6,9 @@ using System.IO;
|
||||||
using Substrate.Nbt;
|
using Substrate.Nbt;
|
||||||
using Substrate.Core;
|
using Substrate.Core;
|
||||||
|
|
||||||
namespace Substrate
|
namespace Substrate.Core
|
||||||
{
|
{
|
||||||
public interface IRegion : IChunkContainer
|
|
||||||
{
|
|
||||||
int X { get; }
|
|
||||||
|
|
||||||
int Z { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the appropriate filename for this region.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The filename of the region with encoded coordinates.</returns>
|
|
||||||
string GetFileName ();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the full path of the region's backing file.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Gets the path of the region's file based on the <see cref="IRegionManager"/>'s region path and the region's on filename.</returns>
|
|
||||||
string GetFilePath ();
|
|
||||||
|
|
||||||
NbtTree GetChunkTree (int lcx, int lcz);
|
|
||||||
bool SaveChunkTree (int lcx, int lcz, NbtTree tree);
|
|
||||||
bool SaveChunkTree (int lcx, int lcz, NbtTree tree, int timestamp);
|
|
||||||
Stream GetChunkOutStream (int lcx, int lcz);
|
|
||||||
|
|
||||||
int ChunkCount ();
|
|
||||||
ChunkRef GetChunkRef (int lcx, int lcz);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new chunk at the given local coordinates relative to this region and returns a new <see cref="ChunkRef"/> for it.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <returns>A <see cref="ChunkRef"/> for the newly created chunk.</returns>
|
|
||||||
/// <remarks>If the local coordinates are out of bounds for this region, the action will be forwarded to the correct region
|
|
||||||
/// transparently.</remarks>
|
|
||||||
ChunkRef CreateChunk (int lcx, int lcz);
|
|
||||||
|
|
||||||
int GetChunkTimestamp (int lcx, int lcz);
|
|
||||||
void SetChunkTimestamp (int lcx, int lcz, int timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BetaRegion : Region
|
|
||||||
{
|
|
||||||
private static Regex _namePattern = new Regex("r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mcr$");
|
|
||||||
|
|
||||||
public BetaRegion (BetaRegionManager rm, ChunkCache cache, int rx, int rz)
|
|
||||||
: base(rm, cache, rx, rz)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inherits />
|
|
||||||
public string GetFileName ()
|
|
||||||
{
|
|
||||||
return "r." + _rx + "." + _rz + ".mcr";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inherits />
|
|
||||||
public string GetFilePath ()
|
|
||||||
{
|
|
||||||
return System.IO.Path.Combine(_regionMan.GetRegionPath(), GetFileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests if the given filename conforms to the general naming pattern for any region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The filename to test.</param>
|
|
||||||
/// <returns>True if the filename is a valid region name; false if it does not conform to the pattern.</returns>
|
|
||||||
public static bool TestFileName (string filename)
|
|
||||||
{
|
|
||||||
Match match = _namePattern.Match(filename);
|
|
||||||
if (!match.Success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses the given filename to extract encoded region coordinates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The region filename to parse.</param>
|
|
||||||
/// <param name="x">This parameter will contain the X-coordinate of a region.</param>
|
|
||||||
/// <param name="z">This parameter will contain the Z-coordinate of a region.</param>
|
|
||||||
/// <returns>True if the filename could be correctly parse; false otherwise.</returns>
|
|
||||||
public static bool ParseFileName (string filename, out int x, out int z)
|
|
||||||
{
|
|
||||||
x = 0;
|
|
||||||
z = 0;
|
|
||||||
|
|
||||||
Match match = _namePattern.Match(filename);
|
|
||||||
if (!match.Success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
x = Convert.ToInt32(match.Groups[1].Value);
|
|
||||||
z = Convert.ToInt32(match.Groups[2].Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IChunk CreateChunkCore (int cx, int cz)
|
|
||||||
{
|
|
||||||
return AlphaChunk.Create(cz, cz);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IChunk CreateChunkVerifiedCore (NbtTree tree)
|
|
||||||
{
|
|
||||||
return AlphaChunk.CreateVerified(tree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnvilRegion : Region
|
|
||||||
{
|
|
||||||
private static Regex _namePattern = new Regex("r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mcr$");
|
|
||||||
|
|
||||||
public AnvilRegion (AnvilRegionManager rm, ChunkCache cache, int rx, int rz)
|
|
||||||
: base(rm, cache, rx, rz)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inherits />
|
|
||||||
public string GetFileName ()
|
|
||||||
{
|
|
||||||
return "r." + _rx + "." + _rz + ".mcr";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inherits />
|
|
||||||
public string GetFilePath ()
|
|
||||||
{
|
|
||||||
return System.IO.Path.Combine(_regionMan.GetRegionPath(), GetFileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests if the given filename conforms to the general naming pattern for any region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The filename to test.</param>
|
|
||||||
/// <returns>True if the filename is a valid region name; false if it does not conform to the pattern.</returns>
|
|
||||||
public static bool TestFileName (string filename)
|
|
||||||
{
|
|
||||||
Match match = _namePattern.Match(filename);
|
|
||||||
if (!match.Success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses the given filename to extract encoded region coordinates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The region filename to parse.</param>
|
|
||||||
/// <param name="x">This parameter will contain the X-coordinate of a region.</param>
|
|
||||||
/// <param name="z">This parameter will contain the Z-coordinate of a region.</param>
|
|
||||||
/// <returns>True if the filename could be correctly parse; false otherwise.</returns>
|
|
||||||
public static bool ParseFileName (string filename, out int x, out int z)
|
|
||||||
{
|
|
||||||
x = 0;
|
|
||||||
z = 0;
|
|
||||||
|
|
||||||
Match match = _namePattern.Match(filename);
|
|
||||||
if (!match.Success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
x = Convert.ToInt32(match.Groups[1].Value);
|
|
||||||
z = Convert.ToInt32(match.Groups[2].Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IChunk CreateChunkCore (int cx, int cz)
|
|
||||||
{
|
|
||||||
return AlphaChunk.Create(cz, cz);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IChunk CreateChunkVerifiedCore (NbtTree tree)
|
|
||||||
{
|
|
||||||
return AlphaChunk.CreateVerified(tree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a single region containing 32x32 chunks.
|
/// Represents a single region containing 32x32 chunks.
|
||||||
|
@ -216,17 +38,15 @@ namespace Substrate
|
||||||
|
|
||||||
protected abstract IChunk CreateChunkVerifiedCore (NbtTree tree);
|
protected abstract IChunk CreateChunkVerifiedCore (NbtTree tree);
|
||||||
|
|
||||||
/// <summary>
|
protected abstract bool ParseFileNameCore (string filename, out int x, out int z);
|
||||||
/// Gets the global X-coordinate of the region.
|
|
||||||
/// </summary>
|
/// <inherit />
|
||||||
public int X
|
public int X
|
||||||
{
|
{
|
||||||
get { return _rx; }
|
get { return _rx; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherit />
|
||||||
/// Gets the global Z-coordinate of the region.
|
|
||||||
/// </summary>
|
|
||||||
public int Z
|
public int Z
|
||||||
{
|
{
|
||||||
get { return _rz; }
|
get { return _rz; }
|
||||||
|
@ -248,6 +68,10 @@ namespace Substrate
|
||||||
get { return ZDIM; }
|
get { return ZDIM; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract string GetFileName ();
|
||||||
|
|
||||||
|
public abstract string GetFilePath ();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance of a <see cref="Region"/> for a given set of coordinates.
|
/// Creates an instance of a <see cref="Region"/> for a given set of coordinates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -290,7 +114,7 @@ namespace Substrate
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_regionFile = new WeakReference(null);
|
_regionFile = new WeakReference(null);
|
||||||
|
|
||||||
ParseFileName(filename, out _rx, out _rz);
|
ParseFileNameCore(filename, out _rx, out _rz);
|
||||||
|
|
||||||
if (!File.Exists(Path.Combine(_regionMan.GetRegionPath(), filename))) {
|
if (!File.Exists(Path.Combine(_regionMan.GetRegionPath(), filename))) {
|
||||||
throw new FileNotFoundException();
|
throw new FileNotFoundException();
|
||||||
|
@ -335,62 +159,6 @@ namespace Substrate
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the appropriate filename for this region.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The filename of the region with encoded coordinates.</returns>
|
|
||||||
public string GetFileName ()
|
|
||||||
{
|
|
||||||
return "r." + _rx + "." + _rz + ".mca";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests if the given filename conforms to the general naming pattern for any region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The filename to test.</param>
|
|
||||||
/// <returns>True if the filename is a valid region name; false if it does not conform to the pattern.</returns>
|
|
||||||
public static bool TestFileName (string filename)
|
|
||||||
{
|
|
||||||
Match match = _namePattern.Match(filename);
|
|
||||||
if (!match.Success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses the given filename to extract encoded region coordinates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The region filename to parse.</param>
|
|
||||||
/// <param name="x">This parameter will contain the X-coordinate of a region.</param>
|
|
||||||
/// <param name="z">This parameter will contain the Z-coordinate of a region.</param>
|
|
||||||
/// <returns>True if the filename could be correctly parse; false otherwise.</returns>
|
|
||||||
public static bool ParseFileName (string filename, out int x, out int z)
|
|
||||||
{
|
|
||||||
x = 0;
|
|
||||||
z = 0;
|
|
||||||
|
|
||||||
Match match = _namePattern.Match(filename);
|
|
||||||
if (!match.Success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
x = Convert.ToInt32(match.Groups[1].Value);
|
|
||||||
z = Convert.ToInt32(match.Groups[2].Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the full path of the region's backing file.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Gets the path of the region's file based on the <see cref="RegionManager"/>'s region path and the region's on filename.</returns>
|
|
||||||
public string GetFilePath ()
|
|
||||||
{
|
|
||||||
return System.IO.Path.Combine(_regionMan.GetRegionPath(), GetFileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private RegionFile GetRegionFile ()
|
private RegionFile GetRegionFile ()
|
||||||
{
|
{
|
||||||
RegionFile rf = _regionFile.Target as RegionFile;
|
RegionFile rf = _regionFile.Target as RegionFile;
|
||||||
|
@ -402,12 +170,7 @@ namespace Substrate
|
||||||
return rf;
|
return rf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Gets the <see cref="NbtTree"/> for a chunk given local coordinates into the region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of a chunk within the region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of a chunk within the region.</param>
|
|
||||||
/// <returns>An <see cref="NbtTree"/> for a local chunk, or null if there is no chunk at the given coordinates.</returns>
|
|
||||||
public NbtTree GetChunkTree (int lcx, int lcz)
|
public NbtTree GetChunkTree (int lcx, int lcz)
|
||||||
{
|
{
|
||||||
if (!LocalBoundsCheck(lcx, lcz)) {
|
if (!LocalBoundsCheck(lcx, lcz)) {
|
||||||
|
@ -428,30 +191,13 @@ namespace Substrate
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Exceptions
|
// XXX: Exceptions
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Saves an <see cref="NbtTree"/> for a chunk back to the region's data store at the given local coordinates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of the chunk within the region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of the chunk within the region.</param>
|
|
||||||
/// <param name="tree">The <see cref="NbtTree"/> of a chunk to write back to the region.</param>
|
|
||||||
/// <returns>True if the save succeeded.</returns>
|
|
||||||
/// <remarks>It is up to the programmer to ensure that the global coordinates defined within the chunk's tree
|
|
||||||
/// are consistent with the local coordinates of the region being written into.</remarks>
|
|
||||||
public bool SaveChunkTree (int lcx, int lcz, NbtTree tree)
|
public bool SaveChunkTree (int lcx, int lcz, NbtTree tree)
|
||||||
{
|
{
|
||||||
return SaveChunkTree(lcx, lcz, tree, null);
|
return SaveChunkTree(lcx, lcz, tree, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Saves an <see cref="NbtTree"/> for a chunk back to the region's data store at the given local coordinates and with the given timestamp.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of the chunk within the region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of the chunk within the region.</param>
|
|
||||||
/// <param name="tree">The <see cref="NbtTree"/> of a chunk to write back to the region.</param>
|
|
||||||
/// <param name="timestamp">The timestamp to write to the underlying region file for this chunk.</param>
|
|
||||||
/// <returns>True if the save succeeded.</returns>
|
|
||||||
/// <remarks>It is up to the programmer to ensure that the global coordinates defined within the chunk's tree
|
|
||||||
/// are consistent with the local coordinates of the region being written into.</remarks>
|
|
||||||
public bool SaveChunkTree (int lcx, int lcz, NbtTree tree, int timestamp)
|
public bool SaveChunkTree (int lcx, int lcz, NbtTree tree, int timestamp)
|
||||||
{
|
{
|
||||||
return SaveChunkTree(lcx, lcz, tree, timestamp);
|
return SaveChunkTree(lcx, lcz, tree, timestamp);
|
||||||
|
@ -479,13 +225,7 @@ namespace Substrate
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Gets an output stream for replacing chunk data at the given coordinates within the region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of the chunk to replace within the region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of the chunk to replace within the region.</param>
|
|
||||||
/// <returns>An output stream that can be written to on demand.</returns>
|
|
||||||
/// <remarks>There is no guarantee that any data will be saved until the stream is closed.</remarks>
|
|
||||||
public Stream GetChunkOutStream (int lcx, int lcz)
|
public Stream GetChunkOutStream (int lcx, int lcz)
|
||||||
{
|
{
|
||||||
if (!LocalBoundsCheck(lcx, lcz)) {
|
if (!LocalBoundsCheck(lcx, lcz)) {
|
||||||
|
@ -497,10 +237,7 @@ namespace Substrate
|
||||||
return rf.GetChunkDataOutputStream(lcx, lcz);
|
return rf.GetChunkDataOutputStream(lcx, lcz);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Returns the count of valid chunks stored in this region.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The count of currently stored chunks.</returns>
|
|
||||||
public int ChunkCount ()
|
public int ChunkCount ()
|
||||||
{
|
{
|
||||||
RegionFile rf = GetRegionFile();
|
RegionFile rf = GetRegionFile();
|
||||||
|
@ -518,16 +255,7 @@ namespace Substrate
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Consider revising foreign lookup support
|
// XXX: Consider revising foreign lookup support
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Gets a <see cref="ChunkRef"/> for a chunk at the given local coordinates relative to this region.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <returns>A <see cref="ChunkRef"/> at the given local coordinates, or null if no chunk exists.</returns>
|
|
||||||
/// <remarks>The local coordinates do not strictly need to be within the bounds of the region. If coordinates are detected
|
|
||||||
/// as being out of bounds, the lookup will be delegated to the correct region and the lookup will be performed there
|
|
||||||
/// instead. This allows any <see cref="Region"/> to perform a similar task to <see cref="RegionChunkManager"/>, but with a
|
|
||||||
/// region-local frame of reference instead of a global frame of reference.</remarks>
|
|
||||||
public ChunkRef GetChunkRef (int lcx, int lcz)
|
public ChunkRef GetChunkRef (int lcx, int lcz)
|
||||||
{
|
{
|
||||||
if (!LocalBoundsCheck(lcx, lcz)) {
|
if (!LocalBoundsCheck(lcx, lcz)) {
|
||||||
|
@ -552,14 +280,7 @@ namespace Substrate
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Creates a new chunk at the given local coordinates relative to this region and returns a new <see cref="ChunkRef"/> for it.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <returns>A <see cref="ChunkRef"/> for the newly created chunk.</returns>
|
|
||||||
/// <remarks>If the local coordinates are out of bounds for this region, the action will be forwarded to the correct region
|
|
||||||
/// transparently.</remarks>
|
|
||||||
public ChunkRef CreateChunk (int lcx, int lcz)
|
public ChunkRef CreateChunk (int lcx, int lcz)
|
||||||
{
|
{
|
||||||
if (!LocalBoundsCheck(lcx, lcz)) {
|
if (!LocalBoundsCheck(lcx, lcz)) {
|
||||||
|
@ -771,13 +492,7 @@ namespace Substrate
|
||||||
get { return true; }
|
get { return true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Gets the timestamp of a chunk from the underlying region file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <returns>The timestamp of the chunk slot in the region.</returns>
|
|
||||||
/// <remarks>The value returned may differ from any timestamp stored in the chunk data itself.</remarks>
|
|
||||||
public int GetChunkTimestamp (int lcx, int lcz)
|
public int GetChunkTimestamp (int lcx, int lcz)
|
||||||
{
|
{
|
||||||
if (!LocalBoundsCheck(lcx, lcz)) {
|
if (!LocalBoundsCheck(lcx, lcz)) {
|
||||||
|
@ -789,14 +504,7 @@ namespace Substrate
|
||||||
return rf.GetTimestamp(lcx, lcz);
|
return rf.GetTimestamp(lcx, lcz);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Sets the timestamp of a chunk in the underlying region file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
|
||||||
/// <param name="timestamp">The new timestamp value.</param>
|
|
||||||
/// <remarks>This function will only update the timestamp of the chunk slot in the underlying region file. It will not update
|
|
||||||
/// any timestamp information in the chunk data itself.</remarks>
|
|
||||||
public void SetChunkTimestamp (int lcx, int lcz, int timestamp)
|
public void SetChunkTimestamp (int lcx, int lcz, int timestamp)
|
||||||
{
|
{
|
||||||
if (!LocalBoundsCheck(lcx, lcz)) {
|
if (!LocalBoundsCheck(lcx, lcz)) {
|
|
@ -1,9 +1,120 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.IO;
|
||||||
using System.Text;
|
using Substrate.Nbt;
|
||||||
|
|
||||||
namespace Substrate.Core
|
namespace Substrate.Core
|
||||||
{
|
{
|
||||||
|
public interface IRegion : IChunkContainer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the global X-coordinate of the region.
|
||||||
|
/// </summary>
|
||||||
|
int X { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the global Z-coordinate of the region.
|
||||||
|
/// </summary>
|
||||||
|
int Z { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the appropriate filename for this region.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The filename of the region with encoded coordinates.</returns>
|
||||||
|
string GetFileName ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the full path of the region's backing file.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Gets the path of the region's file based on the <see cref="IRegionManager"/>'s region path and the region's on filename.</returns>
|
||||||
|
string GetFilePath ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="NbtTree"/> for a chunk given local coordinates into the region.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of a chunk within the region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of a chunk within the region.</param>
|
||||||
|
/// <returns>An <see cref="NbtTree"/> for a local chunk, or null if there is no chunk at the given coordinates.</returns>
|
||||||
|
NbtTree GetChunkTree (int lcx, int lcz);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves an <see cref="NbtTree"/> for a chunk back to the region's data store at the given local coordinates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of the chunk within the region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of the chunk within the region.</param>
|
||||||
|
/// <param name="tree">The <see cref="NbtTree"/> of a chunk to write back to the region.</param>
|
||||||
|
/// <returns>True if the save succeeded.</returns>
|
||||||
|
/// <remarks>It is up to the programmer to ensure that the global coordinates defined within the chunk's tree
|
||||||
|
/// are consistent with the local coordinates of the region being written into.</remarks>
|
||||||
|
bool SaveChunkTree (int lcx, int lcz, NbtTree tree);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves an <see cref="NbtTree"/> for a chunk back to the region's data store at the given local coordinates and with the given timestamp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of the chunk within the region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of the chunk within the region.</param>
|
||||||
|
/// <param name="tree">The <see cref="NbtTree"/> of a chunk to write back to the region.</param>
|
||||||
|
/// <param name="timestamp">The timestamp to write to the underlying region file for this chunk.</param>
|
||||||
|
/// <returns>True if the save succeeded.</returns>
|
||||||
|
/// <remarks>It is up to the programmer to ensure that the global coordinates defined within the chunk's tree
|
||||||
|
/// are consistent with the local coordinates of the region being written into.</remarks>
|
||||||
|
bool SaveChunkTree (int lcx, int lcz, NbtTree tree, int timestamp);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an output stream for replacing chunk data at the given coordinates within the region.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of the chunk to replace within the region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of the chunk to replace within the region.</param>
|
||||||
|
/// <returns>An output stream that can be written to on demand.</returns>
|
||||||
|
/// <remarks>There is no guarantee that any data will be saved until the stream is closed.</remarks>
|
||||||
|
Stream GetChunkOutStream (int lcx, int lcz);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the count of valid chunks stored in this region.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The count of currently stored chunks.</returns>
|
||||||
|
int ChunkCount ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a <see cref="ChunkRef"/> for a chunk at the given local coordinates relative to this region.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <returns>A <see cref="ChunkRef"/> at the given local coordinates, or null if no chunk exists.</returns>
|
||||||
|
/// <remarks>The local coordinates do not strictly need to be within the bounds of the region. If coordinates are detected
|
||||||
|
/// as being out of bounds, the lookup will be delegated to the correct region and the lookup will be performed there
|
||||||
|
/// instead. This allows any <see cref="Region"/> to perform a similar task to <see cref="RegionChunkManager"/>, but with a
|
||||||
|
/// region-local frame of reference instead of a global frame of reference.</remarks>
|
||||||
|
ChunkRef GetChunkRef (int lcx, int lcz);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new chunk at the given local coordinates relative to this region and returns a new <see cref="ChunkRef"/> for it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <returns>A <see cref="ChunkRef"/> for the newly created chunk.</returns>
|
||||||
|
/// <remarks>If the local coordinates are out of bounds for this region, the action will be forwarded to the correct region
|
||||||
|
/// transparently.</remarks>
|
||||||
|
ChunkRef CreateChunk (int lcx, int lcz);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp of a chunk from the underlying region file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <returns>The timestamp of the chunk slot in the region.</returns>
|
||||||
|
/// <remarks>The value returned may differ from any timestamp stored in the chunk data itself.</remarks>
|
||||||
|
int GetChunkTimestamp (int lcx, int lcz);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the timestamp of a chunk in the underlying region file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lcx">The local X-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <param name="lcz">The local Z-coordinate of a chunk relative to this region.</param>
|
||||||
|
/// <param name="timestamp">The new timestamp value.</param>
|
||||||
|
/// <remarks>This function will only update the timestamp of the chunk slot in the underlying region file. It will not update
|
||||||
|
/// any timestamp information in the chunk data itself.</remarks>
|
||||||
|
void SetChunkTimestamp (int lcx, int lcz, int timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
public interface IRegionContainer
|
public interface IRegionContainer
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,82 +4,8 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Substrate.Core;
|
using Substrate.Core;
|
||||||
|
|
||||||
namespace Substrate
|
namespace Substrate.Core
|
||||||
{
|
{
|
||||||
public class BetaRegionManager : RegionManager
|
|
||||||
{
|
|
||||||
public BetaRegionManager (string regionDir, ChunkCache cache)
|
|
||||||
: base(regionDir, cache)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IRegion CreateRegionCore (int rx, int rz)
|
|
||||||
{
|
|
||||||
return new BetaRegion(this, _chunkCache, rx, rz);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override RegionFile CreateRegionFileCore (int rx, int rz)
|
|
||||||
{
|
|
||||||
string fp = "r." + rx + "." + rz + ".mcr";
|
|
||||||
return new RegionFile(Path.Combine(_regionPath, fp));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DeleteRegionCore (IRegion region)
|
|
||||||
{
|
|
||||||
BetaRegion r = region as BetaRegion;
|
|
||||||
if (r != null) {
|
|
||||||
r.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IRegion GetRegion (string filename)
|
|
||||||
{
|
|
||||||
int rx, rz;
|
|
||||||
if (!BetaRegion.ParseFileName(filename, out rx, out rz)) {
|
|
||||||
throw new ArgumentException("Malformed region file name: " + filename, "filename");
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetRegion(rx, rz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnvilRegionManager : RegionManager
|
|
||||||
{
|
|
||||||
public AnvilRegionManager (string regionDir, ChunkCache cache)
|
|
||||||
: base(regionDir, cache)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IRegion CreateRegionCore (int rx, int rz)
|
|
||||||
{
|
|
||||||
return new AnvilRegion(this, _chunkCache, rx, rz);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override RegionFile CreateRegionFileCore (int rx, int rz)
|
|
||||||
{
|
|
||||||
string fp = "r." + rx + "." + rz + ".mca";
|
|
||||||
return new RegionFile(Path.Combine(_regionPath, fp));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DeleteRegionCore (IRegion region)
|
|
||||||
{
|
|
||||||
AnvilRegion r = region as AnvilRegion;
|
|
||||||
if (r != null) {
|
|
||||||
r.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IRegion GetRegion (string filename)
|
|
||||||
{
|
|
||||||
int rx, rz;
|
|
||||||
if (!AnvilRegion.ParseFileName(filename, out rx, out rz)) {
|
|
||||||
throw new ArgumentException("Malformed region file name: " + filename, "filename");
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetRegion(rx, rz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages the regions of a Beta-compatible world.
|
/// Manages the regions of a Beta-compatible world.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -112,12 +38,7 @@ namespace Substrate
|
||||||
_cache = new Dictionary<RegionKey, IRegion>();
|
_cache = new Dictionary<RegionKey, IRegion>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inherits />
|
||||||
/// Gets a <see cref="Region"/> at the given coordinates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rx">The global X-coordinate of a region.</param>
|
|
||||||
/// <param name="rz">The global Z-coordinate of a region.</param>
|
|
||||||
/// <returns>A <see cref="Region"/> representing a region at the given coordinates, or null if the region does not exist.</returns>
|
|
||||||
public IRegion GetRegion (int rx, int rz)
|
public IRegion GetRegion (int rx, int rz)
|
||||||
{
|
{
|
||||||
RegionKey k = new RegionKey(rx, rz);
|
RegionKey k = new RegionKey(rx, rz);
|
Loading…
Reference in a new issue