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