diff --git a/src/Syroot.Worms.Test/Program.cs b/src/Syroot.Worms.Test/Program.cs index c4a4809..264945b 100644 --- a/src/Syroot.Worms.Test/Program.cs +++ b/src/Syroot.Worms.Test/Program.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Collections.Generic; -using Syroot.Worms.Gen2.Armageddon; +using Syroot.Worms.Gen2; namespace Syroot.Worms.Test { @@ -12,30 +12,13 @@ namespace Syroot.Worms.Test { // ---- CONSTANTS ---------------------------------------------------------------------------------------------- - private static readonly string[] _testPaths = { @"C:\Games\Worms Armageddon 3.7.2.1", @"E:\" }; + private static readonly string[] _testPaths = { @"C:\Games\Worms Armageddon 3.7.2.1" }; // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- private static void Main(string[] args) { - //foreach (string dirFile in GetFiles("*.dir")) - //{ - // Console.WriteLine($"Loading {dirFile}..."); - // Archive archive = new Archive(dirFile); - // foreach (string entry in archive.Keys) - // { - // Console.WriteLine($"\t{entry}"); - // } - //} - - //foreach (string imgFile in GetFiles("*.img")) - //{ - // Console.WriteLine("Loading {imgFile}..."); - // Image image = new Image(imgFile); - //} - Scheme scheme = new Scheme(@"D:\Archive\Games\Worms\Worms Armageddon\Common\User\Schemes\{{13}} The Full Wormage.wsc"); - scheme.Save(@"D:\Pictures\Test2.wsc"); - scheme.Load(@"D:\Pictures\Test2.wsc"); + Palette palette = new Palette("C:\\Games\\Worms World Party\\graphics\\palettes\\wwp.pal"); Console.WriteLine("Done."); Console.ReadLine(); diff --git a/src/Syroot.Worms.UnitTest/Common/TestHelpers.cs b/src/Syroot.Worms.UnitTest/Common/TestHelpers.cs new file mode 100644 index 0000000..b3096a5 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Common/TestHelpers.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Syroot.Worms.Core; + +namespace Syroot.Worms.UnitTest.Common +{ + /// + /// Represents a collection of methods helping in executing tests. + /// + internal static class TestHelpers + { + // ---- MEMBERS ------------------------------------------------------------------------------------------------ + + private static readonly string[] _gamePaths = + { + @"C:\Games\Worms2", + @"C:\Games\Worms Armageddon 3.7.2.1", + @"C:\Games\Worms World Party" + }; + + // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- + + /// + /// Loads the files found with the given . Excludes file names specified in the + /// optional array . + /// + /// The type of the files to load. + /// The wildcard to match. + /// Optionally, the files to exclude. + internal static void LoadFiles(string wildcard, string[] excludedFiles = null) where T : ILoadableFile, new() + { + foreach (string fileName in FindFiles(wildcard)) + { + if (excludedFiles?.Contains(Path.GetFileName(fileName), StringComparer.OrdinalIgnoreCase) == true) + { + Debug.WriteLine($"Skipping {fileName}"); + continue; + } + Debug.Write($"Loading {fileName}..."); + T instance = new T(); + instance.Load(fileName); + Debug.WriteLine($" ok"); + } + } + + // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- + + /// + /// Returns a list of files found in any of the paths provided in the _gamePaths variable which match the + /// specified . + /// + /// The wildcard which files have to match. + /// A list of files matching the search. + private static List FindFiles(string wildcard) + { + List files = new List(); + foreach (string testPath in _gamePaths) + { + files.AddRange(Directory.GetFiles(testPath, wildcard, SearchOption.AllDirectories)); + } + return files; + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Gen2/ArchiveTests.cs b/src/Syroot.Worms.UnitTest/Gen2/ArchiveTests.cs new file mode 100644 index 0000000..c98bfb7 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Gen2/ArchiveTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Syroot.Worms.Gen2; +using Syroot.Worms.UnitTest.Common; + +namespace Syroot.Worms.UnitTest.Gen2 +{ + /// + /// Represents a collection of tests for the class. + /// + [TestCategory("Archive")] + [TestClass] + public class ArchiveTests + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads all files found in any game directories. + /// + [TestMethod] + public void LoadArchives() + { + TestHelpers.LoadFiles("*.dir"); + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Gen2/Armageddon/SchemeTests.cs b/src/Syroot.Worms.UnitTest/Gen2/Armageddon/SchemeTests.cs new file mode 100644 index 0000000..b51b0f4 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Gen2/Armageddon/SchemeTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Syroot.Worms.Gen2.Armageddon; +using Syroot.Worms.UnitTest.Common; + +namespace Syroot.Worms.UnitTest.Gen2.Armageddon +{ + /// + /// Represents a collection of tests for the class. + /// + [TestCategory("Scheme")] + [TestClass] + public class SchemeTests + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads all files found in any game directories. + /// + [TestMethod] + public void LoadSchemes() + { + TestHelpers.LoadFiles("*.wsc"); + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Gen2/ImageTests.cs b/src/Syroot.Worms.UnitTest/Gen2/ImageTests.cs new file mode 100644 index 0000000..8e42ad6 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Gen2/ImageTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Syroot.Worms.Gen2; +using Syroot.Worms.UnitTest.Common; + +namespace Syroot.Worms.UnitTest.Gen2 +{ + /// + /// Represents a collection of tests for the class. + /// + [TestCategory("Image")] + [TestClass] + public class ImageTests + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads all files found in any game directories. + /// + [TestMethod] + public void LoadImages() + { + TestHelpers.LoadFiles("*.img"); + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Gen2/PaletteTests.cs b/src/Syroot.Worms.UnitTest/Gen2/PaletteTests.cs new file mode 100644 index 0000000..a727da1 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Gen2/PaletteTests.cs @@ -0,0 +1,29 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Syroot.Worms.Gen2; +using Syroot.Worms.UnitTest.Common; + +namespace Syroot.Worms.UnitTest.Gen2 +{ + /// + /// Represents a collection of tests for the class. + /// + [TestCategory("Palette")] + [TestClass] + public class PaletteTests + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads all files found in any game directories. + /// + [TestMethod] + public void LoadPalettes() + { + TestHelpers.LoadFiles("*.pal", new string[] + { + "wwp.pal", // Contains 4 bytes of trash after the data chunk. + "wwpmaped.pal" // Contains 4 bytes of trash after the data chunk. + }); + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Gen2/Worms2/SchemeOptionsTests.cs b/src/Syroot.Worms.UnitTest/Gen2/Worms2/SchemeOptionsTests.cs new file mode 100644 index 0000000..0ec9db3 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Gen2/Worms2/SchemeOptionsTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Syroot.Worms.Gen2.Worms2; +using Syroot.Worms.UnitTest.Common; + +namespace Syroot.Worms.UnitTest.Gen2.Worms2 +{ + /// + /// Represents a collection of tests for the class. + /// + [TestCategory("SchemeOptions")] + [TestClass] + public class SchemeOptionsTests + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads all files found in any game directories. + /// + [TestMethod] + public void LoadSchemeOptions() + { + TestHelpers.LoadFiles("*.opt"); + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Gen2/Worms2/SchemeWeaponsTests.cs b/src/Syroot.Worms.UnitTest/Gen2/Worms2/SchemeWeaponsTests.cs new file mode 100644 index 0000000..704a551 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Gen2/Worms2/SchemeWeaponsTests.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Syroot.Worms.Gen2.Worms2; +using Syroot.Worms.UnitTest.Common; + +namespace Syroot.Worms.UnitTest.Gen2.Worms2 +{ + /// + /// Represents a collection of tests for the class. + /// + [TestCategory("SchemeWeapons")] + [TestClass] + public class SchemeWeaponsTests + { + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads all files found in any game directories. + /// + [TestMethod] + public void LoadSchemeWeapons() + { + TestHelpers.LoadFiles("*.wep"); + } + } +} diff --git a/src/Syroot.Worms.UnitTest/Syroot.Worms.UnitTest.csproj b/src/Syroot.Worms.UnitTest/Syroot.Worms.UnitTest.csproj new file mode 100644 index 0000000..1c31b05 --- /dev/null +++ b/src/Syroot.Worms.UnitTest/Syroot.Worms.UnitTest.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp1.1 + + + + + + + + + + + + + + + + + diff --git a/src/Syroot.Worms.sln b/src/Syroot.Worms.sln index 76d41e2..ccddfa7 100644 --- a/src/Syroot.Worms.sln +++ b/src/Syroot.Worms.sln @@ -5,7 +5,9 @@ VisualStudioVersion = 15.0.26403.7 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms", "Syroot.Worms\Syroot.Worms.csproj", "{DD76B6AA-5A5A-4FCD-95AA-9552977525A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms.Test", "Syroot.Worms.Test\Syroot.Worms.Test.csproj", "{9F7486C2-C30E-4457-B528-F4467486E6D8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms.UnitTest", "Syroot.Worms.UnitTest\Syroot.Worms.UnitTest.csproj", "{493816DB-A1A1-4981-9F5F-8044499937B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Syroot.Worms.Test", "Syroot.Worms.Test\Syroot.Worms.Test.csproj", "{2D796945-A523-4A22-BDEE-702D6BA36F69}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,10 +19,14 @@ Global {DD76B6AA-5A5A-4FCD-95AA-9552977525A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD76B6AA-5A5A-4FCD-95AA-9552977525A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD76B6AA-5A5A-4FCD-95AA-9552977525A1}.Release|Any CPU.Build.0 = Release|Any CPU - {9F7486C2-C30E-4457-B528-F4467486E6D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F7486C2-C30E-4457-B528-F4467486E6D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F7486C2-C30E-4457-B528-F4467486E6D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F7486C2-C30E-4457-B528-F4467486E6D8}.Release|Any CPU.Build.0 = Release|Any CPU + {493816DB-A1A1-4981-9F5F-8044499937B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {493816DB-A1A1-4981-9F5F-8044499937B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {493816DB-A1A1-4981-9F5F-8044499937B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {493816DB-A1A1-4981-9F5F-8044499937B6}.Release|Any CPU.Build.0 = Release|Any CPU + {2D796945-A523-4A22-BDEE-702D6BA36F69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D796945-A523-4A22-BDEE-702D6BA36F69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D796945-A523-4A22-BDEE-702D6BA36F69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D796945-A523-4A22-BDEE-702D6BA36F69}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Syroot.Worms/Gen2/Archive.cs b/src/Syroot.Worms/Gen2/Archive.cs index 43d33e8..c507d62 100644 --- a/src/Syroot.Worms/Gen2/Archive.cs +++ b/src/Syroot.Worms/Gen2/Archive.cs @@ -22,9 +22,16 @@ namespace Syroot.Worms.Gen2 private const int _hashBits = 10; private const int _hashSize = 1 << _hashBits; - + // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + public Archive() + { + } + /// /// Initializes a new instance of the class, loading the data from the given /// . diff --git a/src/Syroot.Worms/Gen2/Armageddon/LandData.cs b/src/Syroot.Worms/Gen2/Armageddon/LandData.cs new file mode 100644 index 0000000..3558d43 --- /dev/null +++ b/src/Syroot.Worms/Gen2/Armageddon/LandData.cs @@ -0,0 +1,140 @@ +using System.IO; +using System.Text; +using Syroot.IO; +using Syroot.Maths; +using Syroot.Worms.Core; + +namespace Syroot.Worms.Gen2.Armageddon +{ + /// + /// Represents scheme options stored in an OPT file which contains game settings. + /// S. https://worms2d.info/Options_file. + /// + public class LandData : ILoadableFile + { + // ---- CONSTANTS ---------------------------------------------------------------------------------------------- + + private const int _signature = 0x1A444E4C; // "LND", 0x1A + + // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + + /// + /// Initializes a new instance of the class, loading the data from the given + /// . + /// + /// The to load the data from. + public LandData(Stream stream) + { + Load(stream); + } + + /// + /// Initializes a new instance of the class, loading the data from the given file. + /// + /// The name of the file to load the data from. + public LandData(string fileName) + { + Load(fileName); + } + + // ---- PROPERTIES --------------------------------------------------------------------------------------------- + + /// + /// Gets or sets the size of the landscape in pixels. + /// + public Vector2 Size { get; set; } + + /// + /// Gets or sets a value indicating whether an indestructible top border will be enabled. + /// + public bool TopBorder { get; set; } + + /// + /// Gets or sets the height of the water in pixels. + /// + public int WaterHeight { get; set; } + + /// + /// Gets or sets an array of coordinates at which objects can be placed. + /// + public Vector2[] ObjectLocations { get; set; } + + /// + /// Gets or sets the visual foreground image. + /// + public Image Foreground { get; set; } + + /// + /// Gets or sets the collision mask of the landscape. + /// + public Image CollisionMask { get; set; } + + /// + /// Gets or sets the visual background image. + /// + public Image Background { get; set; } + + /// + /// Gets or sets the path to the land image file. + /// + public string LandTexturePath { get; set; } + + /// + /// Gets or sets the path to the Water.dir file. + /// + public string WaterDirPath { get; set; } + + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Loads the data from the given . + /// + /// The to load the data from. + public void Load(Stream stream) + { + using (BinaryDataReader reader = new BinaryDataReader(stream, Encoding.ASCII)) + { + // Read the header. + if (reader.ReadInt32() != _signature) + { + throw new InvalidDataException("Invalid LND file signature."); + } + int fileLength = reader.ReadInt32(); + + // Read the data. + Size = reader.ReadStruct(); + TopBorder = reader.ReadBoolean(BinaryBooleanFormat.NonZeroDword); + WaterHeight = reader.ReadInt32(); + + // Read the possible object coordinate array. + ObjectLocations = new Vector2[reader.ReadInt32()]; + int objectLocationCountAgain = reader.ReadInt32(); // TODO: Repetitive or useful? + for (int i = 0; i < ObjectLocations.Length; i++) + { + ObjectLocations[i] = reader.ReadStruct(); + } + + // Read the image data. + Foreground = new Image(stream); + CollisionMask = new Image(stream); + Background = new Image(stream); + + // Read the file paths. + LandTexturePath = reader.ReadString(BinaryStringFormat.ByteLengthPrefix); + WaterDirPath = reader.ReadString(BinaryStringFormat.ByteLengthPrefix); + } + } + + /// + /// Loads the data from the given file. + /// + /// The name of the file to load the data from. + public void Load(string fileName) + { + using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + Load(stream); + } + } + } +} diff --git a/src/Syroot.Worms/Gen2/Armageddon/Scheme.cs b/src/Syroot.Worms/Gen2/Armageddon/Scheme.cs index 6f298ed..f75e688 100644 --- a/src/Syroot.Worms/Gen2/Armageddon/Scheme.cs +++ b/src/Syroot.Worms/Gen2/Armageddon/Scheme.cs @@ -43,6 +43,15 @@ namespace Syroot.Worms.Gen2.Armageddon // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + public Scheme() + { + Version = SchemeVersion.Extended; + Weapons = new SchemeWeaponSetting[GetWeaponCount()]; + } + /// /// Initializes a new instance of the class, loading the data from the given /// . diff --git a/src/Syroot.Worms/Gen2/Armageddon/SchemeEnums.cs b/src/Syroot.Worms/Gen2/Armageddon/SchemeEnums.cs index 77fc147..303c9d2 100644 --- a/src/Syroot.Worms/Gen2/Armageddon/SchemeEnums.cs +++ b/src/Syroot.Worms/Gen2/Armageddon/SchemeEnums.cs @@ -745,7 +745,12 @@ namespace Syroot.Worms.Gen2.Armageddon /// /// The worms health is reduced to 1. /// - HealthDrop = 2 + HealthDrop = 2, + + /// + /// No special event is triggered and the water only rises in the rate specified in the scheme. + /// + WaterRise = 3 } /// diff --git a/src/Syroot.Worms/Gen2/Image.cs b/src/Syroot.Worms/Gen2/Image.cs index 7399fff..13383db 100644 --- a/src/Syroot.Worms/Gen2/Image.cs +++ b/src/Syroot.Worms/Gen2/Image.cs @@ -15,10 +15,17 @@ namespace Syroot.Worms.Gen2 { // ---- CONSTANTS ---------------------------------------------------------------------------------------------- - private const int _signature = 0x1A474D49; // "IMG", 0x1A + private const int _signature = 0x1A474D49; // "IMG", 0x1A // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + public Image() + { + } + /// /// Initializes a new instance of the class, loading the data from the given /// . @@ -56,15 +63,10 @@ namespace Syroot.Worms.Gen2 public Color[] Palette { get; private set; } /// - /// Gets the width of the image in pixels. + /// Gets the size of the image in pixels. /// - public int Width { get; private set; } - - /// - /// Gets the height of the image in pixels. - /// - public int Height { get; private set; } - + public Vector2 Size { get; private set; } + /// /// Gets the data of the image pixels. /// @@ -115,11 +117,10 @@ namespace Syroot.Worms.Gen2 } } - Width = reader.ReadInt16(); - Height = reader.ReadInt16(); + Size = reader.ReadStruct(); // Read the image data, which might be compressed. - byte[] data = new byte[Width * Height * (BitsPerPixel / 8)]; + byte[] data = new byte[Size.X * Size.Y * (BitsPerPixel / 8)]; if (flags.HasFlag(Flags.Compressed)) { Team17Compression.Decompress(reader.BaseStream, ref data); diff --git a/src/Syroot.Worms/Gen2/Palette.cs b/src/Syroot.Worms/Gen2/Palette.cs index abf1c22..74f8114 100644 --- a/src/Syroot.Worms/Gen2/Palette.cs +++ b/src/Syroot.Worms/Gen2/Palette.cs @@ -18,6 +18,14 @@ namespace Syroot.Worms.Gen2 // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + public Palette() + { + Version = _version; + } + /// /// Initializes a new instance of the class, loading the data from the given /// . diff --git a/src/Syroot.Worms/Gen2/Worms2/SchemeOptions.cs b/src/Syroot.Worms/Gen2/Worms2/SchemeOptions.cs index 3ee266d..a259c04 100644 --- a/src/Syroot.Worms/Gen2/Worms2/SchemeOptions.cs +++ b/src/Syroot.Worms/Gen2/Worms2/SchemeOptions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.IO; using System.Text; using Syroot.IO; @@ -19,6 +17,13 @@ namespace Syroot.Worms.Gen2.Worms2 // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + /// + /// Initializs a new instance of the class. + /// + public SchemeOptions() + { + } + /// /// Initializes a new instance of the class, loading the data from the given /// . diff --git a/src/Syroot.Worms/Gen2/Worms2/SchemeWeapons.cs b/src/Syroot.Worms/Gen2/Worms2/SchemeWeapons.cs index aa4fed5..912e15e 100644 --- a/src/Syroot.Worms/Gen2/Worms2/SchemeWeapons.cs +++ b/src/Syroot.Worms/Gen2/Worms2/SchemeWeapons.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using System.Text; using Syroot.IO; using Syroot.Worms.Core; @@ -22,6 +19,14 @@ namespace Syroot.Worms.Gen2.Worms2 // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class. + /// + public SchemeWeapons() + { + Weapons = new SchemeWeaponSetting[_weaponCount]; + } + /// /// Initializes a new instance of the class, loading the data from the given /// . diff --git a/src/Syroot.Worms/Syroot.Worms.csproj b/src/Syroot.Worms/Syroot.Worms.csproj index f2ea078..4a9b84b 100644 --- a/src/Syroot.Worms/Syroot.Worms.csproj +++ b/src/Syroot.Worms/Syroot.Worms.csproj @@ -21,8 +21,8 @@ - - + +