From 916b6d019e751528e494bb508c96d0984a0c03f3 Mon Sep 17 00:00:00 2001 From: Alistair Chapman Date: Wed, 19 Aug 2020 14:14:27 +1000 Subject: [PATCH] Initial commit --- .config/dotnet-tools.json | 12 + .github/workflows/build.yml | 49 +++ .gitignore | 365 ++++++++++++++++++ .vscode/launch.json | 28 ++ .vscode/tasks.json | 42 ++ README.md | 43 +++ build.cake | 152 ++++++++ build/helpers.cake | 27 ++ build/version.cake | 19 + src/ACInstallerCreator.sln | 34 ++ src/InstallerCreator/.gitignore | 0 src/InstallerCreator/Commands/AppSettings.cs | 14 + src/InstallerCreator/Commands/BuildCommand.cs | 46 +++ src/InstallerCreator/Commands/PackCommand.cs | 32 ++ src/InstallerCreator/CoreExtensions.cs | 15 + src/InstallerCreator/FileHelpers.cs | 46 +++ src/InstallerCreator/InputHelpers.cs | 41 ++ src/InstallerCreator/InstallerCreator.csproj | 26 ++ src/InstallerCreator/LICENSE | 21 + .../ModInstaller/ModInstallerBuilder.cs | 134 +++++++ src/InstallerCreator/Program.cs | 25 ++ src/InstallerCreator/SkinIdentifier.cs | 97 +++++ src/InstallerCreator/SkinReader.cs | 49 +++ src/omnisharp.json | 25 ++ 24 files changed, 1342 insertions(+) create mode 100644 .config/dotnet-tools.json create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 README.md create mode 100644 build.cake create mode 100644 build/helpers.cake create mode 100644 build/version.cake create mode 100644 src/ACInstallerCreator.sln create mode 100644 src/InstallerCreator/.gitignore create mode 100644 src/InstallerCreator/Commands/AppSettings.cs create mode 100644 src/InstallerCreator/Commands/BuildCommand.cs create mode 100644 src/InstallerCreator/Commands/PackCommand.cs create mode 100644 src/InstallerCreator/CoreExtensions.cs create mode 100644 src/InstallerCreator/FileHelpers.cs create mode 100644 src/InstallerCreator/InputHelpers.cs create mode 100644 src/InstallerCreator/InstallerCreator.csproj create mode 100644 src/InstallerCreator/LICENSE create mode 100644 src/InstallerCreator/ModInstaller/ModInstallerBuilder.cs create mode 100644 src/InstallerCreator/Program.cs create mode 100644 src/InstallerCreator/SkinIdentifier.cs create mode 100644 src/InstallerCreator/SkinReader.cs create mode 100644 src/omnisharp.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..00899d6 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "0.38.4", + "commands": [ + "dotnet-cake" + ] + } + } + } \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6fb9eb3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,49 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: ACMI CI Build + +on: + push: + branches: [ master ] + paths-ignore: + - "docs/**" + - ".github/workflows/docs.yml" + tags: + - 'v*.*.*' + pull_request: + branches: [ master ] + paths-ignore: + - "docs/**" + - ".github/workflows/docs.yml" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.100' + - name: Run the Cake script + uses: cake-build/cake-action@v1 + with: + target: Release + cake-bootstrap: true + - uses: actions/upload-artifact@v1 + name: Upload Artifacts + with: + name: acmi-cli + path: dist/publish/ + - name: Create Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + body: | + Download and unpack the archive for your system (Windows/Linux) from below. + files: | + ./dist/archive/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33d5389 --- /dev/null +++ b/.gitignore @@ -0,0 +1,365 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +tools/ +dist/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..aa863c5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/InstallerCreator/bin/Debug/netcoreapp3.1/acmi.dll", + "args": ["build"], + // "args": ["build", "D:\\Mods\\SkiesUntoldHistoricGalm", "--author", "njmksr", "--title", "\"Skies Untold: Historic Galm Pack\"", "--version", "1.0.0"], + "cwd": "${workspaceFolder}/src/InstallerCreator", //you should change this to the folder with your mod files to test. + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0186143 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/InstallerCreator/InstallerCreator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/InstallerCreator/InstallerCreator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/InstallerCreator/InstallerCreator.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c765f3 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# ACMI: AC7 Mod Installers + +> wow that's a horrible name. my bad. + +### What is it? + +This is a simple command-line app that will automatically generate the XML files required for mod installers (like Vortex) to show a nice guided installer rather than users having to pick the right skin files on their own. + +> These installers are usually known as FOMODs, but aren't actually game-specific. + +This way, with essentially no extra effort, you can include the files in your mod uploads (on Nexus, ModDB, wherever) and if someone installs it with Vortex they'll get a nice wizard walking them through which files to choose. + +### Who is it for? + +**Modders**. More specifically, it's for whoever is _packing_ mod archives. Before you actually ZIP up the mod files, just run `acmi build`, answer the prompts, and the files will be generated for you (in their own `fomod` folder). + +At present, **do not** use this with model mods! It will detect model swaps as a skin file and group it with everything else. This will hopefully be fixable, but it will be fine for skins for custom models. + +> You can also run `acmi zip` to immediately pack all the files in the current directory into a ZIP. + +### How does it work? + +The short version is that ACMI will scan through the mod root (defaults to the current directory) and find _any_ `.pak` files then check for the actual object path in the file headers (something like `/Game/f15c_06_D`) and use that to guess what aircraft and slot it's for. Next, we build a big list of all the skins in all the files, group them by aircraft and skin slot and then build the XML that mod installers will use to provide choices to the user. + +### What else can I do? + +- If you have any README files that you want to be installed, make sure they're in the **root** of the mod files, not in a folder of their own. +- We'll try our best to find a suitable image for the installer + - If there's only one `.png`/`.jpg` in the root folder, we'll use that. If there's more than one, we'll use the first one we find with "preview" in the name. +- If you want to also have pictures for your skin files, just include a `.png` or `.jpg` with the _same name_ as the `.pak` file, beside it. + - For example, for a file at `Skins\UntoldGalmPack_F15C_F15ACipher1994_MageSlot.pak`, we would check for a `Skins\UntoldGalmPack_F15C_F15ACipher1994_MageSlot.png` file and use that if it's found. + +## Usage + +#### Download + +Download the [latest release](https://github.com/agc93/acmi/releases) to somewhere convenient on your computer (put it with your packing scripts if you're using them). + +#### Run + +You'll need a command prompt open (Windows PowerShell, Terminal, whatever) then just run `acmi.exe` (if it's in the current directory) to see the basic help. If your mods are in your current directory, just run `acmi.exe build` to build the installer files, or you can pass a specific directory like `acmi.exe build D:/Mods/MyAwesomeSkinPack` if it's not in the current directory. + +If it completes successfully, you should see a new `fomod` directory in your mod files. Just include that folder when you ZIP up your archive (to upload to Nexus/ModDB/wherever) then anyone who installs your archive with a mod manager will get the nice guided installer. Users who want to install manually can just ignore the fomod folder and it will not affect your other files in any way. \ No newline at end of file diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..8ed86f3 --- /dev/null +++ b/build.cake @@ -0,0 +1,152 @@ +#load "build/helpers.cake" +// #addin nuget:?package=Cake.Docker + +/////////////////////////////////////////////////////////////////////////////// +// ARGUMENTS +/////////////////////////////////////////////////////////////////////////////// + +var target = Argument("target", "Default"); +var configuration = Argument("configuration", "Release"); +// var framework = Argument("framework", "netcoreapp3.1"); + +/////////////////////////////////////////////////////////////////////////////// +// VERSIONING +/////////////////////////////////////////////////////////////////////////////// + +var packageVersion = string.Empty; +#load "build/version.cake" + +/////////////////////////////////////////////////////////////////////////////// +// GLOBAL VARIABLES +/////////////////////////////////////////////////////////////////////////////// + +var solutionPath = File("./src/ACInstallerCreator.sln"); +var solution = ParseSolution(solutionPath); +var projects = GetProjects(solutionPath, configuration); +var artifacts = "./dist/"; +var testResultsPath = MakeAbsolute(Directory(artifacts + "./test-results")); + +/////////////////////////////////////////////////////////////////////////////// +// SETUP / TEARDOWN +/////////////////////////////////////////////////////////////////////////////// + +Setup(ctx => +{ + // Executed BEFORE the first task. + Information("Running tasks..."); + packageVersion = BuildVersion(fallbackVersion); + if (FileExists("./build/.dotnet/dotnet.exe")) { + Information("Using local install of `dotnet` SDK!"); + Context.Tools.RegisterFile("./build/.dotnet/dotnet.exe"); + } +}); + +Teardown(ctx => +{ + // Executed AFTER the last task. + Information("Finished running tasks."); +}); + +/////////////////////////////////////////////////////////////////////////////// +// TASK DEFINITIONS +/////////////////////////////////////////////////////////////////////////////// + +Task("Clean") + .Does(() => +{ + // Clean solution directories. + foreach(var path in projects.AllProjectPaths) + { + Information("Cleaning {0}", path); + CleanDirectories(path + "/**/bin/" + configuration); + CleanDirectories(path + "/**/obj/" + configuration); + } + Information("Cleaning common files..."); + CleanDirectory(artifacts); +}); + +Task("Restore") + .Does(() => +{ + // Restore all NuGet packages. + Information("Restoring solution..."); + foreach (var project in projects.AllProjectPaths) { + DotNetCoreRestore(project.FullPath); + } +}); + +Task("Build") + .IsDependentOn("Clean") + .IsDependentOn("Restore") + .Does(() => +{ + Information("Building solution..."); + var settings = new DotNetCoreBuildSettings { + Configuration = configuration, + NoIncremental = true, + ArgumentCustomization = args => args.Append($"/p:Version={packageVersion}") + }; + DotNetCoreBuild(solutionPath, settings); +}); + +Task("Run-Unit-Tests") + .IsDependentOn("Build") + .Does(() => +{ + CreateDirectory(testResultsPath); + if (projects.TestProjects.Any()) { + + var settings = new DotNetCoreTestSettings { + Configuration = configuration + }; + + foreach(var project in projects.TestProjects) { + DotNetCoreTest(project.Path.FullPath, settings); + } + } +}); + +Task("Post-Build") + .IsDependentOn("Build") + .Does(() => +{ + CopyFiles(GetFiles("./Dockerfile*"), artifacts); +}); + +Task("Publish-Runtime") + .IsDependentOn("Post-Build") + .Does(() => +{ + var projectDir = $"{artifacts}publish"; + CreateDirectory(projectDir); + DotNetCorePublish("./src/InstallerCreator/InstallerCreator.csproj", new DotNetCorePublishSettings { + OutputDirectory = projectDir + "/dotnet-any", + Configuration = configuration, + PublishSingleFile = false, + PublishTrimmed = false + }); + var runtimes = new[] { "linux-x64", "win-x64"}; + foreach (var runtime in runtimes) { + var runtimeDir = $"{projectDir}/{runtime}"; + CreateDirectory(runtimeDir); + Information("Publishing for {0} runtime", runtime); + var settings = new DotNetCorePublishSettings { + Runtime = runtime, + Configuration = configuration, + OutputDirectory = runtimeDir, + PublishSingleFile = true, + PublishTrimmed = true + }; + DotNetCorePublish("./src/InstallerCreator/InstallerCreator.csproj", settings); + CreateDirectory($"{artifacts}archive"); + Zip(runtimeDir, $"{artifacts}archive/acmi-{runtime}.zip"); + } +}); + +Task("Default") + .IsDependentOn("Post-Build"); + +Task("Publish") + .IsDependentOn("Publish-Runtime"); + +RunTarget(target); \ No newline at end of file diff --git a/build/helpers.cake b/build/helpers.cake new file mode 100644 index 0000000..7c19b40 --- /dev/null +++ b/build/helpers.cake @@ -0,0 +1,27 @@ +public class ProjectCollection { + public IEnumerable SourceProjects {get;set;} + public IEnumerable SourceProjectPaths {get { return SourceProjects.Select(p => p.Path.GetDirectory()); } } + public IEnumerable TestProjects {get;set;} + public IEnumerable TestProjectPaths { get { return TestProjects.Select(p => p.Path.GetDirectory()); } } + public IEnumerable AllProjects { get { return SourceProjects.Concat(TestProjects); } } + public IEnumerable AllProjectPaths { get { return AllProjects.Select(p => p.Path.GetDirectory()); } } +} + +ProjectCollection GetProjects(FilePath slnPath, string configuration) { + var solution = ParseSolution(slnPath); + var projects = solution.Projects.Where(p => p.Type != "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"); + var testAssemblies = projects.Where(p => p.Name.Contains(".Tests")).Select(p => p.Path.GetDirectory() + "/bin/" + configuration + "/" + p.Name + ".dll"); + return new ProjectCollection { + SourceProjects = projects.Where(p => !p.Name.Contains(".Tests")), + TestProjects = projects.Where(p => p.Name.Contains(".Tests")) + }; + +} + +public Dictionary GetPackageFormats() { + return new Dictionary { + ["fc32"] = "-t rpm -d libunwind -d libicu", + ["el8"] = "-t rpm -d libicu", + ["bionic"] = "-t deb -d libicu60 -d libssl1.0.0" + }; +} diff --git a/build/version.cake b/build/version.cake new file mode 100644 index 0000000..ff9b2a5 --- /dev/null +++ b/build/version.cake @@ -0,0 +1,19 @@ +#module nuget:?package=Cake.DotNetTool.Module&version=0.4.0 +#tool "dotnet:https://api.nuget.org/v3/index.json?package=GitVersion.Tool&version=5.1.3" + +var fallbackVersion = Argument("force-version", EnvironmentVariable("FALLBACK_VERSION") ?? "0.1.0"); + +string BuildVersion(string fallbackVersion) { + var PackageVersion = string.Empty; + try { + Information("Attempting GitVersion..."); + var versionInfo = GitVersion(); + PackageVersion = versionInfo.NuGetVersionV2; + } catch { + Information($"Falling back to version: {fallbackVersion}"); + PackageVersion = fallbackVersion; + } finally { + Information($"Building for version '{PackageVersion}'"); + } + return PackageVersion; +} \ No newline at end of file diff --git a/src/ACInstallerCreator.sln b/src/ACInstallerCreator.sln new file mode 100644 index 0000000..53dadfa --- /dev/null +++ b/src/ACInstallerCreator.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstallerCreator", "InstallerCreator\InstallerCreator.csproj", "{A438D7F9-6841-436B-A512-E74F18D3B30A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Debug|x64.Build.0 = Debug|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Debug|x86.Build.0 = Debug|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Release|Any CPU.Build.0 = Release|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Release|x64.ActiveCfg = Release|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Release|x64.Build.0 = Release|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Release|x86.ActiveCfg = Release|Any CPU + {A438D7F9-6841-436B-A512-E74F18D3B30A}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/InstallerCreator/.gitignore b/src/InstallerCreator/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/InstallerCreator/Commands/AppSettings.cs b/src/InstallerCreator/Commands/AppSettings.cs new file mode 100644 index 0000000..fdd104d --- /dev/null +++ b/src/InstallerCreator/Commands/AppSettings.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Spectre.Cli; + +namespace InstallerCreator.Commands +{ + public class AppSettings : CommandSettings + { + [CommandArgument(0, "[fileRoot]")] + public string ModRootPath {get;set;} = System.Environment.CurrentDirectory; + + [CommandOption("-v|--verbose")] + public bool Verbose {get;set;} + } +} \ No newline at end of file diff --git a/src/InstallerCreator/Commands/BuildCommand.cs b/src/InstallerCreator/Commands/BuildCommand.cs new file mode 100644 index 0000000..4c9af1d --- /dev/null +++ b/src/InstallerCreator/Commands/BuildCommand.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using InstallerCreator.ModInstaller; +using Spectre.Cli; + +namespace InstallerCreator.Commands +{ + public class BuildCommand : Command + { + public override int Execute(CommandContext context, Settings settings) + { + settings = InputHelpers.PromptMissing(settings); + var skins = FileHelpers.GetSkins(settings.ModRootPath, out var extraFiles); + if (skins.Count == 0) { + Console.WriteLine("Could not locate any PAK files"); + return 204; + } + var builder = new ModInstallerBuilder(settings.ModRootPath, settings.Title.Value, settings.Description.IsSet ? settings.Description.Value : null); + var info = builder.GenerateInfoXml(settings.Author.Value, settings.Version.Value, settings.Groups, settings.Description.Value); + + var moduleConfig = builder.GenerateModuleConfigXml(skins, extraFiles); + builder.WriteToInstallerFiles(info, moduleConfig); + return 0; + } + + public class Settings : AppSettings { + [CommandOption("--author [VALUE]")] + public FlagValue Author {get;set;} + + [CommandOption("--title [VALUE]")] + public FlagValue Title {get;set;} + + [CommandOption("--description [VALUE]")] + public FlagValue Description {get;set;} + + [CommandOption("--version [VERSION]")] + [DefaultValue("1.0.0")] + public FlagValue Version {get;set;} + + [CommandOption("--group ")] + public List Groups {get;set;} = new List {"Models and Textures"}; + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/Commands/PackCommand.cs b/src/InstallerCreator/Commands/PackCommand.cs new file mode 100644 index 0000000..c2282d2 --- /dev/null +++ b/src/InstallerCreator/Commands/PackCommand.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.IO.Compression; +using Spectre.Cli; + +namespace InstallerCreator.Commands +{ + public class PackCommand : Command + { + public override int Execute(CommandContext context, Settings settings) { + var targetFile = string.IsNullOrWhiteSpace(settings.TargetFileName) + ? new DirectoryInfo(settings.ModRootPath).Name + : settings.TargetFileName; + targetFile = Path.GetFileNameWithoutExtension(targetFile) + (settings.Extension.IsSet ? settings.Extension.Value : ".zip"); + var absoluteTarget = Path.Combine(settings.ModRootPath, targetFile); + ZipFile.CreateFromDirectory(settings.ModRootPath, absoluteTarget); + Console.WriteLine($"Created archive file at ${absoluteTarget}"); + return File.Exists(absoluteTarget) ? 0 : 500; + } + + public class Settings : AppSettings { + [CommandOption("-e|--extension [value]")] + [DefaultValue(".zip")] + public FlagValue Extension {get;set;} + + [CommandArgument(1, "[archiveFileName]")] + public string TargetFileName {get;set;} + + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/CoreExtensions.cs b/src/InstallerCreator/CoreExtensions.cs new file mode 100644 index 0000000..27f4947 --- /dev/null +++ b/src/InstallerCreator/CoreExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace InstallerCreator { + public static class CoreExtensions { + public static IEnumerable GetFiles(this string path, + string[] searchPatterns, + SearchOption searchOption = SearchOption.TopDirectoryOnly) { + return searchPatterns.AsParallel() + .SelectMany(searchPattern => + Directory.EnumerateFiles(path, searchPattern, searchOption)); + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/FileHelpers.cs b/src/InstallerCreator/FileHelpers.cs new file mode 100644 index 0000000..73545c5 --- /dev/null +++ b/src/InstallerCreator/FileHelpers.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace InstallerCreator +{ + public static class FileHelpers + { + public static IEnumerable GetAllPakFiles(string filePath) { + var rootDir = new DirectoryInfo(filePath); + var pakFiles = rootDir.EnumerateFiles("*.pak", SearchOption.AllDirectories); + return pakFiles; + } + + public static Dictionary GetSkins(string rootPath, out List extraPaks) { + var reader = new SkinReader(); + extraPaks = new List(); + var pakFiles = GetAllPakFiles(rootPath); + var skinFiles = new Dictionary(); + foreach (var file in pakFiles) + { + var ident = reader.ReadSkinSlot(file.FullName); + if (ident != null) { + skinFiles.Add(Path.GetRelativePath(rootPath, file.FullName), ident); + } else { + extraPaks.Add(Path.GetRelativePath(rootPath, file.FullName)); + } + } + return skinFiles; + } + + public static Dictionary GetSkins(string rootPath) { + var reader = new SkinReader(); + var pakFiles = GetAllPakFiles(rootPath); + var skinFiles = new Dictionary(); + foreach (var file in pakFiles) + { + var ident = reader.ReadSkinSlot(file.FullName); + if (ident != null) { + skinFiles.Add(Path.GetRelativePath(rootPath, file.FullName), ident); + } + } + return skinFiles; + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/InputHelpers.cs b/src/InstallerCreator/InputHelpers.cs new file mode 100644 index 0000000..97a4751 --- /dev/null +++ b/src/InstallerCreator/InputHelpers.cs @@ -0,0 +1,41 @@ +using System.IO; +using InquirerCS; +using InstallerCreator.Commands; +using Semver; + +namespace InstallerCreator +{ + public class InputHelpers + { + public static BuildCommand.Settings PromptMissing(BuildCommand.Settings settings) { + if (!settings.Title.IsSet) { + settings.Title.Value = Question.Input("Please enter your mod name") + .WithDefaultValue(new DirectoryInfo(System.Environment.CurrentDirectory).Name) + .Prompt(); + settings.Title.IsSet = true; + } + if (!settings.Author.IsSet) { + settings.Author.Value = Question.Input("Please enter your name") + .WithDefaultValue(System.Environment.UserName) + .Prompt(); + settings.Author.IsSet = true; + } + if (!settings.Version.IsSet) { + settings.Version.Value = Question.Input("Please enter a valid version number") + .WithDefaultValue("1.0.0") + .WithValidation(input => SemVersion.TryParse(input, out var _), (input) => $"{input} is not a valid version number") + .WithConvertToString(input => SemVersion.Parse(input).ToString()) + .Prompt(); + settings.Version.IsSet = true; + } + if (!settings.Description.IsSet && !(settings.Title.IsSet && settings.Author.IsSet && settings.Version.IsSet)) { + settings.Description.Value = Question.Input("Optionally enter a description") + .WithDefaultValue(string.Empty) + .Prompt(); + settings.Description.IsSet = !string.IsNullOrWhiteSpace(settings.Description.Value); + } + System.Console.WriteLine(); + return settings; + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/InstallerCreator.csproj b/src/InstallerCreator/InstallerCreator.csproj new file mode 100644 index 0000000..226f54c --- /dev/null +++ b/src/InstallerCreator/InstallerCreator.csproj @@ -0,0 +1,26 @@ + + + + Exe + netcoreapp3.1 + acmi + + + + true + true + + win-x64 + + + + + + + + + + + + diff --git a/src/InstallerCreator/LICENSE b/src/InstallerCreator/LICENSE new file mode 100644 index 0000000..0d0f14f --- /dev/null +++ b/src/InstallerCreator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Alistair Chapman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/InstallerCreator/ModInstaller/ModInstallerBuilder.cs b/src/InstallerCreator/ModInstaller/ModInstallerBuilder.cs new file mode 100644 index 0000000..05a0681 --- /dev/null +++ b/src/InstallerCreator/ModInstaller/ModInstallerBuilder.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace InstallerCreator.ModInstaller { + public class ModInstallerBuilder { + private readonly string _rootPath; + private readonly string _title; + private string _description; + + public ModInstallerBuilder(string modRootPath, string title, string description = null) + { + _rootPath = modRootPath; + _title = title; + _description = description; + } + public XDocument GenerateInfoXml(string author, string version, IEnumerable groups, string description = null, string website = null) { + var children = new List { + new XElement("Name", _title), + new XElement("Author", author), + new XElement("Version", version), + new XElement("Groups", groups.Select(g => new XElement("element", g))) + }; + var finalDescription = description ?? _description ?? null; + if (!string.IsNullOrWhiteSpace(finalDescription)) { + _description = finalDescription; + children.Add(new XElement("Description", finalDescription)); + } + if (!string.IsNullOrWhiteSpace(website)) { + children.Add(new XElement("Website", website)); + } + var xdoc = new XDocument(new XElement("fomod", children)); + return xdoc; + } + + public XDocument GenerateModuleConfigXml(Dictionary skins, IEnumerable extraPaks) { + string MakeSafePath(string input) { + return Path.GetInvalidFileNameChars().Aggregate(input, (current, c) => current.Replace(c, '-')); + } + var aircraftLookup = skins.ToLookup(k => k.Value.GetAircraftName()); + var moduleChildren = new List { + new XElement("moduleName", _title), + }; + var image = checkForPreview(); + if (image != null) { + moduleChildren.Add(new XElement("moduleImage", new XAttribute("path", image))); + } + var readmes = checkForReadme(); + if (readmes.Count > 0) { + moduleChildren.Add(new XElement("requiredInstallFiles", readmes.Select(r => new XElement("file", new XAttribute("source", r), new XAttribute("destination", Path.Combine(MakeSafePath(_title), new FileInfo(r).Name)))))); + } + moduleChildren.Add(GenerateStepsXml(aircraftLookup, extraPaks.ToList())); + XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance"; + var xdoc = new XDocument(new XElement("config", new XAttribute(XNamespace.Xmlns + "xsi", xsi), new XAttribute(xsi + "noNamespaceSchemaLocation", "http://qconsulting.ca/fo3/ModConfig5.0.xsd"), moduleChildren)); + return xdoc; + } + + public XElement GenerateStepsXml(ILookup> lookup, List extraPaks) { + XElement OptionalTypeDescriptor() { + return new XElement("typeDescriptor", new XElement("type", new XAttribute("name", "Optional"))); + } + XElement EmptyDescription() { + return new XElement("description", string.Empty); + } + XElement GetNonePlugin() { + return new XElement("plugin", new XAttribute("name", "None"), EmptyDescription(), OptionalTypeDescriptor()); + } + XElement GetPluginElement(string fileName) { + var children = new List { + EmptyDescription(), + }; + var matchingImage = GetImagePath(fileName); + if (!string.IsNullOrWhiteSpace(matchingImage)) { + children.Add(new XElement("image", new XAttribute("path", Path.GetRelativePath(_rootPath, matchingImage)))); + } + children.Add(new XElement("files", new XElement("file", new XAttribute("source", fileName), new XAttribute("destination", new FileInfo(fileName).Name), new XAttribute("priority", "0")))); + children.Add(OptionalTypeDescriptor()); + return new XElement("plugin", new XAttribute("name", new FileInfo(fileName).Name), children); + } + var steps = new List(); + steps.Add(new XElement("installStep", new XAttribute("name", "Introduction"), new XElement("optionalFileGroups", new XAttribute("order", "Explicit"), new XElement("group", new XAttribute("name", "Introduction"), new XAttribute("type", "SelectAll"), new XElement("plugins", new XAttribute("order", "Explicit"), new XElement("plugin", new XAttribute("name", "Introduction"), new XElement("description", GetDescription()), OptionalTypeDescriptor())))))); + foreach (var aircraft in lookup) { + steps.Add(new XElement("installStep", new XAttribute("name", aircraft.Key), new XElement("optionalFileGroups", new XAttribute("order", "Explicit"), aircraft.GroupBy(a => a.Value.GetSlotName()).Select(gs => new XElement("group", new XAttribute("name", gs.Key), new XAttribute("type", "SelectExactlyOne"), new XElement("plugins", new XAttribute("order", "Explicit"), GetNonePlugin(), gs.Select(ssf => GetPluginElement(ssf.Key)))))))); + } + if (extraPaks != null && extraPaks.Any()) { + steps.Add(new XElement("installStep", new XAttribute("name", "Other Files"), new XElement("optionalFileGroups", new XAttribute("order", "Explicit"), new XElement("group", new XAttribute("name", "Other mod files"), new XAttribute("type", "SelectAny"), new XElement("plugins", new XAttribute("order", "Explicit"), extraPaks.Select(ep => GetPluginElement(ep))))))); + } + return new XElement("installSteps", new XAttribute("order", "Explicit"), steps); + } + + private string GetImagePath(string fileName) { + var skinFileName = Path.GetFileNameWithoutExtension(fileName); + var pakFileLocation = new FileInfo(Path.Combine(_rootPath, fileName)).Directory; + var possibleImages = new[] {".png", ".jpg"}.Select(e => Path.Join(pakFileLocation.FullName, skinFileName + e)); + var firstValid = possibleImages.FirstOrDefault(pi => File.Exists(pi)); + return firstValid; + } + + public string WriteToInstallerFiles(XDocument infoXml, XDocument moduleConfigXml) { + var fomodPath = Path.Combine(_rootPath, "fomod"); + Directory.CreateDirectory(fomodPath); + infoXml.Save(Path.Combine(fomodPath, "info.xml")); + moduleConfigXml.Save(Path.Combine(fomodPath, "ModuleConfig.xml")); + return fomodPath; + } + + private string GetDescription(string forceDescription = null) { + var desc = forceDescription ?? _description ?? null; + return $"This installer will guide you through choosing which custom skins you want to install for each aircraft and each slot available in this archive. You can choose as few or as many skins as you want from the choices available, but be warned that your choices can still conflict with other mods you may have already installed!\n\n{(string.IsNullOrWhiteSpace(desc) ? string.Empty : desc)}"; + } + + private List checkForReadme() { + var files = Directory.EnumerateFiles(_rootPath, "*.txt", SearchOption.TopDirectoryOnly); + return files.Count() > 0 ? files.Select(f => Path.GetRelativePath(_rootPath, f)).ToList() : new List(); + } + + private string checkForPreview() { + var files = _rootPath.GetFiles(new[] { "*.png", "*.jpg"}, SearchOption.TopDirectoryOnly).ToList(); + if (files.Any()) { + if (files.Count == 1) { + return Path.GetRelativePath(_rootPath, files[0]); + } else { + var preview = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLower().Contains("preview")); + if (preview != null) { + return Path.GetRelativePath(_rootPath, preview); + } + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/Program.cs b/src/InstallerCreator/Program.cs new file mode 100644 index 0000000..ebfb01d --- /dev/null +++ b/src/InstallerCreator/Program.cs @@ -0,0 +1,25 @@ +using System; +using Spectre.Cli; +using Spectre.Console; +using Spectre.Cli.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using InstallerCreator.Commands; +using System.Threading.Tasks; + +namespace InstallerCreator +{ + class Program + { + static async Task Main(string[] args) + { + var services = new ServiceCollection(); + var app = new CommandApp(new DependencyInjectionRegistrar(services)); + app.Configure(c => { + c.SetApplicationName("AC7 Mod Installer Creator"); + c.AddCommand("build"); + c.AddCommand("zip"); + }); + return await app.RunAsync(args); + } + } +} diff --git a/src/InstallerCreator/SkinIdentifier.cs b/src/InstallerCreator/SkinIdentifier.cs new file mode 100644 index 0000000..edc7005 --- /dev/null +++ b/src/InstallerCreator/SkinIdentifier.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace InstallerCreator { + public class SkinIdentifier { + private Dictionary _slotNames = new Dictionary { + ["00"] = "Osea", + ["01"] = "Erusea", + ["02"] = "Special", + ["03"] = "Mage", + ["04"] = "Spare", + ["05"] = "Strider", + ["06"] = "Slot 7", + ["07"] = "Slot 8" + }; + + private Dictionary _aircraftNames = new Dictionary { + ["a10a"] = "A-10C", + ["adf11f"] = "ADF-11F", + ["f02a"] = "F-2A", + ["f04e"] = "F-4E", + ["f14d"] = "F-14D", + ["f15c"] = "F-15C", + ["f15e"] = "F-15E", + ["f15j"] = "F-15J", + ["f16c"] = "F-16C", + ["f18f"] = "F/A-18F", + ["f22a"] = "F-22A", + ["f35c"] = "F-35C", + ["f104"] = "F-104C", + ["f104av"] = "Avril F-104", + ["j39e"] = "Gripen", + ["m21b"] = "MiG-21", + ["m29a"] = "MiG-29", + ["m31b"] = "MiG-31", + ["mr2k"] = "Mirage", + ["mrgn"] = "ADFX-01", + ["pkfa"] = "Su-57", + ["rflm"] = "Rafale M", + ["su30"] = "Su-30M2", + ["su30sm"] = "Su-30SM", + ["su33"] = "Su-33", + ["su34"] = "Su-34", + ["su35"] = "Su-35S", + ["su37"] = "Su-37", + ["su47"] = "Su-47", + ["typn"] = "Typhoon", + ["x02s"] = "X-02S", + ["yf23"] = "YF-23", + ["zoef"] = "FALKEN" + }; + + public static bool TryParse(string value, out SkinIdentifier ident) { + var rex = new System.Text.RegularExpressions.Regex(@"([a-z0-9]+?)_(v?\d+a?\w{1}?)_(\w).*/"); + var match = rex.Match(value); + if (match != null && match.Groups.Count >= 2) { + ident = new SkinIdentifier(match.Groups[0].Value, match.Groups[1].Value, match.Groups[2].Value, match.Groups[3].Value); + return true; + } + ident = null; + return false; + } + private SkinIdentifier(string rawValue, string aircraft, string slot, string type) { + RawValue = rawValue; + Aircraft = aircraft; + Slot = slot; + Type = type; + } + + public string RawValue { get; } + + public string Aircraft { get; } + + public string Slot { get; } + public string Type { get; } + + public string GetSlotName() { + var knownName = _slotNames.TryGetValue(Slot, out var name); + if (knownName) { + return name; + } else { + return Regex.IsMatch(Slot, @"[a-z]") + ? $"NPC {Slot}" + : $"0{(int.TryParse(Slot, out var num) ? (num + 1).ToString() : Slot)}"; + } + } + + public string GetAircraftName() { + var knownName = _aircraftNames.TryGetValue(Aircraft, out var name); + if (knownName) { + return name; + } else { + return Aircraft.ToUpper(); + } + } + } +} \ No newline at end of file diff --git a/src/InstallerCreator/SkinReader.cs b/src/InstallerCreator/SkinReader.cs new file mode 100644 index 0000000..b643c16 --- /dev/null +++ b/src/InstallerCreator/SkinReader.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace InstallerCreator { + public class SkinReader { + public SkinReader() { + } + + public SkinIdentifier ReadSkinSlot(string filePath) { + var rawString = FindSkinIdent(filePath); + var parsed = SkinIdentifier.TryParse(rawString, out var skinId); + if (parsed) { + return skinId; + } else { + return null; + } + } + + private string FindSkinIdent(string filePath) { + using (var stream = File.OpenRead(filePath)) + using (var reader = new BinaryReader(stream, Encoding.UTF8)) + { + const string key = "/Game/"; + int pos = 0; + + while (stream.Position < stream.Length && pos < key.Length) + { + if (reader.ReadByte() == key[pos]) pos++; + else pos = 0; + } + + if (stream.Position == stream.Length) // we went through the entire stream without finding the key + throw new KeyNotFoundException("Could not find key '" + key + "' in pak file"); + + // otherwise pos == key.Length, which means we found it + // int offset = 136 - key.Length - sizeof(int); + // stream.Seek(offset, SeekOrigin.Current); // advance past junk to beginning of string + var offset = stream.Position; + + + var rawBytes = reader.ReadBytes(64); + var rawString = Encoding.UTF8.GetString(rawBytes); + return rawString; + } + } + } +} \ No newline at end of file diff --git a/src/omnisharp.json b/src/omnisharp.json new file mode 100644 index 0000000..775c74a --- /dev/null +++ b/src/omnisharp.json @@ -0,0 +1,25 @@ +{ + "FormattingOptions": { + "newLine": "\n", + "useTabs": false, + "tabSize": 4, + "indentationSize": 4, + + "NewLinesForBracesInTypes": false, + "NewLinesForBracesInMethods": false, + "NewLinesForBracesInProperties": false, + "NewLinesForBracesInAccessors": false, + "NewLinesForBracesInAnonymousMethods": false, + "NewLinesForBracesInControlBlocks": false, + "NewLinesForBracesInAnonymousTypes": false, + "NewLinesForBracesInObjectCollectionArrayInitializers": false, + "NewLinesForBracesInLambdaExpressionBody": false, + + "NewLineForElse": false, + "NewLineForCatch": false, + "NewLineForFinally": false, + "NewLineForMembersInObjectInit": false, + "NewLineForMembersInAnonymousTypes": false, + "NewLineForClausesInQuery": false + } +} \ No newline at end of file