From 3b781d0a023c9d9810115a832e92e58b7bcc6bc0 Mon Sep 17 00:00:00 2001 From: Justin Aquadro Date: Wed, 9 Nov 2011 21:46:19 -0500 Subject: [PATCH] Data resource management (maps) --- SubstrateCS/Source/BetaWorld.cs | 24 +- SubstrateCS/Source/Core/NBTFile.cs | 48 +- SubstrateCS/Source/Data/BetaDataManager.cs | 121 +++++ SubstrateCS/Source/Data/DataExceptions.cs | 46 ++ SubstrateCS/Source/Data/DataManager.cs | 48 ++ SubstrateCS/Source/Data/Map.cs | 342 +++++++++++++ SubstrateCS/Source/Data/MapConverter.cs | 464 ++++++++++++++++++ SubstrateCS/Source/Data/MapFile.cs | 37 ++ SubstrateCS/Source/Data/MapManager.cs | 190 +++++++ .../Source/Data/MapManagerInterface.cs | 37 ++ SubstrateCS/Source/Nbt/NbtIOException.cs | 2 +- SubstrateCS/Source/NbtWorld.cs | 35 +- SubstrateCS/Source/PlayerManager.cs | 4 +- SubstrateCS/Substrate.csproj | 9 + 14 files changed, 1398 insertions(+), 9 deletions(-) create mode 100644 SubstrateCS/Source/Data/BetaDataManager.cs create mode 100644 SubstrateCS/Source/Data/DataExceptions.cs create mode 100644 SubstrateCS/Source/Data/DataManager.cs create mode 100644 SubstrateCS/Source/Data/Map.cs create mode 100644 SubstrateCS/Source/Data/MapConverter.cs create mode 100644 SubstrateCS/Source/Data/MapFile.cs create mode 100644 SubstrateCS/Source/Data/MapManager.cs create mode 100644 SubstrateCS/Source/Data/MapManagerInterface.cs diff --git a/SubstrateCS/Source/BetaWorld.cs b/SubstrateCS/Source/BetaWorld.cs index 05e66f2..edfbc40 100644 --- a/SubstrateCS/Source/BetaWorld.cs +++ b/SubstrateCS/Source/BetaWorld.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Substrate.Core; using Substrate.Nbt; +using Substrate.Data; //TODO: Exceptions (+ Alpha) @@ -26,6 +27,7 @@ namespace Substrate private Dictionary _blockMgrs; private PlayerManager _playerMan; + private BetaDataManager _dataMan; private int _prefCacheSize = 256; @@ -129,6 +131,15 @@ namespace Substrate return GetPlayerManagerVirt() as PlayerManager; } + /// + /// Gets a for managing data resources, such as maps. + /// + /// A for this world. + public new BetaDataManager GetDataManager () + { + return GetDataManagerVirt() as BetaDataManager; + } + /// /// Saves the world's data, and any objects known to have unsaved changes. /// @@ -157,7 +168,7 @@ namespace Substrate /// The path to the directory containing the world's level.dat, or the path to level.dat itself. /// The preferred cache size in chunks for each opened dimension in this world. /// A new object representing an existing world on disk. - public static new BetaWorld Open (string path, int cacheSize) + public static BetaWorld Open (string path, int cacheSize) { BetaWorld world = new BetaWorld().OpenWorld(path); world._prefCacheSize = cacheSize; @@ -230,6 +241,17 @@ namespace Substrate return _playerMan; } + /// + protected override Data.DataManager GetDataManagerVirt () + { + if (_dataMan != null) { + return _dataMan; + } + + _dataMan = new BetaDataManager(this); + return _dataMan; + } + private void OpenDimension (int dim) { string path = Path; diff --git a/SubstrateCS/Source/Core/NBTFile.cs b/SubstrateCS/Source/Core/NBTFile.cs index 7c9f9c3..d568a1c 100644 --- a/SubstrateCS/Source/Core/NBTFile.cs +++ b/SubstrateCS/Source/Core/NBTFile.cs @@ -7,6 +7,14 @@ using Substrate.Nbt; namespace Substrate.Core { + public enum CompressionType + { + None, + Zlib, + Deflate, + GZip, + } + public class NBTFile { private string _filename; @@ -37,7 +45,12 @@ namespace Substrate.Core return Timestamp(File.GetLastWriteTime(_filename)); } - public virtual Stream GetDataInputStream () + public Stream GetDataInputStream () + { + return GetDataInputStream(CompressionType.GZip); + } + + public virtual Stream GetDataInputStream (CompressionType compression) { try { FileStream fstr = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); @@ -50,17 +63,44 @@ namespace Substrate.Core fstr.Close(); - return new GZipStream(new MemoryStream(data), CompressionMode.Decompress); + switch (compression) { + case CompressionType.None: + return new MemoryStream(data); + case CompressionType.GZip: + return new GZipStream(new MemoryStream(data), CompressionMode.Decompress); + case CompressionType.Zlib: + return new ZlibStream(new MemoryStream(data), CompressionMode.Decompress); + case CompressionType.Deflate: + return new DeflateStream(new MemoryStream(data), CompressionMode.Decompress); + default: + throw new ArgumentException("Invalid CompressionType specified", "compression"); + } } catch (Exception ex) { throw new NbtIOException("Failed to open compressed NBT data stream for input.", ex); } } - public virtual Stream GetDataOutputStream () + public Stream GetDataOutputStream () + { + return GetDataOutputStream(CompressionType.GZip); + } + + public virtual Stream GetDataOutputStream (CompressionType compression) { try { - return new GZipStream(new NBTBuffer(this), CompressionMode.Compress); + switch (compression) { + case CompressionType.None: + return new NBTBuffer(this); + case CompressionType.GZip: + return new GZipStream(new NBTBuffer(this), CompressionMode.Compress); + case CompressionType.Zlib: + return new ZlibStream(new NBTBuffer(this), CompressionMode.Compress); + case CompressionType.Deflate: + return new DeflateStream(new NBTBuffer(this), CompressionMode.Compress); + default: + throw new ArgumentException("Invalid CompressionType specified", "compression"); + } } catch (Exception ex) { throw new NbtIOException("Failed to initialize compressed NBT data stream for output.", ex); diff --git a/SubstrateCS/Source/Data/BetaDataManager.cs b/SubstrateCS/Source/Data/BetaDataManager.cs new file mode 100644 index 0000000..dfed6fd --- /dev/null +++ b/SubstrateCS/Source/Data/BetaDataManager.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using Substrate.Nbt; +using System.IO; +using Substrate.Core; + +namespace Substrate.Data +{ + public class BetaDataManager : DataManager, INbtObject + { + private static SchemaNodeCompound _schema = new SchemaNodeCompound() + { + new SchemaNodeScaler("map", TagType.TAG_SHORT), + }; + + private TagNodeCompound _source; + + private NbtWorld _world; + + private short _mapId; + + private MapManager _maps; + + public BetaDataManager (NbtWorld world) + { + _world = world; + + _maps = new MapManager(_world); + } + + public override int CurrentMapId + { + get { return _mapId; } + set { _mapId = (short)value; } + } + + public new MapManager Maps + { + get { return _maps; } + } + + protected override IMapManager GetMapManager () + { + return _maps; + } + + public bool Save () + { + if (_world == null) { + return false; + } + + try { + string path = Path.Combine(_world.Path, _world.DataDirectory); + NBTFile nf = new NBTFile(Path.Combine(path, "idcounts.dat")); + + Stream zipstr = nf.GetDataOutputStream(CompressionType.None); + if (zipstr == null) { + NbtIOException nex = new NbtIOException("Failed to initialize uncompressed NBT stream for output"); + nex.Data["DataManager"] = this; + throw nex; + } + + new NbtTree(BuildTree() as TagNodeCompound).WriteTo(zipstr); + zipstr.Close(); + + return true; + } + catch (Exception ex) { + Exception lex = new Exception("Could not save idcounts.dat file.", ex); + lex.Data["DataManager"] = this; + throw lex; + } + } + + #region INBTObject + + public virtual BetaDataManager LoadTree (TagNode tree) + { + TagNodeCompound ctree = tree as TagNodeCompound; + if (ctree == null) { + return null; + } + + _mapId = ctree["map"].ToTagShort(); + + _source = ctree.Copy() as TagNodeCompound; + + return this; + } + + public virtual BetaDataManager LoadTreeSafe (TagNode tree) + { + if (!ValidateTree(tree)) { + return null; + } + + return LoadTree(tree); + } + + public virtual TagNode BuildTree () + { + TagNodeCompound tree = new TagNodeCompound(); + + tree["map"] = new TagNodeLong(_mapId); + + if (_source != null) { + tree.MergeFrom(_source); + } + + return tree; + } + + public virtual bool ValidateTree (TagNode tree) + { + return new NbtVerifier(tree, _schema).Verify(); + } + + #endregion + } +} diff --git a/SubstrateCS/Source/Data/DataExceptions.cs b/SubstrateCS/Source/Data/DataExceptions.cs new file mode 100644 index 0000000..d42925d --- /dev/null +++ b/SubstrateCS/Source/Data/DataExceptions.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.Serialization; + +namespace Substrate.Data +{ + /// + /// The exception that is thrown when IO errors occur during high-level data resource management operations. + /// + [Serializable] + public class DataIOException : SubstrateException + { + /// + /// Initializes a new instance of the class. + /// + public DataIOException () + : base() + { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// A custom error message. + public DataIOException (string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class with a custom error message and a reference to + /// an InnerException representing the original cause of the exception. + /// + /// A custom error message. + /// A reference to the original exception that caused the error. + public DataIOException (string message, Exception innerException) + : base(message, innerException) + { } + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The object that holds the serialized object data. + /// The contextual information about the source or destination. + protected DataIOException (SerializationInfo info, StreamingContext context) + : base(info, context) + { } + } +} diff --git a/SubstrateCS/Source/Data/DataManager.cs b/SubstrateCS/Source/Data/DataManager.cs new file mode 100644 index 0000000..7d98e63 --- /dev/null +++ b/SubstrateCS/Source/Data/DataManager.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace Substrate.Data +{ + /// + /// Provides a common interface for managing additional data resources in a world. + /// + public abstract class DataManager + { + /// + /// Gets or sets the id of the next map to be created. + /// + public virtual int CurrentMapId + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + /// + /// Gets an for managing data resources. + /// + public IMapManager Maps + { + get { return GetMapManager(); } + } + + /// + /// Gets an for managing data resources. + /// + /// An instance appropriate for the concrete instance. + protected virtual IMapManager GetMapManager () + { + return null; + } + + /// + /// Saves any metadata required by the world for managing data resources. + /// + /// true on success, or false if data could not be saved. + public bool Save () + { + return true; + } + } + + +} diff --git a/SubstrateCS/Source/Data/Map.cs b/SubstrateCS/Source/Data/Map.cs new file mode 100644 index 0000000..f542e08 --- /dev/null +++ b/SubstrateCS/Source/Data/Map.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Substrate.Core; +using Substrate.Nbt; +using System.IO; + +namespace Substrate.Data +{ + /// + /// Represents the complete data of a Map item. + /// + public class Map : INbtObject, ICopyable + { + private static SchemaNodeCompound _schema = new SchemaNodeCompound() + { + new SchemaNodeCompound("data") + { + new SchemaNodeScaler("scale", TagType.TAG_BYTE), + new SchemaNodeScaler("dimension", TagType.TAG_BYTE), + new SchemaNodeScaler("height", TagType.TAG_SHORT), + new SchemaNodeScaler("width", TagType.TAG_SHORT), + new SchemaNodeScaler("xCenter", TagType.TAG_INT), + new SchemaNodeScaler("zCenter", TagType.TAG_INT), + new SchemaNodeArray("colors"), + }, + }; + + private TagNodeCompound _source; + + private NbtWorld _world; + private int _id; + + private byte _scale; + private byte _dimension; + private short _height; + private short _width; + private int _x; + private int _z; + + private byte[] _colors; + + /// + /// Creates a new default object. + /// + public Map () + { + _scale = 3; + _dimension = 0; + _height = 128; + _width = 128; + + _colors = new byte[_width * _height]; + } + + /// + /// Creates a new object with copied data. + /// + /// A to copy data from. + protected Map (Map p) + { + _world = p._world; + _id = p._id; + + _scale = p._scale; + _dimension = p._dimension; + _height = p._height; + _width = p._width; + _x = p._x; + _z = p._z; + + _colors = new byte[_width * _height]; + if (p._colors != null) { + p._colors.CopyTo(_colors, 0); + } + } + + /// + /// Gets or sets the id value associated with this map. + /// + public int Id + { + get { return _id; } + set + { + if (_id < 0 || _id >= 65536) { + throw new ArgumentOutOfRangeException("value", value, "Map Ids must be in the range [0, 65535]."); + } + _id = value; + } + } + + /// + /// Gets or sets the scale of the map. Acceptable values are 0 (1:1) to 4 (1:16). + /// + public int Scale + { + get { return _scale; } + set { _scale = (byte)value; } + } + + /// + /// Gets or sets the (World) Dimension of the map. + /// + public int Dimension + { + get { return _dimension; } + set { _dimension = (byte)value; } + } + + /// + /// Gets or sets the height of the map. + /// + /// If the new height dimension is different, the map's color data will be reset. + /// Thrown if the new height value is zero or negative. + public int Height + { + get { return _height; } + set + { + if (value <= 0) { + throw new ArgumentOutOfRangeException("value", "Height must be a positive number"); + } + if (_height != value) { + _height = (short)value; + _colors = new byte[_width * _height]; + } + } + } + + /// + /// Gets or sets the width of the map. + /// + /// If the new width dimension is different, the map's color data will be reset. + /// Thrown if the new width value is zero or negative. + public int Width + { + get { return _width; } + set + { + if (value <= 0) { + throw new ArgumentOutOfRangeException("value", "Width must be a positive number"); + } + if (_width != value) { + _width = (short)value; + _colors = new byte[_width * _height]; + } + } + } + + /// + /// Gets or sets the global X-coordinate that this map is centered on, in blocks. + /// + public int X + { + get { return _x; } + set { _x = value; } + } + + /// + /// Gets or sets the global Z-coordinate that this map is centered on, in blocks. + /// + public int Z + { + get { return _z; } + set { _z = value; } + } + + /// + /// Gets the raw byte array of the map's color index values. + /// + public byte[] Colors + { + get { return _colors; } + } + + /// + /// Gets or sets a color index value within the map's internal colors bitmap. + /// + /// The X-coordinate to get or set. + /// The Z-coordinate to get or set. + /// Thrown when the X- or Z-coordinates exceed the map dimensions. + public byte this[int x, int z] + { + get + { + if (x < 0 || x >= _width || z < 0 || z >= _height) { + throw new IndexOutOfRangeException(); + } + return _colors[x + _width * z]; + } + + set + { + if (x < 0 || x >= _width || z < 0 || z >= _height) { + throw new IndexOutOfRangeException(); + } + _colors[x + _width * z] = value; + } + } + + + /// + /// Saves a object to disk as a standard compressed NBT stream. + /// + /// True if the map was saved; false otherwise. + /// Thrown when an error is encountered writing out the level. + public bool Save () + { + if (_world == null) { + return false; + } + + try { + string path = Path.Combine(_world.Path, _world.DataDirectory); + NBTFile nf = new NBTFile(Path.Combine(path, "map_" + _id + ".dat")); + + Stream zipstr = nf.GetDataOutputStream(); + if (zipstr == null) { + NbtIOException nex = new NbtIOException("Failed to initialize compressed NBT stream for output"); + nex.Data["Map"] = this; + throw nex; + } + + new NbtTree(BuildTree() as TagNodeCompound).WriteTo(zipstr); + zipstr.Close(); + + return true; + } + catch (Exception ex) { + Exception mex = new Exception("Could not save map file.", ex); // TODO: Exception Type + mex.Data["Map"] = this; + throw mex; + } + } + + + #region INBTObject Members + + /// + /// Attempt to load a Map subtree into the without validation. + /// + /// The root node of a Map subtree. + /// The returns itself on success, or null if the tree was unparsable. + public virtual Map LoadTree (TagNode tree) + { + TagNodeCompound dtree = tree as TagNodeCompound; + if (dtree == null) { + return null; + } + + TagNodeCompound ctree = dtree["data"].ToTagCompound(); + + _scale = ctree["scale"].ToTagByte(); + _dimension = ctree["dimension"].ToTagByte(); + _height = ctree["height"].ToTagShort(); + _width = ctree["width"].ToTagShort(); + _x = ctree["xCenter"].ToTagInt(); + _z = ctree["zCenter"].ToTagInt(); + + _colors = ctree["colors"].ToTagByteArray(); + + _source = ctree.Copy() as TagNodeCompound; + + return this; + } + + /// + /// Attempt to load a Map subtree into the with validation. + /// + /// The root node of a Map subtree. + /// The returns itself on success, or null if the tree failed validation. + public virtual Map LoadTreeSafe (TagNode tree) + { + if (!ValidateTree(tree)) { + return null; + } + + Map map = LoadTree(tree); + + if (map != null) { + if (map._colors.Length != map._width * map._height) { + throw new Exception("Unexpected length of colors byte array in Map"); // TODO: Expception Type + } + } + + return map; + } + + /// + /// Builds a Map subtree from the current data. + /// + /// The root node of a Map subtree representing the current data. + public virtual TagNode BuildTree () + { + TagNodeCompound data = new TagNodeCompound(); + data["scale"] = new TagNodeByte(_scale); + data["dimension"] = new TagNodeByte(_dimension); + data["height"] = new TagNodeShort(_height); + data["width"] = new TagNodeShort(_width); + data["xCenter"] = new TagNodeInt(_x); + data["zCenter"] = new TagNodeInt(_z); + + data["colors"] = new TagNodeByteArray(_colors); + + if (_source != null) { + data.MergeFrom(_source); + } + + TagNodeCompound tree = new TagNodeCompound(); + tree.Add("data", data); + + return tree; + } + + /// + /// Validate a Map subtree against a schema defintion. + /// + /// The root node of a Map subtree. + /// Status indicating whether the tree was valid against the internal schema. + public virtual bool ValidateTree (TagNode tree) + { + return new NbtVerifier(tree, _schema).Verify(); + } + + #endregion + + + #region ICopyable Members + + /// + /// Creates a deep-copy of the . + /// + /// A deep-copy of the . + public virtual Map Copy () + { + return new Map(this); + } + + #endregion + } +} diff --git a/SubstrateCS/Source/Data/MapConverter.cs b/SubstrateCS/Source/Data/MapConverter.cs new file mode 100644 index 0000000..ea7cf35 --- /dev/null +++ b/SubstrateCS/Source/Data/MapConverter.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Drawing; +using System.Drawing.Imaging; + +namespace Substrate.Data +{ + /// + /// Represents a range of color index values that pertains to an individual group of blocks. + /// + public enum ColorGroup + { + Unexplored = 0, + Grass = 4, + Sand = 8, + Other = 12, + Lava = 16, + Ice = 20, + Iron = 24, + Leaves = 28, + Snow = 32, + Clay = 36, + Dirt = 40, + Stone = 44, + Water = 48, + Wood = 52, + } + + /// + /// A utility class for converting colors and data. + /// + public class MapConverter + { + private static Color[] _defaultColorIndex; + + private Color[] _colorIndex; + private ColorGroup[] _blockIndex; + + private int _groupSize = 4; + + private Vector3[] _labIndex; + + /// + /// Creates a new with a Minecraft-default color-index and block-index. + /// + public MapConverter () + { + _colorIndex = new Color[256]; + _labIndex = new Vector3[256]; + _defaultColorIndex.CopyTo(_colorIndex, 0); + + RefreshColorCache(); + + // Setup default block index + _blockIndex = new ColorGroup[256]; + for (int i = 0; i < 256; i++) { + _blockIndex[i] = ColorGroup.Other; + } + + _blockIndex[BlockInfo.Grass.ID] = ColorGroup.Grass; + _blockIndex[BlockInfo.TallGrass.ID] = ColorGroup.Grass; + _blockIndex[BlockInfo.Sand.ID] = ColorGroup.Sand; + _blockIndex[BlockInfo.Gravel.ID] = ColorGroup.Sand; + _blockIndex[BlockInfo.Sandstone.ID] = ColorGroup.Sand; + _blockIndex[BlockInfo.Lava.ID] = ColorGroup.Lava; + _blockIndex[BlockInfo.StationaryLava.ID] = ColorGroup.Lava; + _blockIndex[BlockInfo.Ice.ID] = ColorGroup.Ice; + _blockIndex[BlockInfo.Leaves.ID] = ColorGroup.Leaves; + _blockIndex[BlockInfo.YellowFlower.ID] = ColorGroup.Leaves; + _blockIndex[BlockInfo.RedRose.ID] = ColorGroup.Leaves; + _blockIndex[BlockInfo.Snow.ID] = ColorGroup.Snow; + _blockIndex[BlockInfo.SnowBlock.ID] = ColorGroup.Snow; + _blockIndex[BlockInfo.ClayBlock.ID] = ColorGroup.Clay; + _blockIndex[BlockInfo.Dirt.ID] = ColorGroup.Dirt; + _blockIndex[BlockInfo.Stone.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.Cobblestone.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.CoalOre.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.IronOre.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.GoldOre.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.DiamondOre.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.RedstoneOre.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.LapisOre.ID] = ColorGroup.Stone; + _blockIndex[BlockInfo.Water.ID] = ColorGroup.Water; + _blockIndex[BlockInfo.StationaryWater.ID] = ColorGroup.Water; + _blockIndex[BlockInfo.Wood.ID] = ColorGroup.Wood; + } + + /// + /// Gets or sets the number of color levels within each color group. The Minecraft default is 4. + /// + /// Thrown when the property is assigned a non-positive value. + public int ColorGroupSize + { + get { return _groupSize; } + set + { + if (value <= 0) { + throw new ArgumentOutOfRangeException("The ColorGroupSize property must be a positive number."); + } + _groupSize = value; + } + } + + /// + /// Gets the color index table, used to translate map color index values to RGB color values. + /// + public Color[] ColorIndex + { + get { return _colorIndex; } + } + + /// + /// Gets the block index table, used to translate block IDs to s that represent them. + /// + public ColorGroup[] BlockIndex + { + get { return _blockIndex; } + } + + /// + /// Returns the baseline color index associated with the currently representing the given block ID. + /// + /// The ID of a block. + /// A color index value. + /// Thrown when is out of its normal range. + public int BlockToColorIndex (int blockId) + { + return BlockToColorIndex(blockId, 0); + } + + /// + /// Returns a color index associated with the currently representing the given block ID and based on the given level. + /// + /// The ID of a block. + /// The color level to select from within the derived . + /// A color index value. + /// Thrown when either or are out of their normal ranges. + public int BlockToColorIndex (int blockId, int level) { + if (level < 0 || level >= _groupSize) { + throw new ArgumentOutOfRangeException("level", level, "Argument 'level' must be in range [0, " + (_groupSize - 1) + "]"); + } + if (blockId < 0 || blockId >= 256) { + throw new ArgumentOutOfRangeException("blockId"); + } + + return (int)_blockIndex[blockId] * _groupSize + level; + } + + /// + /// Returns a value assocated with the currently representing the given block ID. + /// + /// The ID of a block. + /// A value. + /// Thrown when is out of its normal range. + /// Thrown when maps to an invalid color index. + public Color BlockToColor (int blockId) + { + return BlockToColor(blockId, 0); + } + + /// + /// Returns a value associated with the currently representing the given block ID and based on the given level. + /// + /// The ID of a block. + /// The color level to select from within the derived . + /// A value. + /// Thrown when either or are out of their normal ranges. + /// Thrown when maps to an invalid color index. + public Color BlockToColor (int blockId, int level) + { + int ci = BlockToColorIndex(blockId, level); + if (ci < 0 || ci >= 256) { + throw new InvalidOperationException("The specified Block ID mapped to an invalid color index."); + } + + return _colorIndex[ci]; + } + + /// + /// Returns the that a particular color index is part of. + /// + /// A color index value. + /// A value. + public ColorGroup ColorIndexToGroup (int colorIndex) + { + int group = colorIndex / _groupSize; + + try { + return (ColorGroup)group; + } + catch { + return ColorGroup.Other; + } + } + + /// + /// Returns the baseline color index within a particular . + /// + /// A value. + /// The baseline (level = 0) color index value for the given . + public int GroupToColorIndex (ColorGroup group) + { + return GroupToColorIndex(group, 0); + } + + /// + /// Returns the color index for a given and group level. + /// + /// A value. + /// A level value within the . + /// The color index value for the given and group level. + /// Thrown when the is out of range with respect to the current parameter. + public int GroupToColorIndex (ColorGroup group, int level) + { + if (level < 0 || level >= _groupSize) { + throw new ArgumentOutOfRangeException("level", level, "Argument 'level' must be in range [0, " + (_groupSize - 1) + "]"); + } + + return (int)group * _groupSize + level; + } + + /// + /// Returns the baseline within a particular . + /// + /// A value. + /// The baseline (level = 0) for the given . + public Color GroupToColor (ColorGroup group) + { + return GroupToColor(group, 0); + } + + /// + /// Returns the for a given and group level. + /// + /// A value. + /// A level value within the and group level. + /// The for the given and group level. + /// Thrown when the is out of range with respect to the current parameter. + /// Thrown when the and map to an invalid color index. + public Color GroupToColor (ColorGroup group, int level) + { + int ci = GroupToColorIndex(group, level); + if (ci < 0 || ci >= 256) { + throw new InvalidOperationException("The specified group mapped to an invalid color index."); + } + + return _colorIndex[ci]; + } + + /// + /// Rebuilds the internal color conversion tables. Should be called after modifying the table. + /// + public void RefreshColorCache () + { + for (int i = 0; i < _colorIndex.Length; i++) { + _labIndex[i] = RgbToLab(_colorIndex[i]); + } + } + + /// + /// Given a , returns the index of the closest matching color from the color index table. + /// + /// The source . + /// The closest matching color index value. + /// This method performs color comparisons in the CIELAB color space, to find the best match according to human perception. + public int NearestColorIndex (Color color) + { + double min = double.MaxValue; + int minIndex = 0; + + Vector3 cr = RgbToLab(color); + + for (int i = 0; i < _colorIndex.Length; i++) { + if (_colorIndex[i].A == 0) { + continue; + } + + double x = cr.X - _labIndex[i].X; + double y = cr.Y - _labIndex[i].Y; + double z = cr.Z - _labIndex[i].Z; + + double err = x * x + y * y + z * z; + if (err < min) { + min = err; + minIndex = i; + } + } + + return minIndex; + } + + /// + /// Given a , returns the cloest matching from the color index table. + /// + /// The source . + /// The closest matching . + /// This method performs color comparisons in the CIELAB color space, to find the best match according to human perception. + public Color NearestColor (Color color) + { + return _colorIndex[NearestColorIndex(color)]; + } + + /// + /// Fills a 's color data using nearest-matching colors from a source . + /// + /// The to modify. + /// The source . + /// Thrown when the and objects have different dimensions. + public void BitmapToMap (Map map, Bitmap bmp) + { + if (map.Width != bmp.Width || map.Height != bmp.Height) { + throw new InvalidOperationException("The source map and bitmap must have the same dimensions."); + } + + for (int x = 0; x < map.Width; x++) { + for (int z = 0; z < map.Height; z++) { + Color c = bmp.GetPixel(x, z); + map[x, z] = (byte)NearestColorIndex(c); + } + } + } + + /// + /// Creates a 32bpp from a . + /// + /// The source object. + /// A 32bpp with the same dimensions and pixel data as the source . + public Bitmap MapToBitmap (Map map) + { + Bitmap bmp = new Bitmap(map.Width, map.Height, PixelFormat.Format32bppArgb); + + for (int x = 0; x < map.Width; x++) { + for (int z = 0; z < map.Height; z++) { + Color c = _colorIndex[map[x, z]]; + bmp.SetPixel(x, z, c); + } + } + + return bmp; + } + + private Vector3 RgbToXyz (Color color) + { + double r = color.R / 255.0; + double g = color.G / 255.0; + double b = color.B / 255.0; + + r = (r > 0.04045) + ? Math.Pow((r + 0.055) / 1.055, 2.4) + : r / 12.92; + g = (g > 0.04045) + ? Math.Pow((g + 0.055) / 1.055, 2.4) + : g / 12.92; + b = (b > 0.04045) + ? Math.Pow((b + 0.055) / 1.055, 2.4) + : b / 12.92; + + r *= 100; + g *= 100; + b *= 100; + + Vector3 xyz = new Vector3(); + + xyz.X = r * 0.4124 + g * 0.3576 + b * 0.1805; + xyz.Y = r * 0.2126 + g * 0.7152 + b * 0.0722; + xyz.Z = r * 0.0193 + g * 0.1192 + b * 0.9505; + + return xyz; + } + + private Vector3 XyzToLab (Vector3 xyz) + { + double x = xyz.X / 95.047; + double y = xyz.Y / 100.0; + double z = xyz.Z / 108.883; + + x = (x > 0.008856) + ? Math.Pow(x, 1.0 / 3.0) + : (7.787 * x) + (16.0 / 116.0); + y = (y > 0.008856) + ? Math.Pow(y, 1.0 / 3.0) + : (7.787 * y) + (16.0 / 116.0); + z = (z > 0.008856) + ? Math.Pow(z, 1.0 / 3.0) + : (7.787 * z) + (16.0 / 116.0); + + Vector3 lab = new Vector3(); + + lab.X = (116 * y) - 16; + lab.Y = 500 * (x - y); + lab.Z = 200 * (y - z); + + return lab; + } + + private Vector3 RgbToLab (Color rgb) + { + return XyzToLab(RgbToXyz(rgb)); + } + + static MapConverter () + { + _defaultColorIndex = new Color[] { + Color.FromArgb(0, 0, 0, 0), // Unexplored + Color.FromArgb(0, 0, 0, 0), + Color.FromArgb(0, 0, 0, 0), + Color.FromArgb(0, 0, 0, 0), + Color.FromArgb(89, 125, 39), // Grass + Color.FromArgb(109, 153, 48), + Color.FromArgb(127, 178, 56), + Color.FromArgb(109, 153, 48), + Color.FromArgb(174, 164, 115), // Sand/Gravel + Color.FromArgb(213, 201, 140), + Color.FromArgb(247, 233, 163), + Color.FromArgb(213, 201, 140), + Color.FromArgb(117, 117, 117), // Other + Color.FromArgb(144, 144, 144), + Color.FromArgb(167, 167, 167), + Color.FromArgb(144, 144, 144), + Color.FromArgb(180, 0, 0), // Lava + Color.FromArgb(220, 0, 0), + Color.FromArgb(255, 0, 0), + Color.FromArgb(220, 0, 0), + Color.FromArgb(112, 112, 180), // Ice + Color.FromArgb(138, 138, 220), + Color.FromArgb(160, 160, 255), + Color.FromArgb(138, 138, 220), + Color.FromArgb(117, 117, 117), // Iron + Color.FromArgb(144, 144, 144), + Color.FromArgb(167, 167, 167), + Color.FromArgb(144, 144, 144), + Color.FromArgb(0, 87, 0), // Leaves/Flowers + Color.FromArgb(0, 106, 0), + Color.FromArgb(0, 124, 0), + Color.FromArgb(0, 106, 0), + Color.FromArgb(180, 180, 180), // Snow + Color.FromArgb(220, 220, 220), + Color.FromArgb(255, 255, 255), + Color.FromArgb(220, 220, 220), + Color.FromArgb(115, 118, 129), // Clay + Color.FromArgb(141, 144, 158), + Color.FromArgb(164, 168, 184), + Color.FromArgb(141, 144, 158), + Color.FromArgb(129, 74, 33), // Dirt + Color.FromArgb(157, 91, 40), + Color.FromArgb(183, 106, 47), + Color.FromArgb(157, 91, 40), + Color.FromArgb(79, 79, 79), // Stone/Cobblestone/Ore + Color.FromArgb(96, 96, 96), + Color.FromArgb(112, 112, 112), + Color.FromArgb(96, 96, 96), + Color.FromArgb(45, 45, 180), // Water + Color.FromArgb(55, 55, 220), + Color.FromArgb(64, 64, 255), + Color.FromArgb(55, 55, 220), + Color.FromArgb(73, 58, 35), // Log/Tree/Wood + Color.FromArgb(89, 71, 43), + Color.FromArgb(104, 83, 50), + Color.FromArgb(89, 71, 43), + }; + } + } +} diff --git a/SubstrateCS/Source/Data/MapFile.cs b/SubstrateCS/Source/Data/MapFile.cs new file mode 100644 index 0000000..6a27d18 --- /dev/null +++ b/SubstrateCS/Source/Data/MapFile.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Ionic.Zlib; +using Substrate.Core; + +namespace Substrate.Data +{ + public class MapFile : NBTFile + { + public MapFile (string path) + : base(path) + { + } + + public MapFile (string path, int id) + : base("") + { + if (!Directory.Exists(path)) { + Directory.CreateDirectory(path); + } + + string file = "map_" + id + ".dat"; + FileName = Path.Combine(path, file); + } + + public static int IdFromFilename (string filename) + { + if (filename.EndsWith(".dat")) { + return Convert.ToInt32(filename.Substring(4).Remove(filename.Length - 4)); + } + + throw new FormatException("Filename '" + filename + "' is not a .dat file"); + } + } +} diff --git a/SubstrateCS/Source/Data/MapManager.cs b/SubstrateCS/Source/Data/MapManager.cs new file mode 100644 index 0000000..f8bfadc --- /dev/null +++ b/SubstrateCS/Source/Data/MapManager.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using Substrate.Nbt; +using System.Text.RegularExpressions; + +namespace Substrate.Data +{ + /// + /// Functions to manage all data resources. + /// + /// This manager is intended for map files stored in standard compressed NBT format. + public class MapManager : IMapManager, IEnumerable + { + private NbtWorld _world; + + /// + /// Create a new for a given world. + /// + /// World containing data files. + public MapManager (NbtWorld world) + { + _world = world; + } + + /// + /// Gets a representing the backing NBT data stream. + /// + /// The id of the map to fetch. + /// A for the given map. + protected MapFile GetMapFile (int id) + { + return new MapFile(Path.Combine(_world.Path, _world.DataDirectory), id); + } + + /// + /// Gets a raw of data for the given map. + /// + /// The id of the map to fetch. + /// An containing the given map's raw data. + /// Thrown when the manager cannot read in an NBT data stream. + public NbtTree GetMapTree (int id) + { + MapFile mf = GetMapFile(id); + Stream nbtstr = mf.GetDataInputStream(); + if (nbtstr == null) { + throw new NbtIOException("Failed to initialize NBT data stream for input."); + } + + return new NbtTree(nbtstr); + } + + /// + /// Saves a raw representing a map to the given map's file. + /// + /// The id of the map to write data to. + /// The map's data as an . + /// Thrown when the manager cannot initialize an NBT data stream for output. + public void SetMapTree (int id, NbtTree tree) + { + MapFile mf = GetMapFile(id); + Stream zipstr = mf.GetDataOutputStream(); + if (zipstr == null) { + throw new NbtIOException("Failed to initialize NBT data stream for output."); + } + + tree.WriteTo(zipstr); + zipstr.Close(); + } + + #region IMapManager Members + + /// + /// Thrown when the manager cannot read in a map that should exist. + public Map GetMap (int id) + { + if (!MapExists(id)) { + return null; + } + + try { + Map m = new Map().LoadTreeSafe(GetMapTree(id).Root); + m.Id = id; + return m; + } + catch (Exception ex) { + DataIOException pex = new DataIOException("Could not load map", ex); + pex.Data["MapId"] = id; + throw pex; + } + } + + /// + /// Thrown when the manager cannot write out the map + public void SetMap (int id, Map map) + { + try { + SetMapTree(id, new NbtTree(map.BuildTree() as TagNodeCompound)); + } + catch (Exception ex) { + DataIOException pex = new DataIOException("Could not save map", ex); + pex.Data["MapId"] = id; + throw pex; + } + } + + /// + /// Saves a object's data back to file given the id set in the object. + /// + /// The object containing the data to write back. + /// Thrown when the manager cannot write out the map + public void SetMap (Map map) + { + SetMap(map.Id, map); + } + + /// + public bool MapExists (int id) + { + return new MapFile(Path.Combine(_world.Path, _world.DataDirectory), id).Exists(); + } + + /// + /// Thrown when the manager cannot delete the map. + public void DeleteMap (int id) + { + try { + new MapFile(Path.Combine(_world.Path, _world.DataDirectory), id).Delete(); + } + catch (Exception ex) { + DataIOException pex = new DataIOException("Could not remove map", ex); + pex.Data["MapId"] = id; + throw pex; + } + } + + #endregion + + #region IEnumerable Members + + /// + /// Gets an enumerator that iterates through all the maps in the world's data directory. + /// + /// An enumerator for this manager. + public IEnumerator GetEnumerator () + { + string path = Path.Combine(_world.Path, _world.DataDirectory); + + if (!Directory.Exists(path)) { + throw new DirectoryNotFoundException(); + } + + string[] files = Directory.GetFiles(path); + foreach (string file in files) { + string basename = Path.GetFileName(file); + + if (!ParseFileName(basename)) { + continue; + } + + int id = MapFile.IdFromFilename(basename); + yield return GetMap(id); + } + } + + #endregion + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () + { + return GetEnumerator(); + } + + #endregion + + private bool ParseFileName (string filename) + { + Match match = _namePattern.Match(filename); + if (!match.Success) { + return false; + } + + return true; + } + + private static Regex _namePattern = new Regex("^map_[0-9]+\\.dat$"); + } +} diff --git a/SubstrateCS/Source/Data/MapManagerInterface.cs b/SubstrateCS/Source/Data/MapManagerInterface.cs new file mode 100644 index 0000000..d90e67d --- /dev/null +++ b/SubstrateCS/Source/Data/MapManagerInterface.cs @@ -0,0 +1,37 @@ +using System; + +namespace Substrate.Data +{ + /// + /// An interface of basic manipulations on an abstract data store for map data. + /// + public interface IMapManager + { + /// + /// Gets a object for the given map id from the underlying data store. + /// + /// The id of a map data resource. + /// A object for the given map id, or null if the map doesn't exist. + Map GetMap (int id); + + /// + /// Saves a object's data back to the underlying data store for the given map id. + /// + /// The id of the map to write back data for. + /// The object containing data to write back. + void SetMap (int id, Map map); + + /// + /// Checks if a map exists in the underlying data store. + /// + /// The id of the map to look up. + /// True if map data was found; false otherwise. + bool MapExists (int id); + + /// + /// Deletes a map with the given id from the underlying data store. + /// + /// The id of the map to delete. + void DeleteMap (int id); + } +} diff --git a/SubstrateCS/Source/Nbt/NbtIOException.cs b/SubstrateCS/Source/Nbt/NbtIOException.cs index 2033148..c98a6de 100644 --- a/SubstrateCS/Source/Nbt/NbtIOException.cs +++ b/SubstrateCS/Source/Nbt/NbtIOException.cs @@ -6,7 +6,7 @@ namespace Substrate.Nbt /// /// The exception that is thrown when errors occur during Nbt IO operations. /// - /// In most cases, the property will contain more detailed information on the + /// In most cases, the property will contain more detailed information on the /// error that occurred. [Serializable] public class NbtIOException : SubstrateException diff --git a/SubstrateCS/Source/NbtWorld.cs b/SubstrateCS/Source/NbtWorld.cs index eb7d8fc..84348a7 100644 --- a/SubstrateCS/Source/NbtWorld.cs +++ b/SubstrateCS/Source/NbtWorld.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.IO; using Substrate.Core; using Substrate.Nbt; +using Substrate.Data; namespace Substrate { @@ -18,12 +19,17 @@ namespace Substrate /// open worlds of the new format. public abstract class NbtWorld { + private const string _DATA_DIR = "data"; + private string _path; + private string _dataDir; /// /// Creates a new instance of an object. /// - protected NbtWorld () { } + protected NbtWorld () { + _dataDir = _DATA_DIR; + } /// /// Gets or sets the path to the directory containing the world. @@ -34,6 +40,15 @@ namespace Substrate set { _path = value; } } + /// + /// Gets or sets the directory containing data resources, rooted in the world directory. + /// + public string DataDirectory + { + get { return _dataDir; } + set { _dataDir = value; } + } + /// /// Gets a reference to this world's object. /// @@ -86,6 +101,15 @@ namespace Substrate return GetPlayerManagerVirt(); } + /// + /// Gets a for managing data resources, such as maps. + /// + /// A for this world. + public DataManager GetDataManager () + { + return GetDataManagerVirt(); + } + /// /// Attempts to determine the best matching world type of the given path, and open the world as that type. /// @@ -139,6 +163,15 @@ namespace Substrate /// An for the given dimension in the world. protected abstract IPlayerManager GetPlayerManagerVirt (); + /// + /// Virtual implementor of + /// + /// A for the given dimension in the world. + protected virtual DataManager GetDataManagerVirt () + { + throw new NotImplementedException(); + } + #endregion static NbtWorld () diff --git a/SubstrateCS/Source/PlayerManager.cs b/SubstrateCS/Source/PlayerManager.cs index 8bcfe05..df634e4 100644 --- a/SubstrateCS/Source/PlayerManager.cs +++ b/SubstrateCS/Source/PlayerManager.cs @@ -144,7 +144,7 @@ namespace Substrate new PlayerFile(_playerPath, name).Delete(); } catch (Exception ex) { - PlayerIOException pex = new PlayerIOException("Could not save player", ex); + PlayerIOException pex = new PlayerIOException("Could not remove player", ex); pex.Data["PlayerName"] = name; throw pex; } @@ -153,7 +153,7 @@ namespace Substrate #region IEnumerable Members /// - /// Gets an enumerator that iterates through all the chunks in the world. + /// Gets an enumerator that iterates through all the players in the world. /// /// An enumerator for this manager. public IEnumerator GetEnumerator () diff --git a/SubstrateCS/Substrate.csproj b/SubstrateCS/Substrate.csproj index a05fa87..4b71b9a 100644 --- a/SubstrateCS/Substrate.csproj +++ b/SubstrateCS/Substrate.csproj @@ -60,6 +60,7 @@ Assemblies\Ionic.Zlib.dll + @@ -67,9 +68,17 @@ + + + + + + + +