diff --git a/OpenRA.Mods.Cnc/FileSystem/MegFile.cs b/OpenRA.Mods.Cnc/FileSystem/MegFile.cs
new file mode 100644
index 0000000000..affbc301d8
--- /dev/null
+++ b/OpenRA.Mods.Cnc/FileSystem/MegFile.cs
@@ -0,0 +1,138 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using OpenRA.FileSystem;
+using OpenRA.Primitives;
+
+namespace OpenRA.Mods.Cnc.FileSystem
+{
+ ///
+ /// This class supports loading unencrypted V3 .meg files using
+ /// reference documentation from here https://modtools.petrolution.net/docs/MegFileFormat
+ ///
+ public class MegV3Loader : IPackageLoader
+ {
+ const uint UnencryptedMegID = 0xFFFFFFFF;
+
+ // Float value 0.99, but it is simpler to read and compare as an integer
+ const uint MegVersion = 0x3F7D70A4;
+
+ public bool TryParsePackage(Stream s, string filename, OpenRA.FileSystem.FileSystem context, out IReadOnlyPackage package)
+ {
+ var position = s.Position;
+
+ var id = s.ReadUInt32();
+ var version = s.ReadUInt32();
+
+ s.Position = position;
+
+ if (id != UnencryptedMegID || version != MegVersion)
+ {
+ package = null;
+ return false;
+ }
+
+ package = new MegFile(s, filename);
+ return true;
+ }
+
+ public sealed class MegFile : IReadOnlyPackage
+ {
+ readonly Stream s;
+
+ readonly Dictionary contents = new Dictionary();
+
+ public MegFile(Stream s, string filename)
+ {
+ Name = filename;
+ this.s = s;
+
+ var id = s.ReadUInt32();
+ var version = s.ReadUInt32();
+
+ if (id != UnencryptedMegID || version != MegVersion)
+ throw new Exception("Invalid file signature for meg file");
+
+ var headerSize = s.ReadUInt32();
+ var numStrings = s.ReadUInt32();
+ var numFiles = s.ReadUInt32();
+ var stringsSize = s.ReadUInt32();
+ var stringsStart = s.Position;
+
+ var filenames = new List();
+
+ // The file names are an indexed array of strings
+ for (var i = 0; i < numStrings; i++)
+ {
+ var length = s.ReadUInt16();
+ filenames.Add(s.ReadASCII(length));
+ }
+
+ // The header indicates where we should be, so verify it
+ if (s.Position != stringsSize + stringsStart)
+ throw new Exception("File name table in .meg file inconsistent");
+
+ // Now we load each file entry and associated info
+ for (var i = 0; i < numFiles; i++)
+ {
+ // Ignore flags, crc, index
+ s.Position += 10;
+ var size = s.ReadUInt32();
+ var offset = s.ReadUInt32();
+ var nameIndex = s.ReadUInt16();
+ contents[filenames[nameIndex]] = (offset, (int)size);
+ }
+
+ if (s.Position != headerSize)
+ throw new Exception("Expected to be at data start offset");
+ }
+
+ public string Name { get; }
+
+ public IEnumerable Contents => contents.Keys;
+
+ public bool Contains(string filename)
+ {
+ return contents.ContainsKey(filename);
+ }
+
+ public void Dispose()
+ {
+ s.Dispose();
+ }
+
+ public Stream GetStream(string filename)
+ {
+ // Look up the index of the filename
+ if (!contents.TryGetValue(filename, out var index))
+ return null;
+
+ return SegmentStream.CreateWithoutOwningStream(s, index.Offset, index.Length);
+ }
+
+ public IReadOnlyPackage OpenPackage(string filename, OpenRA.FileSystem.FileSystem context)
+ {
+ var childStream = GetStream(filename);
+ if (childStream == null)
+ return null;
+
+ if (context.TryParsePackage(childStream, filename, out var package))
+ return package;
+
+ childStream.Dispose();
+ return null;
+ }
+ }
+ }
+}