~ruther/NosSmooth

05c2624a4a566cda726bfd76e8a8dc4affc463b1 — František Boháček 3 years ago b86adad
feat(data): add database nostale data implementation
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.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

M Data/NosSmooth.Data.Database/NosSmooth.Data.Database.csproj => Data/NosSmooth.Data.Database/NosSmooth.Data.Database.csproj +10 -0
@@ 6,4 6,14 @@
        <Nullable>enable</Nullable>
    </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

Do not follow this link