~ruther/NosSmooth

18df52d4e7cd27e582c8203259f9d9247bef2966 — František Boháček 3 years ago 97512a4
feat(data): add .nos files parsing support
A Data/NosSmooth.Data.Abstractions/NostaleData.cs => Data/NosSmooth.Data.Abstractions/NostaleData.cs +19 -0
@@ 0,0 1,19 @@
//
//  NostaleData.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.Abstractions;

public record NostaleData
(
    IReadOnlyDictionary<Language.Language, IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>> Translations,
    IReadOnlyDictionary<int, IItemInfo> Items,
    IReadOnlyDictionary<int, IMonsterInfo> Monsters,
    IReadOnlyDictionary<int, ISkillInfo> Skills,
    IReadOnlyDictionary<int, IMapInfo> Maps
);
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/InfoService.cs => Data/NosSmooth.Data.NOSFiles/InfoService.cs +72 -0
@@ 0,0 1,72 @@
//
//  InfoService.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Parsers;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles;

/// <inheritdoc />
internal class InfoService : IInfoService
{
    private readonly NostaleData _nostaleData;

    /// <summary>
    /// Initializes a new instance of the <see cref="InfoService"/> class.
    /// </summary>
    /// <param name="nostaleData">The parsed data.</param>
    public InfoService(NostaleData nostaleData)
    {
        _nostaleData = nostaleData;
    }

    /// <inheritdoc />
    public Result<IItemInfo> GetItemInfo(int vnum)
    {
        if (!_nostaleData.Items.ContainsKey(vnum))
        {
            return new NotFoundError($"Couldn't find item {vnum}");
        }

        return Result<IItemInfo>.FromSuccess(_nostaleData.Items[vnum]);
    }

    /// <inheritdoc />
    public Result<IMapInfo> GetMapInfo(int id)
    {
        if (!_nostaleData.Maps.ContainsKey(id))
        {
            return new NotFoundError($"Couldn't find item {id}");
        }

        return Result<IMapInfo>.FromSuccess(_nostaleData.Maps[id]);
    }

    /// <inheritdoc />
    public Result<IMonsterInfo> GetMonsterInfo(int vnum)
    {
        if (!_nostaleData.Monsters.ContainsKey(vnum))
        {
            return new NotFoundError($"Couldn't find item {vnum}");
        }

        return Result<IMonsterInfo>.FromSuccess(_nostaleData.Monsters[vnum]);
    }

    /// <inheritdoc />
    public Result<ISkillInfo> GetSkillInfo(int vnum)
    {
        if (!_nostaleData.Skills.ContainsKey(vnum))
        {
            return new NotFoundError($"Couldn't find item {vnum}");
        }

        return Result<ISkillInfo>.FromSuccess(_nostaleData.Skills[vnum]);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Infos/MapInfo.cs => Data/NosSmooth.Data.NOSFiles/Infos/MapInfo.cs +58 -0
@@ 0,0 1,58 @@
//
//  MapInfo.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.NOSFiles.Infos;

/// <inheritdoc />
internal class MapInfo : IMapInfo
{
    private readonly byte[] _data;

    /// <summary>
    /// Initializes a new instance of the <see cref="MapInfo"/> class.
    /// </summary>
    /// <param name="id">The VNum.</param>
    /// <param name="name">The name of the map.</param>
    /// <param name="width">The width.</param>
    /// <param name="height">The height.</param>
    /// <param name="grid">The grid data.</param>
    public MapInfo(int id, TranslatableString name, short width, short height, byte[] grid)
    {
        Id = id;
        Name = name;
        Width = width;
        Height = height;
        _data = grid;
    }

    /// <inheritdoc />
    public int Id { get; }

    /// <inheritdoc />
    public TranslatableString Name { get; }

    /// <inheritdoc />
    public short Width { get; }

    /// <inheritdoc />
    public short Height { get; }

    /// <inheritdoc />
    public byte GetData(short x, short y)
    {
        return _data[(y * Width) + x];
    }

    /// <inheritdoc />
    public bool IsWalkable(short x, short y)
    {
        var val = GetData(x, y);
        return val == 0 || val == 2 || (val >= 16 && val <= 19);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/LanguageService.cs => Data/NosSmooth.Data.NOSFiles/LanguageService.cs +73 -0
@@ 0,0 1,73 @@
//
//  LanguageService.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Security.Cryptography;
using NosSmooth.Data.Abstractions.Language;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles;

/// <inheritdoc />
internal class LanguageService : ILanguageService
{
    private readonly IReadOnlyDictionary<Language, IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>> _translations;
    private readonly LanguageServiceOptions _options;

    /// <summary>
    /// Initializes a new instance of the <see cref="LanguageService"/> class.
    /// </summary>
    /// <param name="translations">The translations.</param>
    /// <param name="options">The options.</param>
    public LanguageService(IReadOnlyDictionary<Language, IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>> translations, LanguageServiceOptions options)
    {
        CurrentLanguage = options.Language;
        _translations = translations;
        _options = options;
    }

    /// <inheritdoc/>
    public Language CurrentLanguage { get; set; }

    /// <inheritdoc/>
    public Result<string> GetTranslation(TranslationRoot root, string key, Language? language = default)
    {
        if (!_translations.ContainsKey(language ?? CurrentLanguage))
        {
            return new NotFoundError($"The requested language {language ?? CurrentLanguage} is not parsed.");
        }

        var translations = _translations[language ?? CurrentLanguage];
        if (!translations.ContainsKey(root))
        {
            return key;
        }

        var keyTranslations = translations[root];
        if (!keyTranslations.ContainsKey(key))
        {
            return key;
        }

        return keyTranslations[key];
    }

    /// <inheritdoc/>
    public Result<string> GetTranslation(TranslatableString translatableString, Language? language = default)
    {
        var translation = GetTranslation(translatableString.Root, translatableString.Key, language);
        if (!translation.IsSuccess)
        {
            return translation;
        }

        if (_options.FillTranslatableStrings)
        {
            translatableString.Fill(translation.Entity);
        }

        return translation;
    }
}
\ No newline at end of file

M Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj => Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj +5 -0
@@ 8,8 8,13 @@

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
      <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
      <PackageReference Include="Remora.Results" Version="7.1.0" />
      <PackageReference Include="SharpZipLib" Version="1.3.3" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\NosSmooth.Data.Abstractions\NosSmooth.Data.Abstractions.csproj" />
    </ItemGroup>

</Project>

A Data/NosSmooth.Data.NOSFiles/NostaleDataFilesManager.cs => Data/NosSmooth.Data.NOSFiles/NostaleDataFilesManager.cs +94 -0
@@ 0,0 1,94 @@
//
//  NostaleDataFilesManager.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Language;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles;

/// <summary>
/// Nostale .NOS files manager.
/// </summary>
public class NostaleDataFilesManager
{
    private readonly NostaleDataParser _parser;
    private ILanguageService? _languageService;
    private IInfoService? _infoService;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleDataFilesManager"/> class.
    /// </summary>
    /// <param name="parser">The parser.</param>
    public NostaleDataFilesManager(NostaleDataParser parser)
    {
        _parser = parser;
    }

    /// <summary>
    /// Gets the language service.
    /// </summary>
    public ILanguageService LanguageService
    {
        get
        {
            if (_languageService is null)
            {
                throw new InvalidOperationException
                    ("The language service is null, did you forget to call NostaleDataManager.Initialize?");
            }

            return _languageService;
        }
    }

    /// <summary>
    /// Gets the info service.
    /// </summary>
    public IInfoService InfoService
    {
        get
        {
            if (_infoService is null)
            {
                throw new InvalidOperationException
                    ("The info service is null, did you forget to call NostaleDataManager.Initialize?");
            }

            return _infoService;
        }
    }

    /// <summary>
    /// Initialize the info and language services.
    /// </summary>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Initialize()
    {
        if (_languageService is null)
        {
            var languageServiceResult = _parser.CreateLanguageService();
            if (!languageServiceResult.IsSuccess)
            {
                return Result.FromError(languageServiceResult);
            }
            _languageService = languageServiceResult.Entity;
        }

        if (_infoService is null)
        {
            var infoServiceResult = _parser.CreateInfoService();
            if (!infoServiceResult.IsSuccess)
            {
                return Result.FromError(infoServiceResult);
            }
            _infoService = infoServiceResult.Entity;
        }

        return Result.FromSuccess();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/NostaleDataParser.cs => Data/NosSmooth.Data.NOSFiles/NostaleDataParser.cs +196 -0
@@ 0,0 1,196 @@
//
//  NostaleDataParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.Options;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Files;
using NosSmooth.Data.NOSFiles.Options;
using NosSmooth.Data.NOSFiles.Parsers;
using NosSmooth.Data.NOSFiles.Readers;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles;

/// <summary>
/// Parser of NosTale .NOS files.
/// </summary>
public class NostaleDataParser
{
    private readonly FileReader _fileReader;
    private readonly NostaleDataOptions _options;
    private readonly LanguageServiceOptions _languageOptions;
    private NostaleData? _parsed;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleDataParser"/> class.
    /// </summary>
    /// <param name="fileReader">The file reader.</param>
    /// <param name="options">The options.</param>
    /// <param name="languageOptions">The language options.</param>
    public NostaleDataParser
    (
        FileReader fileReader,
        IOptions<NostaleDataOptions> options,
        IOptions<LanguageServiceOptions> languageOptions
    )
    {
        _fileReader = fileReader;
        _options = options.Value;
        _languageOptions = languageOptions.Value;
    }

    /// <summary>
    /// Extract NosTale files from archives.
    /// </summary>
    /// <param name="path">The path to the nostale data files.</param>
    /// <param name="languages">The languages to include.</param>
    /// <returns>The nostale files.</returns>
    public Result<NostaleFiles> GetFiles(string? path = null, params Language[] languages)
    {
        string datFilesPath = Path.Combine(path ?? _options.NostaleDataPath, _options.InfosFileName);
        string mapGridsFilesPath = Path.Combine(path ?? _options.NostaleDataPath, _options.MapGridsFileName);
        string languageFilesPath = Path.Combine(path ?? _options.NostaleDataPath, _options.LanguageFileName);

        var datFile = _fileReader.ReadFileSystemFile<FileArchive>(datFilesPath);
        if (!datFile.IsSuccess)
        {
            return Result<NostaleFiles>.FromError(datFile);
        }

        var mapGridsFile = _fileReader.ReadFileSystemFile<FileArchive>(mapGridsFilesPath);
        if (!mapGridsFile.IsSuccess)
        {
            return Result<NostaleFiles>.FromError(mapGridsFile);
        }

        var languageFiles = new Dictionary<Language, FileArchive>();
        foreach (var language in languages.Concat(_options.SupportedLanguages))
        {
            var langString = language.ToString().ToLower();
            var langPath = languageFilesPath.Replace("%lang%", langString);
            var languageFile = _fileReader.ReadFileSystemFile<FileArchive>(langPath);

            if (!languageFile.IsSuccess)
            {
                return Result<NostaleFiles>.FromError(languageFile);
            }

            languageFiles.Add(language, languageFile.Entity.Content);
        }

        return new NostaleFiles(languageFiles, datFile.Entity.Content, mapGridsFile.Entity.Content);
    }

    /// <summary>
    /// Parse the nostale files.
    /// </summary>
    /// <param name="path">The path to the files.</param>
    /// <param name="languages">The languages to parse.</param>
    /// <returns>Parsed data or an error.</returns>
    public Result<NostaleData> ParseFiles(string? path = null, params Language[] languages)
    {
        try
        {
            if (_parsed is not null)
            {
                return _parsed;
            }

            var filesResult = GetFiles(path, languages);
            if (!filesResult.IsSuccess)
            {
                return Result<NostaleData>.FromError(filesResult);
            }
            var files = filesResult.Entity;

            var skillParser = new SkillParser();
            var skillsResult = skillParser.Parse(files);
            if (!skillsResult.IsSuccess)
            {
                return Result<NostaleData>.FromError(skillsResult);
            }

            var mapParser = new MapParser();
            var mapsResult = mapParser.Parse(files);
            if (!mapsResult.IsSuccess)
            {
                return Result<NostaleData>.FromError(mapsResult);
            }

            var itemParser = new ItemParser();
            var itemsResult = itemParser.Parse(files);
            if (!itemsResult.IsSuccess)
            {
                return Result<NostaleData>.FromError(itemsResult);
            }

            var monsterParser = new MonsterParser();
            var monstersResult = monsterParser.Parse(files);
            if (!monstersResult.IsSuccess)
            {
                return Result<NostaleData>.FromError(monstersResult);
            }

            var langParser = new LangParser();
            var translations
                = new Dictionary<Language, IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>>();
            foreach (var language in files.LanguageFiles.Keys)
            {
                var languageParseResult = langParser.Parse(files, language);
                if (!languageParseResult.IsSuccess)
                {
                    return Result<NostaleData>.FromError(languageParseResult);
                }

                translations.Add(language, languageParseResult.Entity);
            }

            return _parsed = new NostaleData
            (
                translations,
                itemsResult.Entity,
                monstersResult.Entity,
                skillsResult.Entity,
                mapsResult.Entity
            );
        }
        catch (Exception e)
        {
            return e;
        }
    }

    /// <summary>
    /// Create a language service from parsed files.
    /// </summary>
    /// <returns>A language service or an error.</returns>
    public Result<ILanguageService> CreateLanguageService()
    {
        var parsed = ParseFiles();
        if (!parsed.IsSuccess)
        {
            return Result<ILanguageService>.FromError(parsed);
        }

        return new LanguageService(parsed.Entity.Translations, _languageOptions);
    }

    /// <summary>
    /// Create an info service from parsed files.
    /// </summary>
    /// <returns>An info service or an error.</returns>
    public Result<IInfoService> CreateInfoService()
    {
        var parsed = ParseFiles();
        if (!parsed.IsSuccess)
        {
            return Result<IInfoService>.FromError(parsed);
        }

        return new InfoService(parsed.Entity);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/NostaleFiles.cs => Data/NosSmooth.Data.NOSFiles/NostaleFiles.cs +44 -0
@@ 0,0 1,44 @@
//
//  NostaleFiles.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Files;

namespace NosSmooth.Data.NOSFiles;

/// <summary>
/// Contains some of the NosTale NOS file archives.
/// </summary>
public class NostaleFiles
{
    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleFiles"/> class.
    /// </summary>
    /// <param name="languageFiles">The language files.</param>
    /// <param name="datFiles">The dat files.</param>
    /// <param name="mapGridsFiles">The map grids files.</param>
    public NostaleFiles(IReadOnlyDictionary<Language, FileArchive> languageFiles, FileArchive datFiles, FileArchive mapGridsFiles)
    {
        LanguageFiles = languageFiles;
        DatFiles = datFiles;
        MapGridsFiles = mapGridsFiles;
    }

    /// <summary>
    /// Gets the file archives containing language text files.
    /// </summary>
    public IReadOnlyDictionary<Language, FileArchive> LanguageFiles { get; }

    /// <summary>
    /// Gets the dat files archive.
    /// </summary>
    public FileArchive DatFiles { get; }

    /// <summary>
    /// Gets the map grids files archive.
    /// </summary>
    public FileArchive MapGridsFiles { get; }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Options/NostaleDataOptions.cs => Data/NosSmooth.Data.NOSFiles/Options/NostaleDataOptions.cs +43 -0
@@ 0,0 1,43 @@
//
//  NostaleDataOptions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.NOSFiles.Options;

/// <summary>
/// Options for loading nostale files.
/// </summary>
public class NostaleDataOptions
{
    /// <summary>
    /// Gets or sets the supported languages that will be loaded into the memory..
    /// </summary>
    public Language[] SupportedLanguages { get; set; } = new Language[] { Language.En };

    /// <summary>
    /// Gets or sets the path to .nos files.
    /// </summary>
    public string NostaleDataPath { get; set; } = "NostaleData";

    /// <summary>
    /// Gets or sets the name of the file with map grid data.
    /// </summary>
    public string MapGridsFileName { get; set; } = "NStcData.NOS";

    /// <summary>
    /// Gets or sets the name of the file with infos data.
    /// </summary>
    public string InfosFileName { get; set; } = "NSgtdData.NOS";

    /// <summary>
    /// Gets or sets the name of the file with language data.
    /// </summary>
    /// <remarks>
    /// Should contain %lang% string to be replaced with the language abbrev.
    /// </remarks>
    public string LanguageFileName { get; set; } = "NSlangData_%lang%.NOS";
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatEntry.cs => Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatEntry.cs +54 -0
@@ 0,0 1,54 @@
//
//  DatEntry.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Data.NOSFiles.Parsers.Dat;

/// <summary>
/// An entry for a <see cref="DatItem"/>.
/// </summary>
public struct DatEntry
{
    private readonly IReadOnlyList<string> _data;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatEntry"/> struct.
    /// </summary>
    /// <param name="key">The key of the entry.</param>
    /// <param name="data">The data of the entry.</param>
    public DatEntry(string key, IReadOnlyList<string> data)
    {
        Key = key;
        _data = data;
    }

    /// <summary>
    /// Gets the key of the entry.
    /// </summary>
    public string Key { get; }

    /// <summary>
    /// Read a value on the given index.
    /// </summary>
    /// <param name="index">The index to read at.</param>
    /// <typeparam name="T">The type.</typeparam>
    /// <returns>Read value.</returns>
    public T Read<T>(int index)
    {
        return (T)Convert.ChangeType(_data[index], typeof(T));
    }

    /// <summary>
    /// Get the values of the current entry.
    /// </summary>
    /// <remarks>
    /// Skips the header.
    /// </remarks>
    /// <returns>An array with the values.</returns>
    public string[] GetValues()
    {
        return _data.Skip(1).ToArray();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatItem.cs => Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatItem.cs +59 -0
@@ 0,0 1,59 @@
//
//  DatItem.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Data.NOSFiles.Parsers.Dat;

/// <summary>
/// An item from a dat file obtained using <see cref="DatReader"/>.
/// </summary>
public struct DatItem
{
    private readonly IReadOnlyDictionary<string, IReadOnlyList<DatEntry>> _entries;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatItem"/> struct.
    /// </summary>
    /// <param name="entries">The entries of the item.</param>
    public DatItem(IReadOnlyDictionary<string, IReadOnlyList<DatEntry>> entries)
    {
        _entries = entries;
    }

    /// <summary>
    /// Gets the entry with the given name.
    /// </summary>
    /// <param name="name">The name of the entry.</param>
    /// <returns>An entry, or null, if not found.</returns>
    public DatEntry? GetNullableEntry(string name)
    {
        return GetEntries(name)?.FirstOrDefault() ?? null;
    }

    /// <summary>
    /// Gets the entry with the given name.
    /// </summary>
    /// <param name="name">The name of the entry.</param>
    /// <returns>An entry, or null, if not found.</returns>
    public DatEntry GetEntry(string name)
    {
        return GetEntries(name).First();
    }

    /// <summary>
    /// Gets the entry with the given name.
    /// </summary>
    /// <param name="name">The name of the entry.</param>
    /// <returns>An entry, or null, if not found.</returns>
    public IReadOnlyList<DatEntry> GetEntries(string name)
    {
        if (!_entries.ContainsKey(name))
        {
            return Array.Empty<DatEntry>();
        }

        return _entries[name];
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatReader.cs => Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatReader.cs +94 -0
@@ 0,0 1,94 @@
//
//  DatReader.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;
using System.Text;
using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers.Dat;

/// <summary>
/// Reader of .dat files.
/// </summary>
public class DatReader
{
    private readonly RawFile _file;
    private IReadOnlyList<string> _lines;
    private int _currentLine;
    private string _separator;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatReader"/> class.
    /// </summary>
    /// <param name="file">The file to read.</param>
    public DatReader(RawFile file)
    {
        _lines = Encoding.ASCII.GetString(file.Content).Split('\n').ToArray();
        _currentLine = 0;
        _file = file;
        _separator = "VNUM";
    }

    /// <summary>
    /// Gets whether the reader has reached the end.
    /// </summary>
    public bool ReachedEnd => _currentLine + 1 >= _lines.Count;

    /// <summary>
    /// Sets the separator of a new item.
    /// </summary>
    /// <param name="separator">The separator of new item.</param>
    public void SetSeparatorField(string separator)
    {
        _separator = separator;
    }

    /// <summary>
    /// Read next item.
    /// </summary>
    /// <param name="item">The read item.</param>
    /// <returns>Whether an item was read.</returns>
    public bool ReadItem([NotNullWhen(true)] out DatItem? item)
    {
        if (ReachedEnd)
        {
            item = null;
            return false;
        }

        bool readFirstItem = _currentLine > 0;
        int startLine = _currentLine;

        while (!ReachedEnd && !_lines[_currentLine].StartsWith(_separator))
        {
            _currentLine++;
        }

        if (!readFirstItem)
        {
            return ReadItem(out item);
        }

        var dictionary = new Dictionary<string, IReadOnlyList<DatEntry>>();
        for (int i = startLine; i < _currentLine; i++)
        {
            var line = _lines[i];
            var splitted = line.Split('\t');
            var key = splitted[0];
            var entry = new DatEntry(key, splitted);
            if (!dictionary.ContainsKey(key))
            {
                dictionary.Add(key, new List<DatEntry>());
            }

            ((List<DatEntry>)dictionary[key]).Add(entry);
        }

        item = new DatItem(dictionary);
        return true;
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/IInfoParser.cs => Data/NosSmooth.Data.NOSFiles/Parsers/IInfoParser.cs +24 -0
@@ 0,0 1,24 @@
//
//  IInfoParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers;

/// <summary>
/// A parser of info.
/// </summary>
/// <typeparam name="TType">The type of the info to parse.</typeparam>
public interface IInfoParser<TType>
{
    /// <summary>
    /// Parse the data from the given file.
    /// </summary>
    /// <param name="files">The NosTale files.</param>
    /// <returns>The list of the parsed entries.</returns>
    public Result<Dictionary<int, TType>> Parse(NostaleFiles files);
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/ItemParser.cs => Data/NosSmooth.Data.NOSFiles/Parsers/ItemParser.cs +64 -0
@@ 0,0 1,64 @@
//
//  ItemParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Parsers.Dat;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers;

/// <inheritdoc />
public class ItemParser : IInfoParser<IItemInfo>
{
    /// <inheritdoc />
    public Result<Dictionary<int, IItemInfo>> Parse(NostaleFiles files)
    {
        var itemDatResult = files.DatFiles.FindFile("Item.dat");
        if (!itemDatResult.IsSuccess)
        {
            return Result<Dictionary<int, IItemInfo>>.FromError(itemDatResult);
        }
        var reader = new DatReader(itemDatResult.Entity);
        var result = new Dictionary<int, IItemInfo>();

        while (reader.ReadItem(out var itemNullable))
        {
            var item = itemNullable.Value;
            var indexEntry = item.GetEntry("INDEX");

            var vnum = item.GetEntry("VNUM").Read<int>(1);
            var nameKey = item.GetEntry("NAME").Read<string>(1);
            result.Add
            (
                vnum,
                new ItemInfo
                (
                    vnum,
                    new TranslatableString(TranslationRoot.Item, nameKey),
                    indexEntry.Read<int>(2),
                    indexEntry.Read<int>(3),
                    (BagType)indexEntry.Read<int>(1),
                    item.GetEntry("DATA").GetValues()
                )
            );
        }

        return result;
    }

    private record ItemInfo
        (
            int VNum,
            TranslatableString Name,
            int Type,
            int SubType,
            BagType BagType,
            string[] Data
        )
        : IItemInfo;
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/LangParser.cs => Data/NosSmooth.Data.NOSFiles/Parsers/LangParser.cs +96 -0
@@ 0,0 1,96 @@
//
//  LangParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers;

/// <summary>
/// Language txt file parser.
/// </summary>
public class LangParser
{
    /// <summary>
    /// Parse the given language.
    /// </summary>
    /// <param name="files">The NosTale files.</param>
    /// <param name="language">The language to parse.</param>
    /// <returns>Translations or an error.</returns>
    public Result<IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>> Parse(NostaleFiles files, Language language)
    {
        if (!files.LanguageFiles.ContainsKey(language))
        {
            return new NotFoundError($"Could not find the language file for {language}.");
        }

        var archive = files.LanguageFiles[language];
        var encoding = LanguageEncoding.GetEncoding(language);
        var dictionary = new Dictionary<TranslationRoot, IReadOnlyDictionary<string, string>>();

        var itemParsedResult = ParseFile(archive, encoding, $"code_{language.ToString().ToLower()}_Item.txt");
        if (!itemParsedResult.IsSuccess)
        {
            return Result<IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>>.FromError
                (itemParsedResult);
        }
        dictionary.Add(TranslationRoot.Item, itemParsedResult.Entity);

        var monsterParsedResult = ParseFile(archive, encoding, $"code_{language.ToString().ToLower()}_monster.txt");
        if (!monsterParsedResult.IsSuccess)
        {
            return Result<IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>>.FromError
                (monsterParsedResult);
        }
        dictionary.Add(TranslationRoot.Monster, itemParsedResult.Entity);

        var skillParsedResult = ParseFile(archive, encoding, $"code_{language.ToString().ToLower()}_Skill.txt");
        if (!skillParsedResult.IsSuccess)
        {
            return Result<IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>>.FromError
                (skillParsedResult);
        }
        dictionary.Add(TranslationRoot.Skill, itemParsedResult.Entity);

        var mapParsedResult = ParseFile(archive, encoding, $"code_{language.ToString().ToLower()}_MapIDData.txt");
        if (!mapParsedResult.IsSuccess)
        {
            return Result<IReadOnlyDictionary<TranslationRoot, IReadOnlyDictionary<string, string>>>.FromError
                (mapParsedResult);
        }
        dictionary.Add(TranslationRoot.Map, itemParsedResult.Entity);

        return dictionary;
    }

    private Result<IReadOnlyDictionary<string, string>> ParseFile(FileArchive files, Encoding encoding, string name)
    {
        var fileResult = files.FindFile(name);
        if (!fileResult.IsSuccess)
        {
            return Result<IReadOnlyDictionary<string, string>>.FromError(fileResult);
        }
        var fileContent = encoding.GetString(fileResult.Entity.Content);

        var dictionary = new Dictionary<string, string>();
        var lines = fileContent.Split('\n');
        foreach (var line in lines)
        {
            var splitted = line.Split('\t');
            if (splitted.Length != 2)
            {
                continue;
            }

            dictionary.Add(splitted[0], splitted[1]);
        }

        return dictionary;
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/MapParser.cs => Data/NosSmooth.Data.NOSFiles/Parsers/MapParser.cs +68 -0
@@ 0,0 1,68 @@
//
//  MapParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Infos;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers;

/// <inheritdoc />
public class MapParser : IInfoParser<IMapInfo>
{
    /// <inheritdoc />
    public Result<Dictionary<int, IMapInfo>> Parse(NostaleFiles files)
    {
        var mapDatResult = files.DatFiles.FindFile("MapIDData.dat");
        if (!mapDatResult.IsSuccess)
        {
            return Result<Dictionary<int, IMapInfo>>.FromError(mapDatResult);
        }
        var mapGridsArchive = files.MapGridsFiles;
        var mapDatContent = Encoding.ASCII.GetString(mapDatResult.Entity.Content).Split('\n');

        var mapNames = new Dictionary<int, TranslatableString>();
        foreach (var line in mapDatContent)
        {
            var splitted = line.Split('\t');
            if (splitted.Length != 5)
            {
                continue;
            }

            var first = int.Parse(splitted[0]);
            var second = int.Parse(splitted[1]);
            if (first == second)
            {
                mapNames[first] = new TranslatableString(TranslationRoot.Map, splitted.Last());
            }

            for (int i = first; i < second; i++)
            {
                mapNames[i] = new TranslatableString(TranslationRoot.Map, splitted.Last());
            }
        }

        var result = new Dictionary<int, IMapInfo>();
        foreach (var file in mapGridsArchive.Files)
        {
            var id = int.Parse(file.Path);
            var grid = file.Content;
            result[id] = new MapInfo
            (
                id,
                mapNames[id],
                BitConverter.ToInt16(grid.Take(2).ToArray()),
                BitConverter.ToInt16(grid.Skip(2).Take(2).ToArray()),
                grid.Skip(4).ToArray()
            );
        }

        return result;
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/MonsterParser.cs => Data/NosSmooth.Data.NOSFiles/Parsers/MonsterParser.cs +51 -0
@@ 0,0 1,51 @@
//
//  MonsterParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Parsers.Dat;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers;

/// <inheritdoc />
public class MonsterParser : IInfoParser<IMonsterInfo>
{
    /// <inheritdoc />
    public Result<Dictionary<int, IMonsterInfo>> Parse(NostaleFiles files)
    {
        var monsterDatResult = files.DatFiles.FindFile("Monster.dat");
        if (!monsterDatResult.IsSuccess)
        {
            return Result<Dictionary<int, IMonsterInfo>>.FromError(monsterDatResult);
        }
        var reader = new DatReader(monsterDatResult.Entity);
        var result = new Dictionary<int, IMonsterInfo>();

        while (reader.ReadItem(out var itemNullable))
        {
            var item = itemNullable.Value;

            var vnum = item.GetEntry("VNUM").Read<int>(1);
            var nameKey = item.GetEntry("NAME").Read<string>(1);
            result.Add
            (
                vnum,
                new MonsterInfo
                (
                    vnum,
                    new TranslatableString(TranslationRoot.Monster, nameKey),
                    item.GetEntry("LEVEL").Read<int>(1)
                )
            );
        }

        return result;
    }

    private record MonsterInfo(int VNum, TranslatableString Name, int Level) : IMonsterInfo;
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Parsers/SkillParser.cs => Data/NosSmooth.Data.NOSFiles/Parsers/SkillParser.cs +77 -0
@@ 0,0 1,77 @@
//
//  SkillParser.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Parsers.Dat;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Parsers;

/// <summary>
/// Parses Skill.dat.
/// </summary>
public class SkillParser : IInfoParser<ISkillInfo>
{
    /// <inheritdoc />
    public Result<Dictionary<int, ISkillInfo>> Parse(NostaleFiles files)
    {
        var skillDatResult = files.DatFiles.FindFile("Skill.dat");
        if (!skillDatResult.IsSuccess)
        {
            return Result<Dictionary<int, ISkillInfo>>.FromError(skillDatResult);
        }
        var reader = new DatReader(skillDatResult.Entity);
        var result = new Dictionary<int, ISkillInfo>();

        while (reader.ReadItem(out var itemNullable))
        {
            var item = itemNullable.Value;
            var typeEntry = item.GetEntry("TYPE");
            var targetEntry = item.GetEntry("TARGET");
            var dataEntry = item.GetEntry("DATA");

            var vnum = item.GetEntry("VNUM").Read<int>(1);
            var nameKey = item.GetEntry("NAME").Read<string>(1);
            result.Add
            (
                vnum,
                new SkillInfo
                (
                    vnum,
                    new TranslatableString(TranslationRoot.Skill, nameKey),
                    targetEntry.Read<short>(3),
                    targetEntry.Read<short>(4),
                    dataEntry.Read<int>(5),
                    dataEntry.Read<int>(6),
                    (SkillType)typeEntry.Read<int>(1),
                    dataEntry.Read<int>(7),
                    typeEntry.Read<short>(2),
                    (TargetType)targetEntry.Read<int>(1),
                    (HitType)targetEntry.Read<int>(2)
                )
            );
        }

        return result;
    }

    private record SkillInfo
    (
        int VNum,
        TranslatableString Name,
        short Range,
        short ZoneRange,
        int CastTime,
        int Cooldown,
        SkillType SkillType,
        int MpCost,
        short CastId,
        TargetType TargetType,
        HitType HitType
    ) : ISkillInfo;
}
\ No newline at end of file

Do not follow this link