~ruther/NosSmooth

fcd0751c2f76ffee45134260ce9d7c0bddf837d2 — František Boháček 3 years ago bb9a527 + 3191e71
Merge pull request #22 from Rutherther/data

Add NosTale data information
66 files changed, 3613 insertions(+), 0 deletions(-)

A Data/NosSmooth.Data.Abstractions/Enums/BagType.cs
A Data/NosSmooth.Data.Abstractions/Enums/HitType.cs
A Data/NosSmooth.Data.Abstractions/Enums/SkillType.cs
A Data/NosSmooth.Data.Abstractions/Enums/TargetType.cs
A Data/NosSmooth.Data.Abstractions/IInfoService.cs
A Data/NosSmooth.Data.Abstractions/Infos/IItemInfo.cs
A Data/NosSmooth.Data.Abstractions/Infos/IMapInfo.cs
A Data/NosSmooth.Data.Abstractions/Infos/IMonsterInfo.cs
A Data/NosSmooth.Data.Abstractions/Infos/ISkillInfo.cs
A Data/NosSmooth.Data.Abstractions/Infos/IVNumInfo.cs
A Data/NosSmooth.Data.Abstractions/Language/ILanguageService.cs
A Data/NosSmooth.Data.Abstractions/Language/Language.cs
A Data/NosSmooth.Data.Abstractions/Language/LanguageEncoding.cs
A Data/NosSmooth.Data.Abstractions/Language/LanguageServiceOptions.cs
A Data/NosSmooth.Data.Abstractions/Language/TranslatableString.cs
A Data/NosSmooth.Data.Abstractions/Language/TranslationRoot.cs
A Data/NosSmooth.Data.Abstractions/NosSmooth.Data.Abstractions.csproj
A Data/NosSmooth.Data.Abstractions/NostaleData.cs
A Data/NosSmooth.Data.CLI/Commands/ExtractNosFileCommand.cs
A Data/NosSmooth.Data.CLI/Commands/MigrateDatabaseCommand.cs
A Data/NosSmooth.Data.CLI/NosSmooth.Data.CLI.csproj
A Data/NosSmooth.Data.CLI/Program.cs
A Data/NosSmooth.Data.Database/Data/ItemInfo.cs
A Data/NosSmooth.Data.Database/Data/MapInfo.cs
A Data/NosSmooth.Data.Database/Data/MonsterInfo.cs
A Data/NosSmooth.Data.Database/Data/SkillInfo.cs
A Data/NosSmooth.Data.Database/Data/Translation.cs
A Data/NosSmooth.Data.Database/DatabaseInfoService.cs
A Data/NosSmooth.Data.Database/DatabaseLanguageService.cs
A Data/NosSmooth.Data.Database/DatabaseMigrator.cs
A Data/NosSmooth.Data.Database/Extensions/ServiceCollectionExtensions.cs
A Data/NosSmooth.Data.Database/NosSmooth.Data.Database.csproj
A Data/NosSmooth.Data.Database/NostaleDataContext.cs
A Data/NosSmooth.Data.Database/NostaleDataContextOptions.cs
A Data/NosSmooth.Data.NOSFiles/Decryptors/DatDecryptor.cs
A Data/NosSmooth.Data.NOSFiles/Decryptors/IDecryptor.cs
A Data/NosSmooth.Data.NOSFiles/Decryptors/LstDecryptor.cs
A Data/NosSmooth.Data.NOSFiles/Errors/UnknownFileTypeError.cs
A Data/NosSmooth.Data.NOSFiles/Extensions/ServiceCollectionExtensions.cs
A Data/NosSmooth.Data.NOSFiles/Files/FileArchive.cs
A Data/NosSmooth.Data.NOSFiles/Files/FileType.cs
A Data/NosSmooth.Data.NOSFiles/Files/RawFile.cs
A Data/NosSmooth.Data.NOSFiles/Files/ReadFile.cs
A Data/NosSmooth.Data.NOSFiles/InfoService.cs
A Data/NosSmooth.Data.NOSFiles/Infos/MapInfo.cs
A Data/NosSmooth.Data.NOSFiles/LanguageService.cs
A Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj
A Data/NosSmooth.Data.NOSFiles/NostaleDataFilesManager.cs
A Data/NosSmooth.Data.NOSFiles/NostaleDataParser.cs
A Data/NosSmooth.Data.NOSFiles/NostaleFiles.cs
A Data/NosSmooth.Data.NOSFiles/Options/NostaleDataOptions.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatEntry.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatItem.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/Dat/DatReader.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/IInfoParser.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/ItemParser.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/LangParser.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/MapParser.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/MonsterParser.cs
A Data/NosSmooth.Data.NOSFiles/Parsers/SkillParser.cs
A Data/NosSmooth.Data.NOSFiles/Readers/FileReader.cs
A Data/NosSmooth.Data.NOSFiles/Readers/IFileTypeReader.cs
A Data/NosSmooth.Data.NOSFiles/Readers/Types/BaseFileTypeReader.cs
A Data/NosSmooth.Data.NOSFiles/Readers/Types/NosTextFileTypeReader.cs
A Data/NosSmooth.Data.NOSFiles/Readers/Types/NosZlibFileTypeReader.cs
M NosSmooth.sln
A Data/NosSmooth.Data.Abstractions/Enums/BagType.cs => Data/NosSmooth.Data.Abstractions/Enums/BagType.cs +24 -0
@@ 0,0 1,24 @@
//
//  BagType.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;

#pragma warning disable CS1591
namespace NosSmooth.Data.Abstractions.Enums;

/// <summary>
/// The type of a bag the item belongs to.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Self-explanatory.")]
public enum BagType
{
    Equipment = 0,
    Main = 1,
    Etc = 2,
    Miniland = 3,
    Specialist = 6,
    Costume = 7
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Enums/HitType.cs => Data/NosSmooth.Data.Abstractions/Enums/HitType.cs +36 -0
@@ 0,0 1,36 @@
//
//  HitType.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.Abstractions.Enums;

/// <summary>
/// A hit type of a skill.
/// </summary>
public enum HitType
{
    /// <summary>
    /// The skill is for just one target.
    /// </summary>
    TargetOnly,

    /// <summary>
    /// The skill will hit enemies in a zone.
    /// </summary>
    /// <remarks>
    /// Can be AOE skill or a targeted skill that targets more enemies.
    /// </remarks>
    EnemiesInZone,

    /// <summary>
    /// The skill will hit allies in a zone, this is a buff.
    /// </summary>
    AlliesInZone,

    /// <summary>
    /// UNKNOWN TODO.
    /// </summary>
    SpecialArea
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Enums/SkillType.cs => Data/NosSmooth.Data.Abstractions/Enums/SkillType.cs +43 -0
@@ 0,0 1,43 @@
//
//  SkillType.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.Abstractions.Enums;

/// <summary>
/// A type of a skill.
/// </summary>
public enum SkillType
{
    /// <summary>
    /// The skill is a passive, used automatically.
    /// </summary>
    Passive,

    /// <summary>
    /// The skill is for players.
    /// </summary>
    Player,

    /// <summary>
    /// UNKNOWN TODO.
    /// </summary>
    Upgrade,

    /// <summary>
    /// Unknown TODO.
    /// </summary>
    Emote,

    /// <summary>
    /// The skill is for monsters.
    /// </summary>
    Monster,

    /// <summary>
    /// The skill is for partners.
    /// </summary>
    Partner
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Enums/TargetType.cs => Data/NosSmooth.Data.Abstractions/Enums/TargetType.cs +33 -0
@@ 0,0 1,33 @@
//
//  TargetType.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.Abstractions.Enums;

/// <summary>
/// Type of a target of a skill.
/// </summary>
public enum TargetType
{
    /// <summary>
    /// The skill has a (enemy) target.
    /// </summary>
    Target,

    /// <summary>
    /// The skill can be targeted only on self.
    /// </summary>
    Self,

    /// <summary>
    /// The skill can be targeted on self or a (enemy) target.
    /// </summary>
    SelfOrTarget,

    /// <summary>
    /// The skill has no target. UNKNOWN TODO.
    /// </summary>
    NoTarget
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/IInfoService.cs => Data/NosSmooth.Data.Abstractions/IInfoService.cs +48 -0
@@ 0,0 1,48 @@
//
//  IInfoService.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 Remora.Results;

namespace NosSmooth.Data.Abstractions;

/// <summary>
/// Service for retrieving information about NosTale objects.
/// </summary>
public interface IInfoService
{
    /// <summary>
    /// Gets the information about an item.
    /// </summary>
    /// <param name="vnum">The vnum identifier of the item.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>An item info or an error.</returns>
    public Task<Result<IItemInfo>> GetItemInfoAsync(int vnum, CancellationToken ct = default);

    /// <summary>
    /// Gets the information about a map.
    /// </summary>
    /// <param name="id">The identifier of the map.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A map info or an error.</returns>
    public Task<Result<IMapInfo>> GetMapInfoAsync(int id, CancellationToken ct = default);

    /// <summary>
    /// Gets the information about a monster.
    /// </summary>
    /// <param name="vnum">The vnum identifier of the monster.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A monster or an error.</returns>
    public Task<Result<IMonsterInfo>> GetMonsterInfoAsync(int vnum, CancellationToken ct = default);

    /// <summary>
    /// Gets the information about a skill.
    /// </summary>
    /// <param name="vnum">The vnum identifier of the skill.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A map or an error.</returns>
    public Task<Result<ISkillInfo>> GetSkillInfoAsync(int vnum, CancellationToken ct = default);
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Infos/IItemInfo.cs => Data/NosSmooth.Data.Abstractions/Infos/IItemInfo.cs +41 -0
@@ 0,0 1,41 @@
//
//  IItemInfo.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.Language;

namespace NosSmooth.Data.Abstractions.Infos;

/// <summary>
/// The NosTale item information.
/// </summary>
public interface IItemInfo : IVNumInfo
{
    /// <summary>
    /// Gets the translatable name of the item.
    /// </summary>
    TranslatableString Name { get; }

    /// <summary>
    /// Gets the type of the item. TODO UNKNOWN.
    /// </summary>
    int Type { get; }

    /// <summary>
    /// Gets the subtype of the item. TODO UNKNOWN.
    /// </summary>
    int SubType { get; }

    /// <summary>
    /// Gets the bag the item belongs to.
    /// </summary>
    BagType BagType { get; }

    /// <summary>
    /// Gets the data of the item.
    /// </summary>
    string[] Data { get; }
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Infos/IMapInfo.cs => Data/NosSmooth.Data.Abstractions/Infos/IMapInfo.cs +51 -0
@@ 0,0 1,51 @@
//
//  IMapInfo.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.Abstractions.Infos;

/// <summary>
/// The NosTale map information.
/// </summary>
public interface IMapInfo
{
    /// <summary>
    /// Gets the Id of the map.
    /// </summary>
    public int Id { get; }

    /// <summary>
    /// Gets the translatable name of the map.
    /// </summary>
    public TranslatableString Name { get; }

    /// <summary>
    /// Gets the width of the grid.
    /// </summary>
    public short Width { get; }

    /// <summary>
    /// Gets the height of the grid.
    /// </summary>
    public short Height { get; }

    /// <summary>
    /// Gets grid data for the given position.
    /// </summary>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <returns>The grid value.</returns>
    public byte GetData(short x, short y);

    /// <summary>
    /// Gets whether the given position is walkable.
    /// </summary>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <returns>Whether the position is walkable.</returns>
    public bool IsWalkable(short x, short y);
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Infos/IMonsterInfo.cs => Data/NosSmooth.Data.Abstractions/Infos/IMonsterInfo.cs +25 -0
@@ 0,0 1,25 @@
//
//  IMonsterInfo.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.Abstractions.Infos;

/// <summary>
/// The NosTale monster information.
/// </summary>
public interface IMonsterInfo : IVNumInfo
{
    /// <summary>
    /// Gets the name of the monster.
    /// </summary>
    TranslatableString Name { get; }

    /// <summary>
    /// Gets the default level of the monster.
    /// </summary>
    int Level { get; }
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Infos/ISkillInfo.cs => Data/NosSmooth.Data.Abstractions/Infos/ISkillInfo.cs +66 -0
@@ 0,0 1,66 @@
//
//  ISkillInfo.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.Language;

namespace NosSmooth.Data.Abstractions.Infos;

/// <summary>
/// The NosTale skill information.
/// </summary>
public interface ISkillInfo : IVNumInfo
{
    /// <summary>
    /// Gets the translatable name of the skill.
    /// </summary>
    TranslatableString Name { get; }

    /// <summary>
    /// Gets the tile range of the skill.
    /// </summary>
    short Range { get; }

    /// <summary>
    /// Gets the zone tiles range.
    /// </summary>
    short ZoneRange { get; }

    /// <summary>
    /// Gets the time it takes to cast this skill. Units UNKNOWN TODO.
    /// </summary>
    int CastTime { get; }

    /// <summary>
    /// Gets the time of the cooldown. Units UNKNOWN TODO.
    /// </summary>
    int Cooldown { get; }

    /// <summary>
    /// Gets the type of the skill.
    /// </summary>
    SkillType SkillType { get; }

    /// <summary>
    /// Gets the mana points the skill cast costs.
    /// </summary>
    int MpCost { get; }

    /// <summary>
    /// Gets the cast id of the skill used in u_s, su packets.
    /// </summary>
    short CastId { get; }

    /// <summary>
    /// Gets the type of the target.
    /// </summary>
    TargetType TargetType { get; }

    /// <summary>
    /// Gets the hit type of the skill.
    /// </summary>
    HitType HitType { get; }
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Infos/IVNumInfo.cs => Data/NosSmooth.Data.Abstractions/Infos/IVNumInfo.cs +18 -0
@@ 0,0 1,18 @@
//
//  IVNumInfo.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.Abstractions.Infos;

/// <summary>
/// A NosTale info with a vnum key.
/// </summary>
public interface IVNumInfo
{
    /// <summary>
    /// Gets the VNum of the info entry.
    /// </summary>
    public int VNum { get; }
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Language/ILanguageService.cs => Data/NosSmooth.Data.Abstractions/Language/ILanguageService.cs +39 -0
@@ 0,0 1,39 @@
//
//  ILanguageService.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 Remora.Results;

namespace NosSmooth.Data.Abstractions.Language;

/// <summary>
/// Service for translating NosTale strings.
/// </summary>
public interface ILanguageService
{
    /// <summary>
    /// Gets or sets the current language.
    /// </summary>
    public Language CurrentLanguage { get; set; }

    /// <summary>
    /// Gets the translation of the given key.
    /// </summary>
    /// <param name="root">The root type of the key.</param>
    /// <param name="key">The key to translate.</param>
    /// <param name="language">The language, <see cref="CurrentLanguage"/> will be used if null.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The translated string or an error.</returns>
    public Task<Result<string>> GetTranslationAsync(TranslationRoot root, string key, Language? language = default, CancellationToken ct = default);

    /// <summary>
    /// Gets the translation of the given key.
    /// </summary>
    /// <param name="translatableString">The translatable string containing .</param>
    /// <param name="language">The language, <see cref="CurrentLanguage"/> will be used if null.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The translated string or an error.</returns>
    public Task<Result<string>> GetTranslationAsync(TranslatableString translatableString, Language? language = default, CancellationToken ct = default);
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Language/Language.cs => Data/NosSmooth.Data.Abstractions/Language/Language.cs +58 -0
@@ 0,0 1,58 @@
//
//  Language.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.Abstractions.Language;

/// <summary>
/// Languages supported by NosTale.
/// </summary>
public enum Language
{
    /// <summary>
    /// English language.
    /// </summary>
    Uk,

    /// <summary>
    /// German language.
    /// </summary>
    De,

    /// <summary>
    /// French language.
    /// </summary>
    Fr,

    /// <summary>
    /// Italian language.
    /// </summary>
    It,

    /// <summary>
    /// Polish language.
    /// </summary>
    Pl,

    /// <summary>
    /// Spanish language.
    /// </summary>
    Es,

    /// <summary>
    /// Russian language.
    /// </summary>
    Ru,

    /// <summary>
    /// Czech language.
    /// </summary>
    Cz,

    /// <summary>
    /// Turkish language.
    /// </summary>
    Tr
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Language/LanguageEncoding.cs => Data/NosSmooth.Data.Abstractions/Language/LanguageEncoding.cs +39 -0
@@ 0,0 1,39 @@
//
//  LanguageEncoding.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;

namespace NosSmooth.Data.Abstractions.Language;

/// <summary>
/// Mapping of language encoding.
/// </summary>
public class LanguageEncoding
{
    /// <summary>
    /// Get encoding for the given language.
    /// </summary>
    /// <param name="lang">The language.</param>
    /// <returns>An encoding.</returns>
    public static Encoding GetEncoding(Language lang)
    {
        switch (lang)
        {
            case Language.Tr:
                return CodePagesEncodingProvider.Instance.GetEncoding(1254) ?? Encoding.ASCII;
            case Language.Uk:
            case Language.Es:
                return CodePagesEncodingProvider.Instance.GetEncoding(1252) ?? Encoding.ASCII;
            case Language.Cz:
            case Language.De:
            case Language.Pl:
            case Language.It:
                return CodePagesEncodingProvider.Instance.GetEncoding(1250) ?? Encoding.ASCII;
        }

        return Encoding.ASCII;
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Language/LanguageServiceOptions.cs => Data/NosSmooth.Data.Abstractions/Language/LanguageServiceOptions.cs +23 -0
@@ 0,0 1,23 @@
//
//  LanguageServiceOptions.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.Abstractions.Language;

/// <summary>
/// Options for <see cref="ILanguageService"/>.
/// </summary>
public class LanguageServiceOptions
{
    /// <summary>
    /// Get or sets the default language.
    /// </summary>
    public Language Language { get; set; } = Language.Uk;

    /// <summary>
    /// Gets or sets whether to fill <see cref="TranslatableString"/> translations with the default language.
    /// </summary>
    public bool FillTranslatableStrings { get; set; } = true;
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Language/TranslatableString.cs => Data/NosSmooth.Data.Abstractions/Language/TranslatableString.cs +33 -0
@@ 0,0 1,33 @@
//
//  TranslatableString.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;

namespace NosSmooth.Data.Abstractions.Language;

/// <summary>
/// Represents a string that may be translated.
/// </summary>
/// <param name="Root">The root key of the translations.</param>
/// <param name="Key">The key of the string translation.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Standard.")]
public record struct TranslatableString(TranslationRoot Root, string Key)
{
    /// <summary>
    /// Gets the translated string, if available.
    /// If not available, the key will be returned.
    /// </summary>
    public string Translated { get; private set; } = Key;

    /// <summary>
    /// Fill this translatable string with a translation.
    /// </summary>
    /// <param name="translation">The translation to fill.</param>
    public void Fill(string translation)
    {
        Translated = translation;
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/Language/TranslationRoot.cs => Data/NosSmooth.Data.Abstractions/Language/TranslationRoot.cs +33 -0
@@ 0,0 1,33 @@
//
//  TranslationRoot.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.Abstractions.Language;

/// <summary>
/// Root type of a translation.
/// </summary>
public enum TranslationRoot
{
    /// <summary>
    /// The translation is for an item.
    /// </summary>
    Item,

    /// <summary>
    /// The translation is for a monster.
    /// </summary>
    Monster,

    /// <summary>
    /// The translation for a skill.
    /// </summary>
    Skill,

    /// <summary>
    /// The translation for a map.
    /// </summary>
    Map
}
\ No newline at end of file

A Data/NosSmooth.Data.Abstractions/NosSmooth.Data.Abstractions.csproj => Data/NosSmooth.Data.Abstractions/NosSmooth.Data.Abstractions.csproj +15 -0
@@ 0,0 1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <Description>Nos smooth's abstractions for information from NosTale data files.</Description>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Remora.Results" Version="7.1.0" />
      <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
    </ItemGroup>

</Project>

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.CLI/Commands/ExtractNosFileCommand.cs => Data/NosSmooth.Data.CLI/Commands/ExtractNosFileCommand.cs +63 -0
@@ 0,0 1,63 @@
//
//  ExtractNosFileCommand.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.Net;
using System.Text;
using NosSmooth.Data.NOSFiles.Files;
using NosSmooth.Data.NOSFiles.Readers;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Results;

namespace NosSmooth.Data.CLI.Commands;

/// <summary>
/// A command to extract files from a NosTale archive.
/// </summary>
public class ExtractNosFileCommand : CommandGroup
{
    private readonly FileReader _reader;

    /// <summary>
    /// Initializes a new instance of the <see cref="ExtractNosFileCommand"/> class.
    /// </summary>
    /// <param name="reader">The file reader.</param>
    public ExtractNosFileCommand(FileReader reader)
    {
        _reader = reader;
    }

    /// <summary>
    /// Handles extract command.
    /// </summary>
    /// <param name="inputFile">The input nos archive.</param>
    /// <param name="outputDirectory">The output directory to put the extracted files to.</param>
    /// <returns>A result that may or may not have succeeded..</returns>
    [Command("extract")]
    public async Task<Result> HandleExtract
    (
        string inputFile,
        [Option('o', "option")]
        string outputDirectory = "out"
    )
    {
        Directory.CreateDirectory(outputDirectory);

        var readResult = _reader.ReadFileSystemFile<FileArchive>(inputFile);
        if (!readResult.IsSuccess)
        {
            return Result.FromError(readResult);
        }

        foreach (var file in readResult.Entity.Content.Files)
        {
            var outputPath = Path.Combine(outputDirectory, file.Path);
            await File.WriteAllBytesAsync(outputPath, file.Content, CancellationToken);
        }

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

A Data/NosSmooth.Data.CLI/Commands/MigrateDatabaseCommand.cs => Data/NosSmooth.Data.CLI/Commands/MigrateDatabaseCommand.cs +63 -0
@@ 0,0 1,63 @@
//
//  MigrateDatabaseCommand.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.Database;
using NosSmooth.Data.NOSFiles;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Results;

namespace NosSmooth.Data.CLI.Commands;

/// <summary>
/// Create a database from nos files.
/// </summary>
public class MigrateDatabaseCommand : CommandGroup
{
    private readonly DatabaseMigrator _migrator;
    private readonly NostaleDataParser _parser;

    /// <summary>
    /// Initializes a new instance of the <see cref="MigrateDatabaseCommand"/> class.
    /// </summary>
    /// <param name="migrator">The database migrator.</param>
    /// <param name="parser">The data parser.</param>
    public MigrateDatabaseCommand(DatabaseMigrator migrator, NostaleDataParser parser)
    {
        _migrator = migrator;
        _parser = parser;
    }

    /// <summary>
    /// Migrate the database using nos files.
    /// </summary>
    /// <param name="nostaleDataPath">The directory with nostale data files.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    [Command("migrate")]
    public async Task<Result> HandleMigrate([Greedy] string nostaleDataPath)
    {
        var parsingResult = _parser.ParseFiles
        (
            nostaleDataPath,
            Language.Cz,
            Language.De,
            Language.Uk,
            Language.Es,
            Language.Fr,
            Language.It,
            Language.Pl,
            Language.Ru,
            Language.Tr
        );
        if (!parsingResult.IsSuccess)
        {
            return Result.FromError(parsingResult);
        }

        return await _migrator.Migrate(parsingResult.Entity);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.CLI/NosSmooth.Data.CLI.csproj => Data/NosSmooth.Data.CLI/NosSmooth.Data.CLI.csproj +22 -0
@@ 0,0 1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <OutputType>Exe</OutputType>
        <IsPackable>false</IsPackable>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
      <PackageReference Include="Remora.Commands" Version="9.0.0" />
    </ItemGroup>

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

</Project>

A Data/NosSmooth.Data.CLI/Program.cs => Data/NosSmooth.Data.CLI/Program.cs +76 -0
@@ 0,0 1,76 @@
//
//  Program.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.DependencyInjection;
using NosSmooth.Data.CLI.Commands;
using NosSmooth.Data.Database.Extensions;
using NosSmooth.Data.NOSFiles.Extensions;
using NosSmooth.Data.NOSFiles.Files;
using NosSmooth.Data.NOSFiles.Readers;
using NosSmooth.Data.NOSFiles.Readers.Types;
using Remora.Commands.Extensions;
using Remora.Commands.Services;
using Remora.Results;

namespace NosSmooth.Data.CLI;

/// <summary>
/// Entrypoint class.
/// </summary>
public class Program
{
    /// <summary>
    /// Entrypoint method.
    /// </summary>
    /// <param name="arguments">The arguments.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    public static async Task Main(string[] arguments)
    {
        var services = CreateServices();
        var commandService = services.GetRequiredService<CommandService>();
        var preparedCommandResult = await commandService.TryPrepareCommandAsync(string.Join(' ', arguments), services);
        if (!preparedCommandResult.IsSuccess)
        {
            Console.Error.WriteLine($"There was an error, {preparedCommandResult.Error.Message}");
            return;
        }

        if (preparedCommandResult.Entity is null)
        {
            Console.Error.WriteLine("You must enter a command such ast list or inject.");
            return;
        }

        var executionResult = await commandService.TryExecuteAsync(preparedCommandResult.Entity, services);
        if (!executionResult.Entity.IsSuccess)
        {
            switch (executionResult.Entity.Error)
            {
                case ExceptionError exc:
                    Console.Error.WriteLine($"There was an exception, {exc.Exception.Message}");
                    break;
                default:
                    Console.Error.WriteLine($"There was an error, {executionResult.Entity.Error!.Message}");
                    break;
            }
        }
    }

    private static IServiceProvider CreateServices()
    {
        var collection = new ServiceCollection();

        collection
            .AddNostaleDataParsing()
            .AddNostaleDatabaseMigrator()
            .AddCommands()
            .AddCommandTree()
            .WithCommandGroup<MigrateDatabaseCommand>()
            .WithCommandGroup<ExtractNosFileCommand>();

        return collection.BuildServiceProvider();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/Data/ItemInfo.cs => Data/NosSmooth.Data.Database/Data/ItemInfo.cs +53 -0
@@ 0,0 1,53 @@
//
//  ItemInfo.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.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.Database.Data;

/// <inheritdoc />
public class ItemInfo : IItemInfo
{
    private string _nameKey = null!;

    /// <inheritdoc />
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Key]
    public int VNum { get; set; }

    /// <summary>
    /// The name translation key.
    /// </summary>
    public string NameKey
    {
        get => _nameKey;
        set
        {
            _nameKey = value;
            Name = new TranslatableString(TranslationRoot.Item, value);
        }
    }

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

    /// <inheritdoc />
    public int Type { get; set; }

    /// <inheritdoc />
    public int SubType { get; set; }

    /// <inheritdoc />
    public BagType BagType { get; set; }

    /// <inheritdoc/>
    public string[] Data { get; set; } = null!;
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/Data/MapInfo.cs => Data/NosSmooth.Data.Database/Data/MapInfo.cs +63 -0
@@ 0,0 1,63 @@
//
//  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 System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.Database.Data;

/// <inheritdoc />
public class MapInfo : IMapInfo
{
    private string _nameKey = null!;

    /// <inheritdoc />
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Key]
    public int Id { get; set; }

    /// <summary>
    /// The name translation key.
    /// </summary>
    public string NameKey
    {
        get => _nameKey;
        set
        {
            _nameKey = value;
            Name = new TranslatableString(TranslationRoot.Map, value);
        }
    }

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

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

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

    /// <summary>
    /// Gets or sets the grid data of the map.
    /// </summary>
    public byte[] Grid { get; set; } = null!;

    /// <inheritdoc />
    public byte GetData(short x, short y)
    {
        return Grid[(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.Database/Data/MonsterInfo.cs => Data/NosSmooth.Data.Database/Data/MonsterInfo.cs +42 -0
@@ 0,0 1,42 @@
//
//  MonsterInfo.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.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.Database.Data;

/// <inheritdoc />
public class MonsterInfo : IMonsterInfo
{
    private string _nameKey = null!;

    /// <inheritdoc />
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Key]
    public int VNum { get; set; }

    /// <summary>
    /// The name translation key.
    /// </summary>
    public string NameKey
    {
        get => _nameKey;
        set
        {
            _nameKey = value;
            Name = new TranslatableString(TranslationRoot.Monster, value);
        }
    }

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

    /// <inheritdoc />
    public int Level { get; set; }
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/Data/SkillInfo.cs => Data/NosSmooth.Data.Database/Data/SkillInfo.cs +67 -0
@@ 0,0 1,67 @@
//
//  SkillInfo.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.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.Database.Data;

/// <inheritdoc />
public class SkillInfo : ISkillInfo
{
    private string _nameKey = null!;

    /// <inheritdoc />
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Key]
    public int VNum { get; set; }

    /// <summary>
    /// The name translation key.
    /// </summary>
    public string NameKey
    {
        get => _nameKey;
        set
        {
            _nameKey = value;
            Name = new TranslatableString(TranslationRoot.Skill, value);
        }
    }

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

    /// <inheritdoc />
    public short Range { get;  set; }

    /// <inheritdoc />
    public short ZoneRange { get; set;  }

    /// <inheritdoc />
    public int CastTime { get; set;  }

    /// <inheritdoc />
    public int Cooldown { get; set;  }

    /// <inheritdoc />
    public SkillType SkillType { get; set;  }

    /// <inheritdoc />
    public int MpCost { get;  set; }

    /// <inheritdoc />
    public short CastId { get; set;  }

    /// <inheritdoc />
    public TargetType TargetType { get; set;  }

    /// <inheritdoc />
    public HitType HitType { get; set;  }
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/Data/Translation.cs => Data/NosSmooth.Data.Database/Data/Translation.cs +35 -0
@@ 0,0 1,35 @@
//
//  Translation.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.Database.Data;

/// <summary>
/// A translation of NosTale string.
/// </summary>
public class Translation
{
    /// <summary>
    /// Gets or sets the language of the translation.
    /// </summary>
    public Language Language { get; set; }

    /// <summary>
    /// Gets or sets the root key of the translation.
    /// </summary>
    public TranslationRoot Root { get; set; }

    /// <summary>
    /// Gets or sets the key of the translation.
    /// </summary>
    public string Key { get; set; } = null!;

    /// <summary>
    /// Gets or sets the translation string.
    /// </summary>
    public string Translated { get; set; } = null!;
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/DatabaseInfoService.cs => Data/NosSmooth.Data.Database/DatabaseInfoService.cs +79 -0
@@ 0,0 1,79 @@
//
//  DatabaseInfoService.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.EntityFrameworkCore;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Infos;
using Remora.Results;

namespace NosSmooth.Data.Database;

/// <inheritdoc />
public class DatabaseInfoService : IInfoService
{
    private readonly IDbContextFactory<NostaleDataContext> _dbContextFactory;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatabaseInfoService"/> class.
    /// </summary>
    /// <param name="dbContextFactory">The database context factory.</param>
    public DatabaseInfoService(IDbContextFactory<NostaleDataContext> dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }

    /// <inheritdoc />
    public async Task<Result<IItemInfo>> GetItemInfoAsync(int vnum, CancellationToken ct = default)
    {
        await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
        var item = await context.Items.AsNoTracking().FirstOrDefaultAsync(x => x.VNum == vnum, ct);
        if (item is null)
        {
            return new NotFoundError($"Couldn't find item {vnum}");
        }

        return item;
    }

    /// <inheritdoc />
    public async Task<Result<IMapInfo>> GetMapInfoAsync(int id, CancellationToken ct = default)
    {
        await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
        var item = await context.Maps.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
        if (item is null)
        {
            return new NotFoundError($"Couldn't find map {id}");
        }

        return item;
    }

    /// <inheritdoc />
    public async Task<Result<IMonsterInfo>> GetMonsterInfoAsync(int vnum, CancellationToken ct = default)
    {
        await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
        var item = await context.Monsters.AsNoTracking().FirstOrDefaultAsync(x => x.VNum == vnum, ct);
        if (item is null)
        {
            return new NotFoundError($"Couldn't find monster {vnum}");
        }

        return item;
    }

    /// <inheritdoc />
    public async Task<Result<ISkillInfo>> GetSkillInfoAsync(int vnum, CancellationToken ct = default)
    {
        await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
        var item = await context.Skills.AsNoTracking().FirstOrDefaultAsync(x => x.VNum == vnum, ct);
        if (item is null)
        {
            return new NotFoundError($"Couldn't find skill {vnum}");
        }

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

A Data/NosSmooth.Data.Database/DatabaseLanguageService.cs => Data/NosSmooth.Data.Database/DatabaseLanguageService.cs +79 -0
@@ 0,0 1,79 @@
//
//  DatabaseLanguageService.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.Runtime.InteropServices.ComTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NosSmooth.Data.Abstractions.Language;
using Remora.Results;

namespace NosSmooth.Data.Database;

/// <inheritdoc />
public class DatabaseLanguageService : ILanguageService
{
    private readonly IDbContextFactory<NostaleDataContext> _dbContextFactory;
    private readonly LanguageServiceOptions _options;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatabaseLanguageService"/> class.
    /// </summary>
    /// <param name="dbContextFactory">The database context factory.</param>
    /// <param name="options">The options.</param>
    public DatabaseLanguageService
    (
        IDbContextFactory<NostaleDataContext> dbContextFactory,
        IOptions<LanguageServiceOptions> options
    )
    {
        CurrentLanguage = options.Value.Language;
        _dbContextFactory = dbContextFactory;
        _options = options.Value;
    }

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

    /// <inheritdoc />
    public async Task<Result<string>> GetTranslationAsync(TranslationRoot root, string key, Language? language = default, CancellationToken ct = default)
    {
        try
        {
            language ??= CurrentLanguage;
            await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
            var translation = await context.Translations.AsNoTracking().FirstOrDefaultAsync
                (x => x.Root == root && x.Key == key && x.Language == language, ct);
            if (translation is null)
            {
                return new NotFoundError($"Could not find translation for {root} {key}");
            }

            return translation.Translated;
        }
        catch (Exception e)
        {
            return e;
        }
    }

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

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

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

A Data/NosSmooth.Data.Database/DatabaseMigrator.cs => Data/NosSmooth.Data.Database/DatabaseMigrator.cs +193 -0
@@ 0,0 1,193 @@
//
//  DatabaseMigrator.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.EntityFrameworkCore;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Database.Data;
using Remora.Results;

namespace NosSmooth.Data.Database;

/// <summary>
/// Migrates Nostale data into sqlite database.
/// </summary>
public class DatabaseMigrator
{
    private readonly IDbContextFactory<NostaleDataContext> _dbContextFactory;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatabaseMigrator"/> class.
    /// </summary>
    /// <param name="dbContextFactory">The database context factory.</param>
    public DatabaseMigrator(IDbContextFactory<NostaleDataContext> dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }

    /// <summary>
    /// Migrates the data into the database.
    /// </summary>
    /// <param name="data">The NosTale data.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
    public async Task<Result> Migrate(NostaleData data, CancellationToken ct = default)
    {
        await using var context = await _dbContextFactory.CreateDbContextAsync(ct);

        var itemsResult = await MigrateItems(context, data);
        if (!itemsResult.IsSuccess)
        {
            return itemsResult;
        }

        var skillsResult = await MigrateSkills(context, data);
        if (!skillsResult.IsSuccess)
        {
            return skillsResult;
        }

        var monstersResult = await MigrateMonsters(context, data);
        if (!monstersResult.IsSuccess)
        {
            return monstersResult;
        }

        var mapsResult = await MigrateMaps(context, data);
        if (!mapsResult.IsSuccess)
        {
            return mapsResult;
        }

        var translationsResult = await MigrateTranslations(context, data);
        if (!translationsResult.IsSuccess)
        {
            return translationsResult;
        }

        await context.Database.EnsureDeletedAsync(ct);
        await context.Database.EnsureCreatedAsync(ct);

        try
        {
            await context.SaveChangesAsync(ct);
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    private async Task<Result> MigrateTranslations(NostaleDataContext context, NostaleData data)
    {
        foreach (var languageTranslation in data.Translations)
        {
            foreach (var rootTranslations in languageTranslation.Value)
            {
                foreach (var translations in rootTranslations.Value)
                {
                    var translation = new Translation
                    {
                        Key = translations.Key,
                        Root = rootTranslations.Key,
                        Language = languageTranslation.Key,
                        Translated = translations.Value
                    };
                    context.Add(translation);
                }
            }
        }

        return Result.FromSuccess();
    }

    private Task<Result> MigrateItems(NostaleDataContext dbContext, NostaleData data)
    {
        foreach (var item in data.Items.Values)
        {
            var itemInfo = new ItemInfo
            {
                BagType = item.BagType,
                Data = item.Data,
                NameKey = item.Name.Key,
                SubType = item.SubType,
                Type = item.Type,
                VNum = item.VNum
            };
            dbContext.Add(itemInfo);
        }

        return Task.FromResult(Result.FromSuccess());
    }

    private Task<Result> MigrateSkills(NostaleDataContext dbContext, NostaleData data)
    {
        foreach (var skill in data.Skills.Values)
        {
            var skillInfo = new SkillInfo
            {
                CastId = skill.CastId,
                CastTime = skill.CastTime,
                Cooldown = skill.Cooldown,
                HitType = skill.HitType,
                MpCost = skill.MpCost,
                NameKey = skill.Name.Key,
                Range = skill.Range,
                SkillType = skill.SkillType,
                TargetType = skill.TargetType,
                VNum = skill.VNum,
                ZoneRange = skill.ZoneRange
            };
            dbContext.Add(skillInfo);
        }

        return Task.FromResult(Result.FromSuccess());
    }

    private Task<Result> MigrateMonsters(NostaleDataContext dbContext, NostaleData data)
    {
        foreach (var monster in data.Monsters.Values)
        {
            var monsterInfo = new MonsterInfo
            {
                VNum = monster.VNum,
                NameKey = monster.Name.Key,
                Level = monster.Level
            };
            dbContext.Add(monsterInfo);
        }

        return Task.FromResult(Result.FromSuccess());
    }

    private Task<Result> MigrateMaps(NostaleDataContext dbContext, NostaleData data)
    {
        foreach (var map in data.Maps.Values)
        {
            var grid = new byte[map.Height * map.Width];
            for (short y = 0; y < map.Height; y++)
            {
                for (short x = 0; x < map.Width; x++)
                {
                    grid[(y * map.Width) + x] = map.GetData(x, y);
                }
            }

            var mapInfo = new MapInfo
            {
                Height = map.Height,
                Width = map.Width,
                NameKey = map.Name.Key,
                Id = map.Id,
                Grid = grid
            };
            dbContext.Add(mapInfo);
        }

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

A Data/NosSmooth.Data.Database/Extensions/ServiceCollectionExtensions.cs => Data/NosSmooth.Data.Database/Extensions/ServiceCollectionExtensions.cs +49 -0
@@ 0,0 1,49 @@
//
//  ServiceCollectionExtensions.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.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Language;

namespace NosSmooth.Data.Database.Extensions;

/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Adds NosTale data language and info services using a database.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleDataDatabase
        (this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddSingleton<IInfoService, DatabaseInfoService>()
            .AddSingleton<ILanguageService, DatabaseLanguageService>()
            .AddDbContextFactory<NostaleDataContext>
            (
                (provider, builder) => builder.UseSqlite
                    (provider.GetRequiredService<IOptions<NostaleDataContextOptions>>().Value.ConnectionString)
            );
    }

    /// <summary>
    /// Adds <see cref="DatabaseMigrator"/> used for migrating data to the database.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleDatabaseMigrator(this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddNostaleDataDatabase()
            .AddSingleton<DatabaseMigrator>();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/NosSmooth.Data.Database.csproj => Data/NosSmooth.Data.Database/NosSmooth.Data.Database.csproj +20 -0
@@ 0,0 1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <Description>Nos smooth's database implementation of NosTale data using EF core.</Description>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
      <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
      <PackageReference Include="Remora.Results" Version="7.1.0" />
    </ItemGroup>

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

</Project>

A Data/NosSmooth.Data.Database/NostaleDataContext.cs => Data/NosSmooth.Data.Database/NostaleDataContext.cs +70 -0
@@ 0,0 1,70 @@
//
//  NostaleDataContext.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.EntityFrameworkCore;
using NosSmooth.Data.Database.Data;

namespace NosSmooth.Data.Database;

/// <summary>
/// Database context with NosTale data.
/// </summary>
public class NostaleDataContext : DbContext
{
    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleDataContext"/> class.
    /// </summary>
    /// <param name="options">The options.</param>
    public NostaleDataContext(DbContextOptions<NostaleDataContext> options)
        : base(options)
    {
    }

    /// <summary>
    /// Gets the translations set.
    /// </summary>
    public DbSet<Translation> Translations => Set<Translation>();

    /// <summary>
    /// Gets the items set.
    /// </summary>
    public DbSet<ItemInfo> Items => Set<ItemInfo>();

    /// <summary>
    /// Gets the maps set.
    /// </summary>
    public DbSet<MapInfo> Maps => Set<MapInfo>();

    /// <summary>
    /// Gets the monsters set.
    /// </summary>
    public DbSet<MonsterInfo> Monsters => Set<MonsterInfo>();

    /// <summary>
    /// Gets the skills set.
    /// </summary>
    public DbSet<SkillInfo> Skills => Set<SkillInfo>();

    /// <inheritdoc />
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ItemInfo>()
            .Property(x => x.Data)
            .HasConversion
            (
                x => string.Join("|", x),
                x => x.Split(',', StringSplitOptions.RemoveEmptyEntries)
            );

        modelBuilder.Entity<Translation>().HasKey("Language", "Root", "Key");

        modelBuilder.Entity<ItemInfo>().Ignore(x => x.Name);
        modelBuilder.Entity<SkillInfo>().Ignore(x => x.Name);
        modelBuilder.Entity<MonsterInfo>().Ignore(x => x.Name);
        modelBuilder.Entity<MapInfo>().Ignore(x => x.Name);
        base.OnModelCreating(modelBuilder);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.Database/NostaleDataContextOptions.cs => Data/NosSmooth.Data.Database/NostaleDataContextOptions.cs +18 -0
@@ 0,0 1,18 @@
//
//  NostaleDataContextOptions.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.Database;

/// <summary>
/// Options for <see cref="NostaleDataContext"/>.
/// </summary>
public class NostaleDataContextOptions
{
    /// <summary>
    /// Gets or sets the sqlite3 connection string.
    /// </summary>
    public string ConnectionString { get; set; } = "Data Source=nossmooth.sqlite3;";
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Decryptors/DatDecryptor.cs => Data/NosSmooth.Data.NOSFiles/Decryptors/DatDecryptor.cs +77 -0
@@ 0,0 1,77 @@
//
//  DatDecryptor.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 Remora.Results;

namespace NosSmooth.Data.NOSFiles.Decryptors;

/// <inheritdoc />
public class DatDecryptor : IDecryptor
{
    private readonly byte[] _cryptoArray;

    /// <summary>
    /// Initializes a new instance of the <see cref="DatDecryptor"/> class.
    /// </summary>
    public DatDecryptor()
    {
        _cryptoArray = new byte[] { 0x00, 0x20, 0x2D, 0x2E, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x0A, 0x00 };
    }

    /// <inheritdoc />
    public Result<byte[]> Decrypt(ReadOnlySpan<byte> data)
    {
        using var output = new MemoryStream();
        int i = 0;
        while (i < data.Length)
        {
            byte currentByte = data[i];
            i++;

            if (currentByte == 0xFF)
            {
                output.WriteByte(0xD);
                continue;
            }

            int validate = currentByte & 0x7F;
            if ((currentByte & 0x80) != 0)
            {
                for (; validate > 0 && i < data.Length; validate -= 2)
                {
                    currentByte = data[i];
                    i++;
                    byte firstByte = _cryptoArray[(currentByte & 0xF0) >> 4];
                    output.WriteByte(firstByte);

                    if (validate <= 1)
                    {
                        break;
                    }

                    byte secondByte = _cryptoArray[currentByte & 0x0F];
                    if (secondByte == 0)
                    {
                        break;
                    }
                    output.WriteByte(secondByte);
                }
            }
            else
            {
                for (; validate > 0 && i < data.Length; validate--)
                {
                    currentByte = data[i];
                    output.WriteByte((byte)(currentByte ^ 0x33));
                    i++;
                }
            }
        }

        return output.ToArray();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Decryptors/IDecryptor.cs => Data/NosSmooth.Data.NOSFiles/Decryptors/IDecryptor.cs +22 -0
@@ 0,0 1,22 @@
//
//  IDecryptor.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 Remora.Results;

namespace NosSmooth.Data.NOSFiles.Decryptors;

/// <summary>
/// A decryptor interface.
/// </summary>
public interface IDecryptor
{
    /// <summary>
    /// Decrypts the given data.
    /// </summary>
    /// <param name="data">The data.</param>
    /// <returns>An array with data or an error.</returns>
    public Result<byte[]> Decrypt(ReadOnlySpan<byte> data);
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Decryptors/LstDecryptor.cs => Data/NosSmooth.Data.NOSFiles/Decryptors/LstDecryptor.cs +41 -0
@@ 0,0 1,41 @@
//
//  LstDecryptor.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.Buffers.Binary;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Decryptors;

/// <inheritdoc />
public class LstDecryptor : IDecryptor
{
    /// <inheritdoc />
    public Result<byte[]> Decrypt(ReadOnlySpan<byte> data)
    {
        if (data.Length == 0)
        {
            return Array.Empty<byte>();
        }

        var output = new MemoryStream();
        var linesCount = BinaryPrimitives.ReadInt32LittleEndian(data);
        data = data.Slice(4);
        for (var i = 0; i < linesCount; i++)
        {
            int lineLength = BinaryPrimitives.ReadInt32LittleEndian(data);
            data = data.Slice(4);
            ReadOnlySpan<byte> line = data.Slice(0, lineLength);
            data = data.Slice(lineLength);
            foreach (var c in line)
            {
                output.WriteByte((byte)(c ^ 0x1));
            }
            output.WriteByte((byte)'\n');
        }

        return output.ToArray();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Errors/UnknownFileTypeError.cs => Data/NosSmooth.Data.NOSFiles/Errors/UnknownFileTypeError.cs +16 -0
@@ 0,0 1,16 @@
//
//  UnknownFileTypeError.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.Errors;

/// <summary>
/// The file type of the file does not have a reader registered.
/// </summary>
/// <param name="file">The file.</param>
public record UnknownFileTypeError(RawFile file) : NotFoundError($"Could not find reader for the given file {file.Path}.");
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Extensions/ServiceCollectionExtensions.cs => Data/NosSmooth.Data.NOSFiles/Extensions/ServiceCollectionExtensions.cs +70 -0
@@ 0,0 1,70 @@
//
//  ServiceCollectionExtensions.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.DependencyInjection;
using NosSmooth.Data.Abstractions;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Readers;
using NosSmooth.Data.NOSFiles.Readers.Types;

namespace NosSmooth.Data.NOSFiles.Extensions;

/// <summary>
/// Extension methods for <see cref="IServiceProvider"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Adds the nostale file data info and language service.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleDataFiles(this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddNostaleDataParsing()
            .AddSingleton<NostaleDataFilesManager>()
            .AddSingleton<IInfoService>(p => p.GetRequiredService<NostaleDataFilesManager>().InfoService)
            .AddSingleton<ILanguageService>(p => p.GetRequiredService<NostaleDataFilesManager>().LanguageService);
    }

    /// <summary>
    /// Adds the <see cref="NostaleDataParser"/>.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleDataParsing(this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddFileReader()
            .AddSingleton<NostaleDataParser>();
    }

    /// <summary>
    /// Add the file reader and NosTale type readers.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddFileReader(this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddSingleton<FileReader>()
            .AddFileTypeReader<NosZlibFileTypeReader>()
            .AddFileTypeReader<NosTextFileTypeReader>();
    }

    /// <summary>
    /// Add the given file type reader.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <typeparam name="TTypeReader">The type of the reader.</typeparam>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddFileTypeReader<TTypeReader>(this IServiceCollection serviceCollection)
        where TTypeReader : class, IFileTypeReader
    {
        return serviceCollection.AddSingleton<IFileTypeReader, TTypeReader>();
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Files/FileArchive.cs => Data/NosSmooth.Data.NOSFiles/Files/FileArchive.cs +33 -0
@@ 0,0 1,33 @@
//
//  FileArchive.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 Remora.Results;

namespace NosSmooth.Data.NOSFiles.Files;

/// <summary>
/// An archive of files.
/// </summary>
/// <param name="Files">The files in the archive.</param>
public record FileArchive(IReadOnlyList<RawFile> Files)
{
    /// <summary>
    /// Try to find the given file.
    /// </summary>
    /// <param name="name">The name of the file.</param>
    /// <returns>A file or an error.</returns>
    public Result<RawFile> FindFile(string name)
    {
        var foundFile = Files.OfType<RawFile?>().FirstOrDefault
            (x => Path.GetFileName(((RawFile)x!).Path) == name, null);
        if (foundFile is null)
        {
            return new NotFoundError($"Could not find file {name} in archive.");
        }

        return (RawFile)foundFile;
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Files/FileType.cs => Data/NosSmooth.Data.NOSFiles/Files/FileType.cs +23 -0
@@ 0,0 1,23 @@
//
//  FileType.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.Files;

/// <summary>
/// A type of a file.
/// </summary>
public enum FileType
{
    /// <summary>
    /// The file is a text file.
    /// </summary>
    Text,

    /// <summary>
    /// The file is a binary file with special meaning.
    /// </summary>
    Binary
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Files/RawFile.cs => Data/NosSmooth.Data.NOSFiles/Files/RawFile.cs +27 -0
@@ 0,0 1,27 @@
//
//  RawFile.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.Net.Mime;
using System.Security.Cryptography;

namespace NosSmooth.Data.NOSFiles.Files;

/// <summary>
/// A file.
/// </summary>
/// <param name="FileType">The type of the file, if known.</param>
/// <param name="Path">The path to the file.</param>
/// <param name="Length">The length of the content.</param>
/// <param name="Content">The binary content of the file.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Upper case is standard.")]
public record struct RawFile
(
    FileType? FileType,
    string Path,
    long Length,
    byte[] Content
);
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Files/ReadFile.cs => Data/NosSmooth.Data.NOSFiles/Files/ReadFile.cs +16 -0
@@ 0,0 1,16 @@
//
//  ReadFile.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;

namespace NosSmooth.Data.NOSFiles.Files;

[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Upper case is standard.")]
public record struct ReadFile<TContent>
(
    string Path,
    TContent Content
);
\ 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 Task<Result<IItemInfo>> GetItemInfoAsync(int vnum, CancellationToken ct = default)
    {
        if (!_nostaleData.Items.ContainsKey(vnum))
        {
            return Task.FromResult(Result<IItemInfo>.FromError(new NotFoundError($"Couldn't find item {vnum}")));
        }

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

    /// <inheritdoc />
    public Task<Result<IMapInfo>> GetMapInfoAsync(int id, CancellationToken ct = default)
    {
        if (!_nostaleData.Maps.ContainsKey(id))
        {
            return Task.FromResult(Result<IMapInfo>.FromError(new NotFoundError($"Couldn't find map {id}")));
        }

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

    /// <inheritdoc />
    public Task<Result<IMonsterInfo>> GetMonsterInfoAsync(int vnum, CancellationToken ct = default)
    {
        if (!_nostaleData.Monsters.ContainsKey(vnum))
        {
            return Task.FromResult(Result<IMonsterInfo>.FromError(new NotFoundError($"Couldn't find monster {vnum}")));
        }

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

    /// <inheritdoc />
    public Task<Result<ISkillInfo>> GetSkillInfoAsync(int vnum, CancellationToken ct = default)
    {
        if (!_nostaleData.Skills.ContainsKey(vnum))
        {
            return Task.FromResult(Result<ISkillInfo>.FromError(new NotFoundError($"Couldn't find skill {vnum}")));
        }

        return Task.FromResult(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 +96 -0
@@ 0,0 1,96 @@
//
//  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 Task<Result<string>> GetTranslationAsync
    (
        TranslationRoot root,
        string key,
        Language? language = default,
        CancellationToken ct = default
    )
    {
        if (!_translations.ContainsKey(language ?? CurrentLanguage))
        {
            return Task.FromResult
            (
                Result<string>.FromError
                    (new NotFoundError($"The requested language {language ?? CurrentLanguage} is not parsed."))
            );
        }

        var translations = _translations[language ?? CurrentLanguage];
        if (!translations.ContainsKey(root))
        {
            return Task.FromResult(Result<string>.FromSuccess(key));
        }

        var keyTranslations = translations[root];
        if (!keyTranslations.ContainsKey(key))
        {
            return Task.FromResult(Result<string>.FromSuccess(key));
        }

        return Task.FromResult(Result<string>.FromSuccess(keyTranslations[key]));
    }

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

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

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

A Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj => Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj +21 -0
@@ 0,0 1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <Description>Nos smooth's NosTale data implementation using .NOS files.</Description>
    </PropertyGroup>

    <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).Distinct())
        {
            var langString = language.ToString().ToUpper();
            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.Uk };

    /// <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 +102 -0
@@ 0,0 1,102 @@
//
//  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('\r', '\n').ToArray();
        _currentLine = 0;
        _file = file;
        _separator = "VNUM";
    }

    /// <summary>
    /// Gets whether the reader has reached the end.
    /// </summary>
    public bool ReachedEnd => _currentLine >= _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)
    {
        return ReadItem(out item, _currentLine > 0);
    }

    private bool ReadItem([NotNullWhen(true)] out DatItem? item, bool readFirstItem)
    {
        if (ReachedEnd)
        {
            item = null;
            return false;
        }

        int startLine = _currentLine;
        if (readFirstItem && _lines[_currentLine].Trim().StartsWith(_separator))
        {
            _currentLine++;
        }

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

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

        var dictionary = new Dictionary<string, IReadOnlyList<DatEntry>>();
        for (int i = startLine; i < _currentLine; i++)
        {
            var line = _lines[i];
            var splitted = line.Trim().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('\r', '\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('\r', '\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.GetValueOrDefault(id, new TranslatableString(TranslationRoot.Map, "Map")),
                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

A Data/NosSmooth.Data.NOSFiles/Readers/FileReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/FileReader.cs +98 -0
@@ 0,0 1,98 @@
//
//  FileReader.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.Runtime.InteropServices;
using NosSmooth.Data.NOSFiles.Errors;
using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Readers;

/// <summary>
/// Reader of files.
/// </summary>
public class FileReader
{
    private readonly IReadOnlyList<IFileTypeReader> _typeReaders;

    /// <summary>
    /// Initializes a new instance of the <see cref="FileReader"/> class.
    /// </summary>
    /// <param name="typeReaders">The readers of specific types.</param>
    public FileReader(IEnumerable<IFileTypeReader> typeReaders)
    {
        _typeReaders = typeReaders.ToArray();
    }

    /// <summary>
    /// Get a file type reader for the given file.
    /// </summary>
    /// <param name="file">The file.</param>
    /// <returns>A type reader or an error.</returns>
    public Result<IFileTypeReader> GetFileTypeReader(RawFile file)
    {
        foreach (var typeReader in _typeReaders)
        {
            if (typeReader.SupportsFile(file))
            {
                return Result<IFileTypeReader>.FromSuccess(typeReader);
            }
        }

        return new UnknownFileTypeError(file);
    }

    /// <summary>
    /// Read the given file.
    /// </summary>
    /// <param name="file">The raw file.</param>
    /// <typeparam name="TContent">The type of the content to assume.</typeparam>
    /// <returns>The content or an error.</returns>
    public Result<ReadFile<TContent>> Read<TContent>(RawFile file)
    {
        var fileReaderResult = GetFileTypeReader(file);
        if (!fileReaderResult.IsSuccess)
        {
            return Result<ReadFile<TContent>>.FromError(fileReaderResult);
        }

        var fileReader = fileReaderResult.Entity;
        var readResult = fileReader.Read(file);
        if (!readResult.IsSuccess)
        {
            return Result<ReadFile<TContent>>.FromError(readResult);
        }

        try
        {
            var content = (ReadFile<TContent>)readResult.Entity;
            return content;
        }
        catch (Exception e)
        {
            return e;
        }
    }

    /// <summary>
    /// Read a file from a filesystem path.
    /// </summary>
    /// <param name="path">The path to the file.</param>
    /// <typeparam name="TContent">The type of the content to assume.</typeparam>
    /// <returns>The content or an error.</returns>
    public Result<ReadFile<TContent>> ReadFileSystemFile<TContent>(string path)
    {
        try
        {
            var readData = File.ReadAllBytes(path);
            return Read<TContent>(new RawFile(null, path, readData.Length, readData));
        }
        catch (Exception e)
        {
            return e;
        }
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Readers/IFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/IFileTypeReader.cs +45 -0
@@ 0,0 1,45 @@
//
//  IFileTypeReader.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.Xml;
using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Readers;

/// <summary>
/// Reader of a particular file type.
/// </summary>
public interface IFileTypeReader
{
    /// <summary>
    /// Checks whether the given raw file is supported by this reader.
    /// </summary>
    /// <param name="file">The file to check.</param>
    /// <returns>Whether the file can be read by this reader.</returns>
    public bool SupportsFile(RawFile file);

    /// <summary>
    /// Read the given file contents.
    /// </summary>
    /// <param name="file">The file to read.</param>
    /// <returns>Contents of the file or an error.</returns>
    public Result<object> Read(RawFile file);
}

/// <summary>
/// Reader of a particular file type.
/// </summary>
/// <typeparam name="TContent">The content of the file.</typeparam>
public interface IFileTypeReader<TContent> : IFileTypeReader
{
    /// <summary>
    /// Read the given file contents.
    /// </summary>
    /// <param name="file">The file to read.</param>
    /// <returns>Contents of the file or an error.</returns>
    public Result<ReadFile<TContent>> ReadExact(RawFile file);
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Readers/Types/BaseFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/Types/BaseFileTypeReader.cs +32 -0
@@ 0,0 1,32 @@
//
//  BaseFileTypeReader.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.Readers.Types;

/// <inheritdoc />
public abstract class BaseFileTypeReader<TContent> : IFileTypeReader<TContent>
{
    /// <inheritdoc />
    public abstract Result<ReadFile<TContent>> ReadExact(RawFile file);

    /// <inheritdoc />
    public abstract bool SupportsFile(RawFile file);

    /// <inheritdoc />
    public Result<object> Read(RawFile file)
    {
        var readResult = ReadExact(file);
        if (!readResult.IsSuccess)
        {
            return Result<object>.FromError(readResult);
        }

        return Result<object>.FromSuccess(readResult.Entity);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Readers/Types/NosTextFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/Types/NosTextFileTypeReader.cs +99 -0
@@ 0,0 1,99 @@
//
//  NosTextFileTypeReader.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.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using NosSmooth.Data.NOSFiles.Decryptors;
using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Readers.Types;

/// <summary>
/// Reader of .NOS files that contain text files.
/// </summary>
public class NosTextFileTypeReader : BaseFileTypeReader<FileArchive>
{
    private readonly IDecryptor _datDecryptor;
    private readonly IDecryptor _lstDecryptor;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosTextFileTypeReader"/> class.
    /// </summary>
    public NosTextFileTypeReader()
    {
        _datDecryptor = new DatDecryptor();
        _lstDecryptor = new LstDecryptor();
    }

    /// <inheritdoc />
    public override bool SupportsFile(RawFile file)
    {
        // Just checks that the number of the first file is zero.
        //  ?Should? be enough for excluding all other types.
        // This is questionable. The thing is
        // that there is not really an header for these files.
        // TODO: maybe try to read till the first file name, size etc.
        // and verify the name is a string, the number or the file is zero etc?
        if (file.Length < 10)
        {
            return false;
        }

        return file.Path.EndsWith
            ("NOS") && file.Content[4] == 1 && file.Content[5] == 0 && file.Content[6] == 0 && file.Content[7] == 0;
    }

    /// <inheritdoc />
    public override Result<ReadFile<FileArchive>> ReadExact(RawFile file)
    {
        List<RawFile> files = new List<RawFile>();
        ReadOnlySpan<byte> content = file.Content;
        var filesCount = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4); // skip file count

        for (var i = 0; i < filesCount; i++)
        {
            var fileResult = ReadFile(ref content);
            if (!fileResult.IsSuccess)
            {
                return Result<ReadFile<FileArchive>>.FromError(fileResult);
            }

            files.Add(fileResult.Entity);
        }

        // TODO: read time information?
        return new ReadFile<FileArchive>(file.Path, new FileArchive(files));
    }

    private Result<RawFile> ReadFile(ref ReadOnlySpan<byte> content)
    {
        // skip file number, it is of no interest, I think.
        content = content.Slice(4);
        var stringNameLength = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);
        var fileName = Encoding.ASCII.GetString(content.Slice(0, stringNameLength).ToArray());
        content = content.Slice(stringNameLength);
        var isDat = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);
        var fileSize = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);
        var fileContent = content.Slice(0, fileSize);
        content = content.Slice(fileSize);

        var datDecryptResult = isDat != 0 ? _datDecryptor.Decrypt(fileContent) : _lstDecryptor.Decrypt(fileContent);
        if (!datDecryptResult.IsSuccess)
        {
            return Result<RawFile>.FromError(datDecryptResult);
        }

        return new RawFile(FileType.Text, fileName, fileSize, datDecryptResult.Entity);
    }
}
\ No newline at end of file

A Data/NosSmooth.Data.NOSFiles/Readers/Types/NosZlibFileTypeReader.cs => Data/NosSmooth.Data.NOSFiles/Readers/Types/NosZlibFileTypeReader.cs +97 -0
@@ 0,0 1,97 @@
//
//  NosZlibFileTypeReader.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.Buffers;
using System.Buffers.Binary;
using System.Data.Common;
using System.IO.Compression;
using System.Text;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using NosSmooth.Data.NOSFiles.Files;
using Remora.Results;

namespace NosSmooth.Data.NOSFiles.Readers.Types;

/// <inheritdoc />
public class NosZlibFileTypeReader : BaseFileTypeReader<FileArchive>
{
    /// <inheritdoc />
    public override Result<ReadFile<FileArchive>> ReadExact(RawFile file)
    {
        using var fileStream = new MemoryStream(file.Content, false);
        ReadOnlySpan<byte> contentFromStart = file.Content;
        ReadOnlySpan<byte> content = contentFromStart;
        content = content.Slice(16); // skip header
        List<RawFile> files = new List<RawFile>();
        var filesCount = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);

        content = content.Slice(1); // separator
        for (var i = 0; i < filesCount; i++)
        {
            var id = BinaryPrimitives.ReadInt32LittleEndian(content);
            var offset = BinaryPrimitives.ReadInt32LittleEndian(content.Slice(4));
            content = content.Slice(8);

            fileStream.Seek(offset, SeekOrigin.Begin);
            var readFileResult = ReadFile(id, fileStream, contentFromStart.Slice(offset));
            if (!readFileResult.IsSuccess)
            {
                return Result<ReadFile<FileArchive>>.FromError(readFileResult);
            }

            files.Add(readFileResult.Entity);
        }

        return new ReadFile<FileArchive>(file.Path, new FileArchive(files));
    }

    /// <inheritdoc />
    public override bool SupportsFile(RawFile file)
    {
        if (file.Length < 16)
        {
            return false;
        }

        var header = GetHeader(file.Content);
        return header.StartsWith("NT Data 05");
    }

    private ReadOnlySpan<char> GetHeader(ReadOnlySpan<byte> content)
    {
        return Encoding.ASCII.GetString(content.Slice(0, 16));
    }

    private Result<RawFile> ReadFile(int id, MemoryStream stream, ReadOnlySpan<byte> content)
    {
        // int creationDate = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);
        int dataSize = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);
        int compressedDataSize = BinaryPrimitives.ReadInt32LittleEndian(content);
        content = content.Slice(4);
        bool isCompressed = content[0] != 0;
        content = content.Slice(1);
        ReadOnlySpan<byte> data = content.Slice(0, compressedDataSize);
        byte[]? dataArray = null;

        if (isCompressed)
        {
            stream.Seek(4 + 4 + 4 + 1, SeekOrigin.Current);
            using var decompressionStream = new InflaterInputStream(stream)
            {
                IsStreamOwner = false
            };
            dataArray = new byte[dataSize];
            using var outputStream = new MemoryStream(dataSize);
            decompressionStream.CopyTo(outputStream);
        }

        // TODO: check that data size matches data.Length?
        return new RawFile(FileType.Binary, id.ToString(), data.Length, dataArray ?? data.ToArray());
    }
}
\ No newline at end of file

M NosSmooth.sln => NosSmooth.sln +62 -0
@@ 28,6 28,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Packets", "Packet
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.PacketSerializer.Abstractions", "Packets\NosSmooth.PacketSerializer.Abstractions\NosSmooth.PacketSerializer.Abstractions.csproj", "{CF03BCEA-EB5B-427F-8576-7DA7EB869BDC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Data.Abstractions", "Data\NosSmooth.Data.Abstractions\NosSmooth.Data.Abstractions.csproj", "{7901F7FF-FB76-4A4C-8DCA-F74543624130}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Data.Database", "Data\NosSmooth.Data.Database\NosSmooth.Data.Database.csproj", "{C4114AC1-72E8-46DA-9B4B-A4C942004492}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Data.NOSFiles", "Data\NosSmooth.Data.NOSFiles\NosSmooth.Data.NOSFiles.csproj", "{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Data.CLI", "Data\NosSmooth.Data.CLI\NosSmooth.Data.CLI.csproj", "{F1884ADF-6412-4E9B-81FD-357DC5761ADF}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU


@@ 122,6 132,54 @@ Global
		{CF03BCEA-EB5B-427F-8576-7DA7EB869BDC}.Release|x64.Build.0 = Release|Any CPU
		{CF03BCEA-EB5B-427F-8576-7DA7EB869BDC}.Release|x86.ActiveCfg = Release|Any CPU
		{CF03BCEA-EB5B-427F-8576-7DA7EB869BDC}.Release|x86.Build.0 = Release|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Debug|x64.ActiveCfg = Debug|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Debug|x64.Build.0 = Debug|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Debug|x86.ActiveCfg = Debug|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Debug|x86.Build.0 = Debug|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Release|Any CPU.Build.0 = Release|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Release|x64.ActiveCfg = Release|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Release|x64.Build.0 = Release|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Release|x86.ActiveCfg = Release|Any CPU
		{7901F7FF-FB76-4A4C-8DCA-F74543624130}.Release|x86.Build.0 = Release|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Debug|x64.ActiveCfg = Debug|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Debug|x64.Build.0 = Debug|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Debug|x86.ActiveCfg = Debug|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Debug|x86.Build.0 = Debug|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Release|Any CPU.Build.0 = Release|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Release|x64.ActiveCfg = Release|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Release|x64.Build.0 = Release|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Release|x86.ActiveCfg = Release|Any CPU
		{C4114AC1-72E8-46DA-9B4B-A4C942004492}.Release|x86.Build.0 = Release|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Debug|x64.ActiveCfg = Debug|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Debug|x64.Build.0 = Debug|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Debug|x86.ActiveCfg = Debug|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Debug|x86.Build.0 = Debug|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Release|Any CPU.Build.0 = Release|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Release|x64.ActiveCfg = Release|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Release|x64.Build.0 = Release|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Release|x86.ActiveCfg = Release|Any CPU
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99}.Release|x86.Build.0 = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Debug|x64.ActiveCfg = Debug|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Debug|x64.Build.0 = Debug|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Debug|x86.ActiveCfg = Debug|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Debug|x86.Build.0 = Debug|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|Any CPU.Build.0 = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x64.ActiveCfg = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x64.Build.0 = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x86.ActiveCfg = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE


@@ 134,6 192,10 @@ Global
		{C4DAFD83-C6DC-4597-AA1F-BA2F3ABB612C} = {54A49AC2-55B3-4156-8023-41C56719EBB5}
		{86B4ED0C-CD28-4C6C-B58E-B4B1F7AAD683} = {54A49AC2-55B3-4156-8023-41C56719EBB5}
		{CF03BCEA-EB5B-427F-8576-7DA7EB869BDC} = {54A49AC2-55B3-4156-8023-41C56719EBB5}
		{7901F7FF-FB76-4A4C-8DCA-F74543624130} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
		{C4114AC1-72E8-46DA-9B4B-A4C942004492} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}

Do not follow this link