From 722dcd763d115992b2a56d4001db80d11e583064 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 15 Apr 2010 00:30:08 +0000 Subject: [PATCH] Entity loading implemented. New parsing method for single characters. Refactored the map reader a bit. Updated documentation related to entity-loading changes. git-svn-id: https://bd85.net/svn/cs3505_group@75 92bb83a3-7c8f-8a45-bc97-515c4e399668 --- .../CarFire/Content/Maps/sandbox.cfmap | 7 +- CarFire/CarFire/CarFire/Map.cs | 103 +++- CarFire/CarFire/CarFire/MapReader.cs | 494 +++++++++++------- CarFire/CarFire/CarFire/Parse.cs | 15 +- CarFire/CarFire/CarFire/XnaGame.cs | 3 +- CarFire/leveleditor/doc/leveleditor.txt | 15 +- 6 files changed, 429 insertions(+), 208 deletions(-) diff --git a/CarFire/CarFire/CarFire/Content/Maps/sandbox.cfmap b/CarFire/CarFire/CarFire/Content/Maps/sandbox.cfmap index 48338b3..9ae2acd 100644 --- a/CarFire/CarFire/CarFire/Content/Maps/sandbox.cfmap +++ b/CarFire/CarFire/CarFire/Content/Maps/sandbox.cfmap @@ -12,9 +12,10 @@ numplayers = <1,4> [A] - entity = SaberMonster + type = SaberMonster path = [1,1] [5,6] wait(5) [45,2] loop = true + create = [5,7] [2,3] [B] condition = has(key) event = wait(2) remove([2,6]) remove([2,7]) @@ -31,10 +32,10 @@ +------------------------------------------------------------------------------+ | | | 1 | -| | +| B | | 2 +---- | | | | -| 3 | | +| 3 | A | | | | | 4 | | | | | diff --git a/CarFire/CarFire/CarFire/Map.cs b/CarFire/CarFire/CarFire/Map.cs index 95a8c2c..c277a7a 100644 --- a/CarFire/CarFire/CarFire/Map.cs +++ b/CarFire/CarFire/CarFire/Map.cs @@ -7,6 +7,7 @@ using System.Runtime.Serialization; using System.Diagnostics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System.Reflection; namespace CarFire { @@ -23,7 +24,7 @@ namespace CarFire #region Public Constants - public const float PixelsToUnitSquares = 64.0f; + public const float PixelsToUnitSquares = 8.0f; #endregion @@ -54,6 +55,16 @@ namespace CarFire public int GridHeight; } + /// + /// The container class for information about an entity defined in the map. + /// + public class RawEntity + { + public char Id; + public Point Position; + public Dictionary Attributes = new Dictionary(); + } + #endregion @@ -92,6 +103,11 @@ namespace CarFire // TODO: This should return whatever object we end up using for tilesets. public string Tileset { get { return mData.Metadata.Tileset; } } + /// + /// Get a list of the raw entity containers loaded with the map. + /// + public List RawEntities { get { return mData.Entities; } } + /// /// Get and set the coordinate of the grid cell that should be in @@ -112,7 +128,7 @@ namespace CarFire /// Construct a map with the provided map data. /// /// Map data. - public Map(Metadata metadata, char[,] grid, Dictionary> entities) + public Map(Metadata metadata, char[,] grid, List entities) { mData = new Modal(metadata, grid, entities); mView = new View(mData); @@ -186,7 +202,7 @@ namespace CarFire /// /// Determine whether or not a cell can be occupied by a game entity. /// - /// X,Y-coordinates. + /// X,Y-coordinates. /// True if cell can be occupied, false otherwise. public bool IsCellOpen(Point point) { @@ -195,14 +211,24 @@ namespace CarFire /// - /// Get the entities loaded from the map file. + /// Get all the entities loaded from the map file. Exceptions could be + /// thrown if there are entities without associated classes. /// - /// Dictionary of entities. The keys are the entity - /// identifiers and the value is a dictionary of key-value pairs - /// associated with that entity. - public Dictionary> GetEntities() + /// List of entity objects loaded. + public List GetAllEntities() { - return mData.Entities; + return mData.GetAllEntities(); + } + + /// + /// Get the entities of a certain type loaded from the map file. Exceptions + /// could be thrown if there are entities without associated classes. + /// + /// Type of the entity you want a list of. + /// List of entity objects loaded. + public List GetEntities() + { + return mData.GetEntities(); } #endregion @@ -214,9 +240,9 @@ namespace CarFire { Metadata mMetadata; char[,] mGrid; - Dictionary> mEntities; + List mEntities; - public Modal(Metadata metadata, char[,] grid, Dictionary> entities) + public Modal(Metadata metadata, char[,] grid, List entities) { Debug.Assert(metadata != null); Debug.Assert(grid != null); @@ -237,7 +263,7 @@ namespace CarFire public Metadata Metadata { get { return mMetadata; } } - public Dictionary> Entities { get { return mEntities; } } + public List Entities { get { return mEntities; } } public bool IsCellOpen(int x, int y) @@ -245,6 +271,59 @@ namespace CarFire // TODO: Still need to define characters for types of scenery. return mGrid[x, y] == ' '; } + + + public List GetAllEntities() + { + List list = new List(); + + foreach (RawEntity raw in mEntities) + { + if (raw.Attributes.ContainsKey("type")) + { + string typename = raw.Attributes["type"]; + + object[] args = new object[3]; + args[0] = raw.Id; + args[1] = raw.Position; + args[2] = raw.Attributes; + + object entity = Activator.CreateInstance(System.Type.GetType("CarFire." + typename), args); + if (entity != null) list.Add(entity); + else Console.WriteLine("Entity of type " + typename + " not loaded because an entity class can't be found."); + } + else + { + Console.WriteLine("Ignoring entity with identifier " + raw.Id + " since it has no type key."); + } + } + + return list; + } + + public List GetEntities() + { + System.Type type = typeof(T); + List list = new List(); + + string typename = typeof(T).Name; + foreach (RawEntity raw in mEntities) + { + if (raw.Attributes.ContainsKey("type") && typename == raw.Attributes["type"]) + { + object[] args = new object[3]; + args[0] = raw.Id; + args[1] = raw.Position; + args[2] = raw.Attributes; + + T entity = (T)Activator.CreateInstance(type, args); + if (entity != null) list.Add(entity); + else Console.WriteLine("Entity of type " + typename + " not loaded because an entity class can't be found."); + } + } + + return list; + } } class View diff --git a/CarFire/CarFire/CarFire/MapReader.cs b/CarFire/CarFire/CarFire/MapReader.cs index fbae4df..cfb1f94 100644 --- a/CarFire/CarFire/CarFire/MapReader.cs +++ b/CarFire/CarFire/CarFire/MapReader.cs @@ -5,6 +5,7 @@ using System.Runtime.Serialization; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using System.Reflection; namespace CarFire { @@ -42,276 +43,393 @@ namespace CarFire protected override Map Read(ContentReader input, Map existingInstance) { - mInput = new LineReader(input); - ReadSectionHeaders(); - return new Map(mMetadata, mGrid, mEntities); + mImpl = new Impl(input); + return mImpl.GetMap(); } #endregion - #region Private Methods + #region Private Types - void ReadSectionHeaders() + /// + /// This private class wraps around ContentReader to make it more + /// convenient to use it as an input stream reader. + /// + class LineReader { - mMetadata = new Map.Metadata(); + ContentReader mInput; + int mLineNumber = 0; + int mExpectedNumberOfLines; + + public LineReader(ContentReader input) + { + mInput = input; + mExpectedNumberOfLines = mInput.ReadInt32(); + } - while (!mInput.End) + public string ReadLine() { - string line = mInput.ReadLine(); + mLineNumber++; + return mInput.ReadString(); + } - while (line != null) + public int LineNumber { get { return mLineNumber; } } + + public bool End { get { return mLineNumber >= mExpectedNumberOfLines; } } + } + + + /// + /// This class is the actual implementation. The implementation is wrapped + /// in a subclass because the invoker seems to only be able to invoke public + /// methods, and this needs to invoke methods that shouldn't be public. + /// + class Impl + { + public Impl(ContentReader input) + { + mInput = new LineReader(input); + ReadSectionHeaders(); + PostProcess(); + } + + public Map GetMap() + { + return new Map(mMetadata, mGrid, mEntities); + } + + + public void ReadSectionHeaders() + { + mMetadata = new Map.Metadata(); + + while (!mInput.End) { - if (!IsLineSignificant(line)) break; + string line = mInput.ReadLine(); - string section = Parse.IniSectionHeader(line); - if (section != null) + while (line != null) { - if (section == "metadata") + if (!IsLineSignificant(line)) break; + + string section = Parse.IniSectionHeader(line); + if (section != null) { - line = ReadMetadataSection(); + if (section == "metadata") + { + line = ReadMetadataSection(); + } + else if (section == "maptable") + { + line = ReadMapTableSection(); + } + else if (section.Length == 1 && IsValidEntityIdentifier(section[0])) + { + line = ReadEntitySection(section[0]); + } + else + { + throw new ParserException("Unexpected section on line " + mInput.LineNumber + ": " + section); + } } - else if (section == "maptable") + else { - line = ReadMapTableSection(); + throw new ParserException("Unexpected text on line " + mInput.LineNumber + ": " + line); } - else if (section.Length == 1) + } + } + } + + string ReadMetadataSection() + { + while (!mInput.End) + { + string line = mInput.ReadLine(); + if (!IsLineSignificant(line)) continue; + + string[] pair = Parse.KeyValuePair(line); + if (pair != null) + { + try { - line = ReadEntitySection(section[0]); + string methodName = "set_" + pair[0].ToLowerInvariant(); + object[] args = new object[1]; + args[0] = pair[1]; + GetType().InvokeMember(methodName, BindingFlags.InvokeMethod, null, this, args); } - else +#pragma warning disable 0168 + catch (System.MissingMethodException ex) +#pragma warning restore 0168 { - throw new ParserException("Unexpected section on line " + mInput.LineNumber + ": " + section); + throw new ParserException("Unexpected key on line " + mInput.LineNumber + ": " + pair[0]); } } else { - throw new ParserException("Unexpected text on line " + mInput.LineNumber + ": " + line); + return line; } } + + return null; } - } - string ReadMetadataSection() - { - while (!mInput.End) + string ReadMapTableSection() { - string line = mInput.ReadLine(); - if (!IsLineSignificant(line)) continue; + if (mMetadata == null || mMetadata.GridWidth == 0 || mMetadata.GridHeight == 0) + { + throw new ParserException("Unexpected section on line " + mInput.LineNumber + + ": You must define the map dimensions before this section."); + } - string[] pair = Parse.KeyValuePair(line); - if (pair != null) + mGrid = new char[mMetadata.GridWidth, mMetadata.GridHeight]; + + int y; + for (y = 0; y < mMetadata.GridHeight && !mInput.End; y++) { - if (pair[0] == "type") + string line = mInput.ReadLine(); + + if (line.Length < mMetadata.GridWidth) { - Map.Type type = Parse.Constant(pair[1]); - if (type != default(Map.Type)) - { - mMetadata.Type = type; - } - else - { - throw new ParserException("Unexpected type on line " + mInput.LineNumber + ": " + pair[1]); - } + throw new ParserException("Unexpected EOL on line " + mInput.LineNumber + + ": The number of characters should match the width dimension (" + mMetadata.GridWidth + ")."); } - else if (pair[0] == "dimensions") + + for (int x = 0; x < mMetadata.GridWidth; x++) { - Point? dimensions = Parse.Coordinates(pair[1]); - if (dimensions != null) - { - mMetadata.GridWidth = dimensions.Value.X; - mMetadata.GridHeight = dimensions.Value.Y; - if (mMetadata.GridWidth <= 0 || mMetadata.GridHeight <= 0) - { - throw new ParserException("Invalid dimensions on line " + mInput.LineNumber + ": " + pair[1]); - } - } - else - { - throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + pair[1]); - } + mGrid[x, y] = line[x]; } - else if (pair[0] == "tileset") + } + + if (y < mMetadata.GridHeight) + { + throw new ParserException("Unexpected EOF on line " + mInput.LineNumber + + ": The number of lines in this section should match the height dimension (" + mMetadata.GridHeight + ")."); + } + + return null; + } + + string ReadEntitySection(char identifier) + { + Dictionary pairs = new Dictionary(); + mEntitySections[identifier] = pairs; + + while (!mInput.End) + { + string line = mInput.ReadLine(); + + string[] pair = Parse.KeyValuePair(line); + if (pair != null) { - string tileset = Parse.String(pair[1]); - if (tileset != null) - { - mMetadata.Tileset = tileset; - } - else - { - throw new ParserException("Unexpected tileset on line " + mInput.LineNumber + ": " + pair[1]); - } + pairs[pair[0]] = pair[1]; } - else if (pair[0] == "numplayers") + else { - string[] list = Parse.List(pair[1]); - if (list != null) + return line; + } + } + + return null; + } + + + void PostProcess() + { + if (mMetadata == null || mGrid == null) + { + throw new ParserException("Missing a required section. Make sure the metadata and grid are there."); + } + + mEntities = new List(); + mPlayerPositions = new Point[mMetadata.NumPlayers.Max() + 1]; + + // create entities defined completely + foreach (char identifier in mEntitySections.Keys) + { + Dictionary pairs = mEntitySections[identifier]; + if (pairs.ContainsKey("create")) + { + string[] list = Parse.List(pairs["create"]); + foreach (string positionString in list) { - foreach (string atom in list) + Point? position = Parse.Coordinates(positionString); + if (position != null) { - int[] range = Parse.Range(atom); - if (range != null) - { - for (int i = range[0]; i <= range[1]; i++) - { - mMetadata.NumPlayers.Add(i); - } - continue; - } - int? integer = Parse.Integer(atom); - if (integer != null) - { - mMetadata.NumPlayers.Add(integer.Value); - continue; - } - - throw new ParserException("Unexpected atom on line " + mInput.LineNumber + ": " + atom); + Map.RawEntity createEntity = new Map.RawEntity(); + createEntity.Id = identifier; + createEntity.Position = position.Value; + createEntity.Attributes = pairs; + mEntities.Add(createEntity); } - if (mMetadata.NumPlayers.Count == 0) + else { - throw new ParserException("No numbers given on line " + mInput.LineNumber + ": " + pair[1]); + throw new ParserException("Unexpected value of key `create' defined for entity " + identifier + "."); } } - else - { - throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + pair[1]); - } + pairs.Remove("create"); } - else if (pair[0] == "author") - { - string author = Parse.String(pair[1]); - if (author != null) - { - mMetadata.Author = author; - } - else - { - throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + pair[1]); - } - } - else if (pair[0] == "levelname") + } + + // create entities with positions defined on the grid + // and get player starting positions + for (int x = 0; x < mMetadata.GridWidth; x++) + { + for (int y = 0; y < mMetadata.GridHeight; y++) { - string level = Parse.String(pair[1]); - if (level != null) + char identifier = mGrid[x, y]; + if (IsValidEntityIdentifier(identifier)) { - mMetadata.Name = level; + if (mEntitySections.ContainsKey(identifier)) + { + Map.RawEntity createEntity = new Map.RawEntity(); + createEntity.Id = identifier; + createEntity.Position = new Point(x, y); + createEntity.Attributes = mEntitySections[identifier]; + mEntities.Add(createEntity); + } + else + { + throw new ParserException("Unexpected entity (" + identifier + + ") placed on the grid at [" + x + "," + y + "] but not defined."); + } + mGrid[x, y] = mDefaultTile; } - else + else if ('1' <= identifier && identifier <= '9') { - throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + pair[1]); + int playerNum = identifier - 48; + if (playerNum < mPlayerPositions.Count()) + { + mPlayerPositions[playerNum] = new Point(x, y); + } + mGrid[x, y] = mDefaultTile; } } - else - { - throw new ParserException("Unexpected key on line " + mInput.LineNumber + ": " + pair[0]); - } } - else + + // check if all needed player positions are defined + for (int i = 1; i < mPlayerPositions.Count(); i++) { - return line; + if (mPlayerPositions[i] == default(Point)) + { + throw new ParserException("Not enough player positions were defined on the grid; " + + "are missing a spot for player " + i + "."); + } } } - return null; - } - string ReadMapTableSection() - { - if (mMetadata == null || mMetadata.GridWidth == 0 || mMetadata.GridHeight == 0) + bool IsLineSignificant(string line) { - throw new ParserException("Unexpected section on line " + mInput.LineNumber + - ": You must define the map dimensions before this section."); + if (line.Trim().Length == 0 || Parse.IniComment(line) != null) return false; + return true; } - mGrid = new char[mMetadata.GridWidth, mMetadata.GridHeight]; - - int y; - for (y = 0; y < mMetadata.GridHeight && !mInput.End; y++) + bool IsValidEntityIdentifier(char id) { - string line = mInput.ReadLine(); + if (('a' <= id && id <= 'z') || ('A' <= id && id <= 'Z')) return true; + return false; + } - if (line.Length < mMetadata.GridWidth) - { - throw new ParserException("Unexpected EOL on line " + mInput.LineNumber + - ": The number of characters should match the width dimension (" + mMetadata.GridWidth + ")."); - } - for (int x = 0; x < mMetadata.GridWidth; x++) - { - mGrid[x, y] = line[x]; - } + public void set_author(string atom) + { + string value = Parse.String(atom); + if (value != null) mMetadata.Author = value; + else throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + atom); } - if (y < mMetadata.GridHeight) + public void set_levelname(string atom) { - throw new ParserException("Unexpected EOF on line " + mInput.LineNumber + - ": The number of lines in this section should match the height dimension (" + mMetadata.GridHeight + ")."); + string value = Parse.String(atom); + if (value != null) mMetadata.Name = value; + else throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + atom); } - return null; - } - - string ReadEntitySection(char entity) - { - Dictionary pairs = new Dictionary(); - mEntities[entity] = pairs; - - while (!mInput.End) + public void set_type(string atom) { - string line = mInput.ReadLine(); + Map.Type value = Parse.Constant(atom); + if (value != default(Map.Type)) mMetadata.Type = value; + else throw new ParserException("Unexpected type on line " + mInput.LineNumber + ": " + atom); + } - string[] pair = Parse.KeyValuePair(line); - if (pair != null) + public void set_dimensions(string atom) + { + Point? dimensions = Parse.Coordinates(atom); + if (dimensions != null) { - pairs[pair[0]] = pair[1]; + mMetadata.GridWidth = dimensions.Value.X; + mMetadata.GridHeight = dimensions.Value.Y; + if (mMetadata.GridWidth <= 0 || mMetadata.GridHeight <= 0) + { + throw new ParserException("Invalid dimensions on line " + mInput.LineNumber + ": " + atom); + } } else { - return line; + throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + atom); } } - return null; - } - - - bool IsLineSignificant(string line) - { - if (line.Trim().Length == 0 || Parse.IniComment(line) != null) return false; - return true; - } - - #endregion - - - #region Private Types - - /// - /// This private class wraps around ContentReader to make it more - /// convenient to use it as an input stream reader. - /// - class LineReader - { - ContentReader mInput; - int mLineNumber = 0; - int mExpectedNumberOfLines; + public void set_tileset(string atom) + { + string value = Parse.String(atom); + if (value != null) mMetadata.Tileset = value; + else throw new ParserException("Unexpected tileset on line " + mInput.LineNumber + ": " + atom); + } - public LineReader(ContentReader input) + public void set_defaulttile(string atom) { - mInput = input; - mExpectedNumberOfLines = mInput.ReadInt32(); + char? value = Parse.Char(atom); + if (value != null) mDefaultTile = value.Value; + else throw new ParserException("Unexpected tile value on line " + mInput.LineNumber + ": " + atom); } - public string ReadLine() + public void set_numplayers(string atom) { - mLineNumber++; - return mInput.ReadString(); + string[] list = Parse.List(atom); + if (list != null) + { + foreach (string item in list) + { + int[] range = Parse.Range(item); + if (range != null) + { + for (int i = range[0]; i <= range[1]; i++) + { + mMetadata.NumPlayers.Add(i); + } + continue; + } + int? integer = Parse.Integer(item); + if (integer != null) + { + mMetadata.NumPlayers.Add(integer.Value); + continue; + } + + throw new ParserException("Unexpected atom on line " + mInput.LineNumber + ": " + item); + } + if (mMetadata.NumPlayers.Count == 0) + { + throw new ParserException("No numbers given on line " + mInput.LineNumber + ": " + atom); + } + } + else + { + throw new ParserException("Unexpected value on line " + mInput.LineNumber + ": " + atom); + } } - public int LineNumber { get { return mLineNumber; } } - public bool End { get { return mLineNumber >= mExpectedNumberOfLines; } } + Map.Metadata mMetadata; + char[,] mGrid; + List mEntities; + Point[] mPlayerPositions; + + Dictionary> mEntitySections = new Dictionary>(); + char mDefaultTile = ' '; + + LineReader mInput; } #endregion @@ -319,11 +437,7 @@ namespace CarFire #region Private Variables - Map.Metadata mMetadata; - char[,] mGrid; - Dictionary> mEntities = new Dictionary>(); - - LineReader mInput; + Impl mImpl; #endregion } diff --git a/CarFire/CarFire/CarFire/Parse.cs b/CarFire/CarFire/CarFire/Parse.cs index b932077..2df3ca7 100644 --- a/CarFire/CarFire/CarFire/Parse.cs +++ b/CarFire/CarFire/CarFire/Parse.cs @@ -9,7 +9,8 @@ namespace CarFire { /// /// Class with handy static methods taking strings and returning objects - /// parsed from those strings. + /// parsed from those strings. For all of these functions, white space is + /// generally ignored, but any superfluous characters will make the parse fail. /// public class Parse { @@ -108,6 +109,18 @@ namespace CarFire return null; } + /// + /// Parses a single character. + /// + /// Text. + /// The character, or null if parsing failed. + public static char? Char(string atom) + { + string str = String(atom); + if (str != null && str.Length == 1) return str[0]; + return null; + } + /// /// Parses a constant from an enum. /// diff --git a/CarFire/CarFire/CarFire/XnaGame.cs b/CarFire/CarFire/CarFire/XnaGame.cs index 6f8a421..da92bf0 100644 --- a/CarFire/CarFire/CarFire/XnaGame.cs +++ b/CarFire/CarFire/CarFire/XnaGame.cs @@ -79,7 +79,8 @@ namespace CarFire #if MAP_TESTING map = Content.Load("Maps/sandbox"); Map.DefaultTile = Content.Load("default"); - map.CenterCell = new Vector2(7, 7); + map.CenterCell = new Vector2(2, 4); + List entities = map.GetAllEntities(); #endif } diff --git a/CarFire/leveleditor/doc/leveleditor.txt b/CarFire/leveleditor/doc/leveleditor.txt index 377cbf9..cb56df8 100644 --- a/CarFire/leveleditor/doc/leveleditor.txt +++ b/CarFire/leveleditor/doc/leveleditor.txt @@ -108,7 +108,9 @@ objects and whatnot. For example, if I wanted to place a monster on the map using the `A' character, I would create a section of the file named `A' and then place one or more `A' characters in the |leveleditor-maptable| section at the coordinates where I want the monsters to be created when the -map file is loaded. +map file is loaded. The static scenery at the places where entities are +placed is determined by the `defaulttile' key in the |leveleditor-metadata| +section. You can also use the same definitions to place identical copies of the same entity at different locations on the map. This is what allows you to @@ -117,6 +119,17 @@ although you are limited to only 52 different types of entities per level. By the way, these sections can appear anywhere in the file, either before or after the |leveleditor-maptable| section. +There is also a way to place entities on the map without putting its +identifying character in the |leveleditor-maptable| section. You might +want to do this if you want to place an entity at some cell on the map that +you don't want to be the default tile. This is accomplished by listing +the coordinates where entities should be place after the `create' key. +Sometime during the map loading process, entities defined with this key and +one or more valid sets of coordinates will be made available to the game in +the usual manner, just as if its identifier was put on the map. The +difference is, you can put any (open) static scenery you want at those +locations. + =========================================================================== B. Basic Syntax *leveleditor-syntax* -- 2.43.0