|
| 1 | +using MESS.Mapping; |
| 2 | +using MESS.Spatial; |
| 3 | +using System.IO; |
| 4 | +using System.Linq; |
| 5 | +using System.Text; |
| 6 | + |
| 7 | +namespace MESS.Formats |
| 8 | +{ |
| 9 | + /// <summary> |
| 10 | + /// The text-based MAP file format only stores entities and brushes. |
| 11 | + /// The special 'worldspawn' entity contains all map properties and world geometry. |
| 12 | + /// </summary> |
| 13 | + public static class MapFormat |
| 14 | + { |
| 15 | + public static Map Load(Stream stream) |
| 16 | + { |
| 17 | + using (var reader = new StreamReader(stream, Encoding.UTF8)) |
| 18 | + { |
| 19 | + var map = new Map(); |
| 20 | + |
| 21 | + while (!reader.EndOfStream) |
| 22 | + { |
| 23 | + var line = reader.ReadLine(); |
| 24 | + if (!line.Trim().StartsWith("{")) |
| 25 | + continue; |
| 26 | + |
| 27 | + var entity = ReadEntity(reader); |
| 28 | + if (entity == null) |
| 29 | + break; |
| 30 | + |
| 31 | + if (entity.ClassName == "worldspawn") |
| 32 | + { |
| 33 | + foreach (var kv in entity.Properties) |
| 34 | + map.Properties[kv.Key] = kv.Value; |
| 35 | + |
| 36 | + map.WorldGeometry.AddRange(entity.Brushes); |
| 37 | + } |
| 38 | + else |
| 39 | + { |
| 40 | + map.Entities.Add(entity); |
| 41 | + } |
| 42 | + } |
| 43 | + return map; |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + public static void Save(Map map, Stream stream) |
| 48 | + { |
| 49 | + using (var writer = new StreamWriter(stream, Encoding.UTF8)) |
| 50 | + { |
| 51 | + var worldspawn = new Entity(); |
| 52 | + worldspawn.ClassName = "worldspawn"; |
| 53 | + worldspawn["sounds"] = "1"; |
| 54 | + worldspawn["MaxRange"] = "4096"; |
| 55 | + worldspawn["mapversion"] = "220"; |
| 56 | + foreach (var kv in map.Properties) |
| 57 | + worldspawn.Properties[kv.Key] = kv.Value; |
| 58 | + worldspawn.Brushes.AddRange(map.WorldGeometry); |
| 59 | + |
| 60 | + WriteEntity(writer, worldspawn); |
| 61 | + foreach (var entity in map.Entities) |
| 62 | + WriteEntity(writer, entity); |
| 63 | + |
| 64 | + foreach (var path in map.Paths) |
| 65 | + WritePath(writer, path); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + |
| 70 | + private static Entity ReadEntity(TextReader reader) |
| 71 | + { |
| 72 | + var entity = new Entity(); |
| 73 | + while (true) |
| 74 | + { |
| 75 | + var line = reader.ReadLine().Trim(); |
| 76 | + if (line.StartsWith("\"")) |
| 77 | + { |
| 78 | + var parts = line.Split('"'); // NOTE: There is no support for escaping double quotes! |
| 79 | + entity.Properties[parts[1]] = parts[3]; |
| 80 | + } |
| 81 | + else if (line.StartsWith("{")) |
| 82 | + { |
| 83 | + entity.Brushes.Add(ReadBrush(reader)); |
| 84 | + } |
| 85 | + else if (line.StartsWith("}")) |
| 86 | + { |
| 87 | + break; |
| 88 | + } |
| 89 | + else |
| 90 | + { |
| 91 | + throw new InvalidDataException($"Expected key-value pair, brush or end of entity, but found '{line}'."); |
| 92 | + } |
| 93 | + } |
| 94 | + return entity; |
| 95 | + } |
| 96 | + |
| 97 | + private static Brush ReadBrush(TextReader reader) |
| 98 | + { |
| 99 | + var brush = new Brush(); |
| 100 | + while (true) |
| 101 | + { |
| 102 | + var line = reader.ReadLine().Trim(); |
| 103 | + if (line.StartsWith("(")) |
| 104 | + brush.Faces.Add(ReadFace(line)); |
| 105 | + else if (line.StartsWith("}")) |
| 106 | + break; |
| 107 | + else |
| 108 | + throw new InvalidDataException($"Expected face or end of brush, but found '{line}'."); |
| 109 | + } |
| 110 | + return brush; |
| 111 | + } |
| 112 | + |
| 113 | + private static Face ReadFace(string line) |
| 114 | + { |
| 115 | + var parts = line.Split(); |
| 116 | + if (parts.Length < 31) |
| 117 | + throw new InvalidDataException($"Unexpected face format: '{line}'."); |
| 118 | + |
| 119 | + return new Face { |
| 120 | + PlanePoints = new[] { |
| 121 | + new Vector3D(float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3])), |
| 122 | + new Vector3D(float.Parse(parts[6]), float.Parse(parts[7]), float.Parse(parts[8])), |
| 123 | + new Vector3D(float.Parse(parts[11]), float.Parse(parts[12]), float.Parse(parts[13])), |
| 124 | + }, |
| 125 | + TextureName = parts[15], |
| 126 | + TextureRightAxis = new Vector3D(float.Parse(parts[17]), float.Parse(parts[18]), float.Parse(parts[19])), |
| 127 | + TextureDownAxis = new Vector3D(float.Parse(parts[23]), float.Parse(parts[24]), float.Parse(parts[25])), |
| 128 | + TextureShift = new Vector2D(float.Parse(parts[20]), float.Parse(parts[26])), |
| 129 | + TextureAngle = float.Parse(parts[28]), |
| 130 | + TextureScale = new Vector2D(float.Parse(parts[29]), float.Parse(parts[30])), |
| 131 | + }; |
| 132 | + } |
| 133 | + |
| 134 | + |
| 135 | + private static void WriteEntity(TextWriter writer, Entity entity) |
| 136 | + { |
| 137 | + writer.WriteLine("{"); |
| 138 | + |
| 139 | + foreach (var property in entity.Properties) |
| 140 | + writer.WriteLine($"\"{property.Key}\" \"{property.Value}\""); |
| 141 | + |
| 142 | + foreach (var brush in entity.Brushes) |
| 143 | + WriteBrush(writer, brush); |
| 144 | + |
| 145 | + writer.WriteLine("}"); |
| 146 | + } |
| 147 | + |
| 148 | + private static void WriteBrush(TextWriter writer, Brush brush) |
| 149 | + { |
| 150 | + writer.WriteLine("{"); |
| 151 | + |
| 152 | + foreach (var face in brush.Faces) |
| 153 | + WriteFace(writer, face); |
| 154 | + |
| 155 | + writer.WriteLine("}"); |
| 156 | + } |
| 157 | + |
| 158 | + private static void WriteFace(TextWriter writer, Face face) |
| 159 | + { |
| 160 | + foreach (var point in face.PlanePoints) |
| 161 | + writer.Write($"( {point.X} {point.Y} {point.Z} ) "); |
| 162 | + |
| 163 | + writer.Write(face.TextureName); |
| 164 | + writer.Write(' '); |
| 165 | + writer.Write($"[ {face.TextureRightAxis.X} {face.TextureRightAxis.Y} {face.TextureRightAxis.Z} {face.TextureShift.X} ] "); |
| 166 | + writer.Write($"[ {face.TextureDownAxis.X} {face.TextureDownAxis.Y} {face.TextureDownAxis.Z} {face.TextureShift.Y} ] "); |
| 167 | + writer.WriteLine($"{face.TextureAngle} {face.TextureScale.X} {face.TextureScale.Y} "); |
| 168 | + } |
| 169 | + |
| 170 | + private static void WritePath(TextWriter writer, Mapping.Path path) |
| 171 | + { |
| 172 | + foreach (var entity in path.GenerateEntities()) |
| 173 | + WriteEntity(writer, entity); |
| 174 | + } |
| 175 | + } |
| 176 | +} |
0 commit comments