diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for tscn-attoparsec
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/bench/Main.hs b/bench/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/Main.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import           Criterion.Main
+
+import           Data.Either           (fromRight)
+import qualified Data.Text.IO          as T
+
+import           Godot.Parser.Resource
+
+import qualified Text.Megaparsec       as P
+
+main :: IO ()
+main = do
+  let parseFile f = fromRight (error $ "Unable to parse " <> f) . P.parse tscnParser "" <$> T.readFile f
+  defaultMain [ bench "EntityMap" $ whnfIO (parseFile "bench/EntityMap.tscn")
+              , bench "Geography" $ whnfIO (parseFile "bench/Geography.tscn")
+              , bench "BigFile" $ whnfIO (parseFile "bench/BigFile.tscn")]
diff --git a/godot-megaparsec.cabal b/godot-megaparsec.cabal
new file mode 100644
--- /dev/null
+++ b/godot-megaparsec.cabal
@@ -0,0 +1,61 @@
+cabal-version:      2.4
+name:               godot-megaparsec
+version:            0.1.0.0
+
+-- A short (one-line) description of the package.
+synopsis:           Megaparsec parser for Godot `tscn` and `gdns` files.
+
+-- A longer description of the package.
+description:
+  Megaparsec parser for Godot `tscn` and `gdns` files.
+  Currently only supports auto-generated files (i.e. files generated by the editor).
+  Probably could parse other resource files, as they share similar formats.
+
+-- A URL where users can report bugs.
+-- bug-reports:
+
+-- The license under which the package is released.
+license:            BSD-3-Clause
+author:             Winston Hartnett
+maintainer:         winstonhartnett@gmail.com
+
+-- A copyright notice.
+-- copyright: BSD3-CLAUSE
+category:           library
+extra-source-files: CHANGELOG.md
+
+source-repository head
+  type:     git
+  location: https://github.com/WinstonHartnett/godot-megaparsec.git
+
+library
+  exposed-modules:  Godot.Parser.Resource
+
+  -- Modules included in this library but not exported.
+  -- other-modules:
+
+  -- LANGUAGE extensions used by modules in this package.
+  -- other-extensions:
+  ghc-options:      -Wall
+  build-depends:
+    , base                  >= 4.14.0 && < 5
+    , generic-lens          >= 2.2.0 && < 2.3
+    , lens                  >= 5.0.1 && < 5.1
+    , megaparsec            >= 9.2.0 && < 9.3
+    , text                  >= 1.2.4 && < 1.3
+    , unordered-containers  >= 0.2.14 && < 0.3
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
+
+executable bench
+  hs-source-dirs:   bench
+  main-is:          Main.hs
+  default-language: Haskell2010
+  ghc-options:      -Wall
+  build-depends:
+    , base                  >= 4.14.0 && < 5
+    , criterion             >= 1.5.11 && < 1.6
+    , godot-megaparsec
+    , megaparsec            >= 9.2.0 && < 9.3
+    , text                  >= 1.2.4 && < 1.3
diff --git a/src/Godot/Parser/Resource.hs b/src/Godot/Parser/Resource.hs
new file mode 100644
--- /dev/null
+++ b/src/Godot/Parser/Resource.hs
@@ -0,0 +1,378 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Godot.Parser.Resource
+  (GodotValue(..)
+  ,GodotSection(..)
+  ,TscnDescriptor(..)
+  ,TscnParsed(..)
+  ,GdnsDescriptor(..)
+  ,GdnsParsed(..)
+  ,GodotParsed(..)
+  ,tscnParser
+  ,gdnsParser) where
+
+import           Control.Applicative        ((<|>),liftA2)
+import           Control.Lens
+import           Control.Monad              (unless)
+
+import           Data.Char                  (isAlphaNum,isDigit,isUpper)
+import           Data.Either                (fromRight)
+import           Data.Foldable              (foldl')
+import           Data.Functor               (($>))
+import qualified Data.HashMap.Lazy          as M
+import qualified Data.HashSet               as S
+import           Data.Maybe                 (fromJust)
+import qualified Data.Text                  as T
+import qualified Data.Text.Read             as T
+import           Data.Void
+
+import           GHC.Generics               (Generic)
+
+import qualified Text.Megaparsec            as P
+import qualified Text.Megaparsec.Char       as P
+import qualified Text.Megaparsec.Char.Lexer as P (decimal,signed)
+
+type Parser = P.Parsec Void T.Text
+
+optionalSign :: Parser T.Text
+optionalSign = P.string "-" <|> P.string "+"
+
+godotFloatP :: Parser Float
+godotFloatP = do
+  sign <- P.option "" optionalSign
+  let takeDigits = P.takeWhile1P Nothing isDigit
+  rational <- takeDigits <> P.string "." <> takeDigits
+  exponent <- P.option "" (P.string "e" <> P.option "" optionalSign <> takeDigits)
+  pure . fst . fromRight undefined . T.rational $ sign <> rational <> exponent
+
+godotIntP :: Parser Int
+godotIntP = P.signed P.space P.decimal
+
+godotBoolP :: Parser Bool
+godotBoolP = (P.string "true" $> True) <|> (P.string "false" $> False)
+
+stringP :: Parser T.Text
+stringP = P.char '"' *> P.takeWhileP Nothing (/= '"') <* P.char '"'
+
+godotStringP :: Parser T.Text
+godotStringP = stringP
+
+godotArrP :: Parser [GodotValue]
+godotArrP = do
+  P.char '['
+  P.space
+  P.manyTill (do
+                gVal <- godotValueP
+                P.char ','
+                P.space
+                pure gVal) (P.char ']')
+
+godotDictP :: Parser (M.HashMap T.Text GodotValue)
+godotDictP = do
+  P.char '{'
+  P.space
+  let kvParser = liftA2 (,) stringP (P.char ':' *> P.hspace *> godotValueP)
+  kvs <- kvParser `P.sepBy` (P.char ',' *> P.newline *> P.hspace)
+  P.space
+  P.char '}'
+  pure . M.fromList $ kvs
+
+godotConstructorP :: Parser (T.Text, [GodotValue])
+godotConstructorP = do
+  let isGodotIdent c = isAlphaNum c || c == '@'
+  constructorName
+    <- P.takeWhile1P Nothing isGodotIdent -- TODO Causes problems w/ other delimiters
+  P.char '('
+  P.space
+  constructorArgs <- godotValueP `P.sepBy` (P.char ',' *> P.hspace)
+  P.space
+  P.char ')'
+  pure (constructorName, constructorArgs)
+
+godotNullP :: Parser GodotValue
+godotNullP = P.string "null" $> GodotNull
+
+godotValueP :: Parser GodotValue
+godotValueP = do
+  nc <- T.head . P.stateInput <$> P.getParserState
+  case nc of
+    '"'       -> GodotString <$> godotStringP
+    '['       -> GodotArr <$> godotArrP
+    '{'       -> GodotDict <$> godotDictP
+    't'       -> GodotBool <$> godotBoolP
+    'f'       -> GodotBool <$> godotBoolP
+    'n'       -> godotNullP
+    l
+      | isUpper l || l == '@' -> GodotConstructor <$> godotConstructorP
+    _ -> P.try (GodotFloat <$> godotFloatP) <|> P.try (GodotInt <$> godotIntP)
+
+-- | Values parsed from a Tscn file.
+data GodotValue
+  = GodotConstructor (T.Text, [GodotValue])
+  | GodotInt Int
+  | GodotFloat Float
+  | GodotBool Bool
+  | GodotString T.Text
+  | GodotDict (M.HashMap T.Text GodotValue)
+  | GodotArr [GodotValue]
+  | GodotNull
+  deriving (Show,Generic,Eq)
+
+makeLenses ''GodotValue
+
+-- There aren't any lenses to unwrap sum types AFAIK :/
+-- Surely there's a better way to do this.
+unGodotConstructor k = fmap (\(GodotConstructor (n, a)) -> (n, a)) . M.lookup k
+
+unGodotConstructor' k = fromJust . unGodotConstructor k
+
+unGodotInt k = fmap (\(GodotInt i) -> i) . M.lookup k
+
+unGodotInt' k = fromJust . unGodotInt k
+
+unGodotFloat k = fmap (\(GodotFloat i) -> i) . M.lookup k
+
+unGodotFloat' k = fromJust . unGodotInt k
+
+unGodotBool k = fmap (\(GodotBool i) -> i) . M.lookup k
+
+unGodotBool' k = fromJust . unGodotBool k
+
+unGodotString k = fmap (\(GodotString i) -> i) . M.lookup k
+
+unGodotString' k = fromJust . unGodotString k
+
+unGodotDict k = fmap (\(GodotDict i) -> i) . M.lookup k
+
+unGodotDict' k = fromJust . unGodotDict k
+
+unGodotArr k = fmap (\(GodotArr i) -> i) . M.lookup k
+
+unGodotArr' k = fromJust . unGodotArr k
+
+collectRest its = M.filterWithKey (\k _ -> k `S.member` S.fromList its)
+
+-- | As of Godot 3.3
+data GodotSection
+  = ExtResourceSection
+    { _extResourceSectionPath    :: T.Text
+    , _extResourceSectionTy      :: T.Text
+    , _extResourceSectionId      :: Int
+      -- | Other header information
+    , _extResourceSectionHeaders :: M.HashMap T.Text GodotValue
+      -- | Body of the configuration entry
+    , _extResourceSectionEntries :: M.HashMap T.Text GodotValue
+    }
+  | SubResourceSection
+    { _subResourceSectionTy      :: T.Text
+    , _subResourceSectionId      :: Int
+      -- | Other header information
+    , _subResourceSectionHeaders :: M.HashMap T.Text GodotValue
+      -- | Body of the configuration entry
+    , _subResourceSectionEntries :: M.HashMap T.Text GodotValue
+    }
+  | NodeSection
+    { _nodeSectionTy :: Maybe T.Text
+    , _nodeSectionName :: T.Text
+      -- | If Nothing, then this node is the root
+    , _nodeSectionParent :: Maybe T.Text
+      -- | Instance is GodotConstructor
+    , _nodeSectionInst :: Maybe Int
+    , _nodeSectionInstPlaceholder :: Maybe T.Text
+    , _nodeSectionOwner :: Maybe T.Text
+    , _nodeSectionIndex :: Maybe Int
+    , _nodeSectionGroups :: Maybe [T.Text]
+      -- | Other header information
+    , _nodeSectionHeaders :: M.HashMap T.Text GodotValue
+      -- | Body of the configuration entry
+    , _nodeSectionEntries :: M.HashMap T.Text GodotValue
+    }
+  | ConnectionSection
+    { _connectionSectionSignal  :: T.Text
+    , _connectionSectionFrom    :: T.Text
+    , _connectionSectionTo      :: T.Text
+    , _connectionSectionMethod  :: T.Text
+      -- | Other header information
+    , _connectionSectionHeaders :: M.HashMap T.Text GodotValue
+      -- | Body of the configuration entry
+    , _connectionSectionEntries :: M.HashMap T.Text GodotValue
+    }
+  | ResourceSection
+    { _resourceSectionResourceName :: Maybe T.Text
+    , _resourceSectionClassName    :: Maybe T.Text
+    , _resourceSectionLibrary      :: Maybe (T.Text, [GodotValue])
+    }
+  | OtherSection
+    { _otherSectionHeader  :: T.Text
+    , _otherSectionHeaders :: M.HashMap T.Text GodotValue
+    , _otherSectionEntries :: M.HashMap T.Text GodotValue
+    }
+  deriving (Show,Generic)
+
+makeFields ''GodotSection
+
+data TscnDescriptor =
+  TscnDescriptor
+  { _tscnDescriptorLoadSteps :: Int
+  , _tscnDescriptorFormat    :: Int
+  }
+  deriving (Show,Generic)
+
+makeFields ''TscnDescriptor
+
+data TscnParsed =
+  TscnParsed
+  { _tscnParsedDescriptor :: TscnDescriptor
+  , _tscnParsedSections   :: [GodotSection]
+  }
+  deriving (Show,Generic)
+
+makeFields ''TscnParsed
+
+data GdnsDescriptor =
+  GdnsDescriptor
+  { _gdnsDescriptorTy        :: T.Text
+  , _gdnsDescriptorLoadSteps :: Int
+  , _gdnsDescriptorFormat    :: Int
+  }
+  deriving (Show,Generic)
+
+makeFields ''GdnsDescriptor
+
+data GdnsParsed =
+  GdnsParsed
+  { _gdnsParsedDescriptor :: GdnsDescriptor
+  , _gdnsParsedSections   :: [GodotSection]
+  }
+  deriving (Show,Generic)
+
+makeFields ''GdnsParsed
+
+data GodotParsed
+  = Tscn TscnParsed
+  | Gdns GdnsParsed
+  deriving (Show,Generic)
+
+makeFields ''GodotParsed
+
+tscnHeaderKVP :: Parser (T.Text, GodotValue)
+tscnHeaderKVP = liftA2 (,) (P.takeWhileP Nothing (/= '=')) (P.char '=' *> godotValueP)
+
+headerKvs :: Parser (M.HashMap T.Text GodotValue)
+headerKvs = M.fromList <$> tscnHeaderKVP `P.sepBy` P.char ' '
+
+bodyAndKvs :: Parser (T.Text, M.HashMap T.Text GodotValue, M.HashMap T.Text GodotValue)
+bodyAndKvs = do
+  headerName <- P.char '[' *> P.takeWhile1P Nothing (/= ' ') <* P.char ' '
+  headerKvs' <- headerKvs
+  P.char ']'
+  P.space
+  let tscnBodyP  = do
+        let parseKV =
+              liftA2 (,) (P.takeWhileP Nothing (/= ' '))
+              (P.string " = " *> godotValueP <* P.newline)
+        M.fromList <$> P.manyTill parseKV (P.try (P.newline $> ()) <|> P.try P.eof)
+      emptyBodyP = pure M.empty
+  body <- P.try tscnBodyP <|> P.try emptyBodyP
+  (headerName, headerKvs', body) <$ P.space
+
+-- | Parse a section header nam, header key-values, and body key-values using a provided
+-- conversion function.
+headerWrapper
+  :: T.Text
+  -> (M.HashMap T.Text GodotValue -> M.HashMap T.Text GodotValue -> GodotSection)
+  -> Parser GodotSection
+headerWrapper targetSect p = do
+  (headerName, headerKvs', bodyKvs) <- bodyAndKvs
+  unless (headerName == targetSect) (fail "mismatched expected header")
+  pure $ p headerKvs' bodyKvs
+
+-- | Parse a `[sub_resource]` section.
+tscnSubResourceP :: Parser GodotSection
+tscnSubResourceP =
+  headerWrapper "sub_resource"
+  (\kvs bodyKvs -> SubResourceSection (unGodotString' "type" kvs) (unGodotInt' "id" kvs)
+   (collectRest ["type", "id"] kvs) bodyKvs)
+
+-- | Parse an `[ext_resource]` section.
+tscnExtResourceP :: Parser GodotSection
+tscnExtResourceP =
+  headerWrapper "ext_resource"
+  (\kvs bodyKvs -> ExtResourceSection (unGodotString' "path" kvs)
+   (unGodotString' "type" kvs) (unGodotInt' "id" kvs)
+   (collectRest ["path", "type", "id"] kvs) bodyKvs)
+
+-- | Parse a `[node]` section.
+tscnNodeP :: Parser GodotSection
+tscnNodeP =
+  headerWrapper "node"
+  (\kvs bodyKvs -> NodeSection (unGodotString "type" kvs) (unGodotString' "name" kvs)
+   (unGodotString "parent" kvs)
+   ((\(GodotInt i) -> i) . head . snd <$> unGodotConstructor "instance" kvs)
+   (unGodotString "instance_placeholder" kvs) (unGodotString "owner" kvs)
+   (unGodotInt "index" kvs) (map (\(GodotString i) -> i) <$> unGodotArr "groups" kvs)
+   (collectRest
+    [ "path"
+    , "type"
+    , "parent"
+    , "name"
+    , "instance"
+    , "instance_placeholder"
+    , "owner"
+    , "index"
+    , "groups"] kvs) bodyKvs)
+
+-- | Parse a `[connection]` section.
+tscnConnectionP :: Parser GodotSection
+tscnConnectionP =
+  headerWrapper "connection"
+  (\kvs bodyKvs -> ConnectionSection (unGodotString' "signal" kvs)
+   (unGodotString' "from" kvs) (unGodotString' "to" kvs) (unGodotString' "method" kvs)
+   (collectRest ["signal", "from", "to", "method"] kvs) bodyKvs)
+
+-- | Parse a `tscn` file.
+tscnParser :: Parser TscnParsed
+tscnParser = do
+  kvs <- P.string "[gd_scene " *> headerKvs <* P.char ']' <* P.space
+  let loadSteps = unGodotInt' "load_steps" kvs
+      format    = unGodotInt' "format" kvs
+      sectionP  =
+        P.choice
+        (map P.try [tscnConnectionP, tscnExtResourceP, tscnSubResourceP, tscnNodeP])
+  sections <- P.manyTill sectionP P.eof
+  pure $ TscnParsed (TscnDescriptor loadSteps format) sections
+
+-- | Parse a `[resource]` section.
+resourceParser :: Parser GodotSection
+resourceParser = do
+  (headerName, headerKvs, bodyKvs) <- bodyAndKvs
+  pure
+    $ ResourceSection (unGodotString "resource_name" bodyKvs)
+    (unGodotString "class_name" bodyKvs) (unGodotConstructor "library" bodyKvs)
+
+-- | Parse a `gdns` file.
+gdnsParser :: Parser GdnsParsed
+gdnsParser = do
+  kvs <- P.string "[gd_resource " *> headerKvs <* P.char ']' <* P.space
+  let ty        = unGodotString' "type" kvs
+      loadSteps = unGodotInt' "load_steps" kvs
+      format    = unGodotInt' "format" kvs
+      sectionP  = P.choice (map P.try [tscnExtResourceP, resourceParser])
+  sections <- P.manyTill sectionP P.eof
+  pure $ GdnsParsed (GdnsDescriptor ty loadSteps format) sections
+
+otherParser :: Parser GodotSection
+otherParser = do
+  (headerName, headerKvs, bodyKvs) <- bodyAndKvs
+  pure $ OtherSection headerName headerKvs bodyKvs
+
+-- | Parse a resource file (currently only `tscn` and `gdns`).
+godotParser :: Parser GodotParsed
+godotParser = P.try (Tscn <$> tscnParser) <|> P.try (Gdns <$> gdnsParser)
