Add phase 1 critical import tool
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/RolemasterDb.App/RolemasterDb.App.csproj" />
|
||||
<Project Path="src/RolemasterDb.ImportTool/RolemasterDb.ImportTool.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
12
sources/critical-import-manifest.json
Normal file
12
sources/critical-import-manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"tables": [
|
||||
{
|
||||
"slug": "slash",
|
||||
"displayName": "Slash Critical Strike Table",
|
||||
"family": "standard",
|
||||
"extractionMethod": "text",
|
||||
"pdfPath": "sources/Slash.pdf",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,12 +12,12 @@ public static class RolemasterDbInitializer
|
||||
|
||||
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
|
||||
|
||||
if (await dbContext.AttackTables.AnyAsync(cancellationToken) || await dbContext.CriticalTables.AnyAsync(cancellationToken))
|
||||
if (await dbContext.AttackTables.AnyAsync(cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RolemasterSeedData.Seed(dbContext);
|
||||
RolemasterSeedData.SeedAttackStarterData(dbContext);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,28 @@ namespace RolemasterDb.App.Data;
|
||||
|
||||
public static class RolemasterSeedData
|
||||
{
|
||||
public static void Seed(RolemasterDbContext dbContext)
|
||||
public static void SeedAttackStarterData(RolemasterDbContext dbContext)
|
||||
{
|
||||
var armorTypes = CreateArmorTypes();
|
||||
dbContext.ArmorTypes.AddRange(armorTypes);
|
||||
List<ArmorType> armorTypes;
|
||||
|
||||
if (dbContext.ArmorTypes.Any())
|
||||
{
|
||||
armorTypes = dbContext.ArmorTypes.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
armorTypes = CreateArmorTypes();
|
||||
dbContext.ArmorTypes.AddRange(armorTypes);
|
||||
}
|
||||
|
||||
if (dbContext.AttackTables.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var armorLookup = armorTypes.ToDictionary(item => item.Code, StringComparer.OrdinalIgnoreCase);
|
||||
var attackTables = CreateAttackTables(armorLookup);
|
||||
dbContext.AttackTables.AddRange(attackTables);
|
||||
|
||||
var criticalTables = CreateCriticalTables();
|
||||
dbContext.CriticalTables.AddRange(criticalTables);
|
||||
}
|
||||
|
||||
private static List<ArmorType> CreateArmorTypes() =>
|
||||
@@ -131,164 +142,6 @@ public static class RolemasterSeedData
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CriticalTable> CreateCriticalTables()
|
||||
{
|
||||
return
|
||||
[
|
||||
CreateCriticalTable(
|
||||
slug: "slash",
|
||||
displayName: "Slash Critical",
|
||||
notes: "Starter subset derived from the critical-table schema design notes.",
|
||||
matrix: new Dictionary<string, (string Description, string Affix)[]>
|
||||
{
|
||||
["A"] =
|
||||
[
|
||||
("Glancing slash across the forearm.", "+2 hits, 1 bleed"),
|
||||
("Rib-level cut knocks the foe back a step.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Deep cut to the thigh unbalances the target.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Shoulder opened; guard collapses.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Neck line opened. The fight is over.", "Instant death")
|
||||
],
|
||||
["B"] =
|
||||
[
|
||||
("Slash cuts through the hand and weapon grip wavers.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Diagonal cut across the chest steals breath.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Hip strike drives the foe to one knee.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Sword arm carved deeply; weapon drops.", "+10 hits, 4 stun, 5 bleed"),
|
||||
("Throat opened in a full red arc.", "Instant death")
|
||||
],
|
||||
["C"] =
|
||||
[
|
||||
("Forearm split to the bone; parry ruined.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Wide cut over the ribs spins the foe.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Hamstring sliced; prone unless magically supported.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Slash rips across face and collar. Vision swims.", "+12 hits, 5 stun, 7 bleed"),
|
||||
("Head nearly severed.", "Instant death")
|
||||
],
|
||||
["D"] =
|
||||
[
|
||||
("Deep abdominal slice spills momentum and blood together.", "+8 hits, 3 stun, 5 bleed"),
|
||||
("Sword tracks from shoulder to sternum. Collapse likely.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Leg cut through muscle; foe falls hard.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Upper arm nearly removed; weapon arm useless.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Killing cut through the neck.", "Instant death")
|
||||
],
|
||||
["E"] =
|
||||
[
|
||||
("Massive slash opens chest and the foe staggers blindly.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Spine-grazing cut destroys posture and control.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Leg severed below the knee.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Torso opened from clavicle to hip.", "+16 hits, 8 stun, 12 bleed"),
|
||||
("Clean decapitation.", "Instant death")
|
||||
]
|
||||
}),
|
||||
CreateCriticalTable(
|
||||
slug: "puncture",
|
||||
displayName: "Puncture Critical",
|
||||
notes: "Starter subset seeded for missile and thrust attacks.",
|
||||
matrix: new Dictionary<string, (string Description, string Affix)[]>
|
||||
{
|
||||
["A"] =
|
||||
[
|
||||
("Point slips into the shoulder.", "+2 hits, 1 bleed"),
|
||||
("Stab bites into the upper arm.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Short thrust catches the ribs.", "+5 hits, 2 stun, 2 bleed"),
|
||||
("Point pierces the thigh. Movement slows.", "+7 hits, 2 stun, 3 bleed"),
|
||||
("Exact thrust to the throat.", "Instant death")
|
||||
],
|
||||
["B"] =
|
||||
[
|
||||
("Puncture through the palm forces a drop check.", "+4 hits, 1 stun, 2 bleed"),
|
||||
("Arrow buries in the flank.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Deep thrust into the belly doubles the foe over.", "+8 hits, 3 stun, 4 bleed"),
|
||||
("Point through the shoulder locks the arm.", "+10 hits, 4 stun, 5 bleed"),
|
||||
("Eye socket struck cleanly.", "Instant death")
|
||||
],
|
||||
["C"] =
|
||||
[
|
||||
("Stab through the bicep ruins the next parry.", "+6 hits, 2 stun, 3 bleed"),
|
||||
("Arrow punches through lung tissue.", "+8 hits, 3 stun, 5 bleed"),
|
||||
("Thrust under the ribs steals breath and footing.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Deep puncture pins the foe in place.", "+12 hits, 5 stun, 7 bleed"),
|
||||
("Heart strike.", "Instant death")
|
||||
],
|
||||
["D"] =
|
||||
[
|
||||
("Weapon point tears through the abdomen.", "+8 hits, 3 stun, 5 bleed"),
|
||||
("Arrow lodges near the spine; movement nearly gone.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Lance-like thrust through the torso.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Point enters the neck and exits behind the shoulder.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Brain pierced.", "Instant death")
|
||||
],
|
||||
["E"] =
|
||||
[
|
||||
("Massive puncture opens the chest cavity.", "+10 hits, 4 stun, 6 bleed"),
|
||||
("Shaft buries to the fletching; foe drops.", "+12 hits, 5 stun, 8 bleed"),
|
||||
("Groin-to-spine thrust destroys the body line.", "+14 hits, 6 stun, 10 bleed"),
|
||||
("Perfect impalement through the throat and neck.", "+16 hits, 8 stun, 12 bleed"),
|
||||
("Instantly fatal puncture.", "Instant death")
|
||||
]
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
private static CriticalTable CreateCriticalTable(
|
||||
string slug,
|
||||
string displayName,
|
||||
string notes,
|
||||
IReadOnlyDictionary<string, (string Description, string Affix)[]> matrix)
|
||||
{
|
||||
var table = new CriticalTable
|
||||
{
|
||||
Slug = slug,
|
||||
DisplayName = displayName,
|
||||
Family = "standard",
|
||||
SourceDocument = "Seeded starter data",
|
||||
Notes = notes
|
||||
};
|
||||
|
||||
table.Columns =
|
||||
[
|
||||
new CriticalColumn { ColumnKey = "A", Label = "A", SortOrder = 1 },
|
||||
new CriticalColumn { ColumnKey = "B", Label = "B", SortOrder = 2 },
|
||||
new CriticalColumn { ColumnKey = "C", Label = "C", SortOrder = 3 },
|
||||
new CriticalColumn { ColumnKey = "D", Label = "D", SortOrder = 4 },
|
||||
new CriticalColumn { ColumnKey = "E", Label = "E", SortOrder = 5 }
|
||||
];
|
||||
|
||||
table.RollBands =
|
||||
[
|
||||
new CriticalRollBand { Label = "01-20", MinRoll = 1, MaxRoll = 20, SortOrder = 1 },
|
||||
new CriticalRollBand { Label = "21-40", MinRoll = 21, MaxRoll = 40, SortOrder = 2 },
|
||||
new CriticalRollBand { Label = "41-60", MinRoll = 41, MaxRoll = 60, SortOrder = 3 },
|
||||
new CriticalRollBand { Label = "61-80", MinRoll = 61, MaxRoll = 80, SortOrder = 4 },
|
||||
new CriticalRollBand { Label = "81-100", MinRoll = 81, MaxRoll = 100, SortOrder = 5 }
|
||||
];
|
||||
|
||||
var columnsByKey = table.Columns.ToDictionary(item => item.ColumnKey, StringComparer.OrdinalIgnoreCase);
|
||||
var bandsByOrder = table.RollBands.ToDictionary(item => item.SortOrder);
|
||||
|
||||
foreach (var (columnKey, rows) in matrix)
|
||||
{
|
||||
for (var index = 0; index < rows.Length; index++)
|
||||
{
|
||||
var row = rows[index];
|
||||
table.Results.Add(new CriticalResult
|
||||
{
|
||||
CriticalColumn = columnsByKey[columnKey],
|
||||
CriticalRollBand = bandsByOrder[index + 1],
|
||||
DescriptionText = row.Description,
|
||||
RawAffixText = row.Affix,
|
||||
RawCellText = $"{row.Description} {row.Affix}",
|
||||
ParsedJson = "{}",
|
||||
ParseStatus = "verified"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private static string BuildAttackNotation(int hits, string? severity, string criticalType)
|
||||
{
|
||||
return severity is null
|
||||
|
||||
Binary file not shown.
103
src/RolemasterDb.ImportTool/CriticalImportCommandRunner.cs
Normal file
103
src/RolemasterDb.ImportTool/CriticalImportCommandRunner.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class CriticalImportCommandRunner
|
||||
{
|
||||
private readonly CriticalImportManifestLoader manifestLoader = new();
|
||||
private readonly PdfTextExtractor pdfTextExtractor = new();
|
||||
private readonly StandardCriticalTableParser standardParser = new();
|
||||
|
||||
public async Task<int> RunAsync(ResetOptions options)
|
||||
{
|
||||
if (!string.Equals(options.Target, "criticals", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.Error.WriteLine("Only 'criticals' is supported by phase 1.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var loader = new CriticalImportLoader(ResolveDatabasePath(options.DatabasePath));
|
||||
var removedTableCount = await loader.ResetCriticalsAsync();
|
||||
Console.WriteLine($"Removed {removedTableCount} critical table records.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(ExtractOptions options)
|
||||
{
|
||||
var entry = GetManifestEntry(options.Table);
|
||||
var artifactPaths = CreateArtifactPaths(entry.Slug);
|
||||
await pdfTextExtractor.ExtractAsync(ResolveRepositoryPath(entry.PdfPath), artifactPaths.ExtractedTextPath);
|
||||
Console.WriteLine($"Extracted {entry.Slug} to {artifactPaths.ExtractedTextPath}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(LoadOptions options)
|
||||
{
|
||||
var entry = GetManifestEntry(options.Table);
|
||||
var artifactPaths = CreateArtifactPaths(entry.Slug);
|
||||
|
||||
if (!File.Exists(artifactPaths.ExtractedTextPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Missing extracted text artifact: {artifactPaths.ExtractedTextPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var extractedText = await File.ReadAllTextAsync(artifactPaths.ExtractedTextPath);
|
||||
var parsedTable = Parse(entry, extractedText);
|
||||
var loader = new CriticalImportLoader(ResolveDatabasePath(options.DatabasePath));
|
||||
var result = await loader.LoadAsync(parsedTable);
|
||||
|
||||
Console.WriteLine(
|
||||
$"Loaded {result.TableSlug}: {result.ColumnCount} columns, {result.RollBandCount} roll bands, {result.ResultCount} results.");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task<int> RunAsync(ImportOptions options)
|
||||
{
|
||||
var extractExitCode = await RunAsync(new ExtractOptions
|
||||
{
|
||||
DatabasePath = options.DatabasePath,
|
||||
Table = options.Table
|
||||
});
|
||||
|
||||
if (extractExitCode != 0)
|
||||
{
|
||||
return extractExitCode;
|
||||
}
|
||||
|
||||
return await RunAsync(new LoadOptions
|
||||
{
|
||||
DatabasePath = options.DatabasePath,
|
||||
Table = options.Table
|
||||
});
|
||||
}
|
||||
|
||||
private CriticalImportManifestEntry GetManifestEntry(string tableSlug)
|
||||
{
|
||||
var manifest = manifestLoader.Load(RepositoryPaths.Discover().ManifestPath);
|
||||
return manifest.Tables
|
||||
.Where(item => item.Enabled)
|
||||
.SingleOrDefault(item => string.Equals(item.Slug, tableSlug.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new InvalidOperationException($"No enabled manifest entry was found for '{tableSlug}'.");
|
||||
}
|
||||
|
||||
private ParsedCriticalTable Parse(CriticalImportManifestEntry entry, string extractedText)
|
||||
{
|
||||
if (!string.Equals(entry.Family, "standard", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Family '{entry.Family}' is not supported by phase 1.");
|
||||
}
|
||||
|
||||
return standardParser.Parse(entry, extractedText);
|
||||
}
|
||||
|
||||
private static ImportArtifactPaths CreateArtifactPaths(string slug) =>
|
||||
ImportArtifactPaths.Create(RepositoryPaths.Discover().ArtifactsRootPath, slug);
|
||||
|
||||
private static string ResolveDatabasePath(string? databasePath) =>
|
||||
Path.GetFullPath(databasePath ?? RepositoryPaths.Discover().DefaultDatabasePath);
|
||||
|
||||
private static string ResolveRepositoryPath(string path) =>
|
||||
Path.GetFullPath(Path.Combine(RepositoryPaths.Discover().RootPath, path));
|
||||
}
|
||||
133
src/RolemasterDb.ImportTool/CriticalImportLoader.cs
Normal file
133
src/RolemasterDb.ImportTool/CriticalImportLoader.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using RolemasterDb.App.Data;
|
||||
using RolemasterDb.App.Domain;
|
||||
using RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class CriticalImportLoader(string databasePath)
|
||||
{
|
||||
public async Task<int> ResetCriticalsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = CreateDbContext();
|
||||
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
|
||||
|
||||
var removedTableCount = await dbContext.CriticalTables.CountAsync(cancellationToken);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
await dbContext.CriticalResults.ExecuteDeleteAsync(cancellationToken);
|
||||
await dbContext.CriticalGroups.ExecuteDeleteAsync(cancellationToken);
|
||||
await dbContext.CriticalColumns.ExecuteDeleteAsync(cancellationToken);
|
||||
await dbContext.CriticalRollBands.ExecuteDeleteAsync(cancellationToken);
|
||||
await dbContext.CriticalTables.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return removedTableCount;
|
||||
}
|
||||
|
||||
public async Task<ImportCommandResult> LoadAsync(ParsedCriticalTable table, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = CreateDbContext();
|
||||
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
await DeleteTableAsync(dbContext, table.Slug, cancellationToken);
|
||||
|
||||
var entity = new CriticalTable
|
||||
{
|
||||
Slug = table.Slug,
|
||||
DisplayName = table.DisplayName,
|
||||
Family = table.Family,
|
||||
SourceDocument = table.SourceDocument,
|
||||
Notes = table.Notes
|
||||
};
|
||||
|
||||
entity.Columns = table.Columns
|
||||
.Select(item => new CriticalColumn
|
||||
{
|
||||
ColumnKey = item.ColumnKey,
|
||||
Label = item.Label,
|
||||
Role = item.Role,
|
||||
SortOrder = item.SortOrder
|
||||
})
|
||||
.ToList();
|
||||
|
||||
entity.RollBands = table.RollBands
|
||||
.Select(item => new CriticalRollBand
|
||||
{
|
||||
Label = item.Label,
|
||||
MinRoll = item.MinRoll,
|
||||
MaxRoll = item.MaxRoll,
|
||||
SortOrder = item.SortOrder
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var columnsByKey = entity.Columns.ToDictionary(item => item.ColumnKey, StringComparer.OrdinalIgnoreCase);
|
||||
var rollBandsByLabel = entity.RollBands.ToDictionary(item => item.Label, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
entity.Results = table.Results
|
||||
.Select(item => new CriticalResult
|
||||
{
|
||||
CriticalColumn = columnsByKey[item.ColumnKey],
|
||||
CriticalRollBand = rollBandsByLabel[item.RollBandLabel],
|
||||
RawCellText = item.RawCellText,
|
||||
DescriptionText = item.DescriptionText,
|
||||
RawAffixText = item.RawAffixText,
|
||||
ParsedJson = "{}",
|
||||
ParseStatus = "raw"
|
||||
})
|
||||
.ToList();
|
||||
|
||||
dbContext.CriticalTables.Add(entity);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
|
||||
return new ImportCommandResult(entity.Slug, entity.Columns.Count, entity.RollBands.Count, entity.Results.Count);
|
||||
}
|
||||
|
||||
private RolemasterDbContext CreateDbContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
|
||||
.UseSqlite($"Data Source={databasePath}")
|
||||
.Options;
|
||||
|
||||
return new RolemasterDbContext(options);
|
||||
}
|
||||
|
||||
private static async Task DeleteTableAsync(
|
||||
RolemasterDbContext dbContext,
|
||||
string slug,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tableId = await dbContext.CriticalTables
|
||||
.Where(item => item.Slug == slug)
|
||||
.Select(item => (int?)item.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (tableId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await dbContext.CriticalResults
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await dbContext.CriticalGroups
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await dbContext.CriticalColumns
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await dbContext.CriticalRollBands
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await dbContext.CriticalTables
|
||||
.Where(item => item.Id == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
6
src/RolemasterDb.ImportTool/CriticalImportManifest.cs
Normal file
6
src/RolemasterDb.ImportTool/CriticalImportManifest.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class CriticalImportManifest
|
||||
{
|
||||
public List<CriticalImportManifestEntry> Tables { get; set; } = [];
|
||||
}
|
||||
11
src/RolemasterDb.ImportTool/CriticalImportManifestEntry.cs
Normal file
11
src/RolemasterDb.ImportTool/CriticalImportManifestEntry.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class CriticalImportManifestEntry
|
||||
{
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string Family { get; set; } = string.Empty;
|
||||
public string ExtractionMethod { get; set; } = string.Empty;
|
||||
public string PdfPath { get; set; } = string.Empty;
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
33
src/RolemasterDb.ImportTool/CriticalImportManifestLoader.cs
Normal file
33
src/RolemasterDb.ImportTool/CriticalImportManifestLoader.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class CriticalImportManifestLoader
|
||||
{
|
||||
public CriticalImportManifest Load(string manifestPath)
|
||||
{
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
throw new FileNotFoundException("The critical import manifest could not be found.", manifestPath);
|
||||
}
|
||||
|
||||
var manifest = JsonSerializer.Deserialize<CriticalImportManifest>(
|
||||
File.ReadAllText(manifestPath),
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (manifest is null || manifest.Tables.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("The critical import manifest is empty.");
|
||||
}
|
||||
|
||||
if (manifest.Tables.Any(item => string.IsNullOrWhiteSpace(item.Slug)))
|
||||
{
|
||||
throw new InvalidOperationException("Each manifest entry must declare a slug.");
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
13
src/RolemasterDb.ImportTool/ExtractOptions.cs
Normal file
13
src/RolemasterDb.ImportTool/ExtractOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
[Verb("extract", HelpText = "Extract a critical table PDF into a text artifact.")]
|
||||
public sealed class ExtractOptions
|
||||
{
|
||||
[Value(0, MetaName = "table", Required = true, HelpText = "The manifest slug of the critical table to extract.")]
|
||||
public string Table { get; set; } = string.Empty;
|
||||
|
||||
[Option('d', "db", HelpText = "Optional SQLite database path. Accepted for command consistency.")]
|
||||
public string? DatabasePath { get; set; }
|
||||
}
|
||||
19
src/RolemasterDb.ImportTool/ImportArtifactPaths.cs
Normal file
19
src/RolemasterDb.ImportTool/ImportArtifactPaths.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class ImportArtifactPaths
|
||||
{
|
||||
private ImportArtifactPaths(string directoryPath, string extractedTextPath)
|
||||
{
|
||||
DirectoryPath = directoryPath;
|
||||
ExtractedTextPath = extractedTextPath;
|
||||
}
|
||||
|
||||
public string DirectoryPath { get; }
|
||||
public string ExtractedTextPath { get; }
|
||||
|
||||
public static ImportArtifactPaths Create(string artifactsRootPath, string tableSlug)
|
||||
{
|
||||
var directoryPath = Path.Combine(artifactsRootPath, tableSlug);
|
||||
return new ImportArtifactPaths(directoryPath, Path.Combine(directoryPath, "extracted.txt"));
|
||||
}
|
||||
}
|
||||
9
src/RolemasterDb.ImportTool/ImportCommandResult.cs
Normal file
9
src/RolemasterDb.ImportTool/ImportCommandResult.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class ImportCommandResult(string tableSlug, int columnCount, int rollBandCount, int resultCount)
|
||||
{
|
||||
public string TableSlug { get; } = tableSlug;
|
||||
public int ColumnCount { get; } = columnCount;
|
||||
public int RollBandCount { get; } = rollBandCount;
|
||||
public int ResultCount { get; } = resultCount;
|
||||
}
|
||||
13
src/RolemasterDb.ImportTool/ImportOptions.cs
Normal file
13
src/RolemasterDb.ImportTool/ImportOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
[Verb("import", HelpText = "Extract and load a critical table in one step.")]
|
||||
public sealed class ImportOptions
|
||||
{
|
||||
[Value(0, MetaName = "table", Required = true, HelpText = "The manifest slug of the critical table to import.")]
|
||||
public string Table { get; set; } = string.Empty;
|
||||
|
||||
[Option('d', "db", HelpText = "Optional SQLite database path.")]
|
||||
public string? DatabasePath { get; set; }
|
||||
}
|
||||
13
src/RolemasterDb.ImportTool/LoadOptions.cs
Normal file
13
src/RolemasterDb.ImportTool/LoadOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
[Verb("load", HelpText = "Load a parsed critical table from its extracted text artifact.")]
|
||||
public sealed class LoadOptions
|
||||
{
|
||||
[Value(0, MetaName = "table", Required = true, HelpText = "The manifest slug of the critical table to load.")]
|
||||
public string Table { get; set; } = string.Empty;
|
||||
|
||||
[Option('d', "db", HelpText = "Optional SQLite database path.")]
|
||||
public string? DatabasePath { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
public sealed class ParsedCriticalColumn(string columnKey, string label, string role, int sortOrder)
|
||||
{
|
||||
public string ColumnKey { get; } = columnKey;
|
||||
public string Label { get; } = label;
|
||||
public string Role { get; } = role;
|
||||
public int SortOrder { get; } = sortOrder;
|
||||
}
|
||||
15
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalResult.cs
Normal file
15
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalResult.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
public sealed class ParsedCriticalResult(
|
||||
string columnKey,
|
||||
string rollBandLabel,
|
||||
string rawCellText,
|
||||
string descriptionText,
|
||||
string? rawAffixText)
|
||||
{
|
||||
public string ColumnKey { get; } = columnKey;
|
||||
public string RollBandLabel { get; } = rollBandLabel;
|
||||
public string RawCellText { get; } = rawCellText;
|
||||
public string DescriptionText { get; } = descriptionText;
|
||||
public string? RawAffixText { get; } = rawAffixText;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
public sealed class ParsedCriticalRollBand(string label, int minRoll, int? maxRoll, int sortOrder)
|
||||
{
|
||||
public string Label { get; } = label;
|
||||
public int MinRoll { get; } = minRoll;
|
||||
public int? MaxRoll { get; } = maxRoll;
|
||||
public int SortOrder { get; } = sortOrder;
|
||||
}
|
||||
21
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalTable.cs
Normal file
21
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalTable.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
public sealed class ParsedCriticalTable(
|
||||
string slug,
|
||||
string displayName,
|
||||
string family,
|
||||
string sourceDocument,
|
||||
string? notes,
|
||||
IReadOnlyList<ParsedCriticalColumn> columns,
|
||||
IReadOnlyList<ParsedCriticalRollBand> rollBands,
|
||||
IReadOnlyList<ParsedCriticalResult> results)
|
||||
{
|
||||
public string Slug { get; } = slug;
|
||||
public string DisplayName { get; } = displayName;
|
||||
public string Family { get; } = family;
|
||||
public string SourceDocument { get; } = sourceDocument;
|
||||
public string? Notes { get; } = notes;
|
||||
public IReadOnlyList<ParsedCriticalColumn> Columns { get; } = columns;
|
||||
public IReadOnlyList<ParsedCriticalRollBand> RollBands { get; } = rollBands;
|
||||
public IReadOnlyList<ParsedCriticalResult> Results { get; } = results;
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
public sealed class StandardCriticalTableParser
|
||||
{
|
||||
private static readonly Regex ColumnRegex = new(@"\b([A-E])\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex RollBandRegex = new(@"^\s*(?<label>\d{2,3}(?:-\d{2,3})?|\d{2,3}\+)\s*$", RegexOptions.Compiled);
|
||||
private static readonly Regex RollBandLineRegex = new(@"^\s*(?<label>\d{2,3}(?:-\d{2,3})?|\d{2,3}\+)(?<rest>\s+.*)?$", RegexOptions.Compiled);
|
||||
|
||||
public ParsedCriticalTable Parse(CriticalImportManifestEntry entry, string extractedText)
|
||||
{
|
||||
var lines = extractedText.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\f', '\n')
|
||||
.Split('\n');
|
||||
|
||||
var headerIndex = Array.FindIndex(lines, IsColumnHeaderLine);
|
||||
if (headerIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException("The standard table header could not be found in the extracted text.");
|
||||
}
|
||||
|
||||
var columnStarts = GetColumnStarts(lines[headerIndex]);
|
||||
var boundaries = GetColumnBoundaries(columnStarts);
|
||||
var columns = columnStarts
|
||||
.Select((item, index) => new ParsedCriticalColumn(item.Label, item.Label, "severity", index + 1))
|
||||
.ToList();
|
||||
|
||||
var firstRollBandIndex = FindNextRollBandIndex(lines, headerIndex + 1);
|
||||
if (firstRollBandIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException("No roll bands were found in the extracted text.");
|
||||
}
|
||||
|
||||
var keyLineIndex = Array.FindIndex(lines, firstRollBandIndex, item => item.TrimStart().StartsWith("Key:", StringComparison.OrdinalIgnoreCase));
|
||||
if (keyLineIndex < 0)
|
||||
{
|
||||
keyLineIndex = lines.Length;
|
||||
}
|
||||
|
||||
var leadingLines = lines[(headerIndex + 1)..firstRollBandIndex]
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
|
||||
var rollBands = new List<ParsedCriticalRollBand>();
|
||||
var results = new List<ParsedCriticalResult>();
|
||||
var currentLabel = string.Empty;
|
||||
var currentRowLines = new List<string>();
|
||||
var rowIndex = 0;
|
||||
|
||||
void FlushCurrentRow()
|
||||
{
|
||||
if (string.IsNullOrEmpty(currentLabel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rowIndex++;
|
||||
var rollBand = CreateRollBand(currentLabel, rowIndex);
|
||||
rollBands.Add(rollBand);
|
||||
|
||||
var cellLines = SplitRowLines(currentRowLines, boundaries, columns.Count);
|
||||
for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
|
||||
{
|
||||
var rawCellLines = cellLines[columnIndex]
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToList();
|
||||
|
||||
var rawAffixLines = rawCellLines
|
||||
.Where(IsAffixLikeLine)
|
||||
.ToList();
|
||||
|
||||
var descriptionLines = rawCellLines
|
||||
.Where(item => !IsAffixLikeLine(item))
|
||||
.ToList();
|
||||
|
||||
results.Add(new ParsedCriticalResult(
|
||||
columns[columnIndex].ColumnKey,
|
||||
rollBand.Label,
|
||||
string.Join(Environment.NewLine, rawCellLines),
|
||||
CollapseWhitespace(string.Join(' ', descriptionLines)),
|
||||
rawAffixLines.Count == 0 ? null : string.Join(Environment.NewLine, rawAffixLines)));
|
||||
}
|
||||
|
||||
currentLabel = string.Empty;
|
||||
currentRowLines = new List<string>();
|
||||
}
|
||||
|
||||
for (var lineIndex = firstRollBandIndex; lineIndex < keyLineIndex; lineIndex++)
|
||||
{
|
||||
if (TryParseRollBandLine(lines[lineIndex], out var label, out var trailingText))
|
||||
{
|
||||
var trailingTextBelongsToCurrentRow = IsAffixLikeLine(trailingText);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trailingText) &&
|
||||
!string.IsNullOrEmpty(currentLabel) &&
|
||||
!trailingTextBelongsToCurrentRow)
|
||||
{
|
||||
currentRowLines.Add(trailingText);
|
||||
}
|
||||
|
||||
FlushCurrentRow();
|
||||
currentLabel = label;
|
||||
if (rowIndex == 0)
|
||||
{
|
||||
currentRowLines.AddRange(leadingLines);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trailingText) && trailingTextBelongsToCurrentRow)
|
||||
{
|
||||
currentRowLines.Add(trailingText);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lines[lineIndex]))
|
||||
{
|
||||
currentRowLines.Add(lines[lineIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
FlushCurrentRow();
|
||||
|
||||
return new ParsedCriticalTable(
|
||||
entry.Slug,
|
||||
entry.DisplayName,
|
||||
entry.Family,
|
||||
Path.GetFileName(entry.PdfPath),
|
||||
"Imported from PDF text extraction.",
|
||||
columns,
|
||||
rollBands,
|
||||
results);
|
||||
}
|
||||
|
||||
private static bool IsColumnHeaderLine(string line)
|
||||
{
|
||||
var matches = ColumnRegex.Matches(line);
|
||||
return matches.Count == 5;
|
||||
}
|
||||
|
||||
private static List<(string Label, int Start)> GetColumnStarts(string headerLine)
|
||||
{
|
||||
var matches = ColumnRegex.Matches(headerLine);
|
||||
return matches
|
||||
.Select(match => (match.Groups[1].Value.ToUpperInvariant(), match.Index))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static int[] GetColumnBoundaries(IReadOnlyList<(string Label, int Start)> columns)
|
||||
{
|
||||
var boundaries = new int[columns.Count - 1];
|
||||
for (var index = 0; index < boundaries.Length; index++)
|
||||
{
|
||||
boundaries[index] = (columns[index].Start + columns[index + 1].Start) / 2;
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
private static int FindNextRollBandIndex(IReadOnlyList<string> lines, int startIndex)
|
||||
{
|
||||
for (var index = startIndex; index < lines.Count; index++)
|
||||
{
|
||||
if (TryParseRollBandLine(lines[index], out _, out _))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool TryParseRollBandLabel(string line, out string label)
|
||||
{
|
||||
var match = RollBandRegex.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
label = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
label = match.Groups[1].Value.Replace(" ", string.Empty, StringComparison.Ordinal);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseRollBandLine(string line, out string label, out string trailingText)
|
||||
{
|
||||
var match = RollBandLineRegex.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
label = string.Empty;
|
||||
trailingText = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
label = match.Groups["label"].Value.Replace(" ", string.Empty, StringComparison.Ordinal);
|
||||
|
||||
var restGroup = match.Groups["rest"];
|
||||
trailingText = restGroup.Success
|
||||
? string.Concat(new string(' ', restGroup.Index), restGroup.Value.TrimEnd())
|
||||
: string.Empty;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ParsedCriticalRollBand CreateRollBand(string label, int sortOrder)
|
||||
{
|
||||
if (label.EndsWith('+'))
|
||||
{
|
||||
return new ParsedCriticalRollBand(label, int.Parse(label[..^1]), null, sortOrder);
|
||||
}
|
||||
|
||||
var parts = label.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return parts.Length == 1
|
||||
? new ParsedCriticalRollBand(label, int.Parse(parts[0]), int.Parse(parts[0]), sortOrder)
|
||||
: new ParsedCriticalRollBand(label, int.Parse(parts[0]), int.Parse(parts[1]), sortOrder);
|
||||
}
|
||||
|
||||
private static List<string>[] SplitRowLines(IReadOnlyList<string> rowLines, int[] boundaries, int columnCount)
|
||||
{
|
||||
var result = Enumerable.Range(0, columnCount)
|
||||
.Select(_ => new List<string>())
|
||||
.ToArray();
|
||||
|
||||
foreach (var line in rowLines)
|
||||
{
|
||||
for (var columnIndex = 0; columnIndex < columnCount; columnIndex++)
|
||||
{
|
||||
var start = columnIndex == 0 ? 0 : boundaries[columnIndex - 1];
|
||||
var end = columnIndex == columnCount - 1
|
||||
? line.Length
|
||||
: Math.Min(boundaries[columnIndex], line.Length);
|
||||
|
||||
if (start >= line.Length || end <= start)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segment = line[start..end].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(segment))
|
||||
{
|
||||
result[columnIndex].Add(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsAffixLikeLine(string line)
|
||||
{
|
||||
var value = line.Trim();
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value == "—")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.StartsWith("with ", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("w/o ", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("without ", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("if ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return value.Contains(':', StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return value.StartsWith("+", StringComparison.Ordinal) ||
|
||||
value.StartsWith('∑') ||
|
||||
value.StartsWith('∏') ||
|
||||
value.StartsWith('π') ||
|
||||
value.StartsWith('∫') ||
|
||||
char.IsDigit(value[0]) ||
|
||||
value.Contains(" – ", StringComparison.Ordinal) ||
|
||||
value.Contains("(-", StringComparison.Ordinal) ||
|
||||
value.Contains("(+", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string CollapseWhitespace(string value) =>
|
||||
Regex.Replace(value.Trim(), @"\s+", " ");
|
||||
}
|
||||
34
src/RolemasterDb.ImportTool/PdfTextExtractor.cs
Normal file
34
src/RolemasterDb.ImportTool/PdfTextExtractor.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class PdfTextExtractor
|
||||
{
|
||||
public async Task ExtractAsync(string pdfPath, string outputPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "pdftotext",
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("-layout");
|
||||
startInfo.ArgumentList.Add(pdfPath);
|
||||
startInfo.ArgumentList.Add(outputPath);
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
process.Start();
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
var error = await process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
throw new InvalidOperationException($"pdftotext failed for '{pdfPath}': {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/RolemasterDb.ImportTool/Program.cs
Normal file
28
src/RolemasterDb.ImportTool/Program.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using CommandLine;
|
||||
|
||||
using RolemasterDb.ImportTool;
|
||||
|
||||
var runner = new CriticalImportCommandRunner();
|
||||
|
||||
var exitCode = await Parser.Default.ParseArguments<ResetOptions, ExtractOptions, LoadOptions, ImportOptions>(args)
|
||||
.MapResult(
|
||||
(ResetOptions options) => ExecuteAsync(() => runner.RunAsync(options)),
|
||||
(ExtractOptions options) => ExecuteAsync(() => runner.RunAsync(options)),
|
||||
(LoadOptions options) => ExecuteAsync(() => runner.RunAsync(options)),
|
||||
(ImportOptions options) => ExecuteAsync(() => runner.RunAsync(options)),
|
||||
_ => Task.FromResult(1));
|
||||
|
||||
return exitCode;
|
||||
|
||||
static async Task<int> ExecuteAsync(Func<Task<int>> command)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await command();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(exception.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
34
src/RolemasterDb.ImportTool/RepositoryPaths.cs
Normal file
34
src/RolemasterDb.ImportTool/RepositoryPaths.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
public sealed class RepositoryPaths
|
||||
{
|
||||
private RepositoryPaths(string rootPath)
|
||||
{
|
||||
RootPath = rootPath;
|
||||
ManifestPath = Path.Combine(rootPath, "sources", "critical-import-manifest.json");
|
||||
DefaultDatabasePath = Path.Combine(rootPath, "src", "RolemasterDb.App", "rolemaster.db");
|
||||
ArtifactsRootPath = Path.Combine(rootPath, "artifacts", "import", "critical");
|
||||
}
|
||||
|
||||
public string RootPath { get; }
|
||||
public string ManifestPath { get; }
|
||||
public string DefaultDatabasePath { get; }
|
||||
public string ArtifactsRootPath { get; }
|
||||
|
||||
public static RepositoryPaths Discover()
|
||||
{
|
||||
var probe = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||
|
||||
while (probe is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx")))
|
||||
{
|
||||
return new RepositoryPaths(probe.FullName);
|
||||
}
|
||||
|
||||
probe = probe.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not discover the repository root from the current directory.");
|
||||
}
|
||||
}
|
||||
13
src/RolemasterDb.ImportTool/ResetOptions.cs
Normal file
13
src/RolemasterDb.ImportTool/ResetOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace RolemasterDb.ImportTool;
|
||||
|
||||
[Verb("reset", HelpText = "Remove importer-managed critical data from SQLite.")]
|
||||
public sealed class ResetOptions
|
||||
{
|
||||
[Value(0, MetaName = "target", Required = true, HelpText = "The reset target. Phase 1 supports only 'criticals'.")]
|
||||
public string Target { get; set; } = string.Empty;
|
||||
|
||||
[Option('d', "db", HelpText = "Optional SQLite database path.")]
|
||||
public string? DatabasePath { get; set; }
|
||||
}
|
||||
18
src/RolemasterDb.ImportTool/RolemasterDb.ImportTool.csproj
Normal file
18
src/RolemasterDb.ImportTool/RolemasterDb.ImportTool.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RolemasterDb.App\RolemasterDb.App.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user