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}