diff --git a/.github/workflows/BuildModCheck.yml b/.github/workflows/BuildModCheck.yml index f6cbc86c..6f4eb937 100644 --- a/.github/workflows/BuildModCheck.yml +++ b/.github/workflows/BuildModCheck.yml @@ -1,9 +1,5 @@ name: Check Build Status -env: - # Change this to point to your solution, or the folder in which your solution - # can be found. - SLN_PATH: Source/ on: push: @@ -17,23 +13,30 @@ on: # use a separate workflow for them. - 'v*' + pull_request: + branches: + - master + - develop + jobs: build: - name: Build on ${{ matrix.operating-system }} + name: Build & Test v${{ matrix.rimworld-version }} on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} strategy: matrix: - # You can configure operating systems to build on here. It shouldn't make a difference - # operating-system: [ubuntu-latest, windows-latest, macOS-latest] operating-system: [ubuntu-latest] + rimworld-version: [1.4, 1.5] steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Setup Dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4.0.0 with: dotnet-version: 8.0.x - name: Build Mod - run: dotnet build ${{ env.SLN_PATH }} --configuration Release + run: dotnet build Source/${{ matrix.rimworld-version }} --configuration Release + + - name: Test Mod + run: dotnet test Source/${{ matrix.rimworld-version }} --configuration Release diff --git a/.github/workflows/GenerateCompatibilityList.yml b/.github/workflows/GenerateCompatibilityList.yml index cc038711..50314814 100644 --- a/.github/workflows/GenerateCompatibilityList.yml +++ b/.github/workflows/GenerateCompatibilityList.yml @@ -1,13 +1,8 @@ -# This is a basic workflow to help you get started with Actions - name: Generate Compatibility List env: - # Change this to point to your solution, or the folder in which your solution - # can be found. - SLN_PATH: Source/ + LATEST_RW_VERSION: 1.5 -# Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the master branch push: @@ -15,40 +10,39 @@ on: paths: - WeaponTweakData/* -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: if: "!contains(github.event.commits[0].message, '[AUTO CI]')" runs-on: windows-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4.1.4 - name: Setup Dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4.0.0 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - - name: Build Mod 1.4 - run: dotnet build ${{ env.SLN_PATH }} --configuration v1.4 + - name: Build Mod ${{ env.LATEST_RW_VERSION }} + run: dotnet build Source/${{ env.LATEST_RW_VERSION }} --configuration Release - - name: Run generator. - run: .\Source\CompatibilityReportGenerator\bin\Debug\net472\CompatibilityReportGenerator.exe --directory "./WeaponTweakData" --output "./WeaponTweakData/Compatible Mods.md" + - name: Run generator + run: .\Source\${{ env.LATEST_RW_VERSION }}\CompatibilityReportGenerator\bin\Debug\net472\CompatibilityReportGenerator.exe --directory "./WeaponTweakData" --output "./WeaponTweakData/Compatible Mods.md" - - name: Check for changes. + - name: Check for changes run: git status - - name: Stage changes. + - name: Stage changes run: git add . - - name: Commit changes. + - name: Commit changes run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git commit -m "[AUTO CI] Update compatible mod list." -m "[skip ci]" - name: Push to ${{ github.ref }}. - uses: ad-m/github-push-action@master + uses: ad-m/github-push-action@v0.8.0 with: github_token: ${{ secrets.CI_TOKEN }} branch: ${{ github.ref }} diff --git a/.github/workflows/GenerateReleaseZip.yml b/.github/workflows/GenerateReleaseZip.yml index 39ed7069..19b94e2f 100644 --- a/.github/workflows/GenerateReleaseZip.yml +++ b/.github/workflows/GenerateReleaseZip.yml @@ -1,9 +1,6 @@ name: Generate Github Release Zip env: - # Change this to point to your solution, or the folder in which your solution - # can be found. - SLN_PATH: Source/ # Change this to what you want your folder name to be in people's Mods/ # folder. It should be unique to your mod. MOD_NAME: MeleeAnimation @@ -12,6 +9,23 @@ env: RELEASE_DRAFT: false RELEASE_PRERELEASE: false + MOD_CONTENTS: | + About/ + Animations/ + Bundles/ + WeaponTweakData/ + Defs/ + Patches/ + Languages/ + Patch_AlienRaces/ + Patch_FacialAnimation/ + Patch_Lightsabers/ + Patch_CAI5000/ + Patch_CombatExtended/ + Sounds/ + Textures/ + loadfolders.xml + on: push: tags: @@ -20,6 +34,7 @@ on: - 'v*' jobs: + build: name: Build on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} @@ -29,58 +44,35 @@ jobs: # cross-platform using mono. I chose Ubuntu because it ran the fastest # for the build steps. operating-system: [ubuntu-latest] + rimworld-version: [1.4, 1.5] # Add Rimworld version here! steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.4 - name: Setup Dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4.0.0 with: dotnet-version: 8.0.x - - name: Build Mod - run: dotnet build ${{ env.SLN_PATH }} --configuration Release - - # I don't know how well testing will work without Rimworld actually installed. - # But if you have unit tests configured to work with dotnet, you may be able - # to uncomment this and add a testing step. - # - name: Test Mod - # run: dotnet test ${{ env.SLN_PATH }} --no-restore --verbosity normal - - # There is no `zip` command on windows so you need to use tar. - # - name: Zip-up Mod - # run: tar --exclude="*." -zcvf dist.tar.gz About/ Assemblies/ Defs/ Languages/ Patches/ RimCI/ Sounds/ Textures/ + - name: Build Mod (RW ${{ matrix.rimworld-version }}) + run: dotnet build Source/${{ matrix.rimworld-version }} --configuration Release # To modify this with your own directory structure, just change the paths to # whatever you want. It will not upload any empty directories, those with only # hidden files will also be excluded. - - name: Upload Mod Artifacts - uses: actions/upload-artifact@v3 + - name: Upload Mod Artifacts (RW ${{ matrix.rimworld-version }}) + uses: actions/upload-artifact@v4.3.3 with: - name: build - retention-days: 7 + name: build-${{ matrix.rimworld-version }} + retention-days: 1 path: | - 1.4/ - 1.5/ - About/ - Animations/ - Bundles/ - WeaponTweakData/ - Defs/ - Patches/ - Languages/ - Patch_AlienRaces/ - Patch_FacialAnimation/ - Patch_Lightsabers/ - Patch_CAI5000/ - Patch_CombatExtended/ - Sounds/ - Textures/ - loadfolders.xml + ${{ matrix.rimworld-version }}/ + ${{ env.MOD_CONTENTS }} !**/.* # This final path is to exclude hidden files such as .gitkeep and .DS_STORE. # I would recommend keeping it, but I don't think it will break anything if # you remove or modify it. + package: name: Package needs: build @@ -90,37 +82,16 @@ jobs: # This is a special syntax for GitHub Actions that sets an environment # variable. See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable run: echo "MOD_PATH=$HOME/$MOD_NAME" >> $GITHUB_ENV - # run: echo "::set-env name=MOD_PATH::$HOME/$MOD_NAME" - name: Create Mod Folder run: mkdir -p ${{ env.MOD_PATH }} - name: Download Mod Artifacts from Build Step - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.7 with: - name: build + # Note that 'name' is not specified, this will cause all artifacts from this run to be downloaded. + merge-multiple: true # Important, this merges the different rimworld version outputs path: ${{ env.MOD_PATH }} - - # If you have any other Rimworld folders that didn't get scooped up in the - # artifacts, add them here. It may be neccessary to change this for v1.1 mods. - - name: Create Mod Folders - run: | - mkdir -p ${{ env.MOD_PATH }}/1.4/ - mkdir -p ${{ env.MOD_PATH }}/1.5/ - mkdir -p ${{ env.MOD_PATH }}/About/ - mkdir -p ${{ env.MOD_PATH }}/Animations/ - mkdir -p ${{ env.MOD_PATH }}/Bundles/ - mkdir -p ${{ env.MOD_PATH }}/WeaponTweakData/ - mkdir -p ${{ env.MOD_PATH }}/Defs/ - mkdir -p ${{ env.MOD_PATH }}/Patches/ - mkdir -p ${{ env.MOD_PATH }}/Languages/ - mkdir -p ${{ env.MOD_PATH }}/Patch_AlienRaces/ - mkdir -p ${{ env.MOD_PATH }}/Patch_FacialAnimation/ - mkdir -p ${{ env.MOD_PATH }}/Patch_Lightsabers/ - mkdir -p ${{ env.MOD_PATH }}/Patch_CAI5000/ - mkdir -p ${{ env.MOD_PATH }}/Patch_CombatExtended/ - mkdir -p ${{ env.MOD_PATH }}/Sounds/ - mkdir -p ${{ env.MOD_PATH }}/Textures/ - name: Zip Mod run: | @@ -143,13 +114,11 @@ jobs: id: get_version # This is a special syntax for GitHub Actions that sets an output # variable. See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable - # run: echo ::set-output name=VERSION::$(echo ${{ github.ref }} | cut -d / -f 3) run: echo "VERSION=$(echo ${{ github.ref }} | cut -d / -f 3)" >> $GITHUB_OUTPUT - name: Set Environment Variables # This is a special syntax for GitHub Actions that sets an environment # variable. See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable - # run: echo "::set-env name=MOD_RELEASE::$MOD_NAME-${{ steps.get_version.outputs.VERSION }}" run: echo "MOD_RELEASE=$MOD_NAME-${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_ENV - name: Download Mod Artifacts from Build Step @@ -160,7 +129,7 @@ jobs: - name: Create Release id: create_release - uses: actions/create-release@v1 + uses: actions/create-release@v1.1.4 env: GITHUB_TOKEN: ${{ secrets.CI_TOKEN }} with: diff --git a/.gitignore b/.gitignore index 931516db..cd438d82 100644 --- a/.gitignore +++ b/.gitignore @@ -373,3 +373,29 @@ Source/packages/ Source/Content/Unity/RimVibes Bundles/Temp/ Source/UnityProject/UserSettings/ + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries diff --git a/Languages/English/Keyed/Keys.xml b/Languages/English/Keyed/Keys.xml index 224be516..a519a740 100644 --- a/Languages/English/Keyed/Keys.xml +++ b/Languages/English/Keyed/Keys.xml @@ -2,6 +2,7 @@ Epicguru's Melee Animation + Loading Melee Animation Animation diff --git a/Source/1.4/AMRetextureSupport/AMRetextureSupport.csproj b/Source/1.4/AMRetextureSupport/AMRetextureSupport.csproj new file mode 100644 index 00000000..6ff72ee5 --- /dev/null +++ b/Source/1.4/AMRetextureSupport/AMRetextureSupport.csproj @@ -0,0 +1,35 @@ + + + + net472 + Library + false + false + 11 + false + false + AM.Retexture + Release + disable + true + none + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/Source/AMRetextureSupport/ActiveTextureReport.cs b/Source/1.4/AMRetextureSupport/ActiveTextureReport.cs similarity index 100% rename from Source/AMRetextureSupport/ActiveTextureReport.cs rename to Source/1.4/AMRetextureSupport/ActiveTextureReport.cs diff --git a/Source/1.4/AMRetextureSupport/RetextureUtility.cs b/Source/1.4/AMRetextureSupport/RetextureUtility.cs new file mode 100644 index 00000000..e4ce3953 --- /dev/null +++ b/Source/1.4/AMRetextureSupport/RetextureUtility.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using UnityEngine; +using Verse; + +namespace AM.Retexture; + +[HotSwapAll] +public static class RetextureUtility +{ + public static Predicate IsMeleeWeapon; + public static int CachedReportCount => reportCache.Count; + public static IEnumerable AllCachedReports => reportCache.Values; + + private static readonly Dictionary reportCache = new Dictionary(128); + private static readonly Dictionary reportCacheIsFull = new Dictionary(128); + private static readonly Dictionary> graphicClassCustomHandlers = new Dictionary>() + { + // AdvancedGraphics Single Randomized, is a collection despite being marked as Graphic_Single. + {"AdvancedGraphics.Graphic_SingleRandomized", (path, pass) => pass switch + { + 0 => (true, path), + _ => (false, null) + } + }, + + // AdvancedGraphics SingleQuality. The tex name has a _Quality suffix. + // If the _Normal graphic cannot be found, the fallback is the raw xml path. + {"AdvancedGraphics.Graphic_SingleQuality", (path, pass) => pass switch + { + 0 => (false, path + "_Normal"), + 1 => (false, path), + _ => (false, null) + } + }, + + }; + private static HashSet OfficialMods; + private static ModContentPack CoreMCP; + + /// + /// Gets the mod that is providing the active texture for this weapon def. + /// + public static ModContentPack GetTextureSupplier(ThingDef weapon) + => weapon == null ? null : GetTextureReport(weapon).ActiveRetextureMod; + + public static TimeSpan PreCacheAllTextureReports(Action onReport, bool full) + { + var sw = Stopwatch.StartNew(); + foreach (var weapon in DefDatabase.AllDefsListForReading.Where(d => IsMeleeWeapon(d))) + { + onReport(GetTextureReport(weapon, 0, full)); + } + sw.Stop(); + return sw.Elapsed; + } + + /// + /// Generates information about a weapon's textures, such as what mod(s) are supplying textures. + /// + public static ActiveTextureReport GetTextureReport(ThingDef weapon, int pass = 0, bool full = true, bool loadFromCache = true, bool saveToCache = true) + { + if (weapon == null) + return default; + + if (loadFromCache && reportCache.TryGetValue(weapon, out var found) && reportCacheIsFull[weapon] == full) + return found; + + // 0. Get active texture. + string texPath = ResolveTexturePath(weapon, pass, out bool canDoAnotherPass); + if (string.IsNullOrWhiteSpace(texPath)) + return new ActiveTextureReport($"Failed to find texture path i.e. graphicData.texPath '{weapon.graphicData.texPath}' ({weapon.graphicData.graphicClass.FullName}) pass {pass}"); + + // Make basic report. Data still missing. + var report = new ActiveTextureReport + { + Weapon = weapon, + AllRetextures = new List<(ModContentPack, Texture2D)>(), + TexturePath = texPath, + }; + + // 1. Find all texture paths. + bool hasFirst = false; + foreach ((ModContentPack mod, Texture2D texture, string path) in GetAllModTextures()) + { + // If the texture path does not match, ignore it... + if (path != texPath) + continue; + + if (!hasFirst) + { + hasFirst = true; + report.ActiveTexture = texture; + report.ActiveRetextureMod = mod; + } + + report.AllRetextures.Add((mod, texture)); + + if (full) + continue; + if (saveToCache) + { + reportCache[weapon] = report; + reportCacheIsFull[weapon] = false; + } + return report; + } + + // 2. Check base game resources + // Resources load, which is how base game and dlc load content... + var resource = Resources.Load($"Textures/{texPath}"); + if (resource != null) + { + OfficialMods ??= LoadedModManager.RunningModsListForReading.Where(m => m.IsOfficialMod).ToHashSet(); + CoreMCP ??= OfficialMods.First(m => m.PackageId == ModContentPack.CoreModPackageId); + + // If the weapon comes from a dlc, then that dlc is the texture provider. + // Otherwise, just set the provider as one of the official mods (core or dlc). + ModContentPack mod = weapon.modContentPack; + if (!OfficialMods.Contains(mod)) + mod = CoreMCP; + + if (!hasFirst) + { + hasFirst = true; + report.ActiveTexture = resource; + report.ActiveRetextureMod = mod; + } + report.AllRetextures.Add((mod, resource)); + if (!full) + { + if (!saveToCache) + return report; + reportCache[weapon] = report; + reportCacheIsFull[weapon] = false; + return report; + } + } + + // 3. Asset bundles scan. + // Used by DLC as well as some weird mods. + foreach (var pair in ScanAssetBundles(texPath)) + { + if (!hasFirst) + { + hasFirst = true; + report.ActiveTexture = pair.texture; + report.ActiveRetextureMod = pair.mod; + } + report.AllRetextures.Add((pair.mod, pair.texture)); + + if (full) + continue; + if (saveToCache) + { + reportCache[weapon] = report; + reportCacheIsFull[weapon] = false; + } + return report; + } + + if (report.AllRetextures.Count == 0) + { + report.ErrorMessage = $"No textures found for path '{report.TexturePath}' ({weapon.graphicData.graphicClass.FullName})"; + if (canDoAnotherPass) + { + return GetTextureReport(weapon, pass + 1, full, loadFromCache, saveToCache); + } + } + + if (!saveToCache) + return report; + + reportCache[weapon] = report; + reportCacheIsFull[weapon] = full; + return report; + } + + public static IEnumerable GetModWeaponReports(ModContentPack mod) + { + if (mod == null) + yield break; + + foreach (var pair in reportCache) + { + if (pair.Value.AllRetextures.Any(p => p.mod == mod)) + yield return pair.Value; + } + } + + private static IEnumerable<(ModContentPack mod, Texture2D texture)> ScanAssetBundles(string texPath) + { + var mods = LoadedModManager.RunningModsListForReading; + string path = Path.Combine("Assets", "Data"); + + for (int i = mods.Count - 1; i >= 0; i--) + { + string path2 = Path.Combine(path, mods[i].FolderName); + foreach (AssetBundle assetBundle in mods[i].assetBundles.loadedAssetBundles) + { + string str = Path.Combine(Path.Combine(path2, GenFilePaths.ContentPath()), texPath); + + foreach (string ext in ModAssetBundlesHandler.TextureExtensions) + { + var loaded = assetBundle.LoadAsset(str + ext); + if (loaded != null) + yield return (mods[i], loaded); + } + } + } + } + + private static IEnumerable<(ModContentPack mod, Texture2D texture, string path)> GetAllModTextures() + { + var mods = LoadedModManager.RunningModsListForReading; + for (int i = mods.Count - 1; i >= 0; i--) + { + var mod = mods[i]; + + var textures = mod.textures?.contentList; + if (textures == null) + continue; + + foreach (var pair in textures) + { + yield return (mod, pair.Value, pair.Key); + } + } + + } + + [DebugOutput("Melee Animation")] + private static void LogAllTextureReports() + { + var meleeWeapons = DefDatabase.AllDefsListForReading.Where(d => IsMeleeWeapon(d)); + + TableDataGetter[] table = new TableDataGetter[6]; + table[0] = new TableDataGetter("Def Name", d => d.defName); + table[1] = new TableDataGetter("Name", d => d.LabelCap); + table[2] = new TableDataGetter("Source", def => $"{GetTextureReport(def).SourceMod?.Name ?? "?"} ({GetTextureReport(def).SourceMod?.PackageId ?? "?"})"); + table[3] = new TableDataGetter("Texture Path", def => $"{GetTextureReport(def).TexturePath ?? "?"}"); + table[4] = new TableDataGetter("Texture Source", def => $"{GetTextureReport(def).ActiveRetextureMod?.Name ?? "?"} ({GetTextureReport(def).ActiveRetextureMod?.PackageId ?? "?"})"); + table[5] = new TableDataGetter("Retextures", def => + { + var report = GetTextureReport(def); + if (report.HasError) + return report.ErrorMessage ?? "Error: ?"; + + if (report.AllRetextures.Count <= 1) + return "---"; + + return string.Join(",\n", report.AllRetextures.Select(p => p.mod.Name)); + }); + + DebugTables.MakeTablesDialog(meleeWeapons, table); + } + + private static string ResolveTexturePath(ThingDef weapon, int pass, out bool canDoAnotherPass) + { + canDoAnotherPass = false; + string xmlPath = weapon.graphicData?.texPath.Replace('\\', '/'); + if (string.IsNullOrWhiteSpace(xmlPath)) + return null; + + var gc = weapon.graphicData.graphicClass; + bool isCollection = gc.IsSubclassOf(typeof(Graphic_Collection)); + + // Attempt to get custom handler for this graphic class (required for some modded classes) + // and override the xmlpath and/or collection status. + if (graphicClassCustomHandlers.TryGetValue(gc.FullName, out var func)) + { + (isCollection, xmlPath) = func(xmlPath, pass); + canDoAnotherPass = xmlPath != null; + } + + // Collections load from sub-folders, normal graphic types don't: + if (!isCollection) + return xmlPath; + + // Other graphic classes will pull images from a sub-folder. + // Scan mod in reverse load order and attempt to find a subfolder with the correct path. + var mods = LoadedModManager.RunningModsListForReading; + for (int i = mods.Count - 1; i >= 0; i--) + { + var mod = mods[i]; + + var textures = mod.textures?.contentListTrie; + if (textures == null) + continue; + + string prefix = (xmlPath[xmlPath.Length - 1] == '/') ? xmlPath : xmlPath + "/"; + foreach (string path in textures.GetByPrefix(prefix).OrderBy(s => s)) + { + return path; + } + } + + // If no mods have it, check vanilla resources. + var inFolder = Resources.LoadAll($"Textures/{xmlPath}"); + if (inFolder is { Length: > 0 }) + { + string name = inFolder.OrderBy(t => t.name).FirstOrDefault().name; + Resources.UnloadUnusedAssets(); + return $"{xmlPath}/{name}"; + } + + // Nothing was found. + return null; + } +} + +public class HotSwapAllAttribute : Attribute { } diff --git a/Source/1.4/AlienRacesPatch/AlienRacesPatch.csproj b/Source/1.4/AlienRacesPatch/AlienRacesPatch.csproj new file mode 100644 index 00000000..89950518 --- /dev/null +++ b/Source/1.4/AlienRacesPatch/AlienRacesPatch.csproj @@ -0,0 +1,43 @@ + + + + net48 + Library + false + false + preview + false + false + Release + AM.AlienRacesPatch + AM.AlienRacesPatch + + + + + False + False + all + + + + + + + refs/1.4/AlienRace14.dll + False + False + + + 1.4.3901 + + + + + none + ..\..\..\Patch_AlienRaces\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/AlienRacesPatch/PatchCore.cs b/Source/1.4/AlienRacesPatch/PatchCore.cs similarity index 100% rename from Source/AlienRacesPatch/PatchCore.cs rename to Source/1.4/AlienRacesPatch/PatchCore.cs diff --git a/Source/AlienRacesPatch/refs/AlienRace.dll b/Source/1.4/AlienRacesPatch/refs/1.4/AlienRace14.dll similarity index 100% rename from Source/AlienRacesPatch/refs/AlienRace.dll rename to Source/1.4/AlienRacesPatch/refs/1.4/AlienRace14.dll diff --git a/Source/1.4/AnimationMod.sln b/Source/1.4/AnimationMod.sln new file mode 100644 index 00000000..fe737d5e --- /dev/null +++ b/Source/1.4/AnimationMod.sln @@ -0,0 +1,80 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31717.71 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnimationMod", "ThingGenerator\AnimationMod.csproj", "{0CB2DDBC-ECE8-46B4-A8E9-9BF2239BE82B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LightsaberPatch", "LightsaberPatch\LightsaberPatch.csproj", "{D9B99467-57BB-4DE7-8147-F5D47554D1CB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AlienRacesPatch", "AlienRacesPatch\AlienRacesPatch.csproj", "{15189C13-7D5D-4F8D-AE17-1CA9F4A744EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModRequestAPI", "ModRequestAPI\ModRequestAPI.csproj", "{603ACB32-6730-4BAF-8E0A-A0E3399D634F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMRetextureSupport", "AMRetextureSupport\AMRetextureSupport.csproj", "{3802FB86-C860-4F98-A6F6-5E9AA47AF564}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompatibilityReportGenerator", "CompatibilityReportGenerator\CompatibilityReportGenerator.csproj", "{67701E3B-9094-4CA2-9DD8-42EBAFCB9F4A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CAI5000Patch", "CAI5000Patch\CAI5000Patch.csproj", "{0B91E675-3505-4587-94BB-6FEFC5ADB24D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CombatExtendedPatch", "CombatExtendedPatch\CombatExtendedPatch.csproj", "{BBB0D084-8A32-458E-8A7F-48E7D90F0489}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModRequestAPI.Models", "ModRequestAPI.Models\ModRequestAPI.Models.csproj", "{DB0FEE4E-5DF7-4DD3-B876-7FC20D310C8E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModRequestAPI.Backend", "ModRequestAPI.Backend\src\ModRequestAPI.Backend\ModRequestAPI.Backend.csproj", "{06C481CC-E8D0-46D6-B02B-127DD9E3A1AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceOptimizerPatch", "PerformanceOptimizerPatch\PerformanceOptimizerPatch.csproj", "{7B881968-1527-40FC-9FB3-85A5A01BEE88}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Reporting", "Reporting", "{3549402C-9759-4ABC-938C-FC4F801C156F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Patches", "Patches", "{7C1CE274-810E-467E-B8EF-4AB2A83E17F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TacticowlPatch", "TacticowlPatch\TacticowlPatch.csproj", "{DEC2223C-6DD4-48EF-9438-8F4E97B6C542}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0CB2DDBC-ECE8-46B4-A8E9-9BF2239BE82B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CB2DDBC-ECE8-46B4-A8E9-9BF2239BE82B}.Release|Any CPU.Build.0 = Release|Any CPU + {D9B99467-57BB-4DE7-8147-F5D47554D1CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9B99467-57BB-4DE7-8147-F5D47554D1CB}.Release|Any CPU.Build.0 = Release|Any CPU + {15189C13-7D5D-4F8D-AE17-1CA9F4A744EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15189C13-7D5D-4F8D-AE17-1CA9F4A744EB}.Release|Any CPU.Build.0 = Release|Any CPU + {603ACB32-6730-4BAF-8E0A-A0E3399D634F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {603ACB32-6730-4BAF-8E0A-A0E3399D634F}.Release|Any CPU.Build.0 = Release|Any CPU + {3802FB86-C860-4F98-A6F6-5E9AA47AF564}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3802FB86-C860-4F98-A6F6-5E9AA47AF564}.Release|Any CPU.Build.0 = Release|Any CPU + {67701E3B-9094-4CA2-9DD8-42EBAFCB9F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67701E3B-9094-4CA2-9DD8-42EBAFCB9F4A}.Release|Any CPU.Build.0 = Release|Any CPU + {0B91E675-3505-4587-94BB-6FEFC5ADB24D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B91E675-3505-4587-94BB-6FEFC5ADB24D}.Release|Any CPU.Build.0 = Release|Any CPU + {BBB0D084-8A32-458E-8A7F-48E7D90F0489}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBB0D084-8A32-458E-8A7F-48E7D90F0489}.Release|Any CPU.Build.0 = Release|Any CPU + {DB0FEE4E-5DF7-4DD3-B876-7FC20D310C8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB0FEE4E-5DF7-4DD3-B876-7FC20D310C8E}.Release|Any CPU.Build.0 = Release|Any CPU + {06C481CC-E8D0-46D6-B02B-127DD9E3A1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06C481CC-E8D0-46D6-B02B-127DD9E3A1AA}.Release|Any CPU.Build.0 = Release|Any CPU + {7B881968-1527-40FC-9FB3-85A5A01BEE88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B881968-1527-40FC-9FB3-85A5A01BEE88}.Release|Any CPU.Build.0 = Release|Any CPU + {DEC2223C-6DD4-48EF-9438-8F4E97B6C542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEC2223C-6DD4-48EF-9438-8F4E97B6C542}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D9B99467-57BB-4DE7-8147-F5D47554D1CB} = {7C1CE274-810E-467E-B8EF-4AB2A83E17F9} + {15189C13-7D5D-4F8D-AE17-1CA9F4A744EB} = {7C1CE274-810E-467E-B8EF-4AB2A83E17F9} + {603ACB32-6730-4BAF-8E0A-A0E3399D634F} = {3549402C-9759-4ABC-938C-FC4F801C156F} + {0B91E675-3505-4587-94BB-6FEFC5ADB24D} = {7C1CE274-810E-467E-B8EF-4AB2A83E17F9} + {BBB0D084-8A32-458E-8A7F-48E7D90F0489} = {7C1CE274-810E-467E-B8EF-4AB2A83E17F9} + {DB0FEE4E-5DF7-4DD3-B876-7FC20D310C8E} = {3549402C-9759-4ABC-938C-FC4F801C156F} + {06C481CC-E8D0-46D6-B02B-127DD9E3A1AA} = {3549402C-9759-4ABC-938C-FC4F801C156F} + {7B881968-1527-40FC-9FB3-85A5A01BEE88} = {7C1CE274-810E-467E-B8EF-4AB2A83E17F9} + {DEC2223C-6DD4-48EF-9438-8F4E97B6C542} = {7C1CE274-810E-467E-B8EF-4AB2A83E17F9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1478B48A-1088-4408-9427-E3B4EADB515C} + EndGlobalSection +EndGlobal diff --git a/Source/1.4/AnimationMod.sln.DotSettings b/Source/1.4/AnimationMod.sln.DotSettings new file mode 100644 index 00000000..0e1e6115 --- /dev/null +++ b/Source/1.4/AnimationMod.sln.DotSettings @@ -0,0 +1,46 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/Source/CAI5000Patch/AntiRetreatPatch.cs b/Source/1.4/CAI5000Patch/AntiRetreatPatch.cs similarity index 100% rename from Source/CAI5000Patch/AntiRetreatPatch.cs rename to Source/1.4/CAI5000Patch/AntiRetreatPatch.cs diff --git a/Source/CAI5000Patch/CAI5000AnimationPatch.cs b/Source/1.4/CAI5000Patch/CAI5000AnimationPatch.cs similarity index 100% rename from Source/CAI5000Patch/CAI5000AnimationPatch.cs rename to Source/1.4/CAI5000Patch/CAI5000AnimationPatch.cs diff --git a/Source/1.4/CAI5000Patch/CAI5000Patch.csproj b/Source/1.4/CAI5000Patch/CAI5000Patch.csproj new file mode 100644 index 00000000..c3b8b6f8 --- /dev/null +++ b/Source/1.4/CAI5000Patch/CAI5000Patch.csproj @@ -0,0 +1,59 @@ + + + + net472 + Library + preview + false + true + false + false + Release + AM.CAI5000Patch + AM.CAI5000Patch + disable + true + none + + + + + False + False + all + + + runtime + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + 1.4.3901 + + + CombatAI_14.dll + False + False + runtime + + + + + + + + + + none + ..\..\..\Patch_CAI5000\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/1.4/CAI5000Patch/CombatAI_14.dll b/Source/1.4/CAI5000Patch/CombatAI_14.dll new file mode 100644 index 00000000..890f7aca Binary files /dev/null and b/Source/1.4/CAI5000Patch/CombatAI_14.dll differ diff --git a/Source/CAI5000Patch/FixCustomRenderInAnimator.cs b/Source/1.4/CAI5000Patch/FixCustomRenderInAnimator.cs similarity index 96% rename from Source/CAI5000Patch/FixCustomRenderInAnimator.cs rename to Source/1.4/CAI5000Patch/FixCustomRenderInAnimator.cs index 766d60d8..3faeb147 100644 --- a/Source/CAI5000Patch/FixCustomRenderInAnimator.cs +++ b/Source/1.4/CAI5000Patch/FixCustomRenderInAnimator.cs @@ -17,8 +17,10 @@ namespace AM.CAI5000Patch; */ public static class FixCustomRenderInAnimator { - public static void PreCustomPawnRender(Pawn pawn, AnimRenderer anim, Map map) + public static void PreCustomPawnRender(Pawn pawn, AnimRenderer anim) { + var map = anim.Map; + // This is CAI code: if (Pawn_Patch.fogThings == null || Pawn_Patch.fogThings.map != map) { @@ -26,7 +28,7 @@ public static void PreCustomPawnRender(Pawn pawn, AnimRenderer anim, Map map) } } - public static void PostCustomPawnRender(Pawn pawn, AnimRenderer anim, Map map) + public static void PostCustomPawnRender(Pawn pawn, AnimRenderer anim) { // CAI always sets fogThings to null after the map has rendered each frame. // I do not know why, probably to make sure it can be GC'd once the player exits to the main menu. diff --git a/Source/CAI5000Patch/PatchCore.cs b/Source/1.4/CAI5000Patch/PatchCore.cs similarity index 100% rename from Source/CAI5000Patch/PatchCore.cs rename to Source/1.4/CAI5000Patch/PatchCore.cs diff --git a/Source/CAI5000Patch/ThingComp_CombatAI_CompTickRare_Transpiler.cs b/Source/1.4/CAI5000Patch/ThingComp_CombatAI_CompTickRare_Transpiler.cs similarity index 100% rename from Source/CAI5000Patch/ThingComp_CombatAI_CompTickRare_Transpiler.cs rename to Source/1.4/CAI5000Patch/ThingComp_CombatAI_CompTickRare_Transpiler.cs diff --git a/Source/CAI5000Patch/ThingComp_CombatAI_OnScanFinished_Transpiler.cs b/Source/1.4/CAI5000Patch/ThingComp_CombatAI_OnScanFinished_Transpiler.cs similarity index 100% rename from Source/CAI5000Patch/ThingComp_CombatAI_OnScanFinished_Transpiler.cs rename to Source/1.4/CAI5000Patch/ThingComp_CombatAI_OnScanFinished_Transpiler.cs diff --git a/Source/CombatExtendedPatch/CombatExtendedOutcomeWorker.cs b/Source/1.4/CombatExtendedPatch/CombatExtendedOutcomeWorker.cs similarity index 100% rename from Source/CombatExtendedPatch/CombatExtendedOutcomeWorker.cs rename to Source/1.4/CombatExtendedPatch/CombatExtendedOutcomeWorker.cs diff --git a/Source/1.4/CombatExtendedPatch/CombatExtendedPatch.csproj b/Source/1.4/CombatExtendedPatch/CombatExtendedPatch.csproj new file mode 100644 index 00000000..2153b8dc --- /dev/null +++ b/Source/1.4/CombatExtendedPatch/CombatExtendedPatch.csproj @@ -0,0 +1,56 @@ + + + + net472 + Library + preview + false + true + false + false + Release + AM.CombatExtendedPatch + zz.AM.CombatExtendedPatch + disable + true + none + + + + + runtime + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + False + False + all + + + + + + + 1.4.3901 + + + CombatExtended_14.dll + False + False + runtime + + + + + + + none + ..\..\..\Patch_CombatExtended\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/1.4/CombatExtendedPatch/CombatExtended_14.dll b/Source/1.4/CombatExtendedPatch/CombatExtended_14.dll new file mode 100644 index 00000000..6cd91e20 Binary files /dev/null and b/Source/1.4/CombatExtendedPatch/CombatExtended_14.dll differ diff --git a/Source/CombatExtendedPatch/PatchCore.cs b/Source/1.4/CombatExtendedPatch/PatchCore.cs similarity index 100% rename from Source/CombatExtendedPatch/PatchCore.cs rename to Source/1.4/CombatExtendedPatch/PatchCore.cs diff --git a/Source/CompatibilityReportGenerator/Assembly-CSharp.dll b/Source/1.4/CompatibilityReportGenerator/Assembly-CSharp.dll similarity index 100% rename from Source/CompatibilityReportGenerator/Assembly-CSharp.dll rename to Source/1.4/CompatibilityReportGenerator/Assembly-CSharp.dll diff --git a/Source/1.4/CompatibilityReportGenerator/CompatibilityReportGenerator.csproj b/Source/1.4/CompatibilityReportGenerator/CompatibilityReportGenerator.csproj new file mode 100644 index 00000000..6d74064f --- /dev/null +++ b/Source/1.4/CompatibilityReportGenerator/CompatibilityReportGenerator.csproj @@ -0,0 +1,48 @@ + + + + Exe + net472 + enable + disable + false + false + 11 + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Source/1.4/CompatibilityReportGenerator/Program.cs b/Source/1.4/CompatibilityReportGenerator/Program.cs new file mode 100644 index 00000000..242c2726 --- /dev/null +++ b/Source/1.4/CompatibilityReportGenerator/Program.cs @@ -0,0 +1,110 @@ +using AM.Tweaks; +using CompatibilityReportGenerator.Properties; +using System.CommandLine; + +namespace CompatibilityReportGenerator; + +public static class Program +{ + public static void Main(string[] args) + { + var rootCmd = new RootCommand("Generates a compatibility report (in markdown format) for tweak data."); + + var dirOption = new Option("--directory") + { + Description = "The directory that the tweak .json files are located in.", + IsRequired = true, + AllowMultipleArgumentsPerToken = false, + Arity = ArgumentArity.ExactlyOne, + }; + + var outputOption = new Option("--output") + { + Description = "The file path to output the markdown file to.", + IsRequired = true, + AllowMultipleArgumentsPerToken = false, + Arity = ArgumentArity.ExactlyOne, + }; + + rootCmd.AddOption(dirOption); + rootCmd.AddOption(outputOption); + + rootCmd.SetHandler(Run, dirOption, outputOption); + + rootCmd.Invoke(args); + } + + private static void Run(DirectoryInfo input, FileInfo output) + { + if (!input.Exists) + throw new DirectoryNotFoundException(input.FullName); + + Dictionary table = new Dictionary(512); + int totalWeaponCount = 0; + + var idToName = (from raw in Directory.EnumerateFiles(input.FullName, "*.txt", SearchOption.AllDirectories) + let name = new FileInfo(raw).Name.Replace(".txt", "") + let contents = File.ReadAllText(raw) + select (name, contents)).ToDictionary(pair => pair.name, pair => pair.contents); + + var files = Directory.GetFiles(input.FullName, "*.json", SearchOption.AllDirectories); + foreach (var file in files) + { + ItemTweakData tweak; + try + { + tweak = ItemTweakData.LoadFrom(file); + } + catch (Exception e) + { + Console.WriteLine($"Exception reading/parsing file '{file}':"); + Console.WriteLine(e); + continue; + } + + string Escape(string i) + { + return i.Replace("|", "|"); + } + + string modID = tweak.TextureModID; + if (!table.TryGetValue(modID, out var row)) + { + if (!idToName.TryGetValue(modID, out string name)) + { + Console.WriteLine($"Failed to find ID -> name mapping for {modID}"); + name = "???"; + } + row = new OutputRow + { + WeaponCount = 0, + ModName = Escape(name), + ModID = Escape(modID) + }; + table.Add(modID, row); + } + + row.WeaponCount++; + totalWeaponCount++; + } + + string template = Resources.Template; + + string time = DateTime.UtcNow.ToString("d MMM yyyy, h:mm tt"); + int modCount = table.Count; + var lines = from row in table.Values + orderby row.ModName + select $"| **{row.ModName}** | {row.ModID} | {row.WeaponCount} | *Epicguru* |"; + + string finalTxt = string.Format(template, totalWeaponCount, modCount, string.Join("\n", lines), time); + + File.WriteAllText(output.FullName, finalTxt); + } + + private class OutputRow + { + public string ModName { get; set; } + public string ModID { get; set; } + public int WeaponCount { get; set; } + } +} \ No newline at end of file diff --git a/Source/CompatibilityReportGenerator/Properties/Resources.Designer.cs b/Source/1.4/CompatibilityReportGenerator/Properties/Resources.Designer.cs similarity index 100% rename from Source/CompatibilityReportGenerator/Properties/Resources.Designer.cs rename to Source/1.4/CompatibilityReportGenerator/Properties/Resources.Designer.cs diff --git a/Source/CompatibilityReportGenerator/Properties/Resources.resx b/Source/1.4/CompatibilityReportGenerator/Properties/Resources.resx similarity index 100% rename from Source/CompatibilityReportGenerator/Properties/Resources.resx rename to Source/1.4/CompatibilityReportGenerator/Properties/Resources.resx diff --git a/Source/CompatibilityReportGenerator/Properties/launchSettings.json b/Source/1.4/CompatibilityReportGenerator/Properties/launchSettings.json similarity index 100% rename from Source/CompatibilityReportGenerator/Properties/launchSettings.json rename to Source/1.4/CompatibilityReportGenerator/Properties/launchSettings.json diff --git a/Source/CompatibilityReportGenerator/Template.md b/Source/1.4/CompatibilityReportGenerator/Template.md similarity index 100% rename from Source/CompatibilityReportGenerator/Template.md rename to Source/1.4/CompatibilityReportGenerator/Template.md diff --git a/Source/CompatibilityReportGenerator/UnityEngine.CoreModule.dll b/Source/1.4/CompatibilityReportGenerator/UnityEngine.CoreModule.dll similarity index 100% rename from Source/CompatibilityReportGenerator/UnityEngine.CoreModule.dll rename to Source/1.4/CompatibilityReportGenerator/UnityEngine.CoreModule.dll diff --git a/Source/CompatibilityReportGenerator/UnityEngine.SharedInternalsModule.dll b/Source/1.4/CompatibilityReportGenerator/UnityEngine.SharedInternalsModule.dll similarity index 100% rename from Source/CompatibilityReportGenerator/UnityEngine.SharedInternalsModule.dll rename to Source/1.4/CompatibilityReportGenerator/UnityEngine.SharedInternalsModule.dll diff --git a/Source/LightsaberPatch/Extensions.cs b/Source/1.4/LightsaberPatch/Extensions.cs similarity index 100% rename from Source/LightsaberPatch/Extensions.cs rename to Source/1.4/LightsaberPatch/Extensions.cs diff --git a/Source/1.4/LightsaberPatch/LightsaberPatch.csproj b/Source/1.4/LightsaberPatch/LightsaberPatch.csproj new file mode 100644 index 00000000..8a87b8b6 --- /dev/null +++ b/Source/1.4/LightsaberPatch/LightsaberPatch.csproj @@ -0,0 +1,57 @@ + + + + net480 + Library + false + false + preview + false + false + Release + AM.LightsaberPatch + AM.LightsaberPatch + + + + + runtime + + + False + False + all + + + + + + + 1.4.3901 + + + refs/1.4/SWSaber.dll + False + False + + + refs/1.4/CompActivatableEffect.dll + False + False + + + refs/1.4/CompSlotLoadable.dll + False + False + + + + + + none + ..\..\..\Patch_Lightsabers\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/LightsaberPatch/LightsaberSweepProvider.cs b/Source/1.4/LightsaberPatch/LightsaberSweepProvider.cs similarity index 100% rename from Source/LightsaberPatch/LightsaberSweepProvider.cs rename to Source/1.4/LightsaberPatch/LightsaberSweepProvider.cs diff --git a/Source/LightsaberPatch/PatchCore.cs b/Source/1.4/LightsaberPatch/PatchCore.cs similarity index 100% rename from Source/LightsaberPatch/PatchCore.cs rename to Source/1.4/LightsaberPatch/PatchCore.cs diff --git a/Source/LightsaberPatch/SaberRenderer.cs b/Source/1.4/LightsaberPatch/SaberRenderer.cs similarity index 100% rename from Source/LightsaberPatch/SaberRenderer.cs rename to Source/1.4/LightsaberPatch/SaberRenderer.cs diff --git a/Source/1.4/LightsaberPatch/refs/1.4/CompActivatableEffect.dll b/Source/1.4/LightsaberPatch/refs/1.4/CompActivatableEffect.dll new file mode 100644 index 00000000..21622c5e Binary files /dev/null and b/Source/1.4/LightsaberPatch/refs/1.4/CompActivatableEffect.dll differ diff --git a/Source/1.4/LightsaberPatch/refs/1.4/CompSlotLoadable.dll b/Source/1.4/LightsaberPatch/refs/1.4/CompSlotLoadable.dll new file mode 100644 index 00000000..2fabd236 Binary files /dev/null and b/Source/1.4/LightsaberPatch/refs/1.4/CompSlotLoadable.dll differ diff --git a/Source/1.4/LightsaberPatch/refs/1.4/SWSaber.dll b/Source/1.4/LightsaberPatch/refs/1.4/SWSaber.dll new file mode 100644 index 00000000..25e32a1f Binary files /dev/null and b/Source/1.4/LightsaberPatch/refs/1.4/SWSaber.dll differ diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Controllers/ModReportingController.cs b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Controllers/ModReportingController.cs similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Controllers/ModReportingController.cs rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Controllers/ModReportingController.cs diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/DAL/ModReportingDAL.cs b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/DAL/ModReportingDAL.cs similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/DAL/ModReportingDAL.cs rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/DAL/ModReportingDAL.cs diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Facade/ModReportingFacade.cs b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Facade/ModReportingFacade.cs similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Facade/ModReportingFacade.cs rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Facade/ModReportingFacade.cs diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LambdaEntryPoint.cs b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LambdaEntryPoint.cs similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LambdaEntryPoint.cs rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LambdaEntryPoint.cs diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LocalEntryPoint.cs b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LocalEntryPoint.cs similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LocalEntryPoint.cs rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/LocalEntryPoint.cs diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/ModRequestAPI.Backend.csproj b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/ModRequestAPI.Backend.csproj similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/ModRequestAPI.Backend.csproj rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/ModRequestAPI.Backend.csproj diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Properties/launchSettings.json b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Properties/launchSettings.json similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Properties/launchSettings.json rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Properties/launchSettings.json diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Readme.md b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Readme.md similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Readme.md rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Readme.md diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Startup.cs b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Startup.cs similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Startup.cs rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/Startup.cs diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.Development.json b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.Development.json similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.Development.json rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.Development.json diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.json b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.json similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.json rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/appsettings.json diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/aws-lambda-tools-defaults.json b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/aws-lambda-tools-defaults.json similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/aws-lambda-tools-defaults.json rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/aws-lambda-tools-defaults.json diff --git a/Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/serverless.template b/Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/serverless.template similarity index 100% rename from Source/ModRequestAPI.Backend/src/ModRequestAPI.Backend/serverless.template rename to Source/1.4/ModRequestAPI.Backend/src/ModRequestAPI.Backend/serverless.template diff --git a/Source/ModRequestAPI.Models/MissingModRequest.cs b/Source/1.4/ModRequestAPI.Models/MissingModRequest.cs similarity index 100% rename from Source/ModRequestAPI.Models/MissingModRequest.cs rename to Source/1.4/ModRequestAPI.Models/MissingModRequest.cs diff --git a/Source/1.4/ModRequestAPI.Models/ModRequestAPI.Models.csproj b/Source/1.4/ModRequestAPI.Models/ModRequestAPI.Models.csproj new file mode 100644 index 00000000..859f87bf --- /dev/null +++ b/Source/1.4/ModRequestAPI.Models/ModRequestAPI.Models.csproj @@ -0,0 +1,11 @@ + + + + net472 + + + + + + + diff --git a/Source/1.4/ModRequestAPI/ModRequestAPI.csproj b/Source/1.4/ModRequestAPI/ModRequestAPI.csproj new file mode 100644 index 00000000..bc2ce84c --- /dev/null +++ b/Source/1.4/ModRequestAPI/ModRequestAPI.csproj @@ -0,0 +1,22 @@ + + + + net472 + Library + disable + disable + false + 11 + none + + + + + + + + + + + + diff --git a/Source/ModRequestAPI/ModRequestClient.cs b/Source/1.4/ModRequestAPI/ModRequestClient.cs similarity index 100% rename from Source/ModRequestAPI/ModRequestClient.cs rename to Source/1.4/ModRequestAPI/ModRequestClient.cs diff --git a/Source/PerformanceOptimizerPatch/PatchCore.cs b/Source/1.4/PerformanceOptimizerPatch/PatchCore.cs similarity index 100% rename from Source/PerformanceOptimizerPatch/PatchCore.cs rename to Source/1.4/PerformanceOptimizerPatch/PatchCore.cs diff --git a/Source/PerformanceOptimizerPatch/Patch_Optimization_PawnUtility_IsInvisible_DoPatches.cs b/Source/1.4/PerformanceOptimizerPatch/Patch_Optimization_PawnUtility_IsInvisible_DoPatches.cs similarity index 100% rename from Source/PerformanceOptimizerPatch/Patch_Optimization_PawnUtility_IsInvisible_DoPatches.cs rename to Source/1.4/PerformanceOptimizerPatch/Patch_Optimization_PawnUtility_IsInvisible_DoPatches.cs diff --git a/Source/PerformanceOptimizerPatch/Patch_Optimization_RefreshRate_DrawSettings.cs b/Source/1.4/PerformanceOptimizerPatch/Patch_Optimization_RefreshRate_DrawSettings.cs similarity index 100% rename from Source/PerformanceOptimizerPatch/Patch_Optimization_RefreshRate_DrawSettings.cs rename to Source/1.4/PerformanceOptimizerPatch/Patch_Optimization_RefreshRate_DrawSettings.cs diff --git a/Source/1.4/PerformanceOptimizerPatch/PerformanceOptimizerPatch.csproj b/Source/1.4/PerformanceOptimizerPatch/PerformanceOptimizerPatch.csproj new file mode 100644 index 00000000..6046eff2 --- /dev/null +++ b/Source/1.4/PerformanceOptimizerPatch/PerformanceOptimizerPatch.csproj @@ -0,0 +1,56 @@ + + + + net472 + Library + preview + false + true + false + false + Release + AM.PerformanceOptimizerPatch + zz.AM.PerformanceOptimizerPatch + disable + true + none + + + + + runtime + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + False + False + all + + + + + + + 1.4.3901 + + + refs/1.4.PerformanceOptimizer.dll + False + False + runtime + + + + + + + none + ..\..\..\Patch_PerformanceOptimizer\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/PerformanceOptimizerPatch/refs/PerformanceOptimizer.dll b/Source/1.4/PerformanceOptimizerPatch/refs/1.4/PerformanceOptimizer.dll similarity index 100% rename from Source/PerformanceOptimizerPatch/refs/PerformanceOptimizer.dll rename to Source/1.4/PerformanceOptimizerPatch/refs/1.4/PerformanceOptimizer.dll diff --git a/Source/TacticowlPatch/PatchCore.cs b/Source/1.4/TacticowlPatch/PatchCore.cs similarity index 100% rename from Source/TacticowlPatch/PatchCore.cs rename to Source/1.4/TacticowlPatch/PatchCore.cs diff --git a/Source/TacticowlPatch/refs/Tacticowl.dll b/Source/1.4/TacticowlPatch/Tacticowl.dll similarity index 100% rename from Source/TacticowlPatch/refs/Tacticowl.dll rename to Source/1.4/TacticowlPatch/Tacticowl.dll diff --git a/Source/1.4/TacticowlPatch/TacticowlPatch.csproj b/Source/1.4/TacticowlPatch/TacticowlPatch.csproj new file mode 100644 index 00000000..6fdb9724 --- /dev/null +++ b/Source/1.4/TacticowlPatch/TacticowlPatch.csproj @@ -0,0 +1,48 @@ + + + + net472 + Library + preview + false + true + false + false + Release + AM.TacticowlPatch + AM.TacticowlPatch + disable + true + none + + + + + False + False + all + + + + + + + 1.4.3901 + + + Tacticowl.dll + False + False + runtime + + + + + + none + ..\..\..\Patch_Tacticowl\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/1.4/ThingGenerator/0ColourPicker.dll b/Source/1.4/ThingGenerator/0ColourPicker.dll new file mode 100644 index 00000000..2e294d35 Binary files /dev/null and b/Source/1.4/ThingGenerator/0ColourPicker.dll differ diff --git a/Source/AnimationMod/AMSettings/Presets/Default.cs b/Source/1.4/ThingGenerator/AMSettings/Presets/Default.cs similarity index 100% rename from Source/AnimationMod/AMSettings/Presets/Default.cs rename to Source/1.4/ThingGenerator/AMSettings/Presets/Default.cs diff --git a/Source/AnimationMod/AMSettings/Presets/NoLassos.cs b/Source/1.4/ThingGenerator/AMSettings/Presets/NoLassos.cs similarity index 100% rename from Source/AnimationMod/AMSettings/Presets/NoLassos.cs rename to Source/1.4/ThingGenerator/AMSettings/Presets/NoLassos.cs diff --git a/Source/AnimationMod/AMSettings/Presets/VanillaPlus.cs b/Source/1.4/ThingGenerator/AMSettings/Presets/VanillaPlus.cs similarity index 100% rename from Source/AnimationMod/AMSettings/Presets/VanillaPlus.cs rename to Source/1.4/ThingGenerator/AMSettings/Presets/VanillaPlus.cs diff --git a/Source/AnimationMod/AMSettings/Settings.cs b/Source/1.4/ThingGenerator/AMSettings/Settings.cs similarity index 98% rename from Source/AnimationMod/AMSettings/Settings.cs rename to Source/1.4/ThingGenerator/AMSettings/Settings.cs index b70fe48f..25345feb 100644 --- a/Source/AnimationMod/AMSettings/Settings.cs +++ b/Source/1.4/ThingGenerator/AMSettings/Settings.cs @@ -4,7 +4,6 @@ using System.Linq; using UnityEngine; using Verse; -using LudeonTK; namespace AM.AMSettings; @@ -426,8 +425,11 @@ float DrawAnim(AnimDef def) { checkbox.x += 110; checkbox.width = 200; - - def.SData.Probability = Widgets.HorizontalSlider(checkbox, def.SData.Probability, 0f, 10f, label: $"Relative Probability: {def.SData.Probability * 100f:F0}%", roundTo: 0.05f); +#if V13 + def.SData.Probability = Widgets.HorizontalSlider(checkbox, def.SData.Probability, 0f, 10f, label: $"Relative Probability: {def.SData.Probability * 100f:F0}%"); +#else + def.SData.Probability = Widgets.HorizontalSlider_NewTemp(checkbox, def.SData.Probability, 0f, 10f, label: $"Relative Probability: {def.SData.Probability * 100f:F0}%", roundTo: 0.05f); +#endif } return rect.height; diff --git a/Source/1.4/ThingGenerator/AMSettings/SimpleSettings.cs b/Source/1.4/ThingGenerator/AMSettings/SimpleSettings.cs new file mode 100644 index 00000000..92dc7cde --- /dev/null +++ b/Source/1.4/ThingGenerator/AMSettings/SimpleSettings.cs @@ -0,0 +1,1272 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Remoting.Messaging; +using System.Security; +using System.Text; +using AM.UI; +using AM.Video; +using ColourPicker; +using RimWorld; +using UnityEngine; +using Verse; + +namespace AM.AMSettings; + +public abstract class SimpleSettingsBase : ModSettings +{ + public string GetName() + { + string translationKey = GetType().FullName; + if (translationKey.TryTranslate(out var result)) + return result; + return PresetName; + } + + public string GetDescription() + { + string translationKey = $"{GetType().FullName}.Desc"; + if (translationKey.TryTranslate(out var result)) + return result; + return PresetDescription; + } + + public virtual string PresetName => null; + public virtual string PresetDescription => null; +} + +public static class SimpleSettings +{ + [DebugOutput("AnimationMod", onlyWhenPlaying = false)] + public static void OutputTranslationKeys() + { + Log.Message(GenerateTranslationKeys(Core.Settings)); + } + + public delegate float DrawHandler(SimpleSettingsBase settings, MemberWrapper member, Rect area); + + public static readonly Dictionary DrawHandlers = new Dictionary + { + //{ typeof(string), DrawStringField }, + { typeof(byte), DrawNumeric }, + { typeof(sbyte), DrawNumeric }, + { typeof(short), DrawNumeric }, + { typeof(ushort), DrawNumeric }, + { typeof(int), DrawNumeric }, + { typeof(uint), DrawNumeric }, + { typeof(long), DrawNumeric }, + { typeof(ulong), DrawNumeric }, + { typeof(float), DrawNumeric }, + { typeof(double), DrawNumeric }, + { typeof(decimal), DrawNumeric }, + { typeof(bool), DrawToggle }, + { typeof(Color), DrawColor } + }; + public static Func SelectDrawHandler { get; set; } = DefaultDrawHandlerSelector; + + private static readonly Dictionary settingsFields = new Dictionary(); + private static readonly Stack saverStack = new Stack(); + private static readonly Stack loaderStack = new Stack(); + private static readonly Stack modeStack = new Stack(); + private static readonly HashSet allHeaders = new HashSet(); + private static readonly List presets = new List(); + private static MemberWrapper highlightedMember; + + internal static string TranslateOrSelf(this string str) => str.TryTranslate(out var found) ? found : str; + + private static string GenerateTranslationKeys(SimpleSettingsBase settings) + { + FieldHolder holder = GetHolder(settings); + + var str = new StringBuilder(1024); + string settingsType = settings.GetType().FullName; + + void Emit(string key, string contents) + { + str.Append(" <").Append(key).Append(">").Append(SecurityElement.Escape(contents)).Append(""); + } + + foreach (var member in holder.Members.Values) + { + var header = member.TryGetCustomAttribute(); + if (header != null) + { + Emit($"{settingsType}.Header.{EscapeXmlName(header.header)}", header.header); + } + + Emit($"{member.TranslationName}", member.DisplayName); + Emit($"{member.TranslationName}.Desc", SecurityElement.Escape(member.GetDescription())); + } + + return str.ToString(); + } + + private static string EscapeXmlName(string name) => ReplaceAll(name, stackalloc char[] { ' ', '&' }, '_'); + + private static string ReplaceAll(string input, ReadOnlySpan chars, char replacement) + { + for (int i = 0; i < chars.Length; i++) + { + input = input.Replace(chars[i], replacement); + } + return input; + } + + public static void Init(SimpleSettingsBase settings) + { + if (settings == null) + return; + + var type = settings.GetType(); + if (settingsFields.ContainsKey(type)) + { + Log.Error($"Already called Init() for settings class: {type.FullName}"); + return; + } + + var def = new FieldHolder(settings, type); + settingsFields.Add(type, def); + + presets.Clear(); + + foreach (var t in Assembly.GetExecutingAssembly().GetTypes()) + { + if (t.IsSubclassOf(type) && !t.IsAbstract) + presets.Add(Activator.CreateInstance(t) as SimpleSettingsBase); + } + } + + private static FieldHolder GetHolder(SimpleSettingsBase settings) + { + if (settingsFields.TryGetValue(settings.GetType(), out var found)) + return found; + + Init(settings); + return settingsFields[settings.GetType()]; + } + + public static void AutoExpose(SimpleSettingsBase settings) + { + var holder = GetHolder(settings); + + foreach (var member in holder.Members.Values) + { + if (member.ShouldExpose) + member.Expose(settings); + } + } + + public static object SmartClone(object obj) + { + if (obj == null) + return null; + + // Values types are just fine being passed back, since it will be a copy by the time it is assigned back. + if (obj.GetType().IsValueType) + return obj; + + // Defs are not cloned. They are just refs. + if (obj.GetType().IsSubclassOf(typeof(Def))) + return obj; + + // Lists... + var type = obj.GetType(); + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + // Make new list of appropriate type. + var param = type.GenericTypeArguments[0]; + var generic = typeof(List<>).MakeGenericType(param); + var list = Activator.CreateInstance(generic) as IList; + + // Copy over each item from the original list. + var origList = obj as IList; + foreach (var item in origList) + list.Add(SmartClone(item)); + return list; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + // Make new dict of appropriate type. + var param = type.GenericTypeArguments[0]; + var param2 = type.GenericTypeArguments[1]; + var generic = typeof(Dictionary<,>).MakeGenericType(param, param2); + var dict = Activator.CreateInstance(generic) as IDictionary; + + // Copy over each item from the original list. + var origDict = obj as IDictionary; + foreach (var key in origDict.Keys) + { + var newKey = SmartClone(key); + var newValue = SmartClone(origDict[key]); + dict.Add(newKey, newValue); + } + return dict; + } + + if (obj is ICloneable cl) + { + return cl.Clone(); + } + + if (obj is not IExposable exp) + { + Log.Warning($"Cannot create clone of type '{type.FullName}' since it is neither IExposable nor ICloneable."); + return obj; + } + + // Try to make clone using IExposable. + if (Activator.CreateInstance(type) is not IExposable created) + { + Log.Error($"Failed to create new instance of '{type.FullName}' because it lacks a public zero argument constructor or is abstract."); + return null; + } + + // Write to temp file. It would be nice if this could be done to a memory stream - unfortunately the fields are not exposed and I don't want to + // make a hacky workaround using reflection, with it being such an important class. + PushNewScribeState(); + try + { + string tempFilePath = Path.GetTempFileName(); + Scribe.saver.InitSaving(tempFilePath, "ROOT"); + exp.ExposeData(); + Scribe.saver.FinalizeSaving(); + + // Load back from the temporary file. + Scribe.loader.InitLoading(tempFilePath); + created.ExposeData(); + Scribe.loader.FinalizeLoading(); + } + catch (Exception e) + { + Log.Error($"Exception when cloning using custom scribe: {e}"); + } + finally + { + PopScribeState(); + } + + + return created; + } + + public static string MakeDebugString(SimpleSettingsBase settings) + { + if (settings == null) + return null; + + var holder = GetHolder(settings); + var str = new StringBuilder(1024); + + foreach (var member in holder.Members.Values) + { + str.Append('[').Append(member.MemberType.Name).Append("] "); + str.Append(member.Name).Append(" : ").AppendLine(member.DefaultValue?.ToString() ?? ""); + } + + return str.ToString(); + } + + private static void ShowPresetsDropdown(SimpleSettingsBase settings, FieldHolder f) + { + var items = new List(presets.Count); + foreach (var pre in presets) + { + var p = pre; + items.Add(new FloatMenuOption(pre.GetName(), () => + { + SetToPreset(settings, p, f); + Core.Log($"Set settings to preset {pre.PresetName} ({pre.GetType()})"); + }) + { + tooltip = pre.GetDescription() + }); + } + Find.WindowStack.Add(new FloatMenu(items)); + } + + public static void DrawWindow(SimpleSettingsBase settings, Rect inRect) + { + if (settings == null) + return; + + SimpleSettingsBase currentPreset = null; + foreach (var p in presets) + { + if (AreEqual(settings, p, GetHolder(settings))) + { + currentPreset = p; + break; + } + } + + var holder = GetHolder(settings); + + if (Widgets.ButtonText(inRect with {height = 24, width = inRect.width * 0.2f}, $"{"SimpleSettings.Preset".Translate()} {currentPreset?.GetName() ?? "SimpleSettings.Preset.Custom".Translate()}")) + ShowPresetsDropdown(settings, holder); + if (currentPreset != null) + TooltipHandler.TipRegion(inRect with {height = 24, width = inRect.width * 0.2f}, currentPreset.GetDescription()); + + var tips = inRect with {height = 24, x = inRect.width * 0.2f + 20}; + Widgets.LabelFit(tips, "SimpleSettings.ClickToReset".Translate()); + Widgets.DrawLineHorizontal(inRect.x, inRect.y + 30, inRect.width); + inRect.yMin += 38; + + var baseArea = inRect; + Rect tabBar = inRect; + tabBar.height = 28; + + highlightedMember = null; + float totalWidth = inRect.width; + inRect.width *= 0.7f; + inRect.y += 28; + inRect.height -= 28; + + Widgets.BeginScrollView(inRect, ref holder.UI_Scroll, new Rect(0, 0, holder.UI_LastSize.x, holder.UI_LastSize.y)); + var size = new Vector2(inRect.width - 20, 0); + var pos = Vector2.zero; + string currentHeader = null; + string selectedHeader = holder.UI_SelectedTab; + allHeaders.Clear(); + + foreach (var member in holder.Members.Values) + { + bool isCurrentTab = currentHeader == selectedHeader; + + var header = member.TryGetCustomAttribute(); + bool didHeader = false; + if (header != null) + { + currentHeader = header.header; + allHeaders.Add(header.header); + isCurrentTab = currentHeader == selectedHeader; + + float headerHeight = 32; + + if(isCurrentTab) + { + var headerRect = new Rect(new Vector2(pos.x, pos.y + 12), new Vector2(inRect.width - 20, headerHeight)); + string headerText = $"{settings.GetType().FullName}.Header.{EscapeXmlName(header.header)}".TryTranslate(out var found) ? found : header.header; + Widgets.Label(headerRect, $"{headerText}"); + + pos.y += headerHeight + 12; + size.y += headerHeight + 12; + didHeader = true; + } + } + + if (!isCurrentTab) + continue; + + var handler = SelectDrawHandler(member); + if (handler == null) + continue; + + // Conditional visibility: + if (!member.ShouldDraw(settings, holder)) + continue; + + if (!didHeader) + { + pos.y += 6; + size.y += 6; + GUI.color = new Color(1, 1, 1, 0.25f); + Widgets.DrawLineHorizontal(pos.x + 20, pos.y, inRect.width); + GUI.color = Color.white; + pos.y += 6; + size.y += 6; + } + else + { + pos.y += 12; + size.y += 12; + } + + var area = new Rect(new Vector2(pos.x + 20, pos.y), new Vector2(inRect.width - 40, inRect.height - pos.y)); + float height = handler(settings, member, area); + + var finalArea = area; + finalArea.height = height; + + if (member.Options.DrawHoverHighlight) + Widgets.DrawHighlightIfMouseover(finalArea); + if (Mouse.IsOver(finalArea)) + highlightedMember = member; + + pos.y += height; + size.y += height; + } + + Widgets.EndScrollView(); + holder.UI_LastSize = size; + holder.UI_SelectedTab ??= allHeaders.First(); + + float tabWidth = (1f / allHeaders.Count) * tabBar.width; + int i = 0; + foreach(string tab in allHeaders) + { + var area = new Rect(tabBar.x + tabWidth * i, tabBar.y, tabWidth, tabBar.height); + bool active = holder.UI_SelectedTab == tab; + GUI.color = active ? Color.grey : Color.white; + string tabText = $"{settings.GetType().FullName}.Header.{tab.Replace(' ', '_')}".TryTranslate(out var found) ? found : tab; + if (Widgets.ButtonText(area.ExpandedBy(-2, 0), $"{tabText}")) + holder.UI_SelectedTab = tab; + if (active) + Widgets.DrawBox(area.ExpandedBy(-2, 0)); + GUI.color = Color.white; + i++; + } + + inRect.x = inRect.xMax; + inRect.width = totalWidth * 0.3f; + GUI.color = Color.white * 0.5f; + Widgets.DrawBox(inRect); + GUI.color = Color.white; + inRect = inRect.ExpandedBy(-5, -5); + + if (highlightedMember == null) + return; + + Text.Anchor = TextAnchor.UpperCenter; + Text.Font = GameFont.Medium; + Widgets.Label(inRect, $"{highlightedMember.DisplayName}"); + float titleHeight = Text.CalcHeight($"{highlightedMember.DisplayName}", inRect.width); + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.UpperLeft; + + if (highlightedMember.Options.DrawDescription) + { + string description = highlightedMember.GetDescription() ?? "No description"; + inRect.y += titleHeight + 14; + Widgets.Label(inRect, description); + + float h = Text.CalcHeight(description, inRect.width) + 16; + inRect.y += h; + } + + if (highlightedMember.Options.AllowReset) + { + string defaultValue = highlightedMember.ValueToString(highlightedMember.GetDefault()); + string desc = $"Default value: {defaultValue}\n\nRight-click to reset to default."; + Widgets.Label(inRect, desc); + float h = Text.CalcHeight(desc, inRect.width) + 16; + inRect.y += h; + + if (Input.GetMouseButtonUp(1)) + { + highlightedMember.Set(settings, highlightedMember.DefaultValue); + highlightedMember.TextBuffer = highlightedMember.DefaultValue.ToString(); + } + } + + if (highlightedMember.WebContent?.BundleName != null) + { + // Web content: + string url = highlightedMember.WebContent.BundleName; + inRect.height = baseArea.height - inRect.y + 72; + + var tex = highlightedMember.WebContent.IsVideo ? VideoPlayerUtil.GetVideoTexture(url, out _) : VideoPlayerUtil.GetStaticTexture(url, out _); + if (tex == null) + { + Widgets.DrawBoxSolid(inRect, new Color(1, 1, 1, 0.1f)); + + var oldFont = Text.Font; + var oldAnchor = Text.Anchor; + Text.Anchor = TextAnchor.MiddleCenter; + Text.Font = GameFont.Medium; + + Widgets.Label(inRect, "Loading..."); + + Text.Font = oldFont; + Text.Anchor = oldAnchor; + } + else + { + var fitted = tex.FitRect(inRect, UI.ScaleMode.Fit); + GUI.DrawTexture(fitted, tex); + } + } + } + + public static DrawHandler DefaultDrawHandlerSelector(MemberWrapper wrapper) + { + if (wrapper == null) + return null; + + if (wrapper.OverrideDrawHandler != null) + return wrapper.OverrideDrawHandler; + + var type = wrapper.MemberType; + + // Enum. + if (type.IsEnum) + return DrawEnum; + + // Everything else. + if (DrawHandlers.TryGetValue(type, out var found)) + return found; + + return null; + } + + private static float GetNumericMin(Type type) + { + return (float)Convert.ChangeType(type.GetField("MinValue", BindingFlags.Public | BindingFlags.Static).GetValue(null), typeof(float)); + } + + private static float GetNumericMax(Type type) + { + return (float)Convert.ChangeType(type.GetField("MaxValue", BindingFlags.Public | BindingFlags.Static).GetValue(null), typeof(float)); + } + + public static float DrawFieldHeader(SimpleSettingsBase settings, MemberWrapper member, Rect area) + { + float height = 26; + + Rect labelrect = area; + labelrect.height = height; + var value = member.Get(settings); + var old = Text.Anchor; + Text.Anchor = TextAnchor.MiddleLeft; + + string label = $"{member.DisplayName}"; + if (member.Options.DrawValue) + label += $": {member.ValueToString(value)}"; + if (member.Options.AllowReset) + label = HighlightIfNotDefault(settings, member, label); + + Widgets.Label(labelrect, label); + Text.Anchor = old; + + return height; + } + + private static string HighlightIfNotDefault(SimpleSettingsBase settings, MemberWrapper member, string str) + { + if (member.IsDefault(settings)) + return str; + + return $"{str}"; + } + + private static void TryGetBounds(MemberWrapper member, out float? min, out float? max) + { + var range = member.TryGetCustomAttribute(); + var minAtr = member.TryGetCustomAttribute(); + var percentage = member.TryGetCustomAttribute(); + + min = null; + max = null; + + if(range != null) + { + min = range.min; + max = range.max; + } + else if (minAtr != null) + { + min = minAtr.min; + } + else if (percentage != null) + { + min = 0; + max = 1; + } + + if (min != null) + min = Mathf.Max(min.Value, GetNumericMin(member.MemberType)); + if (max != null) + max = Mathf.Min(max.Value, GetNumericMax(member.MemberType)); + } + + private static float DrawNumeric(SimpleSettingsBase settings, MemberWrapper member, Rect area) + { + float height = DrawFieldHeader(settings, member, area); + float value = member.Get(settings); + + // Min and max. + TryGetBounds(member, out var min, out var max); + if (min == null || max == null) + { + return DrawNumericTextBased(settings, member, area, min, max); + } + + float sliderHeight = 18; + Rect sliderArea = new(area.x, area.y + height, area.width, sliderHeight); + height += sliderHeight; + + float step = member.TryGetCustomAttribute()?.Step ?? -1; + + // Simple slider for now. +#if V13 + float changed = Widgets.HorizontalSlider(sliderArea, value, min.Value, max.Value, roundTo: step); +#else + float changed = Widgets.HorizontalSlider_NewTemp(sliderArea, value, min.Value, max.Value, roundTo: step); +#endif + if (changed != value) + { + Type type = member.MemberType; + bool isFloatType = type == typeof(float) || type == typeof(double) || type == typeof(decimal); + object writeBack = changed; + if (!isFloatType) + writeBack = (long)Mathf.Round(changed); + member.Set(settings, writeBack); + } + + return height; + } + + private static float DrawNumericTextBased(SimpleSettingsBase settings, MemberWrapper member, Rect area, float? min, float? max) + { + float height = DrawFieldHeader(settings, member, area); + float value = member.Get(settings); + + const float TEXTBOX_HEIGHT = 28; + Rect field = area; + field.y += height; + field.height = TEXTBOX_HEIGHT; + + float minF = min ?? float.MinValue; + float maxF = max ?? float.MaxValue; + float newValue = value; + if (member.TextBuffer.Length == 0) + member.TextBuffer = member.DefaultValue.ToString(); + //Core.Log($"Drawing {member.Name}, {newValue}, {member.TextBuffer}, {minF}, {maxF}"); + Widgets.TextFieldNumeric(field, ref newValue, ref member.TextBuffer, minF, maxF); + if (newValue != value) + member.Set(settings, newValue); + + return TEXTBOX_HEIGHT + height; + } + + private static float DrawToggle(SimpleSettingsBase settings, MemberWrapper member, Rect area) + { + Rect toggleRect = area; + toggleRect.height = 28; + + bool enabled = member.Get(settings); + bool old = enabled; + string txt = HighlightIfNotDefault(settings, member, $"{member.DisplayName}: "); + toggleRect.width = Text.CalcSize(txt).x + 24f + 24f; + Widgets.CheckboxLabeled(toggleRect, txt, ref enabled, placeCheckboxNearText: false); + + if (old != enabled) + member.Set(settings, enabled); + + return toggleRect.height; + } + + private static float DrawColor(SimpleSettingsBase settings, MemberWrapper member, Rect area) + { + float h = 28; + + Color c = member.Get(settings); + float height = DrawFieldHeader(settings, member, area); + + area.yMin += height; + area.height = h; + area = area.ExpandedBy(-2, -2); + + void SelectedColor(Color newColor) + { + member.Set(settings, newColor); + } + + Widgets.DrawBoxSolidWithOutline(area, c, Color.black, 2); + if (Widgets.ClickedInsideRect(area)) + { + var picker = new Dialog_ColourPicker(c, SelectedColor) + { + autoApply = false, + }; + + Find.WindowStack.Add(picker); + } + + return h + height; + } + + private static float DrawEnum(SimpleSettingsBase settings, MemberWrapper member, Rect area) + { + float height = 28; + + Rect rect = area; + rect.height = height; + + string txt = HighlightIfNotDefault(settings, member, $"{member.DisplayName}: "); + float labelWidth = Text.CalcSize(txt).x; + Widgets.Label(rect, txt); + + var value = member.Get(settings); + + if (Widgets.ButtonText(new Rect(rect.x + labelWidth + 10, rect.y, 240, height), member.ValueToString(value))) + { + var values = Enum.GetValues(member.MemberType).OfType(); + FloatMenuUtility.MakeMenu(values, member.ValueToString, o => + () => + { + member.Set(settings, o); + }); + } + + return height; + } + + public static void PushNewScribeState() + { + saverStack.Push(Scribe.saver); + loaderStack.Push(Scribe.loader); + modeStack.Push(Scribe.mode); + + Scribe.saver = new ScribeSaver(); + Scribe.loader = new ScribeLoader(); + Scribe.mode = LoadSaveMode.Inactive; + } + + public static void PopScribeState() + { + Scribe.saver = saverStack.Pop(); + Scribe.loader = loaderStack.Pop(); + Scribe.mode = modeStack.Pop(); + } + + public static bool AreEqual(SimpleSettingsBase current, SimpleSettingsBase preset, FieldHolder f) + { + f ??= new FieldHolder(current, current.GetType()); + foreach (var pair in f.Members) + { + var va = pair.Value.Get(current); + var vb = pair.Value.Get(preset); + + if ((va == null) != (vb == null)) + return false; + if (va == null) + continue; + + if (pair.Value.Options?.IgnoreEqualityForPresets ?? false) + continue; + + // Special check: + if (va is ISettingsEqualityChecker checker && !checker.IsEqualForSettings(vb)) + return false; + + // Regular equality check: + if (!va.Equals(vb)) + return false; + } + + return true; + } + + public static void SetToPreset(SimpleSettingsBase current, SimpleSettingsBase preset, FieldHolder f) + { + f ??= new FieldHolder(current, current.GetType()); + foreach (var pair in f.Members) + { + var vb = pair.Value.Get(preset); + + pair.Value.Set(current, SmartClone(vb)); + } + } + + public class FieldHolder + { + public readonly SimpleSettingsBase ForSettingsObject; + public readonly Type ForType; + public readonly Dictionary Members = new Dictionary(); + public Vector2 UI_LastSize; + public Vector2 UI_Scroll; + public string UI_SelectedTab; + + public FieldHolder(SimpleSettingsBase settings, Type forType) + { + ForSettingsObject = settings; + ForType = forType; + + foreach (var member in ForType.GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (member is not FieldInfo && member is not PropertyInfo) + continue; + + bool isInvalidProp = member is PropertyInfo pi && (!pi.CanWrite || !pi.CanRead); + if (isInvalidProp) + continue; + + if (member.TryGetAttribute() != null) + continue; + + var wrapper = MakeWrapperFor(settings, member); + SetOverrideDrawHandler(settings, wrapper); + + Members.Add(member, wrapper); + } + } + + private MemberWrapper MakeWrapperFor(object obj, MemberInfo member) + { + var type = member switch + { + FieldInfo fi => fi.FieldType, + PropertyInfo pi => pi.PropertyType, + _ => throw new ArgumentException(nameof(member), $"Unexpected type: {member.GetType().FullName}") + }; + + // Dictionaries. + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + return MakeGenericWrapper(typeof(MemberWrapperDict<,>), obj, member, type.GetGenericArguments()); + } + + // Lists. + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + return MakeGenericWrapper(typeof(MemberWrapperList<>), type.GetGenericArguments()[0], obj, member); + } + + // Defs. + if (typeof(Def).IsAssignableFrom(type)) + { + return MakeGenericWrapper(typeof(MemberWrapperDef<>), type, obj, member); + } + + // IExposable or regular value type. + return MakeGenericWrapper(typeof(MemberWrapperGen<>), type, obj, member); + } + + private MemberWrapper MakeGenericWrapper(Type baseGenericType, Type genericParam, object obj, MemberInfo member) + { + var generic = baseGenericType.MakeGenericType(genericParam); + return Activator.CreateInstance(generic, obj, member) as MemberWrapper; + } + + private MemberWrapper MakeGenericWrapper(Type baseGenericType, object obj, MemberInfo member, params Type[] genericParams) + { + var generic = baseGenericType.MakeGenericType(genericParams); + return Activator.CreateInstance(generic, obj, member) as MemberWrapper; + } + + private void SetOverrideDrawHandler(SimpleSettingsBase settings, MemberWrapper member) + { + if (member == null) + return; + + var attr = member.TryGetCustomAttribute(); + if (attr == null) + return; + + member.ShouldExpose = attr.SerializeField; + + const BindingFlags FLAGS = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + var methodInfo = settings.GetType().GetMethod(attr.MethodName, FLAGS); + if (methodInfo == null) + { + Core.Error($"Failed to find method '{attr.MethodName}' in class {settings.GetType()}!"); + return; + } + + DrawHandler handler; + if (methodInfo.IsStatic) + handler = Delegate.CreateDelegate(typeof(DrawHandler), methodInfo, false) as DrawHandler; + else + handler = Delegate.CreateDelegate(typeof(DrawHandler), settings, methodInfo, false) as DrawHandler; + + if (handler == null) + { + Core.Error($"Method '{attr.MethodName}' does not match the {nameof(DrawHandler)} signiture!"); + return; + } + + member.OverrideDrawHandler = handler; + } + + public MemberWrapper TryGetNamedMember(string name) + { + foreach (var mem in Members) + { + if (mem.Value.Name == name) + return mem.Value; + } + return null; + } + } + + private class MemberWrapperDict : MemberWrapper + { + public MemberWrapperDict(object obj, FieldInfo field) : base(obj, field) { } + public MemberWrapperDict(object obj, PropertyInfo prop) : base(obj, prop) { } + + private LookMode GetLookMode() + { + if (typeof(T).GetInterfaces().Contains(typeof(IExposable))) + return LookMode.Deep; + + if (typeof(Def).IsAssignableFrom(typeof(T))) + return LookMode.Def; + + return LookMode.Undefined; + } + + public override void Expose(object obj) + { + var current = Get>(obj); + Scribe_Collections.Look(ref current, NameInXML, GetLookMode(), GetLookMode()); + Set(obj, current); + } + } + + private class MemberWrapperList : MemberWrapper + { + public MemberWrapperList(object obj, FieldInfo field) : base(obj, field) { } + public MemberWrapperList(object obj, PropertyInfo prop) : base(obj, prop) { } + + public LookMode GetLookMode() + { + if (typeof(T).GetInterfaces().Contains(typeof(IExposable))) + return LookMode.Deep; + + if (typeof(Def).IsAssignableFrom(typeof(T))) + return LookMode.Def; + + return LookMode.Undefined; + } + + public override void Expose(object obj) + { + var current = Get>(obj); + Scribe_Collections.Look(ref current, NameInXML, GetLookMode()); + Set(obj, current); + } + } + + private class MemberWrapperDef : MemberWrapperGen where T : Def, new() + { + public MemberWrapperDef(object obj, FieldInfo field) : base(obj, field) { } + public MemberWrapperDef(object obj, PropertyInfo prop) : base(obj, prop) { } + + public override void Expose(object obj) + { + // Do not call base. + // Here we only have to handle defs using the Scribe_Defs. + T current = Get(obj); + Scribe_Defs.Look(ref current, NameInXML); + Set(obj, current); + } + } + + private class MemberWrapperGen : MemberWrapper + { + public MemberWrapperGen(object obj, FieldInfo field) : base(obj, field) { } + public MemberWrapperGen(object obj, PropertyInfo prop) : base(obj, prop) { } + + public override void Expose(object obj) + { + T current = Get(obj); + T defaultValue = (T)DefaultValue; + + // IExposable: use Scribe_Deep. + if (IsIExposable) + { + Scribe_Deep.Look(ref current, NameInXML); + Set(obj, current); + return; + } + + // Default: use Scribe_Values + Scribe_Values.Look(ref current, NameInXML, defaultValue); + Set(obj, current); + } + + public override K Get(object obj) + { + // When unboxing, we need to cast to the exact type. + // However, it is nice to be able to call Get where T is assignable from the real type, + // such as Get when the field is actually a double. + // The cast will fail unless we unbox to the T first before re-boxing, then unboxing into + object temp = base.Get(obj); + if (temp == null) + return default; + + if (typeof(T) != typeof(K) && temp is IConvertible) + { + return (K)Convert.ChangeType(temp, typeof(K)); + } + + return (K)temp; + } + + public override K GetDefault() + { + object temp = (T)DefaultValue; + if (temp == null) + return default; + + if (typeof(K) == typeof(object)) + return (K)temp; + + if (typeof(T) != typeof(K)) + return (K)Convert.ChangeType(temp, typeof(K)); + + return (K)temp; + } + } + + public abstract class MemberWrapper + { + public string DisplayName => _displayName ??= MakeDisplayName(); + public string Name => field?.Name ?? prop?.Name; + public readonly object DefaultValue; + public Type MemberType => field?.FieldType ?? prop.PropertyType; + public Type DeclaringType => field?.DeclaringType ?? prop.DeclaringType; + public string TranslationName => $"{DeclaringType.FullName}.{Name}"; + public bool IsIExposable => MemberType.GetInterfaces().Contains(typeof(IExposable)); + public bool IsValueType => MemberType.IsValueType; + public bool IsDefType => typeof(Def).IsAssignableFrom(MemberType); + public bool IsStatic => field?.IsStatic ?? prop.GetMethod.IsStatic; + public string NameInXML => Name; + public IEnumerable CustomAttributes => field?.GetCustomAttributes() ?? prop.GetCustomAttributes(); + public string TextBuffer = ""; + public DrawHandler OverrideDrawHandler { get; set; } + public bool ShouldExpose { get; set; } = true; + public SettingOptionsAttribute Options { get; protected set; } + public WebContentAttribute WebContent { get; protected set; } + public string VisibleIf{ get; set; } + + protected readonly FieldInfo field; + protected readonly PropertyInfo prop; + private string _displayName; + + protected MemberWrapper(object obj, MemberInfo member) + { + switch (member) + { + case FieldInfo fi: + field = fi; + break; + case PropertyInfo pi: + prop = pi; + break; + default: + throw new ArgumentException(nameof(member), $"Unexpected type: {member.GetType().FullName}"); + } + + Options = member.TryGetAttribute() ?? SettingOptionsAttribute.CreateDefault(); + WebContent = member.TryGetAttribute(); + VisibleIf = member.TryGetAttribute()?.FieldOrMethod; + DefaultValue = GetDefaultValue(obj); + } + + public bool IsDefault(SimpleSettingsBase settings) + { + var current = Get(settings); + bool bothNull = current == null && DefaultValue == null; + + return bothNull || (current != null && (current is ISettingsEqualityChecker c ? c.IsEqualForSettings(DefaultValue) : current.Equals(DefaultValue))); + } + + protected virtual string MakeDisplayName() + { + if (TranslationName.TryTranslate(out var found)) + return found; + + var label = TryGetCustomAttribute(); + if (label != null) + { + return label.Label.TranslateOrSelf(); + } + + var str = new StringBuilder(); + bool lastWasLower = true; + foreach (var c in Name) + { + if (char.IsUpper(c) && lastWasLower) + str.Append(' '); + + lastWasLower = char.IsLower(c); + + if (c == '_') + { + str.Append(' '); + continue; + } + + str.Append(c); + } + + return str.ToString().Trim().CapitalizeFirst(); + } + + public virtual string ValueToString(object value) + { + if (TryGetCustomAttribute() != null && value is float f) + return $"{f*100f:F0}%"; + + return value?.ToString() ?? ""; + } + + public virtual string GetDescription() + { + if ($"{TranslationName}.Desc".TryTranslate(out var found)) + return found; + + var attr = TryGetCustomAttribute(); + if (attr == null) + return string.Empty; + + return attr.Description.TranslateOrSelf(); + } + + public virtual T Get(object obj) + { + if (field != null) + return (T)field.GetValue(IsStatic ? null : obj); + + return (T)prop.GetValue(IsStatic ? null : obj); + } + + public virtual T GetDefault() => (T)DefaultValue; + + public virtual T TryGetCustomAttribute() where T : Attribute + { + return field != null ? field.TryGetAttribute() : prop.TryGetAttribute(); + } + + public void Set(object obj, object value) + { + Type expected = MemberType; + Type got = value?.GetType(); + + if (got != null && got != expected && value is IConvertible) + value = Convert.ChangeType(value, expected); + + if (field != null) + { + field.SetValue(IsStatic ? null : obj, value); + return; + } + + prop.SetValue(IsStatic ? null : obj, value); + } + + public abstract void Expose(object obj); + + private object GetDefaultValue(object obj) + { + object current = Get(obj); + if (current == null) + return null; + + return SmartClone(current); + } + + public virtual bool ShouldDraw(SimpleSettingsBase settings, FieldHolder holder) + { + // Look for field. + if (VisibleIf == null) + return true; + + var found = holder.TryGetNamedMember(VisibleIf); + if (found != null && found.MemberType == typeof(bool)) + return found.Get(settings); + + return true; + } + } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class LabelAttribute : Attribute +{ + public readonly string Label; + public LabelAttribute(string label) + { + this.Label = label; + } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class PercentageAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class StepAttribute : Attribute +{ + public readonly float Step; + public StepAttribute(float step) { Step = step; } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class DescriptionAttribute : Attribute +{ + public readonly string Description; + public DescriptionAttribute(string description) + { + this.Description = description; + } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class DrawMethodAttribute : Attribute +{ + public readonly string MethodName; + public bool SerializeField = true; + + public DrawMethodAttribute(string methodName) + { + this.MethodName = methodName; + } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class WebContentAttribute : Attribute +{ + public readonly string BundleName; + public readonly bool IsVideo; + + public WebContentAttribute(string bundleName, bool isVideo) + { + BundleName = bundleName?.ToLower(); + IsVideo = isVideo; + } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class VisibleIfAttribute : Attribute +{ + public readonly string FieldOrMethod; + + public VisibleIfAttribute(string fieldOrMethod) + { + FieldOrMethod = fieldOrMethod; + } +} + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class SettingOptionsAttribute : Attribute +{ + public static SettingOptionsAttribute CreateDefault() => new SettingOptionsAttribute(); + + public readonly bool DrawDescription = true; + public readonly bool DrawValue = true; + public readonly bool AllowReset = true; + public readonly bool DrawHoverHighlight = true; + public readonly bool IgnoreEqualityForPresets = false; + + public SettingOptionsAttribute(bool drawDescription = true, bool drawValue = true, + bool allowReset = true, bool drawHoverHighlight = true, bool ignoreEqualityForPresets = false) + { + DrawDescription = drawDescription; + DrawValue = drawValue; + AllowReset = allowReset; + DrawHoverHighlight = drawHoverHighlight; + IgnoreEqualityForPresets = ignoreEqualityForPresets; + } + + private SettingOptionsAttribute() { } +} + +public interface ISettingsEqualityChecker +{ + bool IsEqualForSettings(object other); +} diff --git a/Source/1.4/ThingGenerator/AM_DefOf.cs b/Source/1.4/ThingGenerator/AM_DefOf.cs new file mode 100644 index 00000000..8cc13f3d --- /dev/null +++ b/Source/1.4/ThingGenerator/AM_DefOf.cs @@ -0,0 +1,50 @@ +using RimWorld; +using Verse; + +namespace AM +{ + [DefOf] + public static class AM_DefOf + { + static AM_DefOf() + { + DefOfHelper.EnsureInitializedInCtor(typeof(AM_DefOf)); + } + + public static JobDef AM_InAnimation; + public static JobDef AM_GrapplePawn; + public static JobDef AM_WalkToExecution; + public static JobDef AM_DoFriendlyDuel; + public static JobDef AM_SpectateFriendlyDuel; + public static JobDef AM_ChannelAnimation; + + public static RulePackDef AM_Execution_Generic; + + public static ThingDef AM_GrappleFlyer; + public static ThingDef AM_KnockbackFlyer; + + public static StatDef AM_GrappleSpeed; + public static StatDef AM_GrappleCooldown; + public static StatDef AM_ExecutionCooldown; + public static StatDef AM_GrappleRadius; + public static StatDef AM_Lethality; + public static StatDef AM_DuelAbility; + + public static AnimDef AM_Duel_WinFriendlyDuel; + public static AnimDef AM_Duel_WinFriendlyDuel_Reject; + public static AnimDef AM_Execution_Fail; + + public static SoundDef AM_MetalSwordClash; + public static SoundDef AM_StoneSwordClash; + public static SoundDef AM_WoodSwordClash; + + public static ToolCapacityDef Blunt; + public static ToolCapacityDef Cut; + public static ToolCapacityDef Stab; + + public static ThoughtDef AM_FriendlyDuel_Win; + public static ThoughtDef AM_FriendlyDuel_Lose; + + public static HediffDef AM_KnockedOut; + } +} diff --git a/Source/AnimationMod/AnimCellData.cs b/Source/1.4/ThingGenerator/AnimCellData.cs similarity index 100% rename from Source/AnimationMod/AnimCellData.cs rename to Source/1.4/ThingGenerator/AnimCellData.cs diff --git a/Source/1.4/ThingGenerator/AnimDef.cs b/Source/1.4/ThingGenerator/AnimDef.cs new file mode 100644 index 00000000..1cc00545 --- /dev/null +++ b/Source/1.4/ThingGenerator/AnimDef.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Xml.Serialization; +using AM.AMSettings; +using AM.Idle; +using AM.RendererWorkers; +using AM.Reqs; +using AM.Sweep; +using JetBrains.Annotations; +using RimWorld; +using UnityEngine; +using Verse; + +namespace AM; + +[UsedImplicitly(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.WithMembers)] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class AnimDef : Def +{ + #region Static stuff + public static IReadOnlyList AllDefs => allDefs; + + private static List allDefs; + private static Dictionary> defsOfType; + private static readonly HandsVisibilityData defaultHandsVisibilityData = new HandsVisibilityData(); + + public static void Init() + { + allDefs = new List(DefDatabase.AllDefs); + defsOfType = new Dictionary>(); + + foreach(var def in allDefs) + { + var t = def.type; + if(!defsOfType.TryGetValue(t, out var list)) + { + list = new List(); + defsOfType.Add(t, list); + } + list.Add(def); + } + } + + public static IEnumerable GetDefsOfType(AnimType type) + { + if (defsOfType.TryGetValue(type, out var list)) + return list; + return Array.Empty(); + } + + public static IEnumerable GetExecutionAnimationsForPawnAndWeapon(Pawn pawn, ThingDef weaponDef, int? meleeLevel = null) + { + int meleeSkill = meleeLevel ?? pawn.skills?.GetSkill(SkillDefOf.Melee)?.Level ?? 0; + + // TODO maybe cached based on requirement? + return GetDefsOfType(AnimType.Execution).Where(d => + d.Allows(new ReqInput(weaponDef)) && + (d.minMeleeSkill ?? 0) <= meleeSkill && + d.Probability > 0); + } + + [DebugAction("Melee Animation", "Reload all animations", actionType = DebugActionType.Action)] + public static void ReloadAllAnimations() + { + foreach (var def in allDefs) + { + if (def.resolvedData == null) + continue; + + def.resolvedData = AnimData.Load(def.FullDataPath, false); + def.resolvedNonLethalData = File.Exists(def.FullNonLethalDataPath) ? AnimData.Load(def.FullNonLethalDataPath, false) : def.resolvedData; + } + } + + #endregion + + public class SettingsData : IExposable, ISettingsEqualityChecker + { + public bool Enabled = true; + public float Probability = 1; + + public void ExposeData() + { + Scribe_Values.Look(ref Enabled, "Enabled", true); + Scribe_Values.Look(ref Probability, "Probability", 1f); + } + + public bool IsEqualForSettings(object other) + { + return other is SettingsData osd && osd.Enabled == Enabled && Math.Abs(osd.Probability - Probability) < 0.0001f; + } + } + + public string LabelOrFallback => string.IsNullOrEmpty(label) ? defName : LabelCap; + public virtual string FullDataPath + { + get + { + var mod = modContentPack; + if (mod == null) + { + Core.Error($"This def '{defName}' has no modContentPack, so FullDataPath cannot be resolved! Returning relative path instead..."); + return data; + } + + string relative = data.Trim(); + if (string.IsNullOrWhiteSpace(new FileInfo(relative).Extension)) + relative += ".json"; + + return Path.Combine(mod.RootDir, "Animations", relative); + } + } + public virtual string FullNonLethalDataPath => FullDataPath.Replace(".json", "_NL.json"); + public AnimData Data + { + get + { + if (resolvedData == null) + ResolveData(); + + return resolvedData; + } + } + public AnimData DataNonLethal + { + get + { + if (resolvedData == null) + ResolveData(); + + return resolvedNonLethalData ?? resolvedData; + } + } + public string DataPath => data; + /// + /// A mask where a high bit means that the spot must be clear (standable) + /// in a 7x7 cell grid around the animation root cell. + /// + public ulong ClearMask, FlipClearMask; + public float Probability => relativeProbability * UserEditedProbability; + public float UserEditedProbability => (SData?.Enabled ?? true) ? (SData?.Probability ?? 1f) : 0f; + [XmlIgnore] public SettingsData SData; + + public AnimType type = AnimType.Execution; + public string jobString; + public Type rendererWorker; + public int pawnCount; + /// + /// The main and normally only weapon filter. + /// + public Req weaponFilter; + /// + /// The optional secondary weapon filter. + /// Currently only used in some duel animations. + /// Allows filtering out the 'second' pawn based on weapon. + /// For example, if a duel animation only works for knife vs spear, you would have to use both filters. + /// + public Req weaponFilterSecond; + public List cellData = new List(); + public ISweepProvider sweepProvider; + public bool drawDisabledPawns; + public bool shadowDrawFromData; + public int? minMeleeSkill; + public bool canEditProbability = true; + public IdleType idleType; + public bool pointAtTarget; + public int returnToIdleStart, returnToIdleEnd; + public int idleFrame; + public ExecutionOutcome? fixedOutcome; + public List handsVisibility = new List(); + public PromotionData promotionRules; + public List spawnDroppedHeadsForPawnIndices = new List(); + +#pragma warning disable CS0649 // Field 'AnimDef.data' is never assigned to, and will always have its default value null + private string data; +#pragma warning restore CS0649 // Field 'AnimDef.data' is never assigned to, and will always have its default value null + + private Dictionary additionalData = new Dictionary(); + private float relativeProbability = 1; + + private AnimData resolvedData, resolvedNonLethalData; + + public HandsVisibilityData GetHandsVisibility(int pawnIndex) + { + if (pawnIndex < 0) + return defaultHandsVisibilityData; + + foreach (var d in handsVisibility) + { + if (d.pawnIndex == pawnIndex) + return d; + } + + return defaultHandsVisibilityData; + } + + public void SetDefaultSData() + { + SData = new SettingsData() + { + Enabled = true, + Probability = 1f, + }; + } + + protected virtual void ResolveData() + { + if (File.Exists(FullDataPath)) + resolvedData = AnimData.Load(FullDataPath); + + if (File.Exists(FullNonLethalDataPath)) + resolvedNonLethalData = AnimData.Load(FullNonLethalDataPath); + } + + public T TryGetAdditionalData(string id, T defaultValue = default) + { + string value = additionalData.TryGetValue(id); + if (value == null) + return defaultValue; + + // Here, enjoy some hacky shit. + return defaultValue switch + { + string => (T)(object)value, + int => int.TryParse(value, out var i) ? (T)(object)i : defaultValue, + float => float.TryParse(value, out var f) ? (T)(object)f : defaultValue, + bool => bool.TryParse(value, out var b) ? (T)(object)b : defaultValue, + _ => throw new NotSupportedException($"Additional data of type '{typeof(T)}' is not supported.") + }; + } + + public virtual AnimationRendererWorker TryMakeRendererWorker() + { + if (rendererWorker == null) + return null; + + try + { + var instance = Activator.CreateInstance(rendererWorker); + return instance as AnimationRendererWorker; + } + catch (Exception e) + { + Core.Error($"Failed to create instance of SetupWorker class '{rendererWorker}'", e); + return null; + } + } + + public override IEnumerable ConfigErrors() + { + foreach (var item in base.ConfigErrors()) + yield return item; + + if (type == AnimType.Execution && pawnCount < 2) + yield return $"Animation type is Execution, but pawnCount is less than 2! ({pawnCount})"; + + if (string.IsNullOrWhiteSpace(data)) + yield return "Animation has no data path! Please specify the location of the data file using the data tag."; + else if (!File.Exists(FullDataPath)) + yield return $"Failed to find animation file at '{FullDataPath}'!"; + + if (type is AnimType.Execution or AnimType.Duel or AnimType.Idle && weaponFilter == null) + yield return "weaponFilter is not assigned."; + + var p1StartCell = TryGetCell(AnimCellData.Type.PawnStart, false, false, 1); + if (type == AnimType.Execution && (p1StartCell == null || p1StartCell != new IntVec2(1, 0))) + yield return $"This execution animation should have pawn 1 starting at offset (1, 0), but instead they are starting at {p1StartCell}. Change this in the tag."; + + for (int i = 0; i < cellData.Count; i++) + foreach (var error in cellData[i].ConfigErrors()) + yield return $"[CellData, index:{i}] {error}"; + + var indexes = new HashSet(); + foreach (var d in handsVisibility) + { + if (d.pawnIndex < 0) + { + yield return $"There is an item in that has of {d.pawnIndex} which is invalid. should be at least 0."; + } + else if (!indexes.Add(d.pawnIndex)) + { + yield return $"There is an item in that has duplicate of {d.pawnIndex}."; + } + } + } + + public override void PostLoad() + { + base.PostLoad(); + ClearMask = SpaceChecker.MakeClearMask(this, false); + FlipClearMask = SpaceChecker.MakeClearMask(this, true); + + if (promotionRules == null) + return; + + if (relativeProbability != 0) + Core.Warn($"{this} has relativeProbability of {relativeProbability:P1} but it also uses Promotion Rules. This might be a mistake."); + + promotionRules.Def = this; + promotionRules.PostLoad(); + } + + public IntVec2? TryGetCell(AnimCellData.Type type, bool flipX, bool flipY, int? pawnIndex = null) + { + foreach(var cell in cellData) + if (cell.type == type && cell.pawnIndex == pawnIndex) + return Flip(cell.GetCell(), flipX, flipY); + + return null; + } + + public IEnumerable GetCells(AnimCellData.Type type, bool flipX, bool flipY, int? pawnIndex = null) + { + foreach (var cData in cellData) + if (cData.type == type && cData.pawnIndex == pawnIndex) + foreach (var cell in cData.GetCells()) + yield return Flip(cell, flipX, flipY); + } + + public IEnumerable GetMustBeClearCells(bool flipX, bool flipY, IntVec3 offset) + { + foreach (var cells in cellData) + { + foreach (var cell in cells.GetCells()) + { + yield return Flip(cell, flipX, flipY).ToIntVec3 + offset; + } + } + } + + private IntVec2 Flip(in IntVec2 input, bool fx, bool fy) => new IntVec2(fx ? -input.x : input.x, fy ? -input.z : input.z); + + public bool Allows(in ReqInput input) + { + return weaponFilter != null && weaponFilter.Evaluate(input); + } + + public IEnumerable GetAllAllowedWeapons() + { + foreach (var thing in DefDatabase.AllDefsListForReading) + if (thing.IsMeleeWeapon() && Allows(new ReqInput(thing))) + yield return thing; + } + + /* + * Animation promotion is where a selected animation is swapped out for a different animation just before it starts. + * The reason this is done is that certain conditions need to be met for some animations, + * and the inputs for those conditions may not be known until after the animation selection calculations + * have already been done, hence the need to 'promote' an already selected animation to a different one. + */ + public AnimDef TryGetPromotionDef(PromotionInput input) + { + if (input.OriginalAnim != this) + Core.Error($"Original anim should be this one: {this}. Instead got {input.OriginalAnim}"); + + var allPossibles = from def in GetDefsOfType(AnimType.Execution) + where def.promotionRules != null + let chance = def.promotionRules.GetRelativePromotionChanceFor(input) + where chance > 0 + select (def, chance); + + int possibleCount = allPossibles.Count(); + + // Chance to promote at all: + // Needs to be thought out... A future task. + float promotionChanceForAny = Mathf.Min(possibleCount * 0.5f, 0.65f); + if (!Rand.Chance(promotionChanceForAny)) + return null; + + var selected = allPossibles.RandomElementByWeightWithFallback(pair => pair.chance); + + if (selected.def != null) + { + Core.Log($"Promoted animation: {input.OriginalAnim} -> {selected.def} ({promotionChanceForAny:P0} general promotion chance, {selected.chance:P0} relative chance)"); + } + + return selected.def; + } + + public class HandsVisibilityData + { + public int pawnIndex = -1; + public bool? showMainHand = null; + public bool? showAltHand = null; + } + + public class PromotionData + { + public AnimDef Def { get; set; } + + public ExecutionOutcome onlyWhenOutcomeIs = ExecutionOutcome.Nothing; + public float basePromotionRelativeChance = 1f; + public List promotionWorkers = new List(); + + private readonly List workers = new List(); + + public float GetRelativePromotionChanceFor(in PromotionInput input) + { + // Outcome must be in the allowed range. + if (!onlyWhenOutcomeIs.HasFlag(input.Outcome)) + return 0; + + // Must allow the used weapon. + if (!Def.Allows(input.ReqInput)) + return 0f; + + // Base chance from def and user settings. + float chance = basePromotionRelativeChance * Def.UserEditedProbability; + if (chance <= 0f) + return 0f; + + // Must have enough space. + if (!CheckMasks(input)) + return 0f; + + // Do all the specific workers. + foreach (var worker in workers) + { + chance *= worker.GetPromotionRelativeChanceFor(input); + } + + return chance; + } + + private bool CheckMasks(in PromotionInput input) + { + ulong mask = input.FlipX ? Def.FlipClearMask : Def.ClearMask; + return (mask & input.OccupiedMask) == 0; + } + + public void PostLoad() + { + foreach (var type in promotionWorkers) + { + workers.Add(Activator.CreateInstance(type) as IPromotionWorker); + } + } + } + + public interface IPromotionWorker + { + float GetPromotionRelativeChanceFor(in PromotionInput input); + } + + public struct PromotionInput + { + public required Pawn Attacker; + public required Pawn Victim; + public required ReqInput ReqInput; + public required AnimDef OriginalAnim; + public required ExecutionOutcome Outcome; + public required ulong OccupiedMask; + public required bool FlipX; + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/AnimRenderer.cs b/Source/1.4/ThingGenerator/AnimRenderer.cs new file mode 100644 index 00000000..cd3433b5 --- /dev/null +++ b/Source/1.4/ThingGenerator/AnimRenderer.cs @@ -0,0 +1,1470 @@ +using AM.Events; +using AM.Idle; +using AM.Patches; +using AM.Processing; +using AM.RendererWorkers; +using AM.Sweep; +using AM.Tweaks; +using AM.UI; +using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using UnityEngine; +using Verse; +using Verse.AI; +using Color = UnityEngine.Color; +using Debug = UnityEngine.Debug; + +namespace AM; + +/// +/// An is the object that represents and also draws (renders) a currently running animation. +/// Be sure to check the property before interacting with an object of this type. +/// +public class AnimRenderer : IExposable +{ + #region Static stuff + + public static readonly char[] Alphabet = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' }; + public static readonly List PostLoadPendingAnimators = new List(); + public static event Action PrePawnSpecialRender; + public static event Action PostPawnSpecialRender; + public static Material DefaultCutout, DefaultTransparent; + public static IReadOnlyList ActiveRenderers => activeRenderers; + public static IReadOnlyCollection CapturedPawns => pawnToRenderer.Keys; + public static int TotalCapturedPawnCount => pawnToRenderer.Count; + + private static readonly MaterialPropertyBlock shadowMpb = new MaterialPropertyBlock(); + private static readonly List activeRenderers = new List(); + private static readonly Dictionary pawnToRenderer = new Dictionary(); + private static readonly Dictionary textureCache = new Dictionary(); + + /// + /// Tries to get the that a pawn currently belongs to. + /// Will return null if none is found. + /// + /// The renderer, or null. Null indicates that the pawn is not currently part of any animation. + public static AnimRenderer TryGetAnimator(Pawn pawn) + { + if (pawn == null) + return null; + + return pawnToRenderer.GetValueOrDefault(pawn); + } + + public static Texture2D ResolveTexture(in AnimPartSnapshot snapshot) + { + if (snapshot.Renderer == null) + return null; + + var ov = snapshot.Renderer.overrides[snapshot.Part.Index]; + + if (ov.Texture != null) + return ov.Texture; + + if (snapshot.TexturePath == null) + return null; + + return ResolveTexture(snapshot.TexturePath, snapshot); + } + + public static Texture2D ResolveTexture(string texturePath, in AnimPartSnapshot snapshot) + { + if (textureCache.TryGetValue(texturePath, out var found)) + return found; + + Texture2D loaded; + loaded = ContentFinder.Get(texturePath, false); + textureCache.Add(texturePath, loaded); + if (loaded == null) + Core.Error($"Failed to load texture '{texturePath}' for {snapshot.PartName} (frame: {snapshot.FrameIndex})"); + + return loaded; + } + + public static void ClearAll() + { + activeRenderers.Clear(); + pawnToRenderer.Clear(); + PostLoadPendingAnimators.Clear(); + } + + public static void TickAll() + { + AddFromPostLoad(); + + foreach (AnimRenderer renderer in activeRenderers) + { + if (!renderer.IsDestroyed) + renderer.Tick(); + } + } + + public static void DrawAll(float dt, Map map, Action onEvent, in CellRect viewBounds, Action labelDraw = null) + { + AddFromPostLoad(); + + bool shouldSeek = onEvent != null; + + for (int i = 0 ; i < activeRenderers.Count; i++) + { + var renderer = activeRenderers[i]; + if (renderer.Map != map || renderer.IsDestroyed) + continue; + + try + { + DrawSingle(renderer, shouldSeek ? dt : null, onEvent, viewBounds, labelDraw); + } + catch(Exception e) + { + Core.Error($"Exception rendering animator '{renderer}'", e); + } + } + } + + public static void DrawAllGUI(Map map) + { + AddFromPostLoad(); + + for (int i = 0; i < activeRenderers.Count; i++) + { + var renderer = activeRenderers[i]; + + if (renderer.Map != map || renderer.IsDestroyed) + continue; + + try + { + renderer.DrawGUI(); + } + catch (Exception e) + { + Core.Error($"Exception rendering animator '{renderer}'", e); + } + + } + } + + private static void AddFromPostLoad() + { + if (PostLoadPendingAnimators.Count == 0) + return; + + foreach (var item in PostLoadPendingAnimators) + { + if (item == null) + continue; + item.ApplyPostLoad(); + item.Register(); + } + + Core.Log($"Post-Load Init: Registered {PostLoadPendingAnimators.Count} pending animators."); + PostLoadPendingAnimators.Clear(); + } + + public static void RemoveDestroyed() + { + // Remove destroyed. + for (int i = 0; i < activeRenderers.Count; i++) + { + var r = activeRenderers[i]; + if (!r.IsDestroyed) + continue; + + DestroyNew(r); + i--; + } + } + + private static void DrawSingle(AnimRenderer renderer, float? dt, Action onEvent, CellRect viewBounds, Action labelDraw = null) + { + // Draw and handle events. + try + { + bool cull = Core.Settings.OffscreenCulling && !viewBounds.Contains(renderer.RootPosition.ToIntVec3()); + renderer.Draw(null, dt ?? 0, onEvent, cull, labelDraw); + } + catch (Exception e) + { + Core.Error($"Rendering exception when doing animation {renderer}", e); + } + } + + private static void DestroyNew(AnimRenderer renderer) + { + if (renderer.Pawns != null) + { + foreach (var pawn in renderer.Pawns) + { + if (pawn != null) + pawnToRenderer.Remove(pawn); + } + } + + activeRenderers.Remove(renderer); + try + { + renderer.OnEnd(); + } + catch (Exception e) + { + Core.Error("Exception in AnimRenderer.OnEnd:", e); + } + } + + private static void RegisterInt(AnimRenderer renderer) + { + if (activeRenderers.Contains(renderer)) + return; + + activeRenderers.Add(renderer); + foreach (var item in renderer.Pawns) + { + if (item != null) + pawnToRenderer.Add(item, renderer); + } + + renderer.OnStart(); + } + + public static float Remap(float value, float a, float b, float a2, float b2) + => Mathf.Lerp(a2, b2, Mathf.InverseLerp(a, b, value)); + + #endregion + + /// + /// The active animation data. Animation data is loaded from disk upon request. + /// Note that this is not the same as an animation def: see for the definition. + /// + public AnimData Data => ExecutionOutcome <= ExecutionOutcome.Damage ? Def.DataNonLethal : Def.Data; + /// + /// The duration, in seconds, of the current animation. + /// May not accurately represent how long the animation actually plays for, depending on the type of animation (i.e. Duels may last longer). + /// + public float Duration => Data?.Duration ?? 0; + /// + /// Same as the , but expressed in Rimworld ticks. + /// + public int DurationTicks => Mathf.RoundToInt(Duration * 60); + /// + /// Should the animator be saved with the map? + /// + public bool ShouldSave => !IsDestroyed && PawnCount > 0; + /// + /// The root world position of this animation. + /// Derived from . + /// + public Vector3 RootPosition => RootTransform.MultiplyPoint3x4(default); + /// + /// The active animation def. + /// + public AnimDef Def; + /// + /// A list of pawns included in this animation. + /// + public List Pawns = new List(); + /// + /// A list of pawns that are not part of this animation but are used + /// in things like log generation. + /// + public List NonAnimatedPawns = new List(); + /// + /// The base transform that the animation is centered on. + /// Useful functions to modify this are + /// and . + /// + public Matrix4x4 RootTransform = Matrix4x4.identity; + /// + /// The that this animation is running on. + /// + public Map Map; + /// + /// The Unity Camera that this animation renders to. If null, all cameras are targeted. + /// + public Camera Camera; + /// + /// If true, the animation loops rather than ending. + /// + public bool Loop; + /// + /// If true, the animation is mirrored on this axis. + /// + public bool MirrorHorizontal, MirrorVertical; + /// + /// Will only be valid after the animation has already ended (see ). + /// If true, the animator ended prematurely, such as by loosing a pawn or otherwise being ended unexpectedly. + /// If false, the animator ended because the animation reached it's natural end. + /// + public bool WasInterrupted { get; private set; } + /// + /// If true, this animator has finished playing and is no longer active. + /// You should not keep references to destroyed AnimRenderers. + /// + public bool IsDestroyed { get; private set; } + /// + /// The current time, in seconds, that the animation is playing. + /// + public float CurrentTime => time; + /// + /// Gets the number of non-null pawns in the array. + /// + public int PawnCount + { + get + { + int c = 0; + foreach (var p in Pawns) + if (p != null) + c++; + return c; + } + } + /// + /// The outcome of this animation if it is an execution animation. + /// + public ExecutionOutcome ExecutionOutcome = ExecutionOutcome.Nothing; + /// + /// The custom renderer worker, optional. + /// + public AnimationRendererWorker AnimationRendererWorker; + /// + /// Save data such as data used to track duel status. + /// + public SaveData CurrentSaveData = new SaveData(); + /// + /// An action that is invoked when is called. + /// This action is not serialized. + /// + public Action OnEndAction; + /// + /// A scale on the speed of this animation. + /// + public float TimeScale = 1f; + /// + /// If not null, pawns are allowed to have other jobs as long as they are of this def. + /// Upon animation start, the pawn's current job and verbs are not modified if this is specified. + /// + public JobDef CustomJobDef; + /// + /// Is this animation a friendly duel? + /// + public bool IsFriendlyDuel; + + public double DrawMS; + public double SeekMS; + public double SweepMS; + + private readonly Dictionary pawnToParts = new Dictionary(); + private HashSet pawnsValidEvenIfDespawned = new HashSet(); + private AnimPartSnapshot[] snapshots; + private AnimPartOverrideData[] overrides; + private PartWithSweep[] sweeps; + private MaterialPropertyBlock pb; + private float time = -1; + private Pawn[] pawnsForPostLoad; + private bool hasStarted; + private bool lastMirrorX, lastMirrorZ; + private bool hasDoneOnEnd; + private bool delayedDestroy; + + [UsedImplicitly] + public AnimRenderer() + { + + } + + public AnimRenderer(AnimDef def, Map map) + { + Map = map; + Def = def; + Init(); + } + + private void Init() + { + if (snapshots != null) + { + Core.Error("Init called multiple times!"); + return; + } + + snapshots = new AnimPartSnapshot[Data.Parts.Count]; + overrides = new AnimPartOverrideData[Data.Parts.Count]; + pb = new MaterialPropertyBlock(); + + for (int i = 0; i < overrides.Length; i++) + overrides[i] = new AnimPartOverrideData(); + + // Dummy array for sweep meshes. + // Reason: A Unity bug where creating a new mesh (new Mesh()) during loading + // crashes the game. + // Solution: delay creating meshes until the game has started rendering. + sweeps = Array.Empty(); + + Debug.Assert(AnimationRendererWorker == null); + AnimationRendererWorker = Def.TryMakeRendererWorker(); + } + + private void InitSweepMeshes() + { + if (Data.SweepDataCount == sweeps.Length) + return; + + int j = 0; + sweeps = new PartWithSweep[Data.SweepDataCount]; + foreach (var part in Data.PartsWithSweepData) + { + var paths = Data.GetSweepPaths(part); + foreach (var path in paths) + { + sweeps[j++] = new PartWithSweep(this, part, path, new SweepMesh(), BasicSweepProvider.DefaultInstance); + } + } + } + + public void ExposeData() + { + Scribe_Defs.Look(ref Def, "def"); + Scribe_Values.Look(ref time, "time"); + Scribe_Values.Look(ref MirrorHorizontal, "mirrorX"); + Scribe_Values.Look(ref MirrorVertical, "mirrorY"); + Scribe_Values.Look(ref Loop, "loop"); + Scribe_Values.Look(ref hasStarted, "hasStarted"); + Scribe_Collections.Look(ref Pawns, "pawns", LookMode.Reference); + Scribe_Collections.Look(ref NonAnimatedPawns, "pawnsNonAnimated", LookMode.Reference); + Scribe_Collections.Look(ref pawnsValidEvenIfDespawned, "pawnsValidEvenIfDespawned", LookMode.Reference); + Scribe_References.Look(ref Map, "map"); + Scribe_Values.Look(ref TimeScale, "timeScale", 1f); + Scribe_Values.Look(ref ExecutionOutcome, "execOutcome"); + Scribe_Values.Look(ref IsFriendlyDuel, "isFriendlyDuel"); + Scribe_Defs.Look(ref CustomJobDef, "customJobDef"); + Scribe_Deep.Look(ref CurrentSaveData, "saveData"); + CurrentSaveData ??= new SaveData(); + + if (Scribe.mode == LoadSaveMode.PostLoadInit) + { + Init(); + Pawns ??= new List(); + pawnsValidEvenIfDespawned ??= new HashSet(); + pawnsForPostLoad = Pawns.ToArray(); + Pawns.Clear(); + } + + try + { + switch (Scribe.mode) + { + case LoadSaveMode.Saving: + { + // Save the root transform matrix. + string s = ""; + for (int i = 0; i < 16; i++) + { + s += RootTransform[i]; + if (i != 15) + s += ','; + } + Scribe_Values.Look(ref s, "rootTransform"); + break; + } + case LoadSaveMode.LoadingVars: + { + string s = string.Empty; + Scribe_Values.Look(ref s, "rootTransform"); + string[] parts = s.Split(','); + for (int i = 0; i < 16; i++) + { + RootTransform[i] = float.Parse(parts[i]); + } + + break; + } + case LoadSaveMode.PostLoadInit: + //Core.Log($"Final matrix was {RootTransform}"); + break; + } + } + catch (Exception e) + { + Core.Error($"Exception exposing matrix during {Scribe.mode}:", e); + } + + + bool temp = IsDestroyed; + Scribe_Values.Look(ref temp, "isDestroyed"); + IsDestroyed = temp; + + temp = WasInterrupted; + Scribe_Values.Look(ref temp, "wasInterrupted"); + WasInterrupted = temp; + } + + public void FlagAsValidIfDespawned(Pawn pawn) + { + if (pawn == null) + throw new ArgumentNullException(nameof(pawn)); + + if (!Pawns.Contains(pawn)) + { + Core.Error("Should not mark a pawn as valid if despawned if that pawn is not part of this animation!"); + return; + } + + pawnsValidEvenIfDespawned.Add(pawn); + } + + /// + /// Gets the current state (snapshot) for a particular animation part. + /// + public AnimPartSnapshot GetSnapshot(AnimPartData part) => part == null ? default : snapshots[part.Index]; + + /// + /// Gets the override data based on the part index. + /// See . + /// + /// The part index, such as from . + /// The override object, or null if the index is out of bounds. + public AnimPartOverrideData GetOverride(int index) => index >= 0 && index < overrides.Length ? overrides[index] : null; + + /// + /// Gets the override data of a particular part. + /// + /// The override object, or null if the part is null. + public AnimPartOverrideData GetOverride(AnimPartData part) => GetOverride(part?.Index ?? -1); + + /// + /// Gets the override data for a particular snapshot. The + /// object is used to find the override object. + /// + /// The override object, or null if the part is null. + public AnimPartOverrideData GetOverride(in AnimPartSnapshot snapshot) => GetOverride(snapshot.Part); + + /// + /// Causes this animation renderer to be registered with the system, and finish setting up. + /// Note: this is not the preferred way of starting an animation. + /// Instead, consider using . + /// + public bool Register() + { + if (IsDestroyed) + { + Core.Error("Tried to register an already destroyed AnimRenderer"); + return false; + } + + if (activeRenderers.Contains(this)) + { + Core.Error("Tried to register AnimRenderer that is already in active list!"); + return false; + } + + Pawn invalid = GetFirstInvalidPawn(); + if (invalid != null) + { + Core.Error($"Tried to start animation with 1 or more invalid pawn: [{invalid.Faction?.Name ?? "No faction"}] {invalid.NameFullColored}"); + foreach (var pawn in Pawns) + { + if (!IsPawnValid(pawn)) + Core.Error($"Invalid Pawn '{pawn.NameShortColored}': Spawned: {pawn.Spawned}, Dead: {pawn.Dead}, Downed: {pawn.Downed}, HasHolderParent: {pawn.ParentHolder is not Verse.Map} ({pawn.ParentHolder}, {pawn.ParentHolder?.GetType().FullName})"); + } + IsDestroyed = true; + return false; + } + + foreach(var pawn in Pawns) + { + var anim = TryGetAnimator(pawn); + if(anim != null) + { + Core.Error($"Tried to start animation with '{pawn.LabelShortCap}' but that pawn is already in animation {anim.Data.Name}!"); + IsDestroyed = true; + return false; + } + } + + //if (PawnCount != Def.pawnCount) + // Core.Warn($"Started AnimRenderer with bad number of pawns! Expected {Def.pawnCount}, got {PawnCount}. (Def: {Def})"); + + RegisterInt(this); + + if (!hasStarted) + OnStart(); + + // This tempTime nonsense is necessary because time is written to directly by the ExposeData, + // and Seek will not run if time is already the target (seek) time. + float tempTime = time; + time = -1; + Seek(tempTime < 0 ? 0 : tempTime, 0, null); // This will set time back to the correct value. + + AnimationRendererWorker?.SetupRenderer(this); + return true; + } + + /// + /// Cancels and destroys this animator, releasing it's pawns and stopping it from playing. + /// This call may not immediately release pawns, it may be delayed until the end of the frame. + /// + public void Destroy() + { + if (IsDestroyed) + { + //Core.Warn("Tried to destroy renderer that is already destroyed..."); + return; + } + + IsDestroyed = true; + } + + /// + /// Gets the associated with a pawn's body. + /// This part data can then be used to get the current position of the body using . + /// + /// The part data, or null if the pawn is null or is not captured by this animation. + public AnimPartData GetPawnBody(Pawn pawn) + { + if (pawn == null) + return null; + + if (!pawnToParts.TryGetValue(pawn, out var data) || data.BodyIndex == -1) + return null; + + return Data.Parts[data.BodyIndex]; + } + + /// + /// Gets the associated with a pawn's body. + /// This part data can then be used to get the current position of the body using . + /// + /// The part data, or null if the pawn is null or is not captured by this animation. + public AnimPartData GetPawnHead(Pawn pawn) + { + if (pawn == null) + return null; + + if (!pawnToParts.TryGetValue(pawn, out var data) || data.HeadIndex == -1) + return null; + + return Data.Parts[data.HeadIndex]; + } + + /// + /// Should be called once per Rimworld tick. + /// If this animation is managed automatically (such as when using the utility) + /// then you do not need to call this manually, as it will be done for you. + /// + public void Tick() + { + if (IsDestroyed) + return; + + if(Map == null || (Find.TickManager.TicksAbs % 120 == 0 && Map.Index < 0)) + { + WasInterrupted = true; + Destroy(); + return; + } + + if (GetFirstInvalidPawn() != null) + { + WasInterrupted = true; + Destroy(); + return; + } + + foreach (var pawn in Pawns) + { + if (pawn.CurJobDef != (CustomJobDef ?? AM_DefOf.AM_InAnimation)) + { + if (pawnsValidEvenIfDespawned.Contains(pawn)) + continue; + + Core.Error($"{pawn} has bad job: {pawn.CurJobDef}. Cancelling animation."); + WasInterrupted = true; + Destroy(); + return; + } + } + } + + /// + /// Gets the first captured pawn that is in an invalid state. + /// See and . + /// + /// The first invalid pawn or null if all pawns are valid, or there are no pawns in this animation. + public Pawn GetFirstInvalidPawn(bool ignoreNotSpawned = false) + { + foreach(var pawn in Pawns) + { + if (pawn != null && !IsPawnValid(pawn, ignoreNotSpawned)) + return pawn; + } + return null; + } + + /// + /// Is this pawn valid to be used in this animator? + /// Checks simple conditions such as not dead, destroyed, downed, or held by another Thing... + /// + public virtual bool IsPawnValid(Pawn p, bool ignoreNotSpawned = false) + { + // Summary: + // Not dead or downed. + bool basic = p is {Dead: false, Downed: false}; + if (!basic) + return false; + + // Check not spawned exceptions list. + if (!p.Spawned && pawnsValidEvenIfDespawned.Contains(p)) + return true; + + // Spawned (includes things like grappling, flying) + // Is part of the correct map (checks for drop pods, teleporting across maps) + return (p.Spawned || ignoreNotSpawned) && ((p.ParentHolder is Map m && m == Map) || (p.ParentHolder == null && ignoreNotSpawned)); + } + + public void Draw(float? atTime, float dt, Action eventOutput, bool cullDraw, Action labelDraw = null) + { + if (IsDestroyed) + return; + + InitSweepMeshes(); + foreach (var item in sweeps) + if (item != null) + item.MirrorHorizontal = MirrorHorizontal; + + if (eventOutput != null) + Seek(atTime, dt, e => eventOutput(this, e)); + else + SeekMS = 0; + + if (Find.CurrentMap != Map || cullDraw) + { + DrawMS = 0; + if (delayedDestroy) + Destroy(); + return; // Do not actually draw if not on the current map or culled. + } + + var timer = new RefTimer(); + var timer2 = new RefTimer(); + foreach (var path in sweeps) + path.Draw(time); + timer2.GetElapsedMilliseconds(out SweepMS); + + foreach (var snap in snapshots) + { + if (!ShouldDraw(snap)) + continue; + + var tex = ResolveTexture(snap); + if (tex == null) + continue; + + var mat = GetMaterialFor(snap, out bool forceMPB); + if (mat == null) + continue; + + var ov = GetOverride(snap); + var matrix = RootTransform * snap.WorldMatrix; + bool preFx = ov.FlipX ? !snap.FlipX : snap.FlipX; + bool preFy = ov.FlipY ? !snap.FlipY : snap.FlipY; + bool fx = MirrorHorizontal ? !preFx : preFx; + bool fy = MirrorVertical ? !preFy : preFy; + var mesh = AnimData.GetMesh(fx, fy); + + if (ov.CustomRenderer != null) + { + ov.CustomRenderer.TRS = matrix; + ov.CustomRenderer.OverrideData = ov; + ov.CustomRenderer.Part = snap.Part; + ov.CustomRenderer.Snapshot = snap; + ov.CustomRenderer.Renderer = this; + ov.CustomRenderer.TweakData = ov.TweakData; + ov.CustomRenderer.Mesh = mesh; + ov.CustomRenderer.Material = mat; + + bool stop = ov.CustomRenderer.Draw(); + if (stop) + continue; + } + + bool useMPB = forceMPB || ov.UseMPB; + var color = snap.FinalColor; + + int passes = 1; + for (int i = 0; i < passes; i++) + { + if (useMPB) + { + //Core.Log($"Use mpb mat is {mat} for {ov.TweakData?.ItemDefName} on {snap.PartName} with mode {snap.SplitDrawMode}"); + pb.Clear(); + + // Basic texture and color, always used. Color might be replaced, see below. + pb.SetColor("_Color", color); + + if (ov.Material != null) + { + // Check for a mask... + bool doesUseMask = ov.Material.HasProperty(ShaderPropertyIDs.MaskTex); + Texture mask; + if (doesUseMask && (mask = ov.Material.GetTexture(ShaderPropertyIDs.MaskTex)) != null) + { + // Tint is applied to the mask. + pb.SetColor("_Color", color); // Color comes from animation. + pb.SetColor("_ColorTwo", ov.Weapon.DrawColor); // Mask tint + pb.SetTexture(ShaderPropertyIDs.MaskTex, mask); + + } + else + { + // Tint is applied straight to main color. + pb.SetColor("_Color", color * ov.Weapon.DrawColor); + } + } + + if (snap.SplitDrawMode != AnimData.SplitDrawMode.None && snap.SplitDrawPivot != null) + { + ConfigureSplitDraw(snap, ref matrix, pb, ov, i, ref passes, ref tex); + } + + pb.SetTexture("_MainTex", tex); + } + + var finalMpb = useMPB ? pb : null; + + AnimationRendererWorker?.PreRenderPart(snap, ov, ref mesh, ref matrix, ref mat, ref finalMpb); + Graphics.DrawMesh(mesh, matrix, mat, 0, Camera, 0, finalMpb); + } + } + + DrawPawns(labelDraw); + + timer.GetElapsedMilliseconds(out DrawMS); + if (delayedDestroy) + Destroy(); + } + + private void ConfigureSplitDraw(in AnimPartSnapshot part, ref Matrix4x4 matrix, MaterialPropertyBlock pb, AnimPartOverrideData ov, int currentPass, ref int passCount, ref Texture2D texture) + { + bool preFx = ov.FlipX ? !part.FlipX : part.FlipX; + bool preFy = ov.FlipY ? !part.FlipY : part.FlipY; + bool fx = MirrorHorizontal ? !preFx : preFx; + bool fy = MirrorVertical ? !preFy : preFy; + + float textureRot = ov.LocalRotation * Mathf.Deg2Rad; + pb.SetFloat("CutoffAngle", textureRot * (fy ? -1f : 1f)); + var mode = part.SplitDrawMode; + if (mode == AnimData.SplitDrawMode.BeforeAndAfter) + { + mode = currentPass == 0 ? AnimData.SplitDrawMode.Before : AnimData.SplitDrawMode.After; + if (currentPass == 0) + passCount++; + else + matrix *= Matrix4x4.Translate(new Vector3(0, -0.9f, 0)); // This is the actual offset depth here. + } + pb.SetFloat("Polarity", mode == AnimData.SplitDrawMode.Before ? 1f : -1f); + + // The total length of the weapon sprite, in world units (1 unit = 1 cell). + float length = matrix.lossyScale.x * Remap(Mathf.Abs(Mathf.Sin(textureRot * 2f)), 0, 1f, 1f, 1.41421356237f); // sqrt(2) + float distanceScale = Remap(Mathf.Abs(Mathf.Sin(textureRot * 2f)), 0, 1f, 0.5f, 0.70710678118f); // sqrt(0.5) + + Matrix4x4 noOverrideMat = part.Renderer.RootTransform * part.WorldMatrixNoOverride * Matrix4x4.Scale(ov.LocalScaleFactor.ToWorld()); + + Vector2 renderedPos = matrix.MultiplyPoint3x4(Vector3.zero).ToFlat(); + Vector2 startPos = renderedPos - length * 0.5f * noOverrideMat.MultiplyVector(Vector3.right).normalized.ToFlat(); + Vector2 endPos = renderedPos + length * 0.5f * noOverrideMat.MultiplyVector(Vector3.right).normalized.ToFlat(); + Vector2 basePos = GetSnapshot(part.SplitDrawPivot).GetWorldPosition().ToFlat(); + + var ap = basePos - startPos; + var ab = endPos - startPos; + float lerp = Vector2.Dot(ap, ab) / Vector2.Dot(ab, ab); + if (!fx) + lerp = 1 - lerp; + + pb.SetFloat("Distance", distanceScale * (-1f + lerp * 2f)); + + // Experimental, aims to fix issue where split drawing does not use the weapon ideology style. + if (ov.Material?.mainTexture is Texture2D tex) + texture = tex; + } + + public void DrawGUI() + { + AnimationRendererWorker?.DrawGUI(this); + } + + protected void DrawPawns(Action labelDraw = null) + { + foreach (var pawn in Pawns) + { + if (pawn == null || pawn.Destroyed) + continue; + + var bodySS = GetSnapshot(GetPawnBody(pawn)); + + if (!bodySS.Active) + { + if (Def.drawDisabledPawns) + { + // Regular pawn render. + Patch_PawnRenderer_RenderPawnAt.AllowNext = true; + Patch_PawnRenderer_RenderPawnInternal.AllowNext = true; + Patch_PawnRenderer_RenderPawnInternal.DoNotModify = true; // Don't use animation position/rotation. + Patch_PawnRenderer_RenderPawnInternal.NextDrawMode = Patch_PawnRenderer_RenderPawnInternal.DrawMode.Full; + Patch_PawnUtility_IsInvisible.IsRendering = true; + PrePawnSpecialRender?.Invoke(pawn, this); + + pawn.DrawAt(pawn.DrawPosHeld ?? pawn.DrawPos); + + PostPawnSpecialRender?.Invoke(pawn, this); + Patch_PawnRenderer_RenderPawnInternal.DoNotModify = false; + Patch_PawnUtility_IsInvisible.IsRendering = false; + + // Draw label. + Vector3 drawPos2 = pawn.DrawPos; + drawPos2.z -= 0.6f; + Vector2 vector2 = Find.Camera.WorldToScreenPoint(drawPos2) / Prefs.UIScale; + vector2.y = Verse.UI.screenHeight - vector2.y; + labelDraw?.Invoke(pawn, vector2); + + } + continue; + } + + var headSS = GetSnapshot(GetPawnHead(pawn)); + + // ---- Animated pawn draw: ---- + + // Position and direction. + var pos = bodySS.GetWorldPosition(); + var headPos = headSS.GetWorldPosition(); + var bodyPos = pos; // Used for label rendering, pos is modified in second pass. + var dir = bodySS.GetWorldDirection(); + + // Worker pre-draw + AnimationRendererWorker?.PreRenderPawn(bodySS, ref pos, ref dir, pawn); + + // Render. Two passes are required if the pawn is beheaded. + bool isBeheaded = IsPawnBeheaded(pawn, bodySS, headSS); + int passCount = isBeheaded ? 2 : 1; + + for (int i = 0; i < passCount; i++) + { + bool suppressShadow = i > 0 || (Def.shadowDrawFromData && bodySS.DataC > 0.2f); + + // If on second pass (head-only pass). + if (i == 1) + { + // Head uses a different position from the body. + pos = headPos; + + // Head rotation needs to be sent manually because the head + // does not have a direction curve to sample. + Patch_PawnRenderer_RenderPawnInternal.HeadRotation = dir; // Copy body facing direction. + } + + // Render pawn in custom position using patches. + Patch_PawnRenderer_RenderPawnAt.AllowNext = true; + Patch_PawnRenderer_RenderPawnInternal.AllowNext = true; + + Patch_PawnRenderer_RenderPawnInternal.NextDrawMode = !isBeheaded + ? Patch_PawnRenderer_RenderPawnInternal.DrawMode.Full + : i == 0 ? Patch_PawnRenderer_RenderPawnInternal.DrawMode.BodyOnly : Patch_PawnRenderer_RenderPawnInternal.DrawMode.HeadOnly; + + Patch_PawnUtility_IsInvisible.IsRendering = true; + Patch_PawnRenderer_DrawInvisibleShadow.Suppress = suppressShadow; // In 1.4 shadow rendering is baked into RenderPawnAt and may need to be prevented. + + pawn.Drawer.renderer.RenderPawnAt(pos, dir, true); // This direction here is not the final one. + + Patch_PawnRenderer_DrawInvisibleShadow.Suppress = false; + Patch_PawnUtility_IsInvisible.IsRendering = false; + } + + // Render shadow. + if (Def.shadowDrawFromData) + { + // DataC now drives the shadow rendering. + // Value of 0 means regular shadow rendering, + // value of 1 means shadow is a ground-based entity shadow (a-la-minecraft). + Vector3 groundPos = pos with { z = RootPosition.z }; + Vector3 scale = new Vector3(1, 1, 0.5f); + Color color = new Color(0, 0, 0, bodySS.DataC * 0.6f); + + if (bodySS.DataC > 0f) + DrawSimpleShadow(groundPos, scale, color); + } + + // Figure out where to draw the pawn label. + Vector3 drawPos = bodyPos; + drawPos.z -= 0.6f; + Vector2 vector = Find.Camera.WorldToScreenPoint(drawPos) / Prefs.UIScale; + vector.y = Verse.UI.screenHeight - vector.y; + labelDraw?.Invoke(pawn, vector); + } + } + + [Pure] + private bool IsPawnBeheaded(Pawn pawn, in AnimPartSnapshot bodySS, in AnimPartSnapshot headSS) + { + // Not sure if this is okay: are there any pawns that are not humanoid + // but can have their heads removed by rendering separately? + if (pawn.RaceProps?.Humanlike != true) + return false; + + // Render as beheaded if the head and body are not rendering at the same position. + var bodyToHeadDelta = bodySS.GetWorldPosition() - headSS.GetWorldPosition(); + return bodyToHeadDelta.sqrMagnitude > 0.01f * 0.01f; + } + + private void DrawSimpleShadow(Vector3 pos, Vector3 size, Color color) + { + var trs = Matrix4x4.TRS(pos, Quaternion.identity, size); + shadowMpb.SetColor("_Color", color); + shadowMpb.SetTexture("_MainTex", Content.Shadow); + Graphics.DrawMesh(MeshPool.plane10, trs, DefaultTransparent, 0, Camera, 0, shadowMpb); + } + + private void ApplyPostLoad() + { + if (pawnsForPostLoad == null) + return; + + foreach (var pawn in pawnsForPostLoad) + AddPawn(pawn); + + pawnsForPostLoad = null; + } + + /// + /// Changes the current time of this animation. + /// Depending on the parameters specified, this may act as a 'jump' () or a continuous movement (). + /// + public void Seek(float? atTime, float dt, Action eventsOutput, bool delayedDestroy = false) + { + if (IsDestroyed || this.delayedDestroy) + return; + + float t = atTime ?? (time + dt * TimeScale * Core.Settings.GlobalAnimationSpeed); + + bool mirrorsAreSame = lastMirrorX == MirrorHorizontal && lastMirrorZ == MirrorVertical; + + if (Math.Abs(this.time - t) < 0.0001f && mirrorsAreSame) + return; + + var timer = new RefTimer(); + + lastMirrorX = MirrorHorizontal; + lastMirrorZ = MirrorVertical; + SeekInt(t, eventsOutput, MirrorHorizontal, MirrorVertical); + + timer.GetElapsedMilliseconds(out SeekMS); + + // Check if the end of the animation has been reached. + if (t < Data.Duration) + return; + + if (Loop) + { + time = 0; + } + else + { + if (delayedDestroy) + this.delayedDestroy = true; + else + Destroy(); + } + } + + private void SeekInt(float time, Action eventsOutput, bool mirrorX = false, bool mirrorY = false) + { + time = Mathf.Clamp(time, 0f, Duration); + + // Pass 1: Evaluate curves, make local matrices. + for (int i = 0; i < Data.Parts.Count; i++) + { + snapshots[i] = new AnimPartSnapshot(Data.Parts[i], this, time); + } + + // Pass 2: Resolve world matrices using inheritance tree. + for (int i = 0; i < Data.Parts.Count; i++) + snapshots[i].UpdateWorldMatrix(mirrorX, mirrorY); + + float start = Mathf.Min(this.time, time); + float end = Mathf.Max(this.time, time); + this.time = time; + + if (eventsOutput == null) + return; + + // Output relevant events to be handled. + foreach (var e in GetEventsInPeriod(new Vector2(start, end))) + eventsOutput(e); + } + + public void OnStart() + { + if (hasStarted) + { + Core.Error("Started twice!"); + return; + } + + hasStarted = true; + + // Give pawns their jobs. + foreach (var pawn in Pawns) + { + if (pawn.verbTracker?.AllVerbs != null) + foreach (var verb in pawn.verbTracker.AllVerbs) + verb.Reset(); + + if (pawn.equipment?.AllEquipmentVerbs != null) + foreach (var verb in pawn.equipment.AllEquipmentVerbs) + verb.Reset(); + + // Raise skills event. + var skills = pawn.GetComp()?.GetSkills(); + if (skills != null) + { + foreach (var skill in skills) + { + if (skill == null) + continue; + + try + { + skill.OnAnimationStarted(this); + } + catch (Exception e) + { + Core.Error($"Exception when raising the Skill.OnAnimationStarted event for {pawn} on skill {skill}", e); + } + } + } + + // Do not give animation job if a custom job def is specified: + if (CustomJobDef != null) + continue; + + var newJob = JobMaker.MakeJob(AM_DefOf.AM_InAnimation); + pawn.jobs.StartJob(newJob, JobCondition.InterruptForced); + + if (pawn.CurJobDef != AM_DefOf.AM_InAnimation) + { + Core.Error($"CRITICAL ERROR: Failed to force interrupt {pawn}'s job with animation job. Likely a mod conflict."); + } + } + + // Teleport pawns to their starting positions. They should already be there, but check just in case. + IntVec3 basePos = RootTransform.MultiplyPoint3x4(Vector3.zero).ToIntVec3(); + basePos.y = 0; + + for (int i = 0; i < Pawns.Count; i++) + { + var pawn = Pawns[i]; + if (pawn == null) + continue; + + var offset = Def.TryGetCell(AnimCellData.Type.PawnStart, MirrorHorizontal, MirrorVertical, i) ?? IntVec2.Zero; + TeleportPawn(pawn, basePos + offset.ToIntVec3); + } + + // Custom sweep paths: + ISweepProvider sweepProvider = BasicSweepProvider.DefaultInstance; + var weapon = Pawns.Count == 0 ? null : Pawns[0].GetFirstMeleeWeapon(); + var tweak = weapon == null ? null : TweakDataManager.TryGetTweak(weapon.def); + if (tweak?.GetSweepProvider() != null) + sweepProvider = tweak.GetSweepProvider(); + else if (Def.sweepProvider != null) + sweepProvider = Def.sweepProvider; + + InitSweepMeshes(); + foreach (var sweep in sweeps) + { + if (sweep == null) + continue; + sweep.ColorProvider = sweepProvider; + } + } + + public void OnEnd() + { + if (hasDoneOnEnd) + return; + hasDoneOnEnd = true; + + try + { + // Do not teleport to end if animation was interrupted. + if (WasInterrupted) + return; + + // Spawn heads. + if (!Dialog_AnimationDebugger.IsInRehearsalMode) + SpawnDroppedHeads(); + + TeleportPawnsToEnd(); + } + catch (Exception e) + { + Core.Error("Exception in AnimRenderer.OnEnd", e); + } + finally + { + foreach (var sweep in sweeps) + { + sweep?.Mesh?.Dispose(); + } + + OnEndAction?.Invoke(this); + } + } + + private void SpawnDroppedHeads() + { + if (ExecutionOutcome == ExecutionOutcome.Kill) + { + var comp = Map.GetAnimManager(); + + foreach (var index in Def.spawnDroppedHeadsForPawnIndices) + { + if (index < 0 || index >= Pawns.Count) + continue; + + comp.AddDroppedHeadFor(Pawns[index], this); + } + } + } + + public void TeleportPawnsToEnd() + { + // TODO certain animations should have different end positions if the pawn was + // not killed. + IntVec3 basePos = RootTransform.MultiplyPoint3x4(Vector3.zero).ToIntVec3(); + basePos.y = 0; + + for (int i = 0; i < Pawns.Count; i++) { + var pawn = Pawns[i]; + if (pawn == null) + continue; + + var posData = Def.TryGetCell(AnimCellData.Type.PawnEnd, MirrorHorizontal, MirrorVertical, i); + if (posData == null) + continue; + + // Teleport to end position. + var endPos = basePos + posData.Value.ToIntVec3; + TeleportPawn(pawn, endPos); + + // End animation job. + if (pawn.CurJobDef == AM_DefOf.AM_InAnimation) + pawn.jobs?.EndCurrentJob(JobCondition.InterruptForced); + } + } + + protected virtual void TeleportPawn(Pawn pawn, IntVec3 pos) + { + if (pawn == null) + return; + + pawn.Position = pos; + pawn.pather?.StopDead(); + pawn.pather?.Notify_Teleported_Int(); + pawn.Drawer?.tweener?.ResetTweenedPosToRoot(); + } + + public IEnumerable GetEventsInPeriod(Vector2 timePeriod) + { + return Data.GetEventsInPeriod(timePeriod); + } + + public AnimPartData GetPart(string name) => Data.GetPart(name); + + protected virtual bool ShouldDraw(in AnimPartSnapshot snapshot) + { + return snapshot.Active && !GetOverride(snapshot).PreventDraw && snapshot.FinalColor.a > 0; + } + + protected virtual Material GetMaterialFor(in AnimPartSnapshot snapshot, out bool forceMPB) + { + forceMPB = false; + + // If drawing in split mode, must use the split shader. + if (snapshot.SplitDrawMode != AnimData.SplitDrawMode.None && snapshot.SplitDrawPivot != null) + { + // This shader is designed to work with the mpb. + forceMPB = true; + return Content.CustomCutoffMaterial; + } + + // Otherwise check for an override material. + var ov = GetOverride(snapshot); + var ovMat = ov.Material; + if (ovMat != null) + return ovMat; + + // Finally fall back to transparent or cutout. + if (ov.UseDefaultTransparentMaterial || snapshot.Part.TransparentByDefault || snapshot.FinalColor.a < 1f) + return DefaultTransparent; + + return DefaultCutout; + } + + public bool AddPawn(Pawn pawn) => AddPawn(pawn, Pawns.Count, true); + + public bool AddPawn(Pawn pawn, int index, bool register) + { + if (pawn == null) + return false; + + char tagChar = Alphabet[index]; + + if (register) + { + Pawns.Add(pawn); + + pawnToParts[pawn] = new PawnLinkedParts + { + BodyIndex = Data.GetPartIndex($"Body{tagChar}"), + HeadIndex = Data.GetPartIndex($"Head{tagChar}") + }; + } + + // Hands. + ConfigureHandsForPawn(pawn, index); + + // Held item. + string itemName = $"Item{tagChar}"; + var weapon = pawn.GetFirstMeleeWeapon(); + var tweak = weapon == null ? null : TweakDataManager.TryGetTweak(weapon.def); + + // Apply weapon. + var itemPart = GetPart(itemName); + + if (weapon == null || itemPart == null || tweak == null) + return true; + + tweak.Apply(this, itemPart); + var ov = GetOverride(itemPart); + ov.Weapon = weapon; + ov.Material = weapon.Graphic?.MatSingleFor(weapon); + ov.UseMPB = false; // Do not use the material property block, because it will override the material second color and mask. + + // FIX: Certain vanilla textures are set to Clamp instead of Wrap. This breaks flipping. + // Which ones seems random. (Beer is Clamp, Breach Axe is Repeat). + // For now, force them to be Repeat. Not sure if this will have any negative impact elsewhere in the game. Hopefully not. + if (ov.Material != null) + { + var main = ov.Material.mainTexture; + if(main != null) + main.wrapMode = TextureWrapMode.Repeat; + + var mask = ov.Material.HasProperty(ShaderPropertyIDs.MaskTex) ? ov.Material.GetTexture(ShaderPropertyIDs.MaskTex) : null; + if (mask != null) + mask.wrapMode = TextureWrapMode.Repeat; + } + + if (ov.CustomRenderer != null) + ov.CustomRenderer.Item = weapon; + + InitSweepMeshes(); + foreach (var path in sweeps) + { + if (path.Part == itemPart) + { + path.DownDst = tweak.BladeStart; + path.UpDst = tweak.BladeEnd; + } + } + + return true; + } + + private void ConfigureHandsForPawn(Pawn pawn, int index) + { + var weapon = pawn.GetFirstMeleeWeapon(); + var tweak = weapon?.TryGetTweakData(); + var handsMode = tweak?.HandsMode ?? HandsMode.Default; + + // Hands and skin color... + string mainHandName = $"HandA{(index > 0 ? index + 1 : "")}"; + string altHandName = $"HandB{(index > 0 ? index + 1 : "")}"; + + Color skinColor = pawn.story?.SkinColor ?? Color.white; + + // Hand visibility uses the animation data first and foremost, and if the animation does + // not care about hand visibility, then it is dictated by the weapon. + var vis = Def.GetHandsVisibility(index); + + /* + * Order of descending priority for deciding what hand(s) to show: + * - Mod settings. + * - Has hands (humanoids only). + * - Animation requirement. + * - Weapon requirement. + */ + + // Settings: + bool settingsShowHands = Core.Settings.ShowHands; + + // Humanoid? + bool isHumanoid = pawn.RaceProps.Humanlike; + + // Animation requirement. + bool? animMainHand = vis.showMainHand; + bool? animAltHand = vis.showAltHand; + + // Weapon requirement: + bool? weaponMainHand = weapon == null ? null : handsMode != HandsMode.No_Hands; + bool? weaponAltHand = weapon == null ? null : handsMode == HandsMode.Default; + + // Final calculation: + bool showMain = settingsShowHands && isHumanoid && (animMainHand ?? weaponMainHand ?? false); + bool showAlt = settingsShowHands && isHumanoid && (animAltHand ?? weaponAltHand ?? false); + + // Apply main hand. + var mainHandPart = GetPart(mainHandName); + if (mainHandPart != null) + { + var ov = GetOverride(mainHandPart); + ov.PreventDraw = !showMain; + ov.Texture = AnimationManager.HandTexture; + ov.ColorOverride = skinColor; + } + + // Apply alt hand. + var altHandPart = GetPart(altHandName); + if (mainHandPart != null) + { + var ov = GetOverride(altHandPart); + ov.PreventDraw = !showAlt; + ov.Texture = AnimationManager.HandTexture; + ov.ColorOverride = skinColor; + } + } + + public override string ToString() => Def.label == null ? Def.defName : Def.LabelCap; + + public class SaveData : IExposable + { + public HashSet DuelSegmentsDone = new HashSet(); + public int DuelSegmentsDoneCount; + public int TargetDuelSegmentCount; + + public void ExposeData() + { + Scribe_Values.Look(ref DuelSegmentsDoneCount, "duelSegmentsDoneCount"); + Scribe_Values.Look(ref TargetDuelSegmentCount, "targetDuelSegmentCount"); + Scribe_Collections.Look(ref DuelSegmentsDone, "duelSegmentsDone", LookMode.Value); + DuelSegmentsDone ??= new HashSet(); + } + } + + private readonly struct PawnLinkedParts + { + public required int BodyIndex { get; init; } + public required int HeadIndex { get; init; } + } +} \ No newline at end of file diff --git a/Source/AnimationMod/AnimType.cs b/Source/1.4/ThingGenerator/AnimType.cs similarity index 100% rename from Source/AnimationMod/AnimType.cs rename to Source/1.4/ThingGenerator/AnimType.cs diff --git a/Source/1.4/ThingGenerator/AnimationManager.cs b/Source/1.4/ThingGenerator/AnimationManager.cs new file mode 100644 index 00000000..7739a25a --- /dev/null +++ b/Source/1.4/ThingGenerator/AnimationManager.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using AM.Events; +using AM.Heads; +using AM.Processing; +using UnityEngine; +using Verse; + +namespace AM +{ + public class AnimationManager : MapComponent + { + public static ConditionalWeakTable PawnToHeadInstance { get; } = new ConditionalWeakTable(); + + [TweakValue("Melee Animation Mod", 0, 10)] + public static int CullingPadding = 5; + public static bool IsDoingMultithreadedSeek { get; private set; } + public static double MultithreadedSeekTimeMS; + public static Texture2D HandTexture; + + private static ulong frameLastSeeked; + + public static void Init() + { + HandTexture = ContentFinder.Get("AM/Hand"); + } + + public readonly MapPawnProcessor PawnProcessor; + + private readonly List toDraw = new List(); + private readonly List<(Pawn pawn, Vector2 position)> labels = new List<(Pawn pawn, Vector2 position)>(); + private readonly List heads = new List(128); + private HashSet ioRenderers = new HashSet(); + + public AnimationManager(Map map) : base(map) + { + PawnProcessor = new MapPawnProcessor(map); + } + + public override void MapRemoved() + { + base.MapRemoved(); + PawnProcessor.Dispose(); + foreach (var head in heads) + { + PawnToHeadInstance.Remove(head.Pawn); + } + heads.Clear(); + } + + public void AddPostDraw(Action draw) + { + if (draw != null) + toDraw.Add(draw); + } + + public override void MapComponentUpdate() + { + base.MapComponentUpdate(); + + float dt = Time.deltaTime * Find.TickManager.TickRateMultiplier; + if (Find.TickManager.Paused) + dt = 0f; + + RenderHeads(); + Draw(dt); + + + foreach (var action in toDraw) + action?.Invoke(); + toDraw.Clear(); + } + + public override void MapComponentTick() + { + base.MapComponentTick(); + + PawnProcessor.Tick(); + } + + public override void MapComponentOnGUI() + { + base.MapComponentOnGUI(); + + AnimRenderer.DrawAllGUI(map); + + foreach (var pair in labels) + { + GenMapUI.DrawPawnLabel(pair.pawn, pair.position); + } + } + + public void Draw(float deltaTime) + { + labels.Clear(); + IsDoingMultithreadedSeek = Core.Settings.MultithreadedMatrixCalculations && AnimRenderer.ActiveRenderers.Count >= 10 && Core.Settings.MaxProcessingThreads != 1; + if (IsDoingMultithreadedSeek) + SeekMultithreaded(deltaTime); + + var viewBounds = Find.CameraDriver.CurrentViewRect.ExpandedBy(CullingPadding); + + Action onEvent = null; + if (!IsDoingMultithreadedSeek) + onEvent = DoEvent; + + AnimRenderer.DrawAll(deltaTime, map, onEvent, viewBounds, Core.Settings.DrawNamesInAnimation ? DrawLabel : null); + AnimRenderer.RemoveDestroyed(); + } + + public void AddDroppedHeadFor(Pawn pawn, AnimRenderer animRenderer) + { + var head = animRenderer.GetPawnHead(pawn); + var body = animRenderer.GetPawnBody(pawn); + if (head == null || body == null) + { + Core.Warn("Failed to find head and/or body part to spawn dropped head."); + return; + } + + var headSS = animRenderer.GetSnapshot(head); + var bodySS = animRenderer.GetSnapshot(body); + + var instance = new HeadInstance + { + Pawn = pawn, + Direction = bodySS.GetWorldDirection(), + Position = headSS.GetWorldPosition(), + Rotation = headSS.GetWorldRotation(), + TimeToLive = 120, + Map = pawn.Map ?? pawn.Corpse?.Map + }; + + heads.Add(instance); + + PawnToHeadInstance.Add(pawn, instance); + } + + private void RenderHeads() + { + for (int i = 0; i < heads.Count; i++) + { + var head = heads[i]; + bool stayAlive; + try + { + stayAlive = head.Render(); + } + catch (Exception e) + { + Core.Error($"Exception rendering dropped head of {head.Pawn}. Head will be deleted.", e); + stayAlive = false; + } + + if (!stayAlive) + { + PawnToHeadInstance.Remove(head.Pawn); + // Remove at swap back, for speed reasons: + heads[i] = heads[^1]; + heads.RemoveAt(heads.Count - 1); + i--; + } + } + } + + private static readonly List<(AnimRenderer, EventBase)> eventsToDo = new List<(AnimRenderer, EventBase)>(128); + + private static void SeekMultithreaded(float dt) + { + if (frameLastSeeked == GameComp.FrameCounter) + return; + + frameLastSeeked = GameComp.FrameCounter; + + var timer = new RefTimer(); + + if (dt <= 0) + { + timer.GetElapsedMilliseconds(out MultithreadedSeekTimeMS); + return; + } + + // Processing: + eventsToDo.Clear(); + Parallel.For(0, AnimRenderer.ActiveRenderers.Count, i => + { + var animator = AnimRenderer.ActiveRenderers[i]; + + if (animator.IsDestroyed) + return; + + animator.Seek(null, dt, e => eventsToDo.Add((animator, e)), true); + }); + + // Events: + foreach (var pair in eventsToDo) + DoEvent(pair.Item1, pair.Item2); + + timer.GetElapsedMilliseconds(out MultithreadedSeekTimeMS); + } + + private static void DoEvent(AnimRenderer r, EventBase ev) + { + try + { + EventHelper.Handle(ev, r); + } + catch (Exception e) + { + Core.Error($"Exception handling event (mt) for {r} ({ev})", e); + } + } + + private void DrawLabel(Pawn pawn, Vector2 position) + { + labels.Add((pawn, position)); + } + + public override void ExposeData() + { + base.ExposeData(); + + ioRenderers ??= new HashSet(); + + switch (Scribe.mode) + { + case LoadSaveMode.Saving: + ioRenderers.Clear(); + + // Collect the active renderers that are on this map. + foreach (var renderer in AnimRenderer.ActiveRenderers) + { + if (renderer.ShouldSave && renderer.Map == this.map) + { + if (!ioRenderers.Add(renderer)) + Core.Error("There was a duplicate renderer in the list!"); + } + } + + // Save. + Scribe_Collections.Look(ref ioRenderers, "animationRenderers", LookMode.Deep); + break; + + case LoadSaveMode.LoadingVars: + AnimRenderer.ClearAll(); // Remove all previous renderers from the system. + ioRenderers.Clear(); + Scribe_Collections.Look(ref ioRenderers, "animationRenderers", LookMode.Deep); + break; + case LoadSaveMode.ResolvingCrossRefs: + Scribe_Collections.Look(ref ioRenderers, "animationRenderers", LookMode.Deep); + break; + case LoadSaveMode.PostLoadInit: + Scribe_Collections.Look(ref ioRenderers, "animationRenderers", LookMode.Deep); + + // Write back to general system. + AnimRenderer.PostLoadPendingAnimators.AddRange(ioRenderers); + + Core.Log($"Loaded {ioRenderers.Count} animation renderers on map {map}"); + ioRenderers.Clear(); + break; + } + } + } +} diff --git a/Source/1.4/ThingGenerator/AnimationMod.csproj b/Source/1.4/ThingGenerator/AnimationMod.csproj new file mode 100644 index 00000000..5beb413b --- /dev/null +++ b/Source/1.4/ThingGenerator/AnimationMod.csproj @@ -0,0 +1,62 @@ + + + + + net472 + Library + 11 + false + true + false + false + Release + zAnimationMod + AM + disable + true + none + true + + + + + <_Parameter1>$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + 0ColourPicker.dll + + + + + + + + + + + + + + + + + + + ..\..\..\1.4\Assemblies\ + true + TRACE;V14 + + + diff --git a/Source/AnimationMod/AnimationStartParameters.cs b/Source/1.4/ThingGenerator/AnimationStartParameters.cs similarity index 100% rename from Source/AnimationMod/AnimationStartParameters.cs rename to Source/1.4/ThingGenerator/AnimationStartParameters.cs diff --git a/Source/AnimationMod/AudioCredits.txt b/Source/1.4/ThingGenerator/AudioCredits.txt similarity index 100% rename from Source/AnimationMod/AudioCredits.txt rename to Source/1.4/ThingGenerator/AudioCredits.txt diff --git a/Source/AnimationMod/AudioUtility.cs b/Source/1.4/ThingGenerator/AudioUtility.cs similarity index 100% rename from Source/AnimationMod/AudioUtility.cs rename to Source/1.4/ThingGenerator/AudioUtility.cs diff --git a/Source/1.4/ThingGenerator/AutoDuel/AutoFriendlyDuelMapComp.cs b/Source/1.4/ThingGenerator/AutoDuel/AutoFriendlyDuelMapComp.cs new file mode 100644 index 00000000..8c853f48 --- /dev/null +++ b/Source/1.4/ThingGenerator/AutoDuel/AutoFriendlyDuelMapComp.cs @@ -0,0 +1,169 @@ +using AM.Buildings; +using AM.Tweaks; +using JetBrains.Annotations; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Verse; + +namespace AM.AutoDuel; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] +public class AutoFriendlyDuelMapComp : MapComponent +{ + [UsedImplicitly(ImplicitUseKindFlags.Access)] + [DebugAction("Melee Animation", allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static void LogAutoDuelInfo() + { + var map = Find.CurrentMap; + if (map == null) + return; + + var comp = map.GetComponent(); + Core.Log($"There are {comp.DuelSpots.Count} duel spots on the map."); + Core.Log($"Spots with active duels: {comp.GetActiveDuelSpots()}"); + + Core.Log("All possible pawns (not filtered):"); + foreach (var p in comp.EnumeratePossiblePawns()) + { + Core.Log(p.ToString()); + } + + Core.Log("All possible pawns (filtered and cached):"); + foreach (var p in comp.pawnsThatCanDuel) + { + Core.Log(p.ToString()); + } + } + + public readonly HashSet DuelSpots = new HashSet(); + + private readonly HashSet pawnsThatCanDuel = new HashSet(); + + public AutoFriendlyDuelMapComp(Map map) : base(map) { } + + public bool CanPawnMaybeDuel(Pawn pawn) => pawnsThatCanDuel.Contains(pawn); + + public Pawn TryGetRandomDuelPartner(Pawn except) + { + if (pawnsThatCanDuel.Count <= 1) + return null; + + return pawnsThatCanDuel.Except(except).Where(CanPawnDuel).RandomElementWithFallback(); + } + + public Building_DuelSpot TryGetBestDuelSpotFor(Pawn a, Pawn b) + { + var spots = from s in DuelSpots + where !s.IsForbidden && !s.IsForbidden(a) && !s.IsForbidden(b) && !s.IsInUse(out _, out _) + let dst = s.Position.DistanceToSquared(a.Position) + s.Position.DistanceToSquared(b.Position) + orderby dst + select s; + + return spots.FirstOrDefault(); + } + + public IEnumerable GetActiveDuelSpots() + { + foreach (var spot in DuelSpots) + { + if (spot.IsInUse(out var a, out var b)) + { + yield return new ActiveDuelSpot + { + Spot = spot, + PawnA = a, + PawnB = b + }; + } + } + } + + public override void MapComponentTick() + { + base.MapComponentTick(); + + if (Find.TickManager.TicksAbs % (60 * 1) != 0) + return; + + try + { + UpdatePawnsThatCanDuel(); + } + catch (Exception e) + { + Core.Error("Exception updating friendly duel pawn cached list.", e); + } + } + + private void UpdatePawnsThatCanDuel() + { + pawnsThatCanDuel.Clear(); + + if (Core.Settings.MaxProcessingThreads != 1) + { + // Threaded find. + Parallel.ForEach(EnumeratePossiblePawns(), p => + { + if (!CanPawnDuel(p)) + return; + + lock (pawnsThatCanDuel) + { + pawnsThatCanDuel.Add(p); + } + }); + } + else + { + pawnsThatCanDuel.AddRange(EnumeratePossiblePawns().Where(CanPawnDuel)); + } + } + + private IEnumerable EnumeratePossiblePawns() + { + foreach (var p in map.mapPawns.FreeColonistsSpawned) + yield return p; + foreach (var p in map.mapPawns.SlavesOfColonySpawned) + yield return p; + } + + public static bool CanPawnDuel(Pawn pawn) + { + // Not dead, downed or drafted. + if (pawn == null || pawn.Dead || pawn.Downed || pawn.Drafted) + return false; + + // Pawn must have weapon. + var weapon = pawn.GetFirstMeleeWeapon(out var td); + if (weapon == null) + return false; + + const MeleeWeaponType TYPE_MASK = MeleeWeaponType.Long_Stab | + MeleeWeaponType.Long_Sharp | + MeleeWeaponType.Long_Blunt; + + // And that weapon must be long (for now). + if ((td.MeleeWeaponType & TYPE_MASK) == 0) + return false; + + // Duel must not be on cooldown: + if (!pawn.GetMeleeData().IsFriendlyDuelOffCooldown()) + return false; + + // Check pawn is in recreation and not doing anything majorly important. + if (pawn.timetable.CurrentAssignment != TimeAssignmentDefOf.Joy || !(pawn.CurJobDef?.playerInterruptible ?? true)) + return false; + + return true; + } +} + +public readonly struct ActiveDuelSpot +{ + public Building_DuelSpot Spot { get; init; } + public Pawn PawnA { get; init; } + public Pawn PawnB { get; init; } +} diff --git a/Source/1.4/ThingGenerator/AutoDuel/JoyGiver_FriendlyDuel.cs b/Source/1.4/ThingGenerator/AutoDuel/JoyGiver_FriendlyDuel.cs new file mode 100644 index 00000000..4c257ba2 --- /dev/null +++ b/Source/1.4/ThingGenerator/AutoDuel/JoyGiver_FriendlyDuel.cs @@ -0,0 +1,53 @@ +using JetBrains.Annotations; +using RimWorld; +using Verse; +using Verse.AI; + +namespace AM.AutoDuel; + +[UsedImplicitly] +public class JoyGiver_FriendlyDuel : JoyGiver +{ + public override Job TryGiveJob(Pawn pawn) + { + // Duel on cooldown check: + if (!pawn.GetMeleeData().IsFriendlyDuelOffCooldown()) + return null; + + // Get map comp to see if this pawn is eligible: + var comp = pawn.Map?.GetComponent(); + if (comp == null) + return null; + + // The comp will cache any pawns that have valid melee weapons: + if (!comp.CanPawnMaybeDuel(pawn)) + return null; + + // Check still valid (cache could be out of date): + if (!AutoFriendlyDuelMapComp.CanPawnDuel(pawn)) + return null; + + // Try get a duel partner. + var partner = comp.TryGetRandomDuelPartner(pawn); + if (partner == null) + return null; + + // Try get a duel spot for us two: + var spot = comp.TryGetBestDuelSpotFor(pawn, partner); + if (spot == null) + return null; + + // Try give job to opponent: + Core.Log($"Interrupting {partner}'s {partner.CurJob} to start a friendly duel with {pawn}, auto mode."); + + var opJob = spot.MakeDuelJob(pawn, false); + partner.jobs.StartJob(opJob, JobCondition.InterruptForced); + if (partner.CurJobDef != AM_DefOf.AM_DoFriendlyDuel) + { + Core.Error($"Failed to give {partner} the friendly duel job (automatic), probably mod incompatibility."); + return null; + } + + return spot.MakeDuelJob(partner, true); + } +} \ No newline at end of file diff --git a/Source/AnimationMod/AutoDuel/JoyGiver_SpectateFriendlyDuel.cs b/Source/1.4/ThingGenerator/AutoDuel/JoyGiver_SpectateFriendlyDuel.cs similarity index 100% rename from Source/AnimationMod/AutoDuel/JoyGiver_SpectateFriendlyDuel.cs rename to Source/1.4/ThingGenerator/AutoDuel/JoyGiver_SpectateFriendlyDuel.cs diff --git a/Source/AnimationMod/Bezier.cs b/Source/1.4/ThingGenerator/Bezier.cs similarity index 100% rename from Source/AnimationMod/Bezier.cs rename to Source/1.4/ThingGenerator/Bezier.cs diff --git a/Source/AnimationMod/Buildings/Building_DuelSpot.cs b/Source/1.4/ThingGenerator/Buildings/Building_DuelSpot.cs similarity index 100% rename from Source/AnimationMod/Buildings/Building_DuelSpot.cs rename to Source/1.4/ThingGenerator/Buildings/Building_DuelSpot.cs diff --git a/Source/AnimationMod/ColumnWorkers/PawnColumnWorker_Base.cs b/Source/1.4/ThingGenerator/ColumnWorkers/PawnColumnWorker_Base.cs similarity index 100% rename from Source/AnimationMod/ColumnWorkers/PawnColumnWorker_Base.cs rename to Source/1.4/ThingGenerator/ColumnWorkers/PawnColumnWorker_Base.cs diff --git a/Source/AnimationMod/ColumnWorkers/PawnColumnWorker_Execute.cs b/Source/1.4/ThingGenerator/ColumnWorkers/PawnColumnWorker_Execute.cs similarity index 100% rename from Source/AnimationMod/ColumnWorkers/PawnColumnWorker_Execute.cs rename to Source/1.4/ThingGenerator/ColumnWorkers/PawnColumnWorker_Execute.cs diff --git a/Source/AnimationMod/ColumnWorkers/PawnColumnWorker_Lasso.cs b/Source/1.4/ThingGenerator/ColumnWorkers/PawnColumnWorker_Lasso.cs similarity index 100% rename from Source/AnimationMod/ColumnWorkers/PawnColumnWorker_Lasso.cs rename to Source/1.4/ThingGenerator/ColumnWorkers/PawnColumnWorker_Lasso.cs diff --git a/Source/AnimationMod/Content.cs b/Source/1.4/ThingGenerator/Content.cs similarity index 100% rename from Source/AnimationMod/Content.cs rename to Source/1.4/ThingGenerator/Content.cs diff --git a/Source/AnimationMod/Controller/ActionController.cs b/Source/1.4/ThingGenerator/Controller/ActionController.cs similarity index 100% rename from Source/AnimationMod/Controller/ActionController.cs rename to Source/1.4/ThingGenerator/Controller/ActionController.cs diff --git a/Source/AnimationMod/Controller/Reports/DuelAttemptReport.cs b/Source/1.4/ThingGenerator/Controller/Reports/DuelAttemptReport.cs similarity index 100% rename from Source/AnimationMod/Controller/Reports/DuelAttemptReport.cs rename to Source/1.4/ThingGenerator/Controller/Reports/DuelAttemptReport.cs diff --git a/Source/AnimationMod/Controller/Reports/ExecutionAttemptReport.cs b/Source/1.4/ThingGenerator/Controller/Reports/ExecutionAttemptReport.cs similarity index 100% rename from Source/AnimationMod/Controller/Reports/ExecutionAttemptReport.cs rename to Source/1.4/ThingGenerator/Controller/Reports/ExecutionAttemptReport.cs diff --git a/Source/AnimationMod/Controller/Reports/GrappleAttemptReport.cs b/Source/1.4/ThingGenerator/Controller/Reports/GrappleAttemptReport.cs similarity index 100% rename from Source/AnimationMod/Controller/Reports/GrappleAttemptReport.cs rename to Source/1.4/ThingGenerator/Controller/Reports/GrappleAttemptReport.cs diff --git a/Source/AnimationMod/Controller/Requests/DuelAttemptRequest.cs b/Source/1.4/ThingGenerator/Controller/Requests/DuelAttemptRequest.cs similarity index 100% rename from Source/AnimationMod/Controller/Requests/DuelAttemptRequest.cs rename to Source/1.4/ThingGenerator/Controller/Requests/DuelAttemptRequest.cs diff --git a/Source/AnimationMod/Controller/Requests/ExecutionAttemptRequest.cs b/Source/1.4/ThingGenerator/Controller/Requests/ExecutionAttemptRequest.cs similarity index 100% rename from Source/AnimationMod/Controller/Requests/ExecutionAttemptRequest.cs rename to Source/1.4/ThingGenerator/Controller/Requests/ExecutionAttemptRequest.cs diff --git a/Source/AnimationMod/Controller/Requests/GrappleAttemptsRequest.cs b/Source/1.4/ThingGenerator/Controller/Requests/GrappleAttemptsRequest.cs similarity index 100% rename from Source/AnimationMod/Controller/Requests/GrappleAttemptsRequest.cs rename to Source/1.4/ThingGenerator/Controller/Requests/GrappleAttemptsRequest.cs diff --git a/Source/AnimationMod/Core.cs b/Source/1.4/ThingGenerator/Core.cs similarity index 100% rename from Source/AnimationMod/Core.cs rename to Source/1.4/ThingGenerator/Core.cs diff --git a/Source/1.4/ThingGenerator/Data/AnimData.cs b/Source/1.4/ThingGenerator/Data/AnimData.cs new file mode 100644 index 00000000..99febd17 --- /dev/null +++ b/Source/1.4/ThingGenerator/Data/AnimData.cs @@ -0,0 +1,698 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using AM; +using AM.Tweaks; +using Newtonsoft.Json; +using AM.Data.Model; +using AM.Events; +#if !UNITY_EDITOR +using Verse; +#endif + +public class AnimData +{ + #region Static Stuff + + private static Mesh m, mfx, mfy, mfxy; + private static readonly Dictionary cache = new Dictionary(); + + public static Mesh GetMesh(bool flipX, bool flipY) + { + if (m == null) + { + m = MakeMesh(Vector2.one, false, false); + mfx = MakeMesh(Vector2.one, true, false); + mfy = MakeMesh(Vector2.one, false, true); + mfxy = MakeMesh(Vector2.one, true, true); + } + return (flipX && flipY) ? mfxy : flipX ? mfx : flipY ? mfy : m; + } + + private static Mesh MakeMesh(Vector2 size, bool flipX, bool flipY) + { + var normal = new Vector2[] + { + new Vector2(0, 0),// Bottom-left + new Vector2(0, 1),// Top-left + new Vector2(1, 1),// Top-right + new Vector2(1, 0) // Bottom-right + }; + var fx = new Vector2[] + { + new Vector2(1, 0),// Bottom-left + new Vector2(1, 1),// Top-left + new Vector2(0, 1),// Top-right + new Vector2(0, 0) // Bottom-right + }; + var fy = new Vector2[] + { + new Vector2(0, 0), // Bottom-left + new Vector2(0, -1),// Top-left + new Vector2(1, -1),// Top-right + new Vector2(1, 0) // Bottom-right + }; + var fxy = new Vector2[] + { + new Vector2(1, 0), // Bottom-left + new Vector2(1, -1),// Top-left + new Vector2(0, -1),// Top-right + new Vector2(0, 0) // Bottom-right + }; + + Vector3[] verts = new Vector3[4]; + int[] tris = new int[6]; + verts[0] = new Vector3(-0.5f * size.x, 0f, -0.5f * size.y); // Bottom-left + verts[1] = new Vector3(-0.5f * size.x, 0f, 0.5f * size.y); // Top-left + verts[2] = new Vector3(0.5f * size.x, 0f, 0.5f * size.y); // Top-right + verts[3] = new Vector3(0.5f * size.x, 0f, -0.5f * size.y); // Bottom-right + tris[0] = 0; + tris[1] = 1; + tris[2] = 2; + tris[3] = 0; + tris[4] = 2; + tris[5] = 3; + Mesh mesh = new(); + mesh.name = $"AM Mesh: {flipX}, {flipY}"; + mesh.vertices = verts; + mesh.uv = (flipX && flipY) ? fxy : flipX ? fx : flipY ? fy : normal; + mesh.SetTriangles(tris, 0); + mesh.RecalculateNormals(); + mesh.RecalculateBounds(); + return mesh; + } + + public static AnimData Load(string filePath, bool allowFromCache = true, bool saveToCache = true) + { + if (filePath == null) + return null; + + if (allowFromCache) + { + if (cache.TryGetValue(filePath, out var found)) + return found; + } + + var loaded = Load(File.ReadAllText(filePath)); + + if (saveToCache) + cache[filePath] = loaded; + + return loaded; + } + + private static AnimData Load(string json) + { + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new RectConverter()); + var model = JsonConvert.DeserializeObject(json, settings); + + var data = new AnimData() + { + Name = model.Name, + Duration = model.Length, + Bounds = model.Bounds, + ExportTimeUTC = model.ExportTimeUTC + }; + + // Make parts... + var parts = new List(); + var idToPart = new Dictionary(); + int i = 0; + foreach (var part in model.Parts) + { + var dp = new AnimPartData(part) + { + Path = part.Path, + CustomName = part.CustomName, + TexturePath = part.TexturePath, + ID = part.ID, + Index = i++, + TransparentByDefault = part.TransparentByDefault + }; + + parts.Add(dp); + idToPart.Add(dp.ID, dp); + } + + // Part children, parent and split draw pivot. + foreach (var part in model.Parts) + { + var dp = idToPart[part.ID]; + + // Parent & children. + if (part.ParentID != 0) + { + dp.Parent = idToPart[part.ParentID]; + dp.Parent.Children.Add(dp); + } + + // Split draw pivot. + if (part.SplitDrawPivotPartID != 0) + { + dp.SplitDrawPivot = idToPart[part.SplitDrawPivotPartID]; + } + } + + // Make events. + var events = new List(); + foreach (var e in model.Events) + { + var created = EventBase.CreateFromSaveData(e.Data); + if (created == null) + { + Core.Error($"Failed to create EventBase from data '{e.Data}'"); + continue; + } + + created.Time = e.Time; + events.Add(created); + } + + // Make sweep data. + var sweeps = new Dictionary>(); + foreach (var part in model.Parts) + { + var dp = idToPart[part.ID]; + foreach (var sweep in part.SweepPaths) + { + if (!sweeps.TryGetValue(dp, out var list)) + { + list = new List(); + sweeps.Add(dp, list); + } + + list.Add(new SweepPointCollection(sweep)); + } + } + + data.parts = parts.ToArray(); + data.events = events.ToArray(); + data.sweeps = sweeps; + + return data; + } + + public enum SplitDrawMode + { + None, + Before, + After, + BeforeAndAfter, + } + + #endregion + + public string Name { get; private set; } + public float Duration { get; private set; } + public Rect Bounds { get; private set; } + public DateTime ExportTimeUTC { get; private set; } + public IReadOnlyList Parts => parts; + public IReadOnlyList Events => events; + public int SweepDataCount => sweeps.Count; + public IEnumerable PartsWithSweepData => sweeps.Keys; + + private AnimPartData[] parts; + private EventBase[] events; + private Dictionary> sweeps; + + public AnimPartData GetPart(string name) + { + if (name == null) + return null; + + foreach (var part in parts) + { + if (part.Name == name) + return part; + } + + return null; + } + + public int GetPartIndex(string partName) + { + return parts.FirstIndexOf(p => p.Name == partName); + } + + public IReadOnlyList GetSweepPaths(AnimPartData forPart) + { + if (forPart != null && sweeps.TryGetValue(forPart, out var found)) + return found; + return Array.Empty(); + } + + public IEnumerable GetEventsInPeriod(Vector2 range) + { + foreach (var e in events) + { + if (e.IsInTimeWindow(range)) + yield return e; + } + } +} + +public class AnimPartData +{ + private static AnimationCurve MakeConstantCurve(float value) + { + return new AnimationCurve(new Keyframe(0f, value)) { postWrapMode = WrapMode.ClampForever, preWrapMode = WrapMode.ClampForever }; + } + + public string Name => CustomName ?? Path; + + public string Path; + public string CustomName; + public string TexturePath; + public int ID; + public int Index; + public bool TransparentByDefault; + public List Children = new List(); + public AnimPartData Parent; + public AnimPartData SplitDrawPivot; + + public readonly AnimationCurve PosX, PosY, PosZ; + public readonly AnimationCurve RotX, RotY, RotZ; + public readonly AnimationCurve ScaleX, ScaleY, ScaleZ; + public readonly AnimationCurve DataA, DataB, DataC; + public readonly AnimationCurve ColorR, ColorG, ColorB, ColorA; + public readonly AnimationCurve FlipX, FlipY; + public readonly AnimationCurve Active; + public readonly AnimationCurve Direction; + public readonly AnimationCurve SplitDrawModeCurve; + public readonly AnimationCurve FrameIndex; + + public AnimPartData(AnimPartModel model) + { + AnimationCurve GetCurve(string prop) + { + // Try get active curve. + if (model.Curves.TryGetValue(prop, out var found)) + return found.ToAnimationCurve(); + + // Try get a default value curve. + if (model.DefaultValues.TryGetValue(prop, out float def)) + return MakeConstantCurve(def); + + // Not found... + return null; + } + + // Is Active. + Active = GetCurve("GameObject.m_IsActive"); + + // Facing direction. + Direction = GetCurve("PawnBody.Direction"); + + // Position. + PosX = GetCurve("Transform.m_LocalPosition.x"); + PosY = GetCurve("Transform.m_LocalPosition.y"); + PosZ = GetCurve("Transform.m_LocalPosition.z"); + + // Rotation. + RotX = GetCurve("Transform.localEulerAnglesRaw.x"); + RotY = GetCurve("Transform.localEulerAnglesRaw.y"); + RotZ = GetCurve("Transform.localEulerAnglesRaw.z"); + + // Scale. + ScaleX = GetCurve("Transform.m_LocalScale.x"); + ScaleY = GetCurve("Transform.m_LocalScale.y"); + ScaleZ = GetCurve("Transform.m_LocalScale.z"); + + // Data. + DataA = GetCurve("AnimatedPart.DataA"); + DataB = GetCurve("AnimatedPart.DataB"); + DataC = GetCurve("AnimatedPart.DataC"); + + // Tint. + ColorR = GetCurve("AnimatedPart.Tint.r"); + ColorG = GetCurve("AnimatedPart.Tint.g"); + ColorB = GetCurve("AnimatedPart.Tint.b"); + ColorA = GetCurve("AnimatedPart.Tint.a"); + + // Flip. + FlipX = GetCurve("AnimatedPart.FlipX"); + FlipY = GetCurve("AnimatedPart.FlipY"); + + // Misc. + SplitDrawModeCurve = GetCurve("AnimatedPart.SplitDrawMode"); + FrameIndex = GetCurve("AnimatedPart.FrameIndex"); + } + + public AnimPartSnapshot GetSnapshot(AnimRenderer renderer) + { + if (renderer == null) + return default; + return renderer.GetSnapshot(this); + } +} + +public struct AnimPartSnapshot +{ + public bool Valid => Part != null; + public string TexturePath => Part?.TexturePath == null ? null : (Part.TexturePath + (FrameIndex > 0 ? FrameIndex.ToString() : null)); + public string PartName => Part?.Name; + public float Depth => WorldMatrix.MultiplyPoint3x4(Vector3.zero).y; + public AnimPartData SplitDrawPivot => Part?.SplitDrawPivot; + public Color FinalColor + { + get + { + if (Renderer == null) + return default; + + var ov = Renderer.GetOverride(this); + if (ov.ColorOverride != default) + return ov.ColorOverride; + + return Color * ov.ColorTint; + } + } + + public readonly AnimRenderer Renderer; + public readonly AnimPartData Part; + public float Time; + + public Vector3 LocalPosition; + public Vector3 LocalScale; + public Vector3 LocalRotation; + public Color Color; + public float DataA, DataB, DataC; + public bool FlipX, FlipY; + public bool Active; + public Rot4 Direction; + public int FrameIndex; + public AnimData.SplitDrawMode SplitDrawMode; + + public Matrix4x4 LocalMatrix; + public Matrix4x4 WorldMatrix; + public Matrix4x4 WorldMatrixNoOverride; + public Matrix4x4 WorldMatrixPreserveFlip; + + public AnimPartSnapshot(AnimPartData part, AnimRenderer renderer, float time) + { + Part = part ?? throw new ArgumentNullException(nameof(part)); + Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); + Time = time; + + LocalPosition = new Vector3(Eval(part.PosX, time), Eval(part.PosY, time), Eval(part.PosZ, time)); + LocalRotation = new Vector3(Eval(part.RotX, time), Eval(part.RotY, time), Eval(part.RotZ, time)); + LocalScale = new Vector3(Eval(part.ScaleX, time), Eval(part.ScaleY, time), Eval(part.ScaleZ, time)); + + DataA = Eval(part.DataA, time); + DataB = Eval(part.DataB, time); + DataC = Eval(part.DataC, time); + + Color = new Color(Eval(part.ColorR, time), Eval(part.ColorG, time), Eval(part.ColorB, time), Eval(part.ColorA, time)); + + FlipX = Eval(part.FlipX, time) >= 0.5f; + FlipY = Eval(part.FlipY, time) >= 0.5f; + + Active = Eval(part.Active, time) >= 0.5f; + SplitDrawMode = (AnimData.SplitDrawMode)(int)Eval(part.SplitDrawModeCurve, time); + FrameIndex = (int)Eval(part.FrameIndex, time); + + Direction = new Rot4((byte)Eval(part.Direction, time)); + + LocalMatrix = default; + WorldMatrix = default; + WorldMatrixNoOverride = default; + UpdateLocalMatrix(); + } + + public readonly Vector3 GetWorldPosition(Vector3 localPos = default) + { + return (Renderer.RootTransform * WorldMatrix).MultiplyPoint3x4(localPos); + } + + public readonly Vector3 GetWorldPositionNoOverride(Vector3 localPos = default) + { + return (Renderer.RootTransform * WorldMatrixNoOverride).MultiplyPoint3x4(localPos); + } + + public readonly Rot4 GetWorldDirection() + { + // Should this respect flip? + // Override flip too? + bool mx = Renderer.MirrorHorizontal; + bool my = Renderer.MirrorVertical; + return Direction.AsInt switch + { + 0 => my ? Rot4.South : Rot4.North, // North + 1 => mx ? Rot4.West : Rot4.East, // East + 2 => my ? Rot4.North : Rot4.South, // South + 3 => mx ? Rot4.East : Rot4.West, // West + _ => throw new Exception("Invalid") + }; + } + + public readonly float GetWorldRotation() + { + return (Renderer.RootTransform * WorldMatrix).rotation.eulerAngles.y; + } + + public void UpdateLocalMatrix() + { + LocalMatrix = Matrix4x4.TRS(LocalPosition, Quaternion.Euler(LocalRotation), LocalScale); + } + + private readonly Matrix4x4 MakeWorldMatrix() + { + if (Part?.Parent == null) + return LocalMatrix; + + return Renderer.GetSnapshot(Part.Parent).MakeWorldMatrix() * LocalMatrix; + } + + private readonly bool MakeHierarchyActive() + { + if (Part?.Parent == null) + return Active; + return Active && Renderer.GetSnapshot(Part.Parent).MakeHierarchyActive(); + } + + public void UpdateWorldMatrix(bool mirrorX, bool mirrorY) + { + var preProc = Matrix4x4.Scale(new Vector3(mirrorX ? -1f : 1f, 1f, mirrorY ? -1f : 1f)); + var postProc = Matrix4x4.Scale(new Vector3(mirrorX ? -1f : 1f, 1f, mirrorY ? -1f : 1f)); + + var ov = Renderer.GetOverride(this); + bool fx = FlipX; + if (ov.FlipX) + fx = !fx; + bool fy = FlipY; + if (ov.FlipY) + fy = !fy; + + Vector2 off = new(fx ? -ov.LocalOffset.x : ov.LocalOffset.x, fy ? -ov.LocalOffset.y : ov.LocalOffset.y); + float offRot = fx ^ fy ? -ov.LocalRotation : ov.LocalRotation; + var adjust = Matrix4x4.TRS(new Vector3(off.x, 0f, off.y), Quaternion.Euler(0, offRot, 0f), new Vector3(ov.LocalScaleFactor.x, 1f, ov.LocalScaleFactor.y)); + + Active = MakeHierarchyActive(); + + var mat = MakeWorldMatrix(); + WorldMatrix = preProc * mat * adjust * postProc; + WorldMatrixNoOverride = preProc * mat * postProc; + WorldMatrixPreserveFlip = preProc * mat; + } + + private static float Eval(AnimationCurve curve, float time, float fallback = 0f) + { + return curve?.Evaluate(time) ?? fallback; + } + + public readonly override string ToString() + { + if (Part == null) + return ""; + + return $"[{Time:F2}s] {Part.Name}"; + } +} + +public class AnimPartOverrideData +{ + public Texture2D Texture; + public Material Material; + public bool PreventDraw; + public Vector2 LocalOffset; + public float LocalRotation; + public Vector2 LocalScaleFactor = Vector2.one; + public Color ColorTint = Color.white; + public Color ColorOverride; + public bool FlipX, FlipY; + public bool UseMPB = true; + public bool UseDefaultTransparentMaterial; + public PartRenderer CustomRenderer; + public object UserData; + public ItemTweakData TweakData; + public Thing Weapon; +} + +[Serializable] +public class SpaceRequirement +{ + #region Static functions + + public static IEnumerable GetAllMustBeClear(IEnumerable reqs) + { + foreach (var req in reqs) + { + if (req != null && req.Type == RequirementType.MustBeClear) + yield return req; + } + } + + public static (SpaceRequirement start, SpaceRequirement end) GetPawnPositions(IEnumerable reqs, int pawnIndex) + { + SpaceRequirement start = null; + SpaceRequirement end = null; + + foreach (var req in reqs) + { + if (req == null) + continue; + + if (req.Type == RequirementType.PawnStart && req.PawnIndex == pawnIndex) + start = req; + if (req.Type == RequirementType.PawnEnd && req.PawnIndex == pawnIndex) + end = req; + } + + start ??= end; + end ??= start; + + return (start, end); + } + + public static void Write(SpaceRequirement space, BinaryWriter writer) + { + writer.Write((byte)space.Type); + writer.Write(space.PawnIndex); + writer.Write((sbyte)space.Area.xMin); + writer.Write((sbyte)space.Area.yMin); + writer.Write((byte)space.Area.width); + writer.Write((byte)space.Area.height); + } + + public static SpaceRequirement Read(BinaryReader reader) + { + var type = (RequirementType)reader.ReadByte(); + var index = reader.ReadByte(); + int x = reader.ReadSByte(); + int y = reader.ReadSByte(); + int w = reader.ReadByte(); + int h = reader.ReadByte(); + + return new SpaceRequirement() + { + Type = type, + PawnIndex = index, + Area = new RectInt(x, y, w, h) + }; + } + + #endregion + + public enum RequirementType + { + MustBeClear, + PawnStart, + PawnEnd + } + + public int CellCount => Mathf.Abs(Area.width * Area.height); + public RectInt Area; + public RequirementType Type; + public byte PawnIndex; + + public void SetPoint(int x, int z) + { + Area = new RectInt(new Vector2Int(x, z), new Vector2Int(1, 1)); + } + + public (int x, int z) GetCell(bool fx, bool fy) + { + return GetCells(fx, fy).FirstOrDefault(); + } + + public IEnumerable<(int x, int z)> GetCells(bool fx, bool fy) + { + foreach (var cell in Area.allPositionsWithin) + { + var raw = Resolve(cell, fx, fy); + yield return (raw.x, raw.y); + } + } + + private Vector2Int Resolve(Vector2Int vector, bool fx, bool fy) + { + if (fx) + vector.x = -vector.x; + + if (fy) + vector.y = -vector.y; + + return vector; + } + + public void DrawGizmos() + { + if (CellCount == 0) + return; + + Color c = Type switch + { + RequirementType.MustBeClear => Color.cyan, + RequirementType.PawnStart => Color.green, + RequirementType.PawnEnd => Color.red, + _ => Color.white + }; + float shrink = Type switch + { + RequirementType.MustBeClear => 0f, + RequirementType.PawnStart => 0.1f, + RequirementType.PawnEnd => 0.2f, + _ => 1f + }; + + var center = new Vector3(Area.center.x - 0.5f, 0f, Area.center.y - 0.5f); + var size = new Vector3(Area.size.x - shrink, 0f, Area.size.y - shrink); + Gizmos.color = c; + Gizmos.DrawWireCube(center, size); + } +} + +public class SweepPointCollection +{ + public int Count => points?.Length ?? 0; + private readonly SweepPoint[] points; + + public SweepPointCollection(SweepPoint[] points) + { + this.points = points; + } + + public SweepPoint[] CloneWithVelocities(float downDst, float upDst) + { + SweepPoint[] clone = new SweepPoint[points.Length]; + Array.Copy(points, clone, points.Length); + + Vector3 prevDown = default; + Vector3 prevUp = default; + float prevTime = 0; + + for (int i = 0; i < clone.Length; i++) + { + if(i != 0) + clone[i].SetVelocity(downDst, upDst, prevDown, prevUp, prevTime); + + clone[i].GetEndPoints(downDst, upDst, out prevDown, out prevUp); + prevTime = clone[i].Time; + } + + return clone; + } +} diff --git a/Source/AnimationMod/Data/Model/AnimDataModel.cs b/Source/1.4/ThingGenerator/Data/Model/AnimDataModel.cs similarity index 100% rename from Source/AnimationMod/Data/Model/AnimDataModel.cs rename to Source/1.4/ThingGenerator/Data/Model/AnimDataModel.cs diff --git a/Source/AnimationMod/Data/Model/AnimPartModel.cs b/Source/1.4/ThingGenerator/Data/Model/AnimPartModel.cs similarity index 100% rename from Source/AnimationMod/Data/Model/AnimPartModel.cs rename to Source/1.4/ThingGenerator/Data/Model/AnimPartModel.cs diff --git a/Source/AnimationMod/Data/Model/CurveModel.cs b/Source/1.4/ThingGenerator/Data/Model/CurveModel.cs similarity index 100% rename from Source/AnimationMod/Data/Model/CurveModel.cs rename to Source/1.4/ThingGenerator/Data/Model/CurveModel.cs diff --git a/Source/AnimationMod/Data/Model/EventModel.cs b/Source/1.4/ThingGenerator/Data/Model/EventModel.cs similarity index 100% rename from Source/AnimationMod/Data/Model/EventModel.cs rename to Source/1.4/ThingGenerator/Data/Model/EventModel.cs diff --git a/Source/AnimationMod/Data/Model/SweepPoint.cs b/Source/1.4/ThingGenerator/Data/Model/SweepPoint.cs similarity index 100% rename from Source/AnimationMod/Data/Model/SweepPoint.cs rename to Source/1.4/ThingGenerator/Data/Model/SweepPoint.cs diff --git a/Source/AnimationMod/Events/AudioEvent.cs b/Source/1.4/ThingGenerator/Events/AudioEvent.cs similarity index 100% rename from Source/AnimationMod/Events/AudioEvent.cs rename to Source/1.4/ThingGenerator/Events/AudioEvent.cs diff --git a/Source/AnimationMod/Events/CamShakeEvent.cs b/Source/1.4/ThingGenerator/Events/CamShakeEvent.cs similarity index 100% rename from Source/AnimationMod/Events/CamShakeEvent.cs rename to Source/1.4/ThingGenerator/Events/CamShakeEvent.cs diff --git a/Source/AnimationMod/Events/ClashAudioEvent.cs b/Source/1.4/ThingGenerator/Events/ClashAudioEvent.cs similarity index 100% rename from Source/AnimationMod/Events/ClashAudioEvent.cs rename to Source/1.4/ThingGenerator/Events/ClashAudioEvent.cs diff --git a/Source/AnimationMod/Events/DamageEffectEvent.cs b/Source/1.4/ThingGenerator/Events/DamageEffectEvent.cs similarity index 100% rename from Source/AnimationMod/Events/DamageEffectEvent.cs rename to Source/1.4/ThingGenerator/Events/DamageEffectEvent.cs diff --git a/Source/AnimationMod/Events/DuelEvent.cs b/Source/1.4/ThingGenerator/Events/DuelEvent.cs similarity index 100% rename from Source/AnimationMod/Events/DuelEvent.cs rename to Source/1.4/ThingGenerator/Events/DuelEvent.cs diff --git a/Source/AnimationMod/Events/EventBase.cs b/Source/1.4/ThingGenerator/Events/EventBase.cs similarity index 100% rename from Source/AnimationMod/Events/EventBase.cs rename to Source/1.4/ThingGenerator/Events/EventBase.cs diff --git a/Source/AnimationMod/Events/EventHelper.cs b/Source/1.4/ThingGenerator/Events/EventHelper.cs similarity index 100% rename from Source/AnimationMod/Events/EventHelper.cs rename to Source/1.4/ThingGenerator/Events/EventHelper.cs diff --git a/Source/AnimationMod/Events/GoreSplashEvent.cs b/Source/1.4/ThingGenerator/Events/GoreSplashEvent.cs similarity index 100% rename from Source/AnimationMod/Events/GoreSplashEvent.cs rename to Source/1.4/ThingGenerator/Events/GoreSplashEvent.cs diff --git a/Source/AnimationMod/Events/KillPawnEvent.cs b/Source/1.4/ThingGenerator/Events/KillPawnEvent.cs similarity index 100% rename from Source/AnimationMod/Events/KillPawnEvent.cs rename to Source/1.4/ThingGenerator/Events/KillPawnEvent.cs diff --git a/Source/AnimationMod/Events/MoteEvent.cs b/Source/1.4/ThingGenerator/Events/MoteEvent.cs similarity index 100% rename from Source/AnimationMod/Events/MoteEvent.cs rename to Source/1.4/ThingGenerator/Events/MoteEvent.cs diff --git a/Source/AnimationMod/Events/PuntPawnEvent.cs b/Source/1.4/ThingGenerator/Events/PuntPawnEvent.cs similarity index 100% rename from Source/AnimationMod/Events/PuntPawnEvent.cs rename to Source/1.4/ThingGenerator/Events/PuntPawnEvent.cs diff --git a/Source/AnimationMod/Events/TextMoteEvent.cs b/Source/1.4/ThingGenerator/Events/TextMoteEvent.cs similarity index 100% rename from Source/AnimationMod/Events/TextMoteEvent.cs rename to Source/1.4/ThingGenerator/Events/TextMoteEvent.cs diff --git a/Source/AnimationMod/Events/Workers/AnimEventInput.cs b/Source/1.4/ThingGenerator/Events/Workers/AnimEventInput.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/AnimEventInput.cs rename to Source/1.4/ThingGenerator/Events/Workers/AnimEventInput.cs diff --git a/Source/AnimationMod/Events/Workers/AudioWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/AudioWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/AudioWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/AudioWorker.cs diff --git a/Source/AnimationMod/Events/Workers/ClashAudioWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/ClashAudioWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/ClashAudioWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/ClashAudioWorker.cs diff --git a/Source/AnimationMod/Events/Workers/DamageEffectWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/DamageEffectWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/DamageEffectWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/DamageEffectWorker.cs diff --git a/Source/AnimationMod/Events/Workers/DuelSectionWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/DuelSectionWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/DuelSectionWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/DuelSectionWorker.cs diff --git a/Source/AnimationMod/Events/Workers/EventWorkerBase.cs b/Source/1.4/ThingGenerator/Events/Workers/EventWorkerBase.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/EventWorkerBase.cs rename to Source/1.4/ThingGenerator/Events/Workers/EventWorkerBase.cs diff --git a/Source/AnimationMod/Events/Workers/GoreSplashWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/GoreSplashWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/GoreSplashWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/GoreSplashWorker.cs diff --git a/Source/AnimationMod/Events/Workers/KillPawnWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/KillPawnWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/KillPawnWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/KillPawnWorker.cs diff --git a/Source/AnimationMod/Events/Workers/MoteWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/MoteWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/MoteWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/MoteWorker.cs diff --git a/Source/AnimationMod/Events/Workers/PuntPawnWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/PuntPawnWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/PuntPawnWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/PuntPawnWorker.cs diff --git a/Source/AnimationMod/Events/Workers/TextMoteWorker.cs b/Source/1.4/ThingGenerator/Events/Workers/TextMoteWorker.cs similarity index 100% rename from Source/AnimationMod/Events/Workers/TextMoteWorker.cs rename to Source/1.4/ThingGenerator/Events/Workers/TextMoteWorker.cs diff --git a/Source/AnimationMod/ExecutionOutcome.cs b/Source/1.4/ThingGenerator/ExecutionOutcome.cs similarity index 100% rename from Source/AnimationMod/ExecutionOutcome.cs rename to Source/1.4/ThingGenerator/ExecutionOutcome.cs diff --git a/Source/1.4/ThingGenerator/Extensions.cs b/Source/1.4/ThingGenerator/Extensions.cs new file mode 100644 index 00000000..9206c740 --- /dev/null +++ b/Source/1.4/ThingGenerator/Extensions.cs @@ -0,0 +1,321 @@ +using AM.Events; +using AM.Events.Workers; +using AM.Idle; +using AM.PawnData; +using AM.Tweaks; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using UnityEngine; +using Verse; +using Verse.AI; + +namespace AM; + +public static class Extensions +{ + /// + /// Shorthand for .Translate(). + /// + public static TaggedString Trs(this string str) => str.Translate(); + + /// + /// Shorthand for .Translate(). + /// + public static TaggedString Trs(this string str, params NamedArgument[] args) => TranslatorFormattedStringExtensions.Translate(str, args); + + public static AnimationManager GetAnimManager(this Map map) + => map?.GetComponent(); + + public static AnimationManager GetAnimManager(this Pawn pawn) + => pawn?.Map?.GetComponent(); + + public static Matrix4x4 MakeAnimationMatrix(this Pawn pawn, float yOffset = 0) + => Matrix4x4.TRS(pawn.Position.ToVector3ShiftedWithAltitude(pawn.DrawPos.y + yOffset), Quaternion.identity, Vector3.one); + + public static Matrix4x4 MakeAnimationMatrix(this in LocalTargetInfo target) + => Matrix4x4.TRS(new Vector3(target.CenterVector3.x, AltitudeLayer.Pawn.AltitudeFor(), target.CenterVector3.z), Quaternion.identity, Vector3.one); + + public static bool IsInAnimation(this Pawn pawn) + => AnimRenderer.TryGetAnimator(pawn) != null; + + public static bool IsInAnimation(this Pawn pawn, out AnimRenderer animRenderer) + => (animRenderer = AnimRenderer.TryGetAnimator(pawn)) != null; + + public static bool IsInActiveMeleeCombat(this Pawn pawn) + => pawn.jobs?.curDriver is JobDriver_AttackMelee or JobDriver_AttackStatic; + + public static AnimRenderer TryGetAnimator(this Pawn pawn) => AnimRenderer.TryGetAnimator(pawn); + + public static T AsDefOfType(this string defName, T fallback = null) where T : Def + { + if (string.IsNullOrWhiteSpace(defName)) + return fallback; + + return (T)GenGeneric.InvokeStaticMethodOnGenericType(typeof(DefDatabase<>), typeof(T), "GetNamed", defName, true) ?? fallback; + } + + /// + /// Returns true if is true OR this weapon has been manually marked as a melee weapon + /// by this mod. + /// + public static bool IsMeleeWeapon(this ThingDef def) => def.IsMeleeWeapon || Core.ForceConsiderTheseMeleeWeapons.Contains(def); + + public static float ToAngleFlatNew(this in Vector3 vector) => Mathf.Atan2(vector.z, vector.x) * Mathf.Rad2Deg; + + public static bool Polarity(this float f) => f > 0; + + public static WeaponCat ToCategory(this MeleeWeaponType type) + { + WeaponCat cat = 0; + + if (type.HasFlag(MeleeWeaponType.Long_Sharp) || type.HasFlag(MeleeWeaponType.Short_Sharp)) + cat |= WeaponCat.Sharp; + + if (type.HasFlag(MeleeWeaponType.Long_Stab) || type.HasFlag(MeleeWeaponType.Short_Stab)) + cat |= WeaponCat.Stab; + + if (type.HasFlag(MeleeWeaponType.Long_Blunt) || type.HasFlag(MeleeWeaponType.Short_Blunt)) + cat |= WeaponCat.Blunt; + + return cat; + } + + public static BodyPartRecord TryGetPartFromDef(this Pawn pawn, BodyPartDef def) + { + if (def == null) + return null; + if (pawn?.health?.hediffSet == null) + return null; + + foreach (BodyPartRecord bodyPartRecord in pawn.health.hediffSet.GetNotMissingParts()) + { + if (bodyPartRecord.def == def) + return bodyPartRecord; + } + return null; + } + + public static bool IsAttack(this IdleType type) => type is IdleType.AttackHorizontal or IdleType.AttackSouth or IdleType.AttackNorth; + + public static bool IsMove(this IdleType type) => type is IdleType.MoveVertical or IdleType.MoveHorizontal; + + public static bool IsIdle(this IdleType type, bool includeFlavour = true) => type is IdleType.Idle || (includeFlavour && type is IdleType.Flavour); + + /// + /// Attempts to get the equipped melee weapon of this pawn. + /// That includes sidearms if SimpleSidearms is installed. + /// This will only return the melee weapon if said melee weapon is compatible with the animation mod (i.e. it has valid tweak data). + /// + public static ThingWithComps GetFirstMeleeWeapon(this Pawn pawn) => GetFirstMeleeWeapon(pawn, out _); + + /// + /// Attempts to get the equipped melee weapon of this pawn. + /// That includes sidearms if SimpleSidearms is installed. + /// This will only return the melee weapon if said melee weapon is compatible with the animation mod (i.e. it has valid tweak data). + /// + public static ThingWithComps GetFirstMeleeWeapon(this Pawn pawn, out ItemTweakData tweakData) + { + tweakData = null; + if (pawn?.equipment == null) + return null; + + if (pawn.equipment.Primary?.def.IsMeleeWeapon() ?? false) + { + tweakData = TweakDataManager.TryGetTweak(pawn.equipment.Primary.def); + if (tweakData != null) + return pawn.equipment.Primary; + } + + foreach(var item in pawn.equipment.AllEquipmentListForReading) + { + if (item.def.IsMeleeWeapon()) + { + tweakData = TweakDataManager.TryGetTweak(item.def); + if (tweakData != null) + return item; + } + } + + if (Core.IsSimpleSidearmsActive && pawn.inventory?.innerContainer != null) + { + foreach (var item in pawn.inventory.innerContainer) + { + if (item is ThingWithComps twc && item.def.IsMeleeWeapon()) + { + tweakData = TweakDataManager.TryGetTweak(item.def); + if (tweakData != null) + return twc; + } + } + } + + return null; + } + + public static ThingWithComps TryGetLasso(this Pawn pawn) + { + if (pawn?.apparel == null) + return null; + + foreach (var item in pawn.apparel.WornApparel) + { + if (item.def.IsApparel && Content.LassoDefs.Contains(item.def)) + { + return item; + } + } + + return null; + } + + public static PawnMeleeData GetMeleeData(this Pawn pawn) => GameComp.Current?.GetOrCreateData(pawn); + + public static Vector3 ToWorld(this in Vector2 flatVector, float altitude = 0) => new(flatVector.x, altitude, flatVector.y); + + public static Vector2 ToFlat(this in Vector3 worldVector) => new Vector3(worldVector.x, worldVector.z); + + public static T GetWorker(this EventBase e) where T : EventWorkerBase => EventWorkerBase.GetWorker(e.EventID) as T; + + public static float RandomInRange(this in Vector2 range) => Rand.Range(range.x, range.y); + + public static T RandomElementByWeightExcept(this IEnumerable items, Func weight, ICollection except) where T : class + { + if (items.Count() == except.Count) + return null; + + for (int i = 0; i < 1000; i++) + { + var selected = items.RandomElementByWeightWithFallback(weight); + if (selected == null) + return null; + + if (except.Contains(selected)) + continue; + + return selected; + } + + Core.Error("Ran out of iterations selecting random."); + return null; + } + + public static Vector3 AngleToWorldDir(this float angleDeg) => -new Vector3(Mathf.Cos(angleDeg * Mathf.Deg2Rad), 0f, Mathf.Sin(angleDeg * Mathf.Deg2Rad)); + + public static ItemTweakData TryGetTweakData(this Thing weapon) => TweakDataManager.TryGetTweak(weapon.def); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetOccupiedMaskBit(this uint mask, int x, int z) => (((uint)1 << (x + 1) + (z + 1) * 3) & mask) != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetOccupiedMaskBitX(this uint mask, int x) => (((uint)1 << (x + 1) + 3) & mask) != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetOccupiedMaskBitZ(this uint mask, int z) => (((uint)1 << 1 + (z + 1) * 3) & mask) != 0; + + [DebugAction("Melee Animation", "Spawn all melee weapons (tiny)", actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static void GimmeMeleeWeaponsTiny() => GimmeMeleeWeapons(WeaponSize.Tiny); + + [DebugAction("Melee Animation", "Spawn all melee weapons (medium)", actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static void GimmeMeleeWeaponsMedium() => GimmeMeleeWeapons(WeaponSize.Medium); + + [DebugAction("Melee Animation", "Spawn all melee weapons (colossal)", actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static void GimmeMeleeWeaponsColossal() => GimmeMeleeWeapons(WeaponSize.Colossal); + + [DebugAction("Melee Animation", "Spawn all melee weapons", actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static void GimmeMeleeWeaponsAll() => GimmeMeleeWeapons(null); + + [DebugAction("Melee Animation", "Give all selected melee weapons", actionType = DebugActionType.Action, allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static void GiveAllMeleeWeapons() + { + var pawns = Find.Selector.SelectedPawns.Where(p => p.equipment.Primary == null); + + IEnumerable GetWeaponDefs(WeaponSize? onlySize) + { + foreach (var def in DefDatabase.AllDefsListForReading) + { + var tweak = TweakDataManager.TryGetTweak(def); + if (!def.IsMeleeWeapon() || tweak == null) + continue; + + if (onlySize != null) + { + var cat = IdleClassifier.Classify(tweak); + if (cat.size != onlySize.Value) + continue; + } + + yield return def; + } + } + + foreach (var pawn in pawns) + { + var weapon = GetWeaponDefs(null).RandomElementWithFallback(); + if (weapon == null) + continue; + + var spawned = ThingMaker.MakeThing(weapon) as ThingWithComps; + if (spawned == null) + continue; + + pawn.equipment.Primary = spawned; + } + } + + private static void GimmeMeleeWeapons(WeaponSize? onlySize) + { + var pos = Verse.UI.MouseCell(); + foreach (var def in DefDatabase.AllDefsListForReading) + { + var tweak = TweakDataManager.TryGetTweak(def); + if (!def.IsMeleeWeapon() || tweak == null) + continue; + + if (onlySize != null) + { + var cat = IdleClassifier.Classify(tweak); + if (cat.size != onlySize.Value) + continue; + } + + try + { + DebugThingPlaceHelper.DebugSpawn(def, pos, 1, false); + } + catch (Exception e) + { + Core.Warn($"Failed to spawn {def}: [{e.GetType().Name}] {e.Message}"); + } + } + } + + [DebugAction("Melee Animation", "Spawn army", actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] + private static List SpawnArmy() + { + List list = new List(); + foreach (PawnKindDef localKindDef2 in from kd in DefDatabase.AllDefs + orderby kd.defName + select kd) + { + PawnKindDef localKindDef = localKindDef2; + list.Add(new DebugActionNode(localKindDef.defName, DebugActionType.ToolMap, null, null) + { + category = DebugToolsSpawning.GetCategoryForPawnKind(localKindDef), + action = delegate () + { + Faction faction = FactionUtility.DefaultFactionFrom(localKindDef.defaultFactionType); + for (int i = 0; i < 50; i++) + { + Pawn pawn = PawnGenerator.GeneratePawn(localKindDef, faction); + GenSpawn.Spawn(pawn, Verse.UI.MouseCell(), Find.CurrentMap, WipeMode.Vanish); + DebugToolsSpawning.PostPawnSpawn(pawn); + } + } + }); + } + return list; + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/GameComp.cs b/Source/1.4/ThingGenerator/GameComp.cs new file mode 100644 index 00000000..eb1ed240 --- /dev/null +++ b/Source/1.4/ThingGenerator/GameComp.cs @@ -0,0 +1,185 @@ +using AM.Grappling; +using AM.Heads; +using AM.Idle; +using AM.Patches; +using AM.PawnData; +using AM.UI; +using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using Verse; +using Object = UnityEngine.Object; + +namespace AM; + +[UsedImplicitly] +public class GameComp : GameComponent +{ + public static GameComp Current; + public static event Action LazyTick; + public static ulong FrameCounter; + [TweakValue("Melee Animation")] + [UsedImplicitly] +#pragma warning disable CS0649 // Field 'GameComp.drawTextureExtractor' is never assigned to, and will always have its default value false + private static bool drawTextureExtractor; +#pragma warning restore CS0649 // Field 'GameComp.drawTextureExtractor' is never assigned to, and will always have its default value false + + public readonly Game Game; + + private string texPath; + private readonly Dictionary pawnMeleeData = new Dictionary(); + private List allMeleeData = new List(); + + public GameComp(Game game) + { + this.Game = game; + Current = this; + } + + public override void ExposeData() + { + base.ExposeData(); + + if (Scribe.mode == LoadSaveMode.Saving) + { + allMeleeData.RemoveAll(d => !d.ShouldSave()); + } + + Scribe_Collections.Look(ref allMeleeData, "pawnMeleeData", LookMode.Deep); + + allMeleeData ??= new List(); + + if (Scribe.mode != LoadSaveMode.PostLoadInit) + return; + + pawnMeleeData.Clear(); + foreach (var data in allMeleeData) + { + if (!data.ShouldSave()) + continue; + + if (pawnMeleeData.ContainsKey(data.Pawn)) + { + // Adding this check because a user reported this exact error. + // No idea how they managed that. Save editing? + Core.Error("Duplicate pawn data (or data with same pawn!) found when loading!"); + continue; + } + + pawnMeleeData.Add(data.Pawn, data); + } + } + + public PawnMeleeData GetOrCreateData(Pawn pawn) + { + if (pawn == null || pawn.Destroyed) + return null; + + if (pawnMeleeData.TryGetValue(pawn, out var found)) + return found; + + var created = new PawnMeleeData + { + Pawn = pawn + }; + allMeleeData.Add(created); + pawnMeleeData.Add(pawn, created); + return created; + } + + public override void GameComponentTick() + { + if (Find.TickManager.TicksGame % 600 == 0 && LazyTick != null) + { + try + { + LazyTick(); + } + catch (Exception e) + { + Core.Error("Exception during lazy tick:", e); + } + } + + IdleControllerComp.TotalTickTimeMS = 0; + IdleControllerComp.TotalActive = 0; + + base.GameComponentTick(); + + AnimRenderer.TickAll(); + AnimRenderer.RemoveDestroyed(); + GrabUtility.Tick(); + + Patch_Corpse_DrawAt.Tick(); + Patch_PawnRenderer_LayingFacing.Tick(); + + const float DT = 1 / 60f; + + foreach (var data in allMeleeData) + { + data.TimeSinceExecuted += DT; + data.TimeSinceGrappled += DT; + data.TimeSinceFriendlyDueled += DT; + } + } + + public override void GameComponentUpdate() + { + base.GameComponentUpdate(); + FrameCounter++; + } + + public override void GameComponentOnGUI() + { + //GUILayout.Label($"Mem: {System.GC.GetTotalMemory(false)/(1024f*1024f):F1} MB"); + + if (Prefs.DevMode && Dialog_AnimationDebugger.IsInRehearsalMode) + { + GUILayout.Space(100); + GUILayout.Label("IN REHEARSAL MODE!"); + } + + if (!drawTextureExtractor) + return; + + GUILayout.Space(100); + + texPath ??= ""; + texPath = GUILayout.TextField(texPath); + var tex = ContentFinder.Get(texPath, false); + + if (tex == null) + return; + + GUILayout.Box(tex); + + if (!GUILayout.Button("Save")) + return; + + RenderTexture renderTex = RenderTexture.GetTemporary( + tex.width, + tex.height, + 0, + RenderTextureFormat.Default, + RenderTextureReadWrite.Linear); + + Graphics.Blit(tex, renderTex); + RenderTexture previous = RenderTexture.active; + RenderTexture.active = renderTex; + Texture2D readableText = new(tex.width, tex.height); + readableText.ReadPixels(new Rect(0, 0, renderTex.width, renderTex.height), 0, 0); + readableText.Apply(); + RenderTexture.active = previous; + RenderTexture.ReleaseTemporary(renderTex); + + var pngBytes = readableText.EncodeToPNG(); + + Log.Message($"Writing {pngBytes.Length} bytes of {texPath} to Desktop ..."); + File.WriteAllBytes(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), $"{tex.name ?? "grab"}.png"), pngBytes); + + Object.Destroy(readableText); + Object.Destroy(renderTex); + } +} \ No newline at end of file diff --git a/Source/AnimationMod/Grappling/GrabUtility.cs b/Source/1.4/ThingGenerator/Grappling/GrabUtility.cs similarity index 100% rename from Source/AnimationMod/Grappling/GrabUtility.cs rename to Source/1.4/ThingGenerator/Grappling/GrabUtility.cs diff --git a/Source/1.4/ThingGenerator/Grappling/GrappleFlyer.cs b/Source/1.4/ThingGenerator/Grappling/GrappleFlyer.cs new file mode 100644 index 00000000..ded93c4a --- /dev/null +++ b/Source/1.4/ThingGenerator/Grappling/GrappleFlyer.cs @@ -0,0 +1,209 @@ +using System; +using RimWorld; +using UnityEngine; +using Verse; + +namespace AM.Grappling; + +/// +/// Closely based on Royalty's PawnJumper. +/// Look, I know Tynan doesn't like people taking DLC code and putting it in mods, but... +/// This is by far the most compatible and straightforward way of doing things. +/// There's no sense in re-inventing the wheel. +/// +public class GrappleFlyer : PawnFlyer +{ + public static GrappleFlyer MakeGrappleFlyer(Pawn grappler, Pawn victim, in IntVec3 targetPos) + { + if (victim.Position == targetPos || !victim.Spawned) + return null; + + if (grappler.Map == null) + { + // I can't fathom how this is possible, but it happened once and it's not cool. + Core.Error($"Null grappler ({grappler.LabelShortCap}) map!"); + return null; + } + + GrappleFlyer flyer = MakeFlyer(AM_DefOf.AM_GrappleFlyer, victim, targetPos, null, null) as GrappleFlyer; + if (flyer?.FlyingPawn != null) + { + victim.Rotation = Rot4.South; + flyer.Grappler = grappler; + GenSpawn.Spawn(flyer, targetPos, grappler.Map); + return flyer; + } + + Core.Warn("Base MakeFlyer method returned null."); + return null; + } + + private static readonly Func flightSpeed; + private static readonly Func flightCurveHeight = GenMath.InverseParabola; + private static readonly MaterialPropertyBlock mpb = new MaterialPropertyBlock(); + + public int TotalDurationTicks => ticksFlightTime; + public Pawn Grappler; + + private Material cachedShadowMaterial; + private Effecter flightEffecter = null; + private int positionLastComputedTick = -1; + private Vector3 groundPos; + private Vector3 effectivePos; + private float effectiveHeight; + + private Material ShadowMaterial + { + get + { + if (this.cachedShadowMaterial == null && !this.def.pawnFlyer.shadow.NullOrEmpty()) + { + this.cachedShadowMaterial = + MaterialPool.MatFrom(this.def.pawnFlyer.shadow, ShaderDatabase.Transparent); + } + + return this.cachedShadowMaterial; + } + } + + static GrappleFlyer() + { + AnimationCurve animationCurve = new(); + animationCurve.AddKey(0f, 0f); + animationCurve.AddKey(0.1f, 0.15f); + animationCurve.AddKey(1f, 1f); + flightSpeed = animationCurve.Evaluate; + } + + public override Vector3 DrawPos + { + get + { + this.RecomputePosition(); + return this.effectivePos; + } + } + + public override void SpawnSetup(Map map, bool respawningAfterLoad) + { + base.SpawnSetup(map, respawningAfterLoad); + if (respawningAfterLoad) + return; + + if (Grappler == null) + { + Core.Error("Null grappler, cannot determine flight speed factor."); + return; + } + + // Adjust flight time based on grapple speed. + float speed = Grappler.GetStatValue(AM_DefOf.AM_GrappleSpeed); + ticksFlightTime = Mathf.Max(2, (int)(ticksFlightTime / (speed * Core.Settings.GrappleSpeed))); + } + + private void RecomputePosition() + { + if (this.positionLastComputedTick == this.ticksFlying) + { + return; + } + + this.positionLastComputedTick = this.ticksFlying; + float arg = (float)this.ticksFlying / (float)this.ticksFlightTime; + float num = flightSpeed(arg); + this.effectiveHeight = flightCurveHeight(num) * Mathf.Clamp(ticksFlightTime / 60f * 0.5f, 0.3f, 2f); + this.groundPos = Vector3.Lerp(this.startVec, base.DestinationPos, num); + Vector3 a = new(0f, 0f, 2f); + Vector3 b = Altitudes.AltIncVect * this.effectiveHeight; + Vector3 b2 = a * this.effectiveHeight; + this.effectivePos = this.groundPos + b + b2; + } + + public override void DrawAt(Vector3 drawLoc, bool flip = false) + { + RecomputePosition(); + DrawShadow(groundPos, effectiveHeight); + FlyingPawn.DrawAt(effectivePos, flip); // For Pawns, flip does nothing, it's just an inherited param. + + Color ropeColor = Grappler?.TryGetLasso()?.def.graphicData.color ?? Color.magenta; + DrawBoundTexture(FlyingPawn, effectivePos, ropeColor); + DrawGrappleLine(ropeColor); + } + + public void DrawGrappleLine(Color ropeColor) + { + Vector3 from = Grappler.DrawPos; + from += Grappler.Rotation.AsVector2.ToWorld() * 0.4f; + from.y = AltitudeLayer.PawnUnused.AltitudeFor(); + + Vector3 to = effectivePos; + to.y = from.y; + + float bumpMag = Mathf.Clamp(flightCurveHeight(ticksFlying / 15f) * 1.25f, 0f, 1.25f); + float bumpMag2 = Mathf.Clamp(flightCurveHeight(ticksFlying / 10f) * 0.25f, 0f, 0.25f); + Vector3 bump = (Grappler.DrawPos - from).normalized; + Vector3 bump2 = Vector2.Perpendicular(to.ToFlat() - Grappler.DrawPos.ToFlat()).normalized.ToWorld(); + bump.y = 0; + from += bump * bumpMag + bump2 * bumpMag2; + + GrabUtility.DrawRopeFromTo(from, to, ropeColor); + } + + public static void DrawBoundTexture(Pawn pawn, Vector3 drawLoc, Color ropeColor) + { + drawLoc.y += 0.05f; + var tex = GrabUtility.GetBoundPawnTexture(pawn); + if (tex == null) + { + if (pawn.RaceProps.Humanlike) + tex = Content.BoundMaleRope; + else + return; + } + + var mat = AnimRenderer.DefaultCutout; + var trs = Matrix4x4.TRS(drawLoc, Quaternion.identity, Vector3.one * 1.5f * Core.GetBodyDrawSizeFactor(pawn)); + + mpb.SetTexture("_MainTex", tex); // TODO cache id. + mpb.SetColor("_Color", ropeColor); + + Graphics.DrawMesh(MeshPool.plane10, trs, mat, 0, null, 0, mpb); + } + + private void DrawShadow(Vector3 drawLoc, float height) + { + Material shadowMaterial = this.ShadowMaterial; + if (shadowMaterial == null) + { + return; + } + float num = Mathf.Lerp(1f, 0.6f, height); + Vector3 s = new(num, 1f, num); + Matrix4x4 matrix = default(Matrix4x4); + matrix.SetTRS(drawLoc, Quaternion.identity, s); + Graphics.DrawMesh(MeshPool.plane10, matrix, shadowMaterial, 0); + } + + public override void RespawnPawn() + { + FleckMaker.ThrowDustPuff(base.DestinationPos + Gen.RandomHorizontalVector(0.5f), base.Map, 2f); + base.RespawnPawn(); + } + + public override void ExposeData() + { + base.ExposeData(); + Scribe_References.Look(ref Grappler, "AM_grappler", true); + } + + public override void Destroy(DestroyMode mode = DestroyMode.Vanish) + { + Effecter effecter = this.flightEffecter; + if (effecter != null) + { + effecter.Cleanup(); + } + + base.Destroy(mode); + } +} \ No newline at end of file diff --git a/Source/AnimationMod/Grappling/JobDriver_GrapplePawn.cs b/Source/1.4/ThingGenerator/Grappling/JobDriver_GrapplePawn.cs similarity index 100% rename from Source/AnimationMod/Grappling/JobDriver_GrapplePawn.cs rename to Source/1.4/ThingGenerator/Grappling/JobDriver_GrapplePawn.cs diff --git a/Source/1.4/ThingGenerator/Grappling/KnockbackFlyer.cs b/Source/1.4/ThingGenerator/Grappling/KnockbackFlyer.cs new file mode 100644 index 00000000..cf548b3f --- /dev/null +++ b/Source/1.4/ThingGenerator/Grappling/KnockbackFlyer.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace AM.Grappling; + +public class KnockbackFlyer : PawnFlyer +{ + [DebugAction("Melee Animation", actionType = DebugActionType.ToolMapForPawns)] + private static void TestKnockback(Pawn victim) + { + var end = GetEndCell(victim, IntVec3.East, 20); + MakeKnockbackFlyer(victim, end); + } + + public static IntVec3 GetEndCell(Pawn victim, in IntVec3 direction, int maxRange) + { + var map = victim.Map; + IntVec3 end = victim.Position + direction * maxRange; + IntVec3 ret = victim.Position; + foreach (var cell in GetCellsFromTo(victim.Position, end)) + { + ret = cell; + if (IsSolid(victim, cell, map)) + break; + } + + return ret; + } + + public static KnockbackFlyer MakeKnockbackFlyer(Pawn victim, IntVec3 targetPos) + { + if (!victim.Spawned) + return null; + + var map = victim.Map; + var start = victim.DrawPos; + var end = targetPos.ToVector3ShiftedWithAltitude(start.y); + + KnockbackFlyer flyer = MakeFlyer(AM_DefOf.AM_KnockbackFlyer, victim, targetPos, EffecterDefOf.ConstructDirt, SoundDefOf.Pawn_Melee_Punch_HitBuilding) as KnockbackFlyer; + + if (flyer?.FlyingPawn != null) + { + flyer.StartPos = start; + flyer.EndPos = end; + victim.Rotation = flyer.GetPawnRotation(); + + // Important: clear victim job queue, which is normally the animation job. + // Failing to do this means that the animation job is referenced when saving + // but will not be found when loading. + // This normally hard crashes the game! + flyer.jobQueue = null; + + var t = GenSpawn.Spawn(flyer, targetPos, map); + if (t == null) + flyer.RespawnPawn(); + + return t != null ? flyer : null; + } + + return null; + } + + private static bool IsSolid(Pawn pawn, in IntVec3 cell, Map map) + { + if (cell.x < 0 || cell.z < 0 || cell.x >= map.info.Size.x || cell.z >= map.info.Size.z) + return true; + + var things = map.thingGrid.ThingsListAtFast(cell); + return Enumerable.Any(things, thing => thing.BlocksPawn(pawn)); + } + + private static IEnumerable GetCellsFromTo(IntVec3 from, IntVec3 to) + { + bool hor = from.x != to.x; + if (hor) + { + int dir = Mathf.Clamp(to.x - from.x, -1, 1); + for (int x = from.x + dir; x <= to.x; x += dir) + { + yield return new IntVec3(x, from.y, from.z); + } + } + else + { + int dir = Mathf.Clamp(to.z - from.z, -1, 1); + for (int z = from.z + dir; z <= to.z; z += dir) + { + yield return new IntVec3(from.x, from.y, z); + } + } + } + + public override Vector3 DrawPos => Vector3.Lerp(StartPos, EndPos, (float)ticksFlying / ticksFlightTime); + + public Vector3 StartPos, EndPos; + public Effecter FlightEffecter; + + public Rot4 GetPawnRotation() + { + float dx = EndPos.x - StartPos.x; + float dz = EndPos.z - StartPos.z; + + // Ungodly switch statement. + // ReSharper suggested it and I kind of like how it looks. + + return dx switch + { + < 0 => Rot4.East, + > 0 => Rot4.West, + _ => dz switch + { + < 0 => Rot4.North, + > 0 => Rot4.South, + _ => Rot4.South + } + }; + } + + public override void SpawnSetup(Map map, bool respawningAfterLoad) + { + base.SpawnSetup(map, respawningAfterLoad); + + if (!respawningAfterLoad) + { + ticksFlightTime = (int)((StartPos.ToFlat() - EndPos.ToFlat()).magnitude * 3f); + } + } + + public override void DrawAt(Vector3 drawLoc, bool flip = false) + { + FlyingPawn.DrawAt(drawLoc, flip); + } + + public override void RespawnPawn() + { + var p = FlyingPawn; + base.RespawnPawn(); + this.LandingEffects(); + p.Rotation = GetPawnRotation(); + p.stances.stunner.StunFor(30, null, false, true); + } + + public override void ExposeData() + { + base.ExposeData(); + + Scribe_Values.Look(ref StartPos, "startPos"); + Scribe_Values.Look(ref EndPos, "endPos"); + } + + public override void Tick() + { + base.Tick(); + + //if (FlightEffecter == null) + //{ + // //FlightEffecter = def.pawnFlyer.flightEffecterDef.Spawn(); + // FlightEffecter = EffecterDefOf..Spawn(); + // FlightEffecter.Trigger(new TargetInfo(DrawPos.ToIntVec3(), Map), TargetInfo.Invalid); + //} + //else + //{ + // for (int i = 0; i < 1; i++) + // { + // FlightEffecter.EffectTick(new TargetInfo(DrawPos.ToIntVec3(), Map), TargetInfo.Invalid); + // } + //} + + //FleckMaker.ThrowDustPuff(DrawPos, Map, 2f); + + var loc = DrawPos; + loc.z -= 0.4f; + loc.y = AltitudeLayer.FloorCoverings.AltitudeFor(); + var map = Map; + float scale = 0.6f; + + if (!loc.ShouldSpawnMotesAt(map)) + { + return; + } + FleckCreationData dataStatic = FleckMaker.GetDataStatic(loc, map, FleckDefOf.DustPuff, 1.9f * scale); + dataStatic.rotationRate = (float)Rand.Range(-60, 60); + dataStatic.velocityAngle = (float)Rand.Range(0, 360); + dataStatic.velocitySpeed = Rand.Range(0.6f, 0.75f); + map.flecks.CreateFleck(dataStatic); + } + + private void LandingEffects() + { +#if V13 + def.pawnFlyer.soundLanding.PlayOneShot(new TargetInfo(EndPos.ToIntVec3(), Map)); +#else + soundLanding.PlayOneShot(new TargetInfo(EndPos.ToIntVec3(), Map)); +#endif + + for (int i = 0; i < 5; i++) + { + FleckMaker.ThrowDustPuffThick(EndPos + Gen.RandomHorizontalVector(0.5f), Map, 2f, Color.grey); + FleckMaker.ThrowDustPuff(EndPos + Gen.RandomHorizontalVector(0.5f), Map, 2f); + } + } + + public override void Destroy(DestroyMode mode = DestroyMode.Vanish) + { + FlightEffecter?.Cleanup(); + base.Destroy(mode); + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/Heads/HeadInstance.cs b/Source/1.4/ThingGenerator/Heads/HeadInstance.cs new file mode 100644 index 00000000..9bf2415b --- /dev/null +++ b/Source/1.4/ThingGenerator/Heads/HeadInstance.cs @@ -0,0 +1,53 @@ +using AM.Patches; +using UnityEngine; +using Verse; + +namespace AM.Heads; + +/// +/// A decapitated head instance. +/// For simplicity and performance reasons, these are not serialized. +/// +public sealed class HeadInstance +{ + public Pawn Pawn { get; set; } + public Map Map { get; set; } + public Vector3 Position { get; set; } + public float Rotation { get; set; } + public float TimeToLive { get; set; } + public Rot4 Direction { get; set; } + + public bool Render() + { + if (Pawn is not { Dead: true }) + { + return false; + } + + TimeToLive -= Time.unscaledDeltaTime; + if (TimeToLive <= 0f) + return false; + + // Do not actually render if the map is not currently visible. + if (Map != Find.CurrentMap) + return true; + + //Render pawn in custom position using patches. + Patch_PawnRenderer_RenderPawnInternal.NextDrawMode = Patch_PawnRenderer_RenderPawnInternal.DrawMode.HeadStandalone; + Patch_PawnRenderer_RenderPawnInternal.HeadRotation = Direction; + Patch_PawnRenderer_RenderPawnInternal.StandaloneHeadRotation = Rotation; + Patch_PawnRenderer_DrawInvisibleShadow.Suppress = true; // In 1.4 shadow rendering is baked into RenderPawnAt and may need to be prevented. + Patch_PawnRenderer_RenderPawnInternal.AllowNext = true; + + try + { + Pawn.Drawer.renderer.RenderPawnAt(Position, Direction, true); + } + finally + { + Patch_PawnRenderer_RenderPawnInternal.NextDrawMode = Patch_PawnRenderer_RenderPawnInternal.DrawMode.Full; + Patch_PawnRenderer_DrawInvisibleShadow.Suppress = false; + } + return true; + } +} \ No newline at end of file diff --git a/Source/AnimationMod/Health/HediffCompProperties_SingleTendRemove.cs b/Source/1.4/ThingGenerator/Health/HediffCompProperties_SingleTendRemove.cs similarity index 100% rename from Source/AnimationMod/Health/HediffCompProperties_SingleTendRemove.cs rename to Source/1.4/ThingGenerator/Health/HediffCompProperties_SingleTendRemove.cs diff --git a/Source/AnimationMod/Health/HediffComp_SingleTendRemove.cs b/Source/1.4/ThingGenerator/Health/HediffComp_SingleTendRemove.cs similarity index 100% rename from Source/AnimationMod/Health/HediffComp_SingleTendRemove.cs rename to Source/1.4/ThingGenerator/Health/HediffComp_SingleTendRemove.cs diff --git a/Source/1.4/ThingGenerator/Idle/IdleClassifier.cs b/Source/1.4/ThingGenerator/Idle/IdleClassifier.cs new file mode 100644 index 00000000..65cbc01a --- /dev/null +++ b/Source/1.4/ThingGenerator/Idle/IdleClassifier.cs @@ -0,0 +1,72 @@ +using System.Linq; +using AM.Tweaks; +using UnityEngine; +using Verse; + +namespace AM.Idle; + +public static class IdleClassifier +{ + [TweakValue("Melee Animation", 0f, 8f)] + public static float ColossalSizeThreshold = 1.37f; + + [TweakValue("Melee Animation", 0f, 5f)] + public static float TinySizeThreshold = 0.63f; + + public static (WeaponSize size, WeaponCat category) Classify(ItemTweakData tweakData) + { + if (tweakData == null) + return default; + + // Take override from def into consideration. + var def = tweakData.GetDef(); + var overrideData = def != null && MeleeAnimationAdjustmentDef.AllWeaponAdjustments.TryGetValue(def, out var found) ? found : null; + WeaponSize? forceSize = overrideData?.overrideSize; + + float length = GetLength(tweakData); + bool isTiny = length <= TinySizeThreshold; + var cat = tweakData.MeleeWeaponType.ToCategory(); + + if (isTiny) + return (forceSize ?? WeaponSize.Tiny, cat); + + bool isColossal = length >= ColossalSizeThreshold; + return isColossal ? (forceSize ?? WeaponSize.Colossal, cat) : (forceSize ?? WeaponSize.Medium, cat); + } + + private static float GetLength(ItemTweakData tweakData) => Mathf.Max(tweakData.BladeLength, tweakData.MaxDistanceFromHand); + + private struct TableRow + { + public ThingDef Def; + public ItemTweakData Tweak; + public WeaponSize Size; + public WeaponCat Category; + } + + [DebugOutput("Melee Animation")] + private static void LogTextureCategories() + { + var data = from def in DefDatabase.AllDefsListForReading + where def.IsMeleeWeapon() + let tweak = TweakDataManager.TryGetTweak(def) + where tweak != null + let cat = Classify(tweak) + select new TableRow + { + Def = def, + Tweak = tweak, + Category = cat.category, + Size = cat.size + }; + + TableDataGetter[] table = new TableDataGetter[5]; + table[0] = new TableDataGetter("Def Name", row => row.Def.defName); + table[1] = new TableDataGetter("Name", row => row.Def.LabelCap); + table[2] = new TableDataGetter("Size", row => row.Size); + table[3] = new TableDataGetter("Category", row => row.Category); + table[4] = new TableDataGetter("Length", row => GetLength(row.Tweak).ToString("F3")); + + DebugTables.MakeTablesDialog(data, table); + } +} diff --git a/Source/AnimationMod/Idle/IdleControllerComp.cs b/Source/1.4/ThingGenerator/Idle/IdleControllerComp.cs similarity index 98% rename from Source/AnimationMod/Idle/IdleControllerComp.cs rename to Source/1.4/ThingGenerator/Idle/IdleControllerComp.cs index 1384f58f..970d818b 100644 --- a/Source/AnimationMod/Idle/IdleControllerComp.cs +++ b/Source/1.4/ThingGenerator/Idle/IdleControllerComp.cs @@ -9,7 +9,6 @@ using RimWorld; using UnityEngine; using Verse; -using LudeonTK; namespace AM.Idle; @@ -65,8 +64,8 @@ public void PreDraw() try { - // If the animation is about to draw, but it was just destroyed (by the animation ending) - // then immediately tick to get a new animation running before the frame is drawn. + // If the animation is about to draw but it was just destroyed (by the animation ending) + // the immediately tick to get a new animation running before the frame is drawn. // This prevents an annoying flicker. if (CurrentAnimation.IsDestroyed) { @@ -100,8 +99,7 @@ private bool ShouldBeActive(out Thing weapon) } // Vanilla checks. - bool vanillaShouldDraw = PawnRenderUtility.CarryWeaponOpenly(pawn); - + bool vanillaShouldDraw = pawn.drawer.renderer.CarryWeaponOpenly(); if (!vanillaShouldDraw) { // Sometimes the pawn will not be 'openly carrying' but will still be aiming their weapon, such as when casting psycasts. diff --git a/Source/AnimationMod/Idle/IdleType.cs b/Source/1.4/ThingGenerator/Idle/IdleType.cs similarity index 100% rename from Source/AnimationMod/Idle/IdleType.cs rename to Source/1.4/ThingGenerator/Idle/IdleType.cs diff --git a/Source/AnimationMod/Idle/WeaponCat.cs b/Source/1.4/ThingGenerator/Idle/WeaponCat.cs similarity index 100% rename from Source/AnimationMod/Idle/WeaponCat.cs rename to Source/1.4/ThingGenerator/Idle/WeaponCat.cs diff --git a/Source/AnimationMod/Idle/WeaponSize.cs b/Source/1.4/ThingGenerator/Idle/WeaponSize.cs similarity index 100% rename from Source/AnimationMod/Idle/WeaponSize.cs rename to Source/1.4/ThingGenerator/Idle/WeaponSize.cs diff --git a/Source/AnimationMod/Jobs/IDuelEndNotificationReceiver.cs b/Source/1.4/ThingGenerator/Jobs/IDuelEndNotificationReceiver.cs similarity index 100% rename from Source/AnimationMod/Jobs/IDuelEndNotificationReceiver.cs rename to Source/1.4/ThingGenerator/Jobs/IDuelEndNotificationReceiver.cs diff --git a/Source/AnimationMod/Jobs/JobDriver_ChannelAnimation.cs b/Source/1.4/ThingGenerator/Jobs/JobDriver_ChannelAnimation.cs similarity index 100% rename from Source/AnimationMod/Jobs/JobDriver_ChannelAnimation.cs rename to Source/1.4/ThingGenerator/Jobs/JobDriver_ChannelAnimation.cs diff --git a/Source/AnimationMod/Jobs/JobDriver_DoAnimation.cs b/Source/1.4/ThingGenerator/Jobs/JobDriver_DoAnimation.cs similarity index 100% rename from Source/AnimationMod/Jobs/JobDriver_DoAnimation.cs rename to Source/1.4/ThingGenerator/Jobs/JobDriver_DoAnimation.cs diff --git a/Source/AnimationMod/Jobs/JobDriver_DoFriendlyDuel.cs b/Source/1.4/ThingGenerator/Jobs/JobDriver_DoFriendlyDuel.cs similarity index 100% rename from Source/AnimationMod/Jobs/JobDriver_DoFriendlyDuel.cs rename to Source/1.4/ThingGenerator/Jobs/JobDriver_DoFriendlyDuel.cs diff --git a/Source/AnimationMod/Jobs/JobDriver_GoToAnimationSpot.cs b/Source/1.4/ThingGenerator/Jobs/JobDriver_GoToAnimationSpot.cs similarity index 100% rename from Source/AnimationMod/Jobs/JobDriver_GoToAnimationSpot.cs rename to Source/1.4/ThingGenerator/Jobs/JobDriver_GoToAnimationSpot.cs diff --git a/Source/AnimationMod/Jobs/JobDriver_GoToExecutionSpot.cs b/Source/1.4/ThingGenerator/Jobs/JobDriver_GoToExecutionSpot.cs similarity index 100% rename from Source/AnimationMod/Jobs/JobDriver_GoToExecutionSpot.cs rename to Source/1.4/ThingGenerator/Jobs/JobDriver_GoToExecutionSpot.cs diff --git a/Source/AnimationMod/Jobs/JobDriver_SpectateDuel.cs b/Source/1.4/ThingGenerator/Jobs/JobDriver_SpectateDuel.cs similarity index 100% rename from Source/AnimationMod/Jobs/JobDriver_SpectateDuel.cs rename to Source/1.4/ThingGenerator/Jobs/JobDriver_SpectateDuel.cs diff --git a/Source/AnimationMod/Jobs/ToilUtils.cs b/Source/1.4/ThingGenerator/Jobs/ToilUtils.cs similarity index 100% rename from Source/AnimationMod/Jobs/ToilUtils.cs rename to Source/1.4/ThingGenerator/Jobs/ToilUtils.cs diff --git a/Source/AnimationMod/MeleeAnimationAdjustmentDef.cs b/Source/1.4/ThingGenerator/MeleeAnimationAdjustmentDef.cs similarity index 100% rename from Source/AnimationMod/MeleeAnimationAdjustmentDef.cs rename to Source/1.4/ThingGenerator/MeleeAnimationAdjustmentDef.cs diff --git a/Source/AnimationMod/Outcome/IOutcomeWorker.cs b/Source/1.4/ThingGenerator/Outcome/IOutcomeWorker.cs similarity index 100% rename from Source/AnimationMod/Outcome/IOutcomeWorker.cs rename to Source/1.4/ThingGenerator/Outcome/IOutcomeWorker.cs diff --git a/Source/1.4/ThingGenerator/Outcome/OutcomeUtility.cs b/Source/1.4/ThingGenerator/Outcome/OutcomeUtility.cs new file mode 100644 index 00000000..74b32110 --- /dev/null +++ b/Source/1.4/ThingGenerator/Outcome/OutcomeUtility.cs @@ -0,0 +1,554 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AM.Patches; +using JetBrains.Annotations; +using RimWorld; +using UnityEngine; +using Verse; + +namespace AM.Outcome; + +/// +/// Helper class to determine and then execute the outcome of duels and executions. +/// +public static class OutcomeUtility +{ + public static IOutcomeWorker OutcomeWorker = new VanillaOutcomeWorker(); + + public static readonly Func GreaterThan = (x, y) => x > y; + public static readonly Func LessThan = (x, y) => x < y; + + public static readonly Func Pen = a => a.ArmorPen; + public static readonly Func Dmg = a => a.Damage; + public static readonly Func PenXDmg = a => a.Damage * a.ArmorPen; + + [UsedImplicitly] + [TweakValue("Melee Animation")] +#pragma warning disable CS0649 // Field 'OutcomeUtility.debugLogExecutionOutcome' is never assigned to, and will always have its default value false + private static bool debugLogExecutionOutcome; +#pragma warning restore CS0649 // Field 'OutcomeUtility.debugLogExecutionOutcome' is never assigned to, and will always have its default value false + + public ref struct AdditionalArgs + { + public DamageDef DamageDef; + public BodyPartDef BodyPartDef; + public RulePackDef LogGenDef; + public ThingWithComps Weapon; + public float TargetDamageAmount; + } + + [DebugAction("Melee Animation", "Compare Lethality", allowedGameStates = AllowedGameStates.PlayingOnMap, actionType = DebugActionType.ToolMapForPawns)] + private static void CompareLethalityDebug(Pawn pawn) + { + Pawn selected = Find.Selector.SelectedPawns.FirstOrDefault(); + if (selected == null || selected == pawn) + { + Messages.Message("Select another pawn before using the debug tool.", MessageTypeDefOf.RejectInput, false); + return; + } + + Core.Log($"{selected}, {pawn}"); + + float a = selected.GetStatValue(AM_DefOf.AM_Lethality); + float a2 = selected.GetStatValue(AM_DefOf.AM_DuelAbility); + float b = pawn.GetStatValue(AM_DefOf.AM_Lethality); + float b2 = pawn.GetStatValue(AM_DefOf.AM_DuelAbility); + float chanceToBeat = ChanceToBeatInMelee(selected, pawn); + + Messages.Message($"{selected.LabelShortCap} lethality: {a:P1}", MessageTypeDefOf.NeutralEvent, false); + Messages.Message($"{selected.LabelShortCap} duel ability: {a2:P1}", MessageTypeDefOf.NeutralEvent, false); + Messages.Message($"{pawn.LabelShortCap} lethality: {b:P1}", MessageTypeDefOf.NeutralEvent, false); + Messages.Message($"{pawn.LabelShortCap} duel ability: {b2:P1}", MessageTypeDefOf.NeutralEvent, false); + Messages.Message($"{selected.LabelShortCap} vs {pawn.LabelShortCap}: {chanceToBeat:P1} to win.", MessageTypeDefOf.NeutralEvent, false); + + } + + /// + /// Performs the effects of on the target pawn. + /// + public static bool PerformOutcome(ExecutionOutcome outcome, Pawn attacker, Pawn target, in AdditionalArgs args) + { + if (target == null) + return false; + if (attacker == null) + return false; + if (target.Dead || !target.Spawned) + return false; + + switch (outcome) + { + case ExecutionOutcome.Nothing: + return true; + + case ExecutionOutcome.Failure: + return Failure(attacker, target, args); + + case ExecutionOutcome.Damage: + return Damage(attacker, target, args); + + case ExecutionOutcome.Down: + return Down(attacker, target, args); + + case ExecutionOutcome.Kill: + return Kill(attacker, target, args); + + default: + throw new ArgumentOutOfRangeException(nameof(outcome), outcome, null); + } + } + + /// + /// The percentage chance that has to beat in a duel or execution, based on the + /// Duel Ability stat. + /// + public static float ChanceToBeatInMelee(Pawn a, Pawn b) + { + var normal = Core.Settings.GetNormalDistribution(); + float aL = a.GetStatValue(AM_DefOf.AM_DuelAbility); + float bL = b.GetStatValue(AM_DefOf.AM_DuelAbility); + float diff = aL - bL; + + return Mathf.Clamp01((float)normal.LeftProbability(diff)); + } + + public static ExecutionOutcome GenerateRandomOutcome(Pawn attacker, Pawn victim, bool canFail, ProbabilityReport report = null) + { + // Get lethality, adjusted by settings. + float aL = attacker.GetStatValue(AM_DefOf.AM_Lethality); + int meleeSkill = attacker.skills?.GetSkill(SkillDefOf.Melee)?.Level ?? 0; + var weapon = attacker.GetFirstMeleeWeapon(); + var mainAttack = SelectMainAttack(OutcomeWorker.GetMeleeAttacksFor(weapon, attacker)); + var damageDef = mainAttack.DamageDef; + var corePart = GetCoreBodyPart(victim); + + return GenerateRandomOutcome(damageDef, victim, corePart, mainAttack.ArmorPen, meleeSkill, aL, canFail, report); + } + + /// + /// Generates a random execution outcome based on an lethality value. + /// + public static ExecutionOutcome GenerateRandomOutcome(DamageDef dmgDef, Pawn victim, BodyPartRecord bodyPart, float weaponPen, float attackerMeleeSkill, float lethality, bool canFail, ProbabilityReport report = null) + { + static void Log(string msg) + { + if (debugLogExecutionOutcome) + Core.Log(msg); + } + + var outcome = ExecutionOutcome.Nothing; + + // Before anything else: check for failure chance. + float chanceToFail = canFail ? Mathf.Clamp01(RemapClamped(0, 20, Core.Settings.ChanceToFailMinSkill, Core.Settings.ChanceToFailMaxSkill, attackerMeleeSkill)) : 0f; + Log($"Chance to fail: {chanceToFail:P1} based on melee skill {attackerMeleeSkill:F1}"); + bool willFail = Rand.Chance(chanceToFail); + if (willFail && report == null) + { + Log("Outcome: Failed."); + outcome = ExecutionOutcome.Failure; + return outcome; + } + if (report != null) + report.FailureChance = chanceToFail; + + // Armor calculations for down or kill. + var armorStat = dmgDef?.armorCategory?.armorRatingStat ?? StatDefOf.ArmorRating_Sharp; + Log($"Armor stat: {armorStat}, weapon pen: {weaponPen:F2}, lethality: {lethality:F2}"); + float armorMulti = Core.Settings.ExecutionArmorCoefficient; + float chanceToPen = OutcomeWorker.GetChanceToPenAprox(victim, bodyPart, armorStat, weaponPen); + Log($"Chance to pen (base): {chanceToPen:P1}"); + if (armorMulti > 0f) + chanceToPen /= armorMulti; + else + chanceToPen = 1f; + Log($"Chance to pen (post-settings): {chanceToPen:P1}"); + bool canPen = Rand.Chance(chanceToPen); + Log($"Random will pen outcome: {canPen}"); + + // Cap chance to pen to make percentage calculations work. + chanceToPen = Mathf.Clamp01(chanceToPen); + + // Calculate kill chance based on lethality and settings. + bool preventKill = Core.Settings.ExecutionsOnFriendliesAreNotLethal && (victim.IsColonist || victim.IsSlaveOfColony || victim.IsPrisonerOfColony || victim.Faction == Faction.OfPlayerSilentFail); + bool attemptKill = Rand.Chance(lethality); + Log($"Prevent kill: {preventKill}"); + Log($"Attempt kill: {attemptKill} (from random lethality chance)"); + + // Cap lethality to 100% because otherwise it messes up percentage calculations. + lethality = Mathf.Clamp01(lethality); + + float preventKillCoef = preventKill ? 0f : 1f; + if (report != null) + report.KillChance = preventKillCoef * chanceToPen * lethality * (1 - chanceToFail); // Absolute chance to kill. + + if (canPen && !preventKill && attemptKill) + { + Log("Killed!"); + outcome = ExecutionOutcome.Kill; + if (report == null) + return outcome; + } + + Log("Moving on to down or injure..."); + float downChance = RemapClamped(4, 20, 0.2f, 0.9f, attackerMeleeSkill); + Log(canPen ? $"Chance to down, based on melee skill of {attackerMeleeSkill:N1}: {downChance:P1}" : "Cannot down, pen chance failed. Will damage."); + if (report != null) + { + report.DownChance = (1 - report.KillChance - report.FailureChance) * chanceToPen * downChance; + report.InjureChance = (1 - report.KillChance - report.DownChance - report.FailureChance); + + float sum = report.KillChance + report.DownChance + report.InjureChance + report.FailureChance; + if (Math.Abs(sum - 1f) > 0.001f) + Core.Warn($"Bad percentage calculation ({sum})! Please tell the developer he is an idiot."); + } + + if (outcome == ExecutionOutcome.Nothing && canPen && Rand.Chance(downChance)) + { + Log("Downed"); + outcome = ExecutionOutcome.Down; + if (report == null) + return outcome; + } + + // Damage! + if (outcome == ExecutionOutcome.Nothing) + { + Log("Damaged"); + outcome = ExecutionOutcome.Damage; + if (report == null) + return outcome; + } + + report?.Normalize(); + return outcome; + } + + public static float RemapClamped(float baseA, float baseB, float newA, float newB, float value) + { + float t = Mathf.InverseLerp(baseA, baseB, value); + return Mathf.Lerp(newA, newB, t); + } + + private static BodyPartRecord GetCoreBodyPart(Pawn pawn) => pawn.def.race.body.corePart; + + [UsedImplicitly] + [DebugOutput("Melee Animation")] + private static void LogAllMeleeWeaponVerbs() + { + var meleeWeapons = DefDatabase.AllDefsListForReading.Where(d => d.IsMeleeWeapon()); + var created = new HashSet(); + foreach (var def in meleeWeapons) + { + created.Add(ThingMaker.MakeThing(def) as ThingWithComps); + } + + static string VerbToString(Verb verb, ThingWithComps weapon = null) + { + float dmg = OutcomeWorker.GetDamage(weapon, verb, null); + float ap = OutcomeWorker.GetPen(weapon, verb, null); + var armorSt = verb.GetDamageDef()?.armorCategory?.armorRatingStat ?? StatDefOf.ArmorRating_Sharp; + return $"{verb}: {verb.GetDamageDef()} (dmg: {dmg:F2}, ap: {ap:F2}, armor: {armorSt})"; + } + + static string AllVerbs(ThingWithComps t) + { + return string.Join("\n", GetEq(t).AllVerbs.Select(v => VerbToString(v))); + } + + static CompEquippable GetEq(ThingWithComps td) => td.GetComp(); + + TableDataGetter[] table = new TableDataGetter[4]; + table[0] = new TableDataGetter("Def Name", d => d.def.defName); + table[1] = new TableDataGetter("Name", d => d.LabelCap); + table[2] = new TableDataGetter("Main Attack", d => VerbToString(SelectMainAttack(OutcomeWorker.GetMeleeAttacksFor(d, null)).Verb)); + table[3] = new TableDataGetter("All Verbs", AllVerbs); + + DebugTables.MakeTablesDialog(created, table); + + foreach (var t in created) + t.Destroy(); + } + + public static PossibleMeleeAttack SelectMainAttack(IEnumerable attacks) => SelectMainAttack(attacks, Dmg, GreaterThan); + + public static PossibleMeleeAttack SelectMainAttack(IEnumerable attacks, Func valueSelector, Func compareFunc) + { + // Highest pen? + // highest damage? + // Pen x damage? + + PossibleMeleeAttack selected = default; + float? record = null; + + foreach (var a in attacks) + { + float value = valueSelector(a); + + if (record == null) + { + selected = a; + record = value; + continue; + } + + if (!compareFunc(value, record.Value)) + continue; + + selected = a; + record = value; + } + + return selected; + } + + private static bool Damage(Pawn attacker, Pawn pawn, in AdditionalArgs args) + { + // Damage but do not kill or down the target. + + float dmgToDo = args.TargetDamageAmount; + float totalDmgDone = 0; + + bool WouldBeInvalidResult(HediffDef hediff, float dmg, BodyPartRecord bp) + { + return pawn.health.WouldLosePartAfterAddingHediff(hediff, bp, dmg) || + pawn.health.WouldBeDownedAfterAddingHediff(hediff, bp, dmg) || + pawn.health.WouldDieAfterAddingHediff(hediff, bp, dmg); + } + + for (int i = 0; i < 50; i++) + { + // TODO check does this correctly get the right verbs even if the melee weapon is a sidearm? + Verb verb; + int limit = 1000; + do + { + verb = attacker.meleeVerbs.TryGetMeleeVerb(pawn); + if (limit-- == 0) + { + Core.Error($"Failed to find random verb for weapon '{args.Weapon}' on pawn {pawn}. May be a result of an optimization mod or bug.\n" + + $"The possible verbs are {string.Join(", ", attacker.meleeVerbs.GetUpdatedAvailableVerbsList(false).Where(v => v.IsMeleeAttack).Select(v => v.verb))}"); + return false; + } + + // Force verb refresh if required. + if (verb.EquipmentSource != args.Weapon) + { + attacker.meleeVerbs.ChooseMeleeVerb(pawn); + } + + } while (verb.EquipmentSource != args.Weapon); + + float dmg = OutcomeWorker.GetDamage(args.Weapon, verb, attacker); + if (dmg > dmgToDo) + dmg = dmgToDo; + dmgToDo -= dmg; + float armorPenetration = OutcomeWorker.GetPen(args.Weapon, verb, attacker); + + if (debugLogExecutionOutcome) + Core.Log($"Using verb {verb} to hit for {dmg:F1} dmg with {armorPenetration:F2} pen. Rem: {dmgToDo}"); + + DamageDef def = verb.GetDamageDef(); + var bodyPartGroupDef = verb.verbProps.AdjustedLinkedBodyPartsGroup(verb.tool); + + for (int j = 0; j < 5; j++) + { + var part = pawn.health.hediffSet.GetRandomNotMissingPart(def, BodyPartHeight.Middle, BodyPartDepth.Outside); + + if (dmg < 1f) + { + Core.Warn($"Very low damage of {dmg:F3} with verb {verb}. Changing to 1 blunt damage."); + dmg = 1f; + def = DamageDefOf.Blunt; + } + + if (WouldBeInvalidResult(def.hediff, dmg, part)) + { + if (j == 4) + Core.Warn($"Failed to find any hit for {dmg:F2} dmg that would not kill, down or amputate part on {pawn}. Will keep trying for the remaining {dmgToDo:F2} dmg."); + continue; + } + + OutcomeWorker.PreDamage(verb); + + ThingDef source = args.Weapon.def; + Vector3 direction = (pawn.Position - attacker.Position).ToVector3(); + DamageInfo damageInfo = new DamageInfo(def, dmg, armorPenetration, -1f, attacker, null, source); + damageInfo.SetWeaponBodyPartGroup(bodyPartGroupDef); + damageInfo.SetAngle(direction); + damageInfo.SetIgnoreInstantKillProtection(false); + damageInfo.SetAllowDamagePropagation(false); + + var info = pawn.TakeDamage(damageInfo); + if (debugLogExecutionOutcome) + Core.Log($"Hit {part.LabelCap} for {info.totalDamageDealt:F2}/{dmg:F2} dmg, mitigated"); + totalDmgDone += info.totalDamageDealt; + if (pawn.Dead || pawn.Downed) { + Core.Error($"Accidentally killed or downed {pawn} when attempting to just injure: tried to deal {dmg:F1} {def} dmg to {part.LabelCap}. Storyteller, difficulty, hediffs, or mods could have modified the damage to cause this."); + return false; + } + break; + } + + if (dmgToDo <= 0) + break; + } + + Core.Log($"Dealt {totalDmgDone:F2} pts of dmg as part of injury to {pawn} (aimed to deal {args.TargetDamageAmount:F2})."); + return true; + } + + private static bool Down(Pawn attacker, Pawn pawn, in AdditionalArgs _) + { + // Give the downed hediff. + var h = pawn.health.AddHediff(AM_DefOf.AM_KnockedOut); + + if (h == null) + Core.Error($"Failed to give {pawn} the knocked out hediff!"); + + return h != null; + } + + private static bool Failure(Pawn attacker, Pawn target, in AdditionalArgs _) + { + // Stun the attacker for a bit. + attacker.stances?.stunner?.StunFor(60 * 3, attacker, false); + return true; + } + + private static bool IsDeathless(Pawn pawn) + { + return pawn.genes?.HasGene(GeneDefOf.Deathless) ?? false; + } + + private static bool Kill(Pawn killer, Pawn pawn, in AdditionalArgs args) + { + bool isDeathless = IsDeathless(pawn); + + if (Core.Settings.ExecutionsCanDestroyBodyParts || isDeathless) + { + BodyPartDef partDef = args.BodyPartDef; + DamageDef dmgDef = args.DamageDef ?? DamageDefOf.Cut; + RulePackDef logDef = args.LogGenDef ?? AM_DefOf.AM_Execution_Generic; + Thing weapon = args.Weapon; + + // Don't allow destroying the head because this would really kill the + if (isDeathless && partDef == BodyPartDefOf.Head) + { + partDef = GetCoreBodyPart(pawn).def; + Core.Log($"Since {pawn} is deathless, the killing damage has been changed from targeting the Head to instead hit the {partDef}."); + } + + BodyPartRecord part = pawn.TryGetPartFromDef(partDef); + var dInfo = new DamageInfo(dmgDef, 99999, 99999, hitPart: part, instigator: killer, weapon: weapon?.def); + var log = CreateLog(logDef, weapon, killer, pawn); + dInfo.SetAllowDamagePropagation(false); + dInfo.SetIgnoreArmor(true); + dInfo.SetIgnoreInstantKillProtection(true); + + var oldEffecter = pawn.RaceProps?.FleshType?.damageEffecter; + if (oldEffecter != null) + pawn.RaceProps.FleshType.damageEffecter = null; + + // Smack em hard. + DamageWorker.DamageResult result; + try + { + result = pawn.TakeDamage(dInfo); + } + finally + { + if (oldEffecter != null) + pawn.RaceProps.FleshType.damageEffecter = oldEffecter; + } + + // If for some reason they did not die from 9999 damage (magic shield?), just double-kill them the hard way. + if (!pawn.Dead && !isDeathless) + { + Find.BattleLog.RemoveEntry(log); + pawn.Kill(dInfo, result?.hediffs?.FirstOrFallback()); + } + else + { + result?.AssociateWithLog(log); + } + } + else + { + // Magic kill... + BodyPartDef partDef = args.BodyPartDef; + DamageDef dmgDef = args.DamageDef ?? DamageDefOf.Cut; + BodyPartRecord part = pawn.TryGetPartFromDef(partDef); + Thing weapon = args.Weapon; + + // Does 0.01 damage, kills anyway. + var dInfo = new DamageInfo(dmgDef, 0.01f, 0f, hitPart: part, instigator: killer, weapon: weapon?.def); + pawn.Kill(dInfo); + } + + // Apply corpse offset if required. + var animator = pawn.TryGetAnimator(); + if (animator == null) + return true; + + var animPart = animator.GetPawnBody(pawn); + if (animPart == null) + return true; + + var ss = animator.GetSnapshot(animPart); + + if (pawn.Corpse != null) + { + // Do corpse interpolation - interpolates the corpse to the correct position, after the animated position. + Patch_Corpse_DrawAt.Interpolators[pawn.Corpse] = new CorpseInterpolate(pawn.Corpse, ss.GetWorldPosition()); + + Patch_PawnRenderer_LayingFacing.OverrideRotations[pawn] = ss.GetWorldDirection(); + } + else if (!isDeathless) + { + Core.Warn($"{pawn} did not spawn a corpse after death, or the corpse was destroyed..."); + } + + // Update the pawn wiggler so that the pawn corpse matches the final animation state. + var bodyRot = ss.GetWorldRotation(); + pawn.Drawer.renderer.wiggler.downedAngle = bodyRot; + + return true; + } + + private static LogEntry_DamageResult CreateLog(RulePackDef def, Thing weapon, Pawn inst, Pawn vict) + { + //var log = new BattleLogEntry_MeleeCombat(rulePackGetter(this.maneuver), alwaysShow, this.CasterPawn, this.currentTarget.Thing, base.ImplementOwnerType, this.tool.labelUsedInLogging ? this.tool.label : "", (base.EquipmentSource == null) ? null : base.EquipmentSource.def, (base.HediffCompSource == null) ? null : base.HediffCompSource.Def, this.maneuver.logEntryDef); + var log = new BattleLogEntry_MeleeCombat(def, true, inst, vict, ImplementOwnerTypeDefOf.Weapon, weapon?.Label, def: LogEntryDefOf.MeleeAttack); + Find.BattleLog.Add(log); + return log; + } + + public class ProbabilityReport + { + public float FailureChance { get; set; } + public float DownChance { get; set; } + public float InjureChance { get; set; } + public float KillChance { get; set; } + + public void Normalize() + { + float sum = DownChance + InjureChance + KillChance + FailureChance; + if (sum == 0f) + return; + + DownChance /= sum; + InjureChance /= sum; + KillChance /= sum; + FailureChance /= sum; + } + + private static string InColor(float pct) + { + Color c = Color.Lerp(Color.red, Color.green, pct); + string hex = ColorUtility.ToHtmlStringRGB(c); + return $"{pct*100:F0}%"; + } + + public override string ToString() => $"{"AM.ProbReport.Kill".Trs()}: {InColor(KillChance)}\n{"AM.ProbReport.Down".Trs()}: {InColor(DownChance)}\n{"AM.ProbReport.Injure".Trs()}: {InColor(InjureChance)}\n{"AM.ProbReport.Fail".Trs()}: {InColor(FailureChance)}"; + } +} diff --git a/Source/AnimationMod/Outcome/PossibleMeleeAttack.cs b/Source/1.4/ThingGenerator/Outcome/PossibleMeleeAttack.cs similarity index 100% rename from Source/AnimationMod/Outcome/PossibleMeleeAttack.cs rename to Source/1.4/ThingGenerator/Outcome/PossibleMeleeAttack.cs diff --git a/Source/AnimationMod/Outcome/VanillaOutcomeWorker.cs b/Source/1.4/ThingGenerator/Outcome/VanillaOutcomeWorker.cs similarity index 100% rename from Source/AnimationMod/Outcome/VanillaOutcomeWorker.cs rename to Source/1.4/ThingGenerator/Outcome/VanillaOutcomeWorker.cs diff --git a/Source/AnimationMod/PartRenderer.cs b/Source/1.4/ThingGenerator/PartRenderer.cs similarity index 100% rename from Source/AnimationMod/PartRenderer.cs rename to Source/1.4/ThingGenerator/PartRenderer.cs diff --git a/Source/1.4/ThingGenerator/Patches/PatchMaster.cs b/Source/1.4/ThingGenerator/Patches/PatchMaster.cs new file mode 100644 index 00000000..ba6d5f03 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/PatchMaster.cs @@ -0,0 +1,19 @@ +using Verse; + +namespace AM.Patches; + +public static class PatchMaster +{ + private static Pawn lastPawn; + private static AnimRenderer lastRenderer; + + public static AnimRenderer GetAnimator(Pawn pawn) + { + if (pawn == lastPawn && lastRenderer is { IsDestroyed: false }) + return lastRenderer; + + lastPawn = pawn; + lastRenderer = AnimRenderer.TryGetAnimator(pawn); + return lastRenderer; + } +} diff --git a/Source/1.4/ThingGenerator/Patches/Patch_Corpse_DrawAt.cs b/Source/1.4/ThingGenerator/Patches/Patch_Corpse_DrawAt.cs new file mode 100644 index 00000000..32fe29e0 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_Corpse_DrawAt.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using AM.AMSettings; +using HarmonyLib; +using UnityEngine; +using Verse; + +namespace AM.Patches +{ + [HarmonyPatch(typeof(Corpse), nameof(Corpse.DrawAt))] + public static class Patch_Corpse_DrawAt + { + public static readonly Dictionary Interpolators = new Dictionary(); + + public static void Tick() + { + if(GenTicks.TicksGame % (60 * 30) == 0) + Interpolators.RemoveAll(p => !p.Key.Spawned); + } + + [HarmonyPriority(Priority.First)] + static void Prefix(Corpse __instance, ref Vector3 drawLoc) + { + if (Core.Settings.CorpseOffsetMode == CorpseOffsetMode.None) + return; + + if (!Interpolators.TryGetValue(__instance, out var found)) + return; + + if (!found.Update(ref drawLoc)) + Interpolators.Remove(__instance); + } + } + + public class CorpseInterpolate + { + [TweakValue("AM", 0.01f, 100f)] + public static float CorpseLerpSpeed = 5; + + public Vector3 TargetPosition; + public Vector3 CurrentPosition; + public Vector3 InitialOffset; + + public CorpseInterpolate(Corpse corpse, Vector3 startPos) + { + TargetPosition = corpse.DrawPos; + InitialOffset = startPos - TargetPosition; + CurrentPosition = startPos; + } + + public bool Update(ref Vector3 drawLoc) + { + switch (Core.Settings.CorpseOffsetMode) + { + case CorpseOffsetMode.InterpolateToCorrect: + drawLoc += InitialOffset; + InitialOffset = Vector3.MoveTowards(InitialOffset, Vector3.zero, Time.deltaTime * CorpseLerpSpeed); + return InitialOffset.sqrMagnitude > 0.001f; + + case CorpseOffsetMode.KeepOffset: + drawLoc += InitialOffset; + return true; + + default: + return false; + } + } + } +} diff --git a/Source/AnimationMod/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs b/Source/1.4/ThingGenerator/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs similarity index 100% rename from Source/AnimationMod/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs rename to Source/1.4/ThingGenerator/Patches/Patch_FloatMenuMakerMap_AddDraftedOrders.cs diff --git a/Source/1.4/ThingGenerator/Patches/Patch_GlobalTextureAtlasManager_TryGetPawnFrameSet.cs b/Source/1.4/ThingGenerator/Patches/Patch_GlobalTextureAtlasManager_TryGetPawnFrameSet.cs new file mode 100644 index 00000000..8d0a2b97 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_GlobalTextureAtlasManager_TryGetPawnFrameSet.cs @@ -0,0 +1,28 @@ +using HarmonyLib; +using Verse; + +namespace AM.Patches; + +/// +/// Disables the texture caching introduced in Rimworld 1.3. +/// Only applies when a pawn in being animated, or when they have been beheaded. +/// +[HarmonyPatch(typeof(GlobalTextureAtlasManager), nameof(GlobalTextureAtlasManager.TryGetPawnFrameSet))] +public static class Patch_GlobalTextureAtlasManager_TryGetPawnFrameSet +{ + [HarmonyPriority(Priority.First)] + public static bool Prefix(Pawn pawn, ref bool createdNew, ref bool __result) + { + var anim = PatchMaster.GetAnimator(pawn); + if (anim == null) + { + var isBeheaded = AnimationManager.PawnToHeadInstance.TryGetValue(pawn, out _); + if (!isBeheaded) + return true; + } + + createdNew = false; + __result = false; + return false; + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnGenerator_GeneratePawn.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnGenerator_GeneratePawn.cs new file mode 100644 index 00000000..b838c4f7 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnGenerator_GeneratePawn.cs @@ -0,0 +1,77 @@ +using System; +using HarmonyLib; +using JetBrains.Annotations; +using RimWorld; +using UnityEngine; +using Verse; + +namespace AM.Patches; + +/// +/// Used to spawn lassos on melee pawns when they first generate. +/// +[HarmonyPatch(typeof(PawnGenerator), nameof(PawnGenerator.GenerateGearFor))] +[UsedImplicitly] +public static class Patch_PawnGenerator_GeneratePawn +{ + [HarmonyPriority(Priority.Last)] + public static void Postfix(Pawn pawn) + { + try + { + if (pawn == null) + return; + + // Spawn chance from settings: + if (!Rand.Chance(Core.Settings.LassoSpawnChance)) + return; + + // Basic pawn checks. + if (!pawn.def.race.Humanlike || !pawn.def.race.ToolUser || pawn.apparel == null) + return; + + // Only give to pawns with melee weapons. + var weapon = pawn.GetFirstMeleeWeapon(); + if (weapon == null) + return; + + // Don't bother giving to pawns that do not have the required melee skill. + if (Core.Settings.MinMeleeSkillToLasso > 0 && !HasSkillToUseLasso(pawn)) + return; + + GiveLasso(pawn, GetRandomLasso()); + } + catch (Exception e) + { + Core.Error("Exception in pawn generation postfix:", e); + } + } + + private static bool HasSkillToUseLasso(Pawn pawn) + { + int skill = pawn.skills?.GetSkill(SkillDefOf.Melee)?.Level ?? -1; + return skill >= Core.Settings.MinMeleeSkillToLasso; + } + + private static void GiveLasso(Pawn pawn, ThingDef lasso) + { + if (pawn == null || lasso == null) + return; + + var thing = ThingMaker.MakeThing(lasso) as Apparel; + if (thing == null) + { + Core.Warn($"Failed to spawn instance of lass '{lasso}'"); + return; + } + + thing.stackCount = 1; + pawn.apparel.Wear(thing, false); + } + + private static ThingDef GetRandomLasso() + { + // Random lasso with a heavy weight on cheaper ones. + return Content.LassoDefs.RandomElementByWeightWithFallback(l => 1f / Mathf.Pow(l.BaseMarketValue, 3)); + } +} diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_DrawEquipmentAiming.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_DrawEquipmentAiming.cs new file mode 100644 index 00000000..61f3fb47 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_DrawEquipmentAiming.cs @@ -0,0 +1,35 @@ +using AM.Idle; +using AM.Tweaks; +using HarmonyLib; +using Verse; + +namespace AM.Patches; + +/// +/// Used to override drawing melee weapons. +/// +[HarmonyPatch(typeof(PawnRenderer), nameof(PawnRenderer.DrawEquipment))] +public static class Patch_PawnRenderer_DrawEquipment +{ + [HarmonyPriority(Priority.First)] + [HarmonyBefore("com.yayo.yayoAni")] + static bool Prefix(PawnRenderer __instance) + { + var pawn = __instance.pawn; + + if (!Core.Settings.AnimateAtIdle) + return true; + + var comp = pawn.GetComp(); + if (comp == null) + return true; // Why would this ever be the case? Better safe than sorry though. + + // Only for melee weapons... + var wep = pawn.equipment?.Primary?.def; + bool isMeleeWeapon = wep?.IsMeleeWeapon() ?? false; + if (isMeleeWeapon && TweakDataManager.TryGetTweak(wep) == null) + isMeleeWeapon = false; + comp.PreDraw(); + return !isMeleeWeapon; + } +} diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_DrawInvisibleShadow.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_DrawInvisibleShadow.cs new file mode 100644 index 00000000..c0350b33 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_DrawInvisibleShadow.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using JetBrains.Annotations; +using Verse; + +namespace AM.Patches; + +/// +/// Suppresses shadow draw which was added in 1.4 in the . +/// +[HarmonyPatch(typeof(PawnRenderer), nameof(PawnRenderer.DrawInvisibleShadow))] +[UsedImplicitly] +public class Patch_PawnRenderer_DrawInvisibleShadow +{ + public static bool Suppress = false; + + public static bool Prefix() => !Suppress; +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_LayingFacing.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_LayingFacing.cs new file mode 100644 index 00000000..a6be1a15 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_LayingFacing.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using HarmonyLib; +using Verse; + +namespace AM.Patches; + +/// +/// Only used to override corpse direction. +/// +[HarmonyPatch(typeof(PawnRenderer), nameof(PawnRenderer.LayingFacing))] +public static class Patch_PawnRenderer_LayingFacing +{ + public static readonly Dictionary OverrideRotations = new Dictionary(); + + public static void Tick() + { + if (GenTicks.TicksGame % (60 * 30) == 0) + OverrideRotations.RemoveAll(p => !p.Key.SpawnedOrAnyParentSpawned); + } + + static void Postfix(Pawn ___pawn, ref Rot4 __result) + { + if (!___pawn.Dead) + return; + + if (!OverrideRotations.TryGetValue(___pawn, out var found)) + return; + + __result = found; + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_RenderPawnAt.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_RenderPawnAt.cs new file mode 100644 index 00000000..12c8d7f7 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_RenderPawnAt.cs @@ -0,0 +1,34 @@ +using AM.Grappling; +using HarmonyLib; +using Verse; + +namespace AM.Patches; + +/// +/// Simply prevents the regular RenderPawnAt method from running while a pawn in being animated. +/// This disables the regular rendering whenever a pawn in being animated. +/// +[HarmonyPatch(typeof(PawnRenderer), nameof(PawnRenderer.RenderPawnAt))] +public static class Patch_PawnRenderer_RenderPawnAt +{ + public static bool AllowNext; + + [HarmonyPriority(Priority.First)] + public static bool Prefix(Pawn ___pawn) + { + var anim = PatchMaster.GetAnimator(___pawn); + if (anim != null && !AllowNext) + { + return false; + } + + var job = ___pawn.CurJob; + if (job?.def == AM_DefOf.AM_GrapplePawn) + { + JobDriver_GrapplePawn.DrawEnsnaringRope(___pawn, job); + } + + AllowNext = false; + return true; + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_RenderPawnInternal.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_RenderPawnInternal.cs new file mode 100644 index 00000000..4efbe61c --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnRenderer_RenderPawnInternal.cs @@ -0,0 +1,88 @@ +using HarmonyLib; +using Verse; + +namespace AM.Patches; + +/// +/// Overrides various parameters of the pawn rendering, specifically direction (north, east etc.) +/// and body angle. Driven by the active animation. +/// Also used to render severed heads. +/// +[HarmonyPatch(typeof(PawnRenderer), nameof(PawnRenderer.RenderPawnInternal))] +static class Patch_PawnRenderer_RenderPawnInternal +{ + public static bool AllowNext; + public static bool DoNotModify = false; + public static DrawMode NextDrawMode = DrawMode.Full; + public static Rot4 HeadRotation; // Only used when NextDrawMode is HeadOnly or HeadStandalone. + + public static float StandaloneHeadRotation; + + public enum DrawMode + { + Full, + BodyOnly, + HeadOnly, + HeadStandalone + } + + [HarmonyPriority(Priority.Last)] // As late as possible. We want to be the last to modify results. + [HarmonyAfter("com.yayo.yayoAni")] // Go away. + [HarmonyBefore("rimworld.Nals.FacialAnimation")] // Must go before facial animation otherwise the face gets fucky. + public static bool Prefix(Pawn ___pawn, ref Rot4 bodyFacing, ref float angle, ref PawnRenderFlags flags, ref bool renderBody) + { + // Do not affect portrait rendering: + if (flags.HasFlag(PawnRenderFlags.Portrait)) + return true; + + // Standalone head (i.e. dropped head on ground after animation) gets a custom method that does things slightly differently. + if (NextDrawMode == DrawMode.HeadStandalone) + return RenderStandaloneHeadMode(ref bodyFacing, ref flags, ref angle, ref renderBody); + + // Get the animator for this pawn. + var anim = PatchMaster.GetAnimator(___pawn); + if (anim != null) + { + if (!DoNotModify) + { + var part = NextDrawMode == DrawMode.HeadOnly ? anim.GetPawnHead(___pawn) : anim.GetPawnBody(___pawn); + var snapshot = anim.GetSnapshot(part); + angle = snapshot.GetWorldRotation(); + + bodyFacing = NextDrawMode == DrawMode.HeadOnly ? HeadRotation : snapshot.GetWorldDirection(); + + switch (NextDrawMode) + { + case DrawMode.BodyOnly: + // Render head stump, do not render head gear. + flags |= PawnRenderFlags.HeadStump; + flags &= ~PawnRenderFlags.Headgear; + break; + + case DrawMode.HeadOnly: + // Do not render body. + renderBody = false; + break; + } + } + + if (!AllowNext) + return false; + } + + AllowNext = false; + return true; + } + + private static bool RenderStandaloneHeadMode(ref Rot4 bodyFacing, ref PawnRenderFlags flags, ref float angle, ref bool renderBody) + { + // Add headgear, remove head stump. + flags |= PawnRenderFlags.Headgear; + flags &= ~PawnRenderFlags.HeadStump; + + angle = StandaloneHeadRotation; + bodyFacing = HeadRotation; + renderBody = false; + return true; + } +} \ No newline at end of file diff --git a/Source/1.4/ThingGenerator/Patches/Patch_PawnUtility_GetPosture.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnUtility_GetPosture.cs new file mode 100644 index 00000000..db570786 --- /dev/null +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnUtility_GetPosture.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using RimWorld; +using Verse; + +namespace AM.Patches; + +/// +/// Makes it so that pawns that are being animated stand up, ignoring regular posture calculation. +/// +[HarmonyPatch(typeof(PawnUtility), nameof(PawnUtility.GetPosture))] +public static class Patch_PawnUtility_GetPosture +{ + [HarmonyPriority(Priority.First)] + public static bool Prefix(Pawn p, ref PawnPosture __result) + { + var anim = PatchMaster.GetAnimator(p); + if (anim == null) + return true; + + __result = PawnPosture.Standing; + return false; + } +} \ No newline at end of file diff --git a/Source/AnimationMod/Patches/Patch_PawnUtility_IsInvisible.cs b/Source/1.4/ThingGenerator/Patches/Patch_PawnUtility_IsInvisible.cs similarity index 90% rename from Source/AnimationMod/Patches/Patch_PawnUtility_IsInvisible.cs rename to Source/1.4/ThingGenerator/Patches/Patch_PawnUtility_IsInvisible.cs index 2e0c0c27..d7a3f1ba 100644 --- a/Source/AnimationMod/Patches/Patch_PawnUtility_IsInvisible.cs +++ b/Source/1.4/ThingGenerator/Patches/Patch_PawnUtility_IsInvisible.cs @@ -4,7 +4,6 @@ namespace AM.Patches; - /// /// Make pawns be considered invisible during animations. /// This should prevent them from being targeted by enemies. @@ -13,7 +12,7 @@ namespace AM.Patches; /// However, making pawns invincible during animations would be very overpowered and broken, so making them untargettable instead is a nice /// compromise. /// -[HarmonyPatch(typeof(InvisibilityUtility), nameof(InvisibilityUtility.IsPsychologicallyInvisible))] +[HarmonyPatch(typeof(PawnUtility), nameof(PawnUtility.IsInvisible))] public static class Patch_PawnUtility_IsInvisible { public static bool IsRendering; @@ -32,4 +31,4 @@ public static bool Prefix(Pawn pawn, ref bool __result) } return true; } -} +} \ No newline at end of file diff --git a/Source/AnimationMod/Patches/Patch_Pawn_DrawGUIOverlay.cs b/Source/1.4/ThingGenerator/Patches/Patch_Pawn_DrawGUIOverlay.cs similarity index 100% rename from Source/AnimationMod/Patches/Patch_Pawn_DrawGUIOverlay.cs rename to Source/1.4/ThingGenerator/Patches/Patch_Pawn_DrawGUIOverlay.cs diff --git a/Source/AnimationMod/Patches/Patch_Pawn_DrawTracker_Notify_MeleeAttackOn.cs b/Source/1.4/ThingGenerator/Patches/Patch_Pawn_DrawTracker_Notify_MeleeAttackOn.cs similarity index 100% rename from Source/AnimationMod/Patches/Patch_Pawn_DrawTracker_Notify_MeleeAttackOn.cs rename to Source/1.4/ThingGenerator/Patches/Patch_Pawn_DrawTracker_Notify_MeleeAttackOn.cs diff --git a/Source/AnimationMod/Patches/Patch_VBE_Utils_DrawBG.cs b/Source/1.4/ThingGenerator/Patches/Patch_VBE_Utils_DrawBG.cs similarity index 100% rename from Source/AnimationMod/Patches/Patch_VBE_Utils_DrawBG.cs rename to Source/1.4/ThingGenerator/Patches/Patch_VBE_Utils_DrawBG.cs diff --git a/Source/AnimationMod/Patches/Patch_Verb_MeleeAttack_ApplyMeleeDamageToTarget.cs b/Source/1.4/ThingGenerator/Patches/Patch_Verb_MeleeAttack_ApplyMeleeDamageToTarget.cs similarity index 100% rename from Source/AnimationMod/Patches/Patch_Verb_MeleeAttack_ApplyMeleeDamageToTarget.cs rename to Source/1.4/ThingGenerator/Patches/Patch_Verb_MeleeAttack_ApplyMeleeDamageToTarget.cs diff --git a/Source/AnimationMod/Patches/Patch_Verb_MeleeAttack_TryCastShot.cs b/Source/1.4/ThingGenerator/Patches/Patch_Verb_MeleeAttack_TryCastShot.cs similarity index 100% rename from Source/AnimationMod/Patches/Patch_Verb_MeleeAttack_TryCastShot.cs rename to Source/1.4/ThingGenerator/Patches/Patch_Verb_MeleeAttack_TryCastShot.cs diff --git a/Source/AnimationMod/PawnData/AutoOption.cs b/Source/1.4/ThingGenerator/PawnData/AutoOption.cs similarity index 100% rename from Source/AnimationMod/PawnData/AutoOption.cs rename to Source/1.4/ThingGenerator/PawnData/AutoOption.cs diff --git a/Source/AnimationMod/PawnData/PawnMeleeData.cs b/Source/1.4/ThingGenerator/PawnData/PawnMeleeData.cs similarity index 100% rename from Source/AnimationMod/PawnData/PawnMeleeData.cs rename to Source/1.4/ThingGenerator/PawnData/PawnMeleeData.cs diff --git a/Source/AnimationMod/Preview/PreviewRenderer.cs b/Source/1.4/ThingGenerator/Preview/PreviewRenderer.cs similarity index 100% rename from Source/AnimationMod/Preview/PreviewRenderer.cs rename to Source/1.4/ThingGenerator/Preview/PreviewRenderer.cs diff --git a/Source/1.4/ThingGenerator/Processing/MapPawnProcessor.cs b/Source/1.4/ThingGenerator/Processing/MapPawnProcessor.cs new file mode 100644 index 00000000..ec7f8428 --- /dev/null +++ b/Source/1.4/ThingGenerator/Processing/MapPawnProcessor.cs @@ -0,0 +1,625 @@ +using AM.Controller; +using AM.Controller.Requests; +using AM.Grappling; +using AM.Outcome; +using AM.Reqs; +using JetBrains.Annotations; +using RimWorld; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Unity.Jobs.LowLevel.Unsafe; +using UnityEngine; +using Verse; +using Verse.AI; + +namespace AM.Processing; + +public class MapPawnProcessor : IDisposable +{ + [TweakValue("Melee Animation", 0f, 5f)] + [UsedImplicitly] + public static float PawnPerThreadThreshold = 0.2f; + [TweakValue("Melee Animation")] + [UsedImplicitly] + public static bool LogPerformanceToDesktop; + + private static readonly Dictionary taskArrayPool = new Dictionary(); + private static StreamWriter debugWriter; + + private static Task[] GetTaskArray(int count) + { + if (taskArrayPool.TryGetValue(count, out var found)) + { + return found; + } + + found = new Task[count]; + taskArrayPool.Add(count, found); + return found; + } + + public readonly DiagnosticInfo Diagnostics = new DiagnosticInfo(); + + private readonly List attackers = new List(128); + private readonly List slices = new List(32); + private readonly List targetsPool = new List(); + private readonly ConcurrentQueue<(AnimationStartParameters args, IntVec3? lassoToHere, ulong occupiedMask)> toStart = new ConcurrentQueue<(AnimationStartParameters, IntVec3?, ulong)>(); + private readonly ActionController generalController = new ActionController(); + private readonly Map map; + private int targetsIndex; + + public MapPawnProcessor(Map map) + { + this.map = map; + } + + private TaskData GetTaskData() + { + if (targetsIndex == targetsPool.Count) + { + var created = new TaskData(targetsIndex); + targetsIndex++; + targetsPool.Add(created); + return created; + } + + var found = targetsPool[targetsIndex]; + targetsIndex++; + return found; + } + + public void Tick() + { + if (GenTicks.TicksAbs % Core.Settings.ScanTickInterval != 0) + return; + + if (LogPerformanceToDesktop && debugWriter == null) + { + string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "AnimationModPerformanceReport.csv"); + debugWriter = new StreamWriter(path, true); + } + else + { + Dispose(); + } + + var timer = new RefTimer(); + + toStart.Clear(); + + // Get all pawns that need auto processing. + CompileListOfAttackers(); + + // Make slices. + slices.Clear(); + slices.AddRange(MakeProcessingSlices(attackers.Count)); + + // Make tasks. + var timer2 = new RefTimer(); + targetsIndex = 0; + if (slices.Count == 1) + { + // Run sync. + var taskData = GetTaskData(); + taskData.ScheduleTick = RefTimer.GetTickNow(); + ProcessSlice(attackers, new IntRange(0, attackers.Count - 1), map, taskData, toStart, Diagnostics); + } + else if (slices.Count > 1) + { + // Run async. + var arr = GetTaskArray(slices.Count); + + // Generate tasks. + for (int i = 0; i < slices.Count; i++) + { + int j = i; + var taskData = GetTaskData(); + taskData.ScheduleTick = RefTimer.GetTickNow(); + var task = Task.Run(() => ProcessSlice(attackers, slices[j], map, taskData, toStart, Diagnostics)); + arr[i] = task; + } + + // Wait for them to complete. + Task.WaitAll(arr); + + // Clean array. + for (int i = 0; i < arr.Length; i++) + arr[i] = null; + } + + // Debug output, optional. + debugWriter?.WriteLine($"{attackers.Count},{slices.Count},{timer2.GetElapsedMilliseconds().ToString(CultureInfo.InvariantCulture)},{PawnPerThreadThreshold}"); + + // Start all pending animations. + // Must be done here, in main thread, because otherwise + // sweep meshes created will crash unity. + while (toStart.TryDequeue(out var pair)) + { + var args = pair.args; + + // Generate outcome here to avoid threading errors: + var outcome = OutcomeUtility.GenerateRandomOutcome(args.MainPawn, args.SecondPawn, true); + + if (pair.lassoToHere == null) + { + // Instant execution: + + // Instant trigger. + var finalArgs = args with + { + ExecutionOutcome = outcome, + + // Do animation promotion: + Animation = args.Animation.TryGetPromotionDef(new AnimDef.PromotionInput + { + Attacker = args.MainPawn, + Victim = args.SecondPawn, + FlipX = args.FlipX, + OccupiedMask = pair.occupiedMask, + OriginalAnim = args.Animation, + Outcome = outcome, + ReqInput = new ReqInput(args.MainPawn.GetFirstMeleeWeapon()?.def) + }) ?? args.Animation + }; + + // Force failure animation for failure outcome. + if (finalArgs.ExecutionOutcome == ExecutionOutcome.Failure) + { + finalArgs.Animation = AM_DefOf.AM_Execution_Fail; + } + + bool worked = finalArgs.TryTrigger(); + + // Set execution cooldown. + if (worked) + args.MainPawn.GetMeleeData().TimeSinceExecuted = 0; + } + else if (pair.args.Animation != null) + { + // Lasso + execution: + var finalArgs = args with + { + ExecutionOutcome = outcome, + + // Do animation promotion: + Animation = args.Animation.TryGetPromotionDef(new AnimDef.PromotionInput + { + Attacker = args.MainPawn, + Victim = args.SecondPawn, + FlipX = args.FlipX, + OccupiedMask = pair.occupiedMask, + OriginalAnim = args.Animation, + Outcome = outcome, + ReqInput = new ReqInput(args.MainPawn.GetFirstMeleeWeapon()?.def) + }) ?? args.Animation + }; + + // Force failure animation for failure outcome. + if (finalArgs.ExecutionOutcome == ExecutionOutcome.Failure) + { + finalArgs.Animation = AM_DefOf.AM_Execution_Fail; + } + + // Lasso. + if (!JobDriver_GrapplePawn.GiveJob(args.MainPawn, args.SecondPawn, pair.lassoToHere.Value, false, finalArgs)) + { + Core.Error($"Failed to give grapple job to {args.MainPawn}."); + return; + } + + // Set lasso cooldown. Execution cooldown is set buy the job driver. + args.MainPawn.GetMeleeData().TimeSinceGrappled = 0; + } + else + { + // Just lasso: + // Lasso. + if (!JobDriver_GrapplePawn.GiveJob(args.MainPawn, args.SecondPawn, pair.lassoToHere.Value)) + { + Core.Error($"Failed to give grapple job to {args.MainPawn}."); + return; + } + + // Set lasso cooldown. Execution cooldown is set by the job driver. + args.MainPawn.GetMeleeData().TimeSinceGrappled = 0; + } + } + + Diagnostics.ThreadsUsed = slices.Count; + timer2.GetElapsedMilliseconds(out Diagnostics.ProcessTimeMS); + timer.GetElapsedMilliseconds(out Diagnostics.TotalTimeMS); + } + + public void Dispose() + { + if (debugWriter == null) + return; + + debugWriter.Dispose(); + debugWriter = null; + } + + private static void GetPotentialTargets(List output, Pawn attacker, AttackTargetsCache cache, Map map) + { + Thing thing = attacker; + output.Clear(); + Faction faction = thing.Faction; + if (faction != null) + { + foreach (IAttackTarget attackTarget in cache.TargetsHostileToFaction(faction)) + { + if (thing.HostileTo(attackTarget.Thing)) + { + output.Add(attackTarget); + } + } + } + foreach (Pawn pawn in cache.pawnsInAggroMentalState) + { + if (thing.HostileTo(pawn)) + { + output.Add(pawn); + } + } + foreach (Pawn pawn2 in cache.factionlessHumanlikes) + { + if (thing.HostileTo(pawn2)) + { + output.Add(pawn2); + } + } + if (PrisonBreakUtility.IsPrisonBreaking(attacker)) + { + Faction hostFaction = attacker.guest.HostFaction; + List list = map.mapPawns.SpawnedPawnsInFaction(hostFaction); + foreach (Pawn pawn in list) + { + if (thing.HostileTo(pawn)) + { + output.Add(pawn); + } + } + } + if (ModsConfig.IdeologyActive && SlaveRebellionUtility.IsRebelling(attacker)) + { + Faction faction2 = attacker.Faction; + List list2 = map.mapPawns.SpawnedPawnsInFaction(faction2); + for (int j = 0; j < list2.Count; j++) + { + if (thing.HostileTo(list2[j])) + { + output.Add(list2[j]); + } + } + } + } + + private static bool TargetFilter(IAttackTarget target) + { + if (target.Thing is not Pawn targetPawn) + return false; + + // Target cannot be dead or downed, or being targeted by a lasso. + if (targetPawn.Dead || targetPawn.Downed || targetPawn.IsInAnimation() || GrabUtility.IsBeingTargetedForGrapple(targetPawn)) + return false; + + return true; + } + + private static void ProcessSlice(List attackers, IntRange slice, Map map, TaskData taskData, ConcurrentQueue<(AnimationStartParameters, IntVec3?, ulong)> startArgs, DiagnosticInfo diag) + { + diag.StartupTimesMS[taskData.Index] = RefTimer.ToMilliseconds(taskData.ScheduleTick, RefTimer.GetTickNow()); + diag.TargetFindTimesMS[taskData.Index] = 0; + diag.ReportTimesMS[taskData.Index] = 0; + + try + { + for (int i = slice.min; i <= slice.max; i++) + { + var data = attackers[i]; + var pawn = data.Pawn; + + // Make a list of enemies. + var targetsTimer = new RefTimer(); + GetPotentialTargets(taskData.Targets, pawn, map.attackTargetsCache, map); + taskData.Targets.RemoveAll(t => !TargetFilter(t)); + diag.TargetFindTimesMS[taskData.Index] += targetsTimer.GetElapsedMilliseconds(); + + if (taskData.Targets.Count == 0) + continue; + + // Make space mask around attacker. + ulong occupiedMask = SpaceChecker.MakeOccupiedMask(map, pawn.Position, out uint smallMask); + bool westFree = !occupiedMask.GetBit(-1, 0); + bool eastFree = !occupiedMask.GetBit(1, 0); + + // Do instant executions: + PotentialAnimation toPerform = default; + var reportTimer = new RefTimer(); + if (data.CanExecute && (eastFree || westFree)) + { + var reports = taskData.Controller.GetExecutionReport(new ExecutionAttemptRequest + { + CanUseLasso = data.CanGrapple, + CanWalk = false, + EastCell = eastFree, + WestCell = westFree, + Executioner = pawn, + NoErrorMessages = true, + OccupiedMask = occupiedMask, + SmallOccupiedMask = smallMask, + TrustLassoUsability = true, + LassoRange = data.LassoRange, + Targets = taskData.Targets.Select(t => (Pawn)t), + AttackerMeleeLevel = data.PawnMeleeLevel + }); + + foreach (var report in reports) + { + if (!report.CanExecute) + continue; + + if (report.IsFinal) + throw new Exception("Should not be getting final report here!"); + + if (report.IsWalking) + throw new Exception("Walking was disallowed but still showed up!"); + + var selected = report.PossibleExecutions.RandomElementByWeightWithFallback(p => p.Animation.AnimDef.Probability); + if (!selected.IsValid) + { + Core.Warn("Failed to get any valid animation even through they were presented in the report."); + continue; + } + + // Populate the action to be performed: + toPerform = new PotentialAnimation + { + Anim = selected.Animation.AnimDef, + FlipX = selected.Animation.FlipX, + Target = report.Target, + LassoToHere = selected.LassoToHere, + OccupiedMask = occupiedMask + }; + break; + } + + foreach (var report in reports) + report.Dispose(); + } + diag.ReportTimesMS[taskData.Index] += reportTimer.GetElapsedMilliseconds(); + + // Lasso only without execute: + if (!toPerform.IsValid && data.CanGrapple) + { + foreach (var target in taskData.Targets.Select(t => (Pawn) t)) + { + var report = taskData.Controller.GetGrappleReport(new GrappleAttemptRequest + { + Grappler = pawn, + DoNotCheckCooldown = true, + DoNotCheckLasso = true, + GrappleSpotPickingBehaviour = GrappleSpotPickingBehaviour.PreferAdjacent, + LassoRange = data.LassoRange, + NoErrorMessages = true, + OccupiedMask = smallMask, + TrustLassoUsability = true, + Target = target + }); + + if (!report.CanGrapple) + continue; + + toPerform = new PotentialAnimation + { + Anim = null, + FlipX = false, + LassoToHere = report.DestinationCell, + Target = target, + OccupiedMask = occupiedMask + }; + break; + } + + } + + if (toPerform.IsValid) + { + // Start instant execution animation or lasso: + startArgs.Enqueue((new AnimationStartParameters(toPerform.Anim, pawn, toPerform.Target) + { + FlipX = toPerform.FlipX + }, toPerform.LassoToHere, toPerform.OccupiedMask)); + } + } + } + catch (Exception e) + { + Core.Error("Processing thread error:", e); + } + } + + private static void GetSecondsMTB(Pawn pawn, out float execute, out float lasso) + { + if (pawn.IsColonist || pawn.IsSlaveOfColony) + { + execute = Core.Settings.ExecuteAttemptMTBSeconds; + lasso = Core.Settings.GrappleAttemptMTBSeconds; + } + else + { + execute = Core.Settings.ExecuteAttemptMTBSecondsEnemy; + lasso = Core.Settings.GrappleAttemptMTBSecondsEnemy; + } + } + + /// + /// Puts together a list of pawns that have either a melee weapon or a lasso, or both, + /// and have auto-lasso and/or execute enabled. + /// + private void CompileListOfAttackers() + { + var timer = new RefTimer(); + + bool FormalGrappleCheck(Pawn pawn) + { + return generalController.GetGrappleReport(new GrappleAttemptRequest + { + Grappler = pawn, + DoNotCheckCooldown = true, + DoNotCheckLasso = true, + GrappleSpotPickingBehaviour = GrappleSpotPickingBehaviour.Closest, + NoErrorMessages = true + }).CanGrapple; + } + + attackers.Clear(); + foreach (var pawn in map.mapPawns.AllPawnsSpawned) + { + // Not dead or downed. + if (pawn.Dead || pawn.Downed) + continue; + + // Not animal, can ever use tools. + if (pawn.RaceProps.Animal || !pawn.def.race.ToolUser) + continue; + + // Not already in animation or being grappled. + if (pawn.IsInAnimation() || GrabUtility.IsBeingTargetedForGrapple(pawn)) + continue; + + // If fire at will is disabled, don't process them. They should not be attacking or lassoing. + if (pawn.drafter?.FireAtWill == false) + continue; + + // Should this pawn even be scanned? + GetSecondsMTB(pawn, out float execMTB, out float lassoMTB); + bool execRandom = Rand.MTBEventOccurs(execMTB, 60f, Core.Settings.ScanTickInterval); + bool lassoRandom = Rand.MTBEventOccurs(lassoMTB, 60f, Core.Settings.ScanTickInterval); + + if (!execRandom && !lassoRandom) + continue; + + var mData = pawn.GetMeleeData(); + var weapon = pawn.GetFirstMeleeWeapon(); + var lasso = pawn.TryGetLasso(); + + // Melee skill has to be extracted here, on the main thread, + // because concurrent access throws exceptions. + var data = new AttackerData + { + Pawn = pawn, + PawnMeleeLevel = pawn.skills?.GetSkill(SkillDefOf.Melee)?.Level ?? 0, + MeleeWeapon = weapon, + Lasso = lasso, + CanExecute = Core.Settings.EnableExecutions && weapon != null && mData.ResolvedAutoExecute && mData.IsExecutionOffCooldown(), + CanGrapple = lasso != null && mData.ResolvedAutoGrapple && mData.IsGrappleOffCooldown() && FormalGrappleCheck(pawn), + LassoRange = lasso != null ? pawn.GetStatValue(AM_DefOf.AM_GrappleRadius) : 0 + }; + + if (data is { CanGrapple: false, CanExecute: false }) + continue; + + attackers.Add(data); + } + + Diagnostics.PawnCount = attackers.Count; + timer.GetElapsedMilliseconds(out Diagnostics.CompileTimeMS); + } + + public static int GetWorkerThreadCount() => Mathf.Max(1, JobsUtility.JobWorkerCount + 1); + + public static IEnumerable MakeProcessingSlices(int todo) + { + if (todo <= 0) + yield break; + + // Max threads allowed, from settings: + int maxThreads = Core.Settings.MaxProcessingThreads <= 0 ? GetWorkerThreadCount() : Core.Settings.MaxProcessingThreads; + + // Number of threads scales up to maximum allowed. Do not allow more than 1 thread per pawn. + int threads = Mathf.Min(todo, maxThreads); + + // Thread per pawn threshold calculation: + float basicPer = (float)todo / maxThreads; + if (basicPer < PawnPerThreadThreshold) + threads = 1; + + if (threads == 1) + { + yield return new IntRange(0, todo - 1); + yield break; + } + + // Do slicing: + int per = todo / threads; + int extraCount = todo % threads; + int start = 0; + for (int i = 0; i < threads; i++) + { + int count = per; + if (i < extraCount) + count++; + + yield return new IntRange(start, start + count - 1); + start += count; + } + + if (start != todo) + throw new Exception("Failed to slice correctly."); + } + + #region Data structs + private readonly struct AttackerData + { + public required Pawn Pawn { get; init; } + public required int PawnMeleeLevel { get; init; } + public required Thing MeleeWeapon { get; init; } + public required Thing Lasso { get; init; } + public required bool CanGrapple { get; init; } + public required bool CanExecute { get; init; } + public required float LassoRange { get; init; } + } + + private readonly struct PotentialAnimation + { + public bool IsValid => Anim != null || LassoToHere != null; + + public required AnimDef Anim { get; init; } + public required bool FlipX { get; init; } + public required Pawn Target { get; init; } + public required ulong OccupiedMask { get; init; } + public IntVec3? LassoToHere { get; init; } + } + + private class TaskData + { + public readonly List Targets = new List(64); + public readonly ActionController Controller = new ActionController(); + public readonly int Index; + public long ScheduleTick; + + public TaskData(int index) + { + Index = index; + } + } + + public class DiagnosticInfo + { + public readonly double[] StartupTimesMS = new double[64]; + public readonly double[] TargetFindTimesMS = new double[64]; + public readonly double[] ReportTimesMS = new double[64]; + public int ThreadsUsed; + public double CompileTimeMS; + public double ProcessTimeMS; + public double TotalTimeMS; + public int PawnCount; + } + #endregion +} diff --git a/Source/AnimationMod/Processing/RefTimer.cs b/Source/1.4/ThingGenerator/Processing/RefTimer.cs similarity index 100% rename from Source/AnimationMod/Processing/RefTimer.cs rename to Source/1.4/ThingGenerator/Processing/RefTimer.cs diff --git a/Source/AnimationMod/PromotionWorkers/MustHaveHeadPromotionWorker.cs b/Source/1.4/ThingGenerator/PromotionWorkers/MustHaveHeadPromotionWorker.cs similarity index 100% rename from Source/AnimationMod/PromotionWorkers/MustHaveHeadPromotionWorker.cs rename to Source/1.4/ThingGenerator/PromotionWorkers/MustHaveHeadPromotionWorker.cs diff --git a/Source/AnimationMod/RendererWorkers/AnimationRendererWorker.cs b/Source/1.4/ThingGenerator/RendererWorkers/AnimationRendererWorker.cs similarity index 100% rename from Source/AnimationMod/RendererWorkers/AnimationRendererWorker.cs rename to Source/1.4/ThingGenerator/RendererWorkers/AnimationRendererWorker.cs diff --git a/Source/AnimationMod/RendererWorkers/GilgameshRendererWorker.cs b/Source/1.4/ThingGenerator/RendererWorkers/GilgameshRendererWorker.cs similarity index 100% rename from Source/AnimationMod/RendererWorkers/GilgameshRendererWorker.cs rename to Source/1.4/ThingGenerator/RendererWorkers/GilgameshRendererWorker.cs diff --git a/Source/AnimationMod/Reqs/AndReq.cs b/Source/1.4/ThingGenerator/Reqs/AndReq.cs similarity index 100% rename from Source/AnimationMod/Reqs/AndReq.cs rename to Source/1.4/ThingGenerator/Reqs/AndReq.cs diff --git a/Source/AnimationMod/Reqs/AnyCategory.cs b/Source/1.4/ThingGenerator/Reqs/AnyCategory.cs similarity index 100% rename from Source/AnimationMod/Reqs/AnyCategory.cs rename to Source/1.4/ThingGenerator/Reqs/AnyCategory.cs diff --git a/Source/AnimationMod/Reqs/AnySize.cs b/Source/1.4/ThingGenerator/Reqs/AnySize.cs similarity index 100% rename from Source/AnimationMod/Reqs/AnySize.cs rename to Source/1.4/ThingGenerator/Reqs/AnySize.cs diff --git a/Source/AnimationMod/Reqs/AnyType.cs b/Source/1.4/ThingGenerator/Reqs/AnyType.cs similarity index 100% rename from Source/AnimationMod/Reqs/AnyType.cs rename to Source/1.4/ThingGenerator/Reqs/AnyType.cs diff --git a/Source/AnimationMod/Reqs/False.cs b/Source/1.4/ThingGenerator/Reqs/False.cs similarity index 100% rename from Source/AnimationMod/Reqs/False.cs rename to Source/1.4/ThingGenerator/Reqs/False.cs diff --git a/Source/AnimationMod/Reqs/OnlyType.cs b/Source/1.4/ThingGenerator/Reqs/OnlyType.cs similarity index 100% rename from Source/AnimationMod/Reqs/OnlyType.cs rename to Source/1.4/ThingGenerator/Reqs/OnlyType.cs diff --git a/Source/AnimationMod/Reqs/OrReq.cs b/Source/1.4/ThingGenerator/Reqs/OrReq.cs similarity index 100% rename from Source/AnimationMod/Reqs/OrReq.cs rename to Source/1.4/ThingGenerator/Reqs/OrReq.cs diff --git a/Source/AnimationMod/Reqs/Req.cs b/Source/1.4/ThingGenerator/Reqs/Req.cs similarity index 100% rename from Source/AnimationMod/Reqs/Req.cs rename to Source/1.4/ThingGenerator/Reqs/Req.cs diff --git a/Source/AnimationMod/Reqs/ReqInput.cs b/Source/1.4/ThingGenerator/Reqs/ReqInput.cs similarity index 100% rename from Source/AnimationMod/Reqs/ReqInput.cs rename to Source/1.4/ThingGenerator/Reqs/ReqInput.cs diff --git a/Source/AnimationMod/Reqs/SpecificWeapon.cs b/Source/1.4/ThingGenerator/Reqs/SpecificWeapon.cs similarity index 100% rename from Source/AnimationMod/Reqs/SpecificWeapon.cs rename to Source/1.4/ThingGenerator/Reqs/SpecificWeapon.cs diff --git a/Source/AnimationMod/Reqs/True.cs b/Source/1.4/ThingGenerator/Reqs/True.cs similarity index 100% rename from Source/AnimationMod/Reqs/True.cs rename to Source/1.4/ThingGenerator/Reqs/True.cs diff --git a/Source/AnimationMod/SpaceChecker.cs b/Source/1.4/ThingGenerator/SpaceChecker.cs similarity index 100% rename from Source/AnimationMod/SpaceChecker.cs rename to Source/1.4/ThingGenerator/SpaceChecker.cs diff --git a/Source/AnimationMod/Stats/StatWorker_DuelAbility.cs b/Source/1.4/ThingGenerator/Stats/StatWorker_DuelAbility.cs similarity index 100% rename from Source/AnimationMod/Stats/StatWorker_DuelAbility.cs rename to Source/1.4/ThingGenerator/Stats/StatWorker_DuelAbility.cs diff --git a/Source/AnimationMod/Stats/StatWorker_ExecCooldown.cs b/Source/1.4/ThingGenerator/Stats/StatWorker_ExecCooldown.cs similarity index 100% rename from Source/AnimationMod/Stats/StatWorker_ExecCooldown.cs rename to Source/1.4/ThingGenerator/Stats/StatWorker_ExecCooldown.cs diff --git a/Source/AnimationMod/Stats/StatWorker_Lethality.cs b/Source/1.4/ThingGenerator/Stats/StatWorker_Lethality.cs similarity index 100% rename from Source/AnimationMod/Stats/StatWorker_Lethality.cs rename to Source/1.4/ThingGenerator/Stats/StatWorker_Lethality.cs diff --git a/Source/AnimationMod/Sweep/BasicSweepProvider.cs b/Source/1.4/ThingGenerator/Sweep/BasicSweepProvider.cs similarity index 100% rename from Source/AnimationMod/Sweep/BasicSweepProvider.cs rename to Source/1.4/ThingGenerator/Sweep/BasicSweepProvider.cs diff --git a/Source/AnimationMod/Sweep/ISweepProvider.cs b/Source/1.4/ThingGenerator/Sweep/ISweepProvider.cs similarity index 100% rename from Source/AnimationMod/Sweep/ISweepProvider.cs rename to Source/1.4/ThingGenerator/Sweep/ISweepProvider.cs diff --git a/Source/AnimationMod/Sweep/PartWithSweep.cs b/Source/1.4/ThingGenerator/Sweep/PartWithSweep.cs similarity index 100% rename from Source/AnimationMod/Sweep/PartWithSweep.cs rename to Source/1.4/ThingGenerator/Sweep/PartWithSweep.cs diff --git a/Source/AnimationMod/Sweep/SweepMesh.cs b/Source/1.4/ThingGenerator/Sweep/SweepMesh.cs similarity index 100% rename from Source/AnimationMod/Sweep/SweepMesh.cs rename to Source/1.4/ThingGenerator/Sweep/SweepMesh.cs diff --git a/Source/AnimationMod/Tweaks/ColorConverter.cs b/Source/1.4/ThingGenerator/Tweaks/ColorConverter.cs similarity index 100% rename from Source/AnimationMod/Tweaks/ColorConverter.cs rename to Source/1.4/ThingGenerator/Tweaks/ColorConverter.cs diff --git a/Source/AnimationMod/Tweaks/HandsMode.cs b/Source/1.4/ThingGenerator/Tweaks/HandsMode.cs similarity index 100% rename from Source/AnimationMod/Tweaks/HandsMode.cs rename to Source/1.4/ThingGenerator/Tweaks/HandsMode.cs diff --git a/Source/1.4/ThingGenerator/Tweaks/ItemTweakData.cs b/Source/1.4/ThingGenerator/Tweaks/ItemTweakData.cs new file mode 100644 index 00000000..20151d6c --- /dev/null +++ b/Source/1.4/ThingGenerator/Tweaks/ItemTweakData.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AM.Retexture; +using AM.Idle; +using AM.Reqs; +using AM.Sweep; +using Newtonsoft.Json; +using UnityEngine; +using Verse; + +namespace AM.Tweaks +{ + public class ItemTweakData + { + public static string MakeModID(ModContentPack mcp) + { + if (mcp == null) + return null; + + string s = mcp.PackageId; + + foreach (char c in Path.GetInvalidFileNameChars()) + s = s.Replace(c, '_'); + + s = s.Replace("_copy", ""); + s = s.Replace("_Copy", ""); + s = s.Replace("_localcopy", ""); + s = s.Replace("_LocalCopy", ""); + s = s.Replace("_local", ""); + s = s.Replace("_Local", ""); + s = s.Replace("_steam", ""); + s = s.Replace("_Steam", ""); + + return s; + } + + public static string MakeFileName(ThingDef weaponDef, ModContentPack weaponTextureMod) + => $"{weaponDef.defName}_{MakeModID(weaponTextureMod)}.json"; + + public static ItemTweakData LoadFrom(string filePath) + { + return JsonConvert.DeserializeObject(File.ReadAllText(filePath), new JsonSerializerSettings + { + Converters = new List() + { + new ColorConverter() + } + }); + } + + [JsonIgnore] + public string FileName => $"{ItemDefName}_{TextureModID}.json"; + [JsonIgnore] + public float BladeLength => Mathf.Abs(BladeStart - BladeEnd); + [JsonIgnore] + public float MaxDistanceFromHand => Mathf.Max(Mathf.Abs(BladeEnd), Mathf.Abs(BladeEnd)); + + public string TextureModID; + public string ItemDefName; + public string ItemType; + public string ItemTypeNamespace; + public float OffX, OffY; + public float Rotation; + [System.ComponentModel.DefaultValue(1f)] + public float ScaleX = 1; + [System.ComponentModel.DefaultValue(1f)] + public float ScaleY = 1; + public bool FlipX, FlipY; + public bool UseDefaultTransparentMaterial; + public HandsMode HandsMode = HandsMode.Default; + public float BladeStart; + [System.ComponentModel.DefaultValue(0.5f)] + public float BladeEnd = 0.5f; + public MeleeWeaponType MeleeWeaponType = MeleeWeaponType.Long_Stab | MeleeWeaponType.Long_Sharp; + [System.ComponentModel.DefaultValue("")] + public string CustomRendererClass; + public string SweepProviderClass; + public Color? TrailTint = null; + + private ThingDef cachedDef; + private Texture2D cachedTex; + private ModContentPack cachedMod; + private ISweepProvider cachedSweepProvider; + private AnimDef idleAnimCached; + private AnimDef moveHorAnimCached; + private AnimDef moveVertAnimCached; + private AnimDef[] flavourAnimsCached; + private AnimDef[][] attackAnimationsCached; + + public ItemTweakData() { } + + public ItemTweakData(ThingDef def, ModContentPack textureMod) + { + if (def == null) + return; + + cachedDef = def; + + ItemDefName = def.defName; + ItemType = def.GetType().Name; + ItemTypeNamespace = def.GetType().Namespace; + + var report = RetextureUtility.GetTextureReport(def); + if (report.HasError) + throw new Exception($"Retexture utility had error: {report.ErrorMessage}"); + var mod = textureMod ?? report.ActiveRetextureMod; + + TextureModID = MakeModID(mod); + + Rotation = 0f; + ScaleX = def.graphic.drawSize.x; + ScaleY = def.graphic.drawSize.y; + HandsMode = HandsMode.Default; + } + + public (WeaponSize size, WeaponCat category) GetCategory() => IdleClassifier.Classify(this); + + public AnimDef GetIdleAnimation() + { + if (idleAnimCached != null) + return idleAnimCached; + + var reqArgs = new ReqInput(this); + idleAnimCached = AnimDef.GetDefsOfType(AnimType.Idle).FirstOrDefault(d => d.idleType == IdleType.Idle && d.Allows(reqArgs)); + + return idleAnimCached; + } + + public IReadOnlyList GetFlavourAnimations() + { + if (flavourAnimsCached != null) + return flavourAnimsCached; + + var reqArgs = new ReqInput(this); + flavourAnimsCached = AnimDef.GetDefsOfType(AnimType.Idle).Where(d => d.idleType == IdleType.Flavour && d.Allows(reqArgs)).ToArray(); + + return flavourAnimsCached; + } + + public AnimDef GetMoveHorizontalAnimation() + { + if (moveHorAnimCached != null) + return moveHorAnimCached; + + var reqArgs = new ReqInput(this); + moveHorAnimCached = AnimDef.GetDefsOfType(AnimType.Idle).FirstOrDefault(d => d.idleType == IdleType.MoveHorizontal && d.Allows(reqArgs)); + + return moveHorAnimCached; + } + + public AnimDef GetMoveVerticalAnimation() + { + if (moveVertAnimCached != null) + return moveVertAnimCached; + + var reqArgs = new ReqInput(this); + moveVertAnimCached = AnimDef.GetDefsOfType(AnimType.Idle).FirstOrDefault(d => d.idleType == IdleType.MoveVertical && d.Allows(reqArgs)); + + return moveVertAnimCached; + } + + public IReadOnlyList GetAttackAnimations(Rot4 direction) + { + if (attackAnimationsCached != null) + return attackAnimationsCached[direction.AsInt]; + + var reqArgs = new ReqInput(this); + attackAnimationsCached = new AnimDef[4][]; + + // Horizontal. + var hor = AnimDef.GetDefsOfType(AnimType.Idle).Where(d => d.idleType == IdleType.AttackHorizontal && d.Allows(reqArgs)).ToArray(); + attackAnimationsCached[Rot4.EastInt] = hor; + attackAnimationsCached[Rot4.WestInt] = hor; + + // Vertical. + attackAnimationsCached[Rot4.NorthInt] = AnimDef.GetDefsOfType(AnimType.Idle).Where(d => d.idleType == IdleType.AttackNorth && d.Allows(reqArgs)).ToArray(); + attackAnimationsCached[Rot4.SouthInt] = AnimDef.GetDefsOfType(AnimType.Idle).Where(d => d.idleType == IdleType.AttackSouth && d.Allows(reqArgs)).ToArray(); + + return attackAnimationsCached[direction.AsInt]; + } + + public void CopyTransformFrom(ItemTweakData data) + { + if (data == null || data == this) + return; + + OffX = data.OffX; + OffY = data.OffY; + ScaleX = data.ScaleX; + ScaleY = data.ScaleY; + Rotation = data.Rotation; + HandsMode = data.HandsMode; + MeleeWeaponType = data.MeleeWeaponType; + FlipX = data.FlipX; + FlipY = data.FlipY; + BladeStart = data.BladeStart; + BladeEnd = data.BladeEnd; + CustomRendererClass = data.CustomRendererClass; + SweepProviderClass = data.SweepProviderClass; + TrailTint = data.TrailTint; + } + + public Texture2D GetTexture(bool allowFromCache = true, bool saveToCache = true) + { + if (!allowFromCache || cachedTex == null) + { + var report = RetextureUtility.GetTextureReport(GetDef()); + if (report.HasError) + throw new Exception($"Retexture utility had error: {report.ErrorMessage}"); + + var found = report.AllRetextures.FirstOrDefault(p => MakeModID(p.mod) == TextureModID).texture; + if (saveToCache) + cachedTex = found; + else + return found; + } + return cachedTex; + } + + public ThingDef GetDef(bool allowFromCache = true, bool saveToCache = true) + { + if (!allowFromCache || cachedDef == null) + { + var type = GenTypes.GetTypeInAnyAssembly(ItemType, ItemTypeNamespace); + if (type == null) + return null; + + var obj = GenGeneric.InvokeStaticMethodOnGenericType(typeof(DefDatabase<>), type, "GetNamedSilentFail", ItemDefName) as ThingDef; + if (obj == null) + Core.Warn($"Failed to find item def '{ItemDefName}' of type {type}. The item was probably removed from the target mod."); + + if (saveToCache) + cachedDef = obj; + else + return obj; + } + return cachedDef; + } + + public ModContentPack GetMod() + { + if (cachedMod != null) + return cachedMod; + + foreach (var mcp in LoadedModManager.RunningModsListForReading) + { + if (MakeModID(mcp) == TextureModID) + { + cachedMod = mcp; + break; + } + } + + return cachedMod; + } + + public ISweepProvider GetSweepProvider() + { + if (string.IsNullOrEmpty(SweepProviderClass)) + return null; + + if (cachedSweepProvider != null) + return cachedSweepProvider; + + Type klass = GenTypes.GetTypeInAnyAssembly(SweepProviderClass); + if (klass == null) + { + Core.Warn($"Failed to find any class called '{SweepProviderClass}' as a sweep provider."); + return null; + } + + if (!typeof(ISweepProvider).IsAssignableFrom(klass)) + { + Core.Error($"{klass.FullName} does not implement {nameof(ISweepProvider)}"); + return null; + } + + ISweepProvider instance; + try + { + instance = Activator.CreateInstance(klass, this) as ISweepProvider; + } + catch + { + try + { + instance = Activator.CreateInstance(klass) as ISweepProvider; + } + catch (Exception e) + { + Core.Error($"Failed to create instance of ISweepProvider '{klass.FullName}':", e); + return null; +; } + } + + cachedSweepProvider = instance; + return cachedSweepProvider; + } + + public virtual AnimPartOverrideData Apply(AnimRenderer renderer, AnimPartData part) + { + if (part == null) + return null; + + var ov = renderer.GetOverride(part); + ov.Texture = GetTexture(); + ov.LocalScaleFactor = new Vector2(ScaleX, ScaleY); + ov.LocalRotation = Rotation; + ov.LocalOffset = new Vector2(OffX, OffY); + ov.FlipX = FlipX; + ov.FlipY = FlipY; + ov.UseDefaultTransparentMaterial = UseDefaultTransparentMaterial; + ov.TweakData = this; + + if (!string.IsNullOrWhiteSpace(CustomRendererClass)) + { + // TODO cache. + // TODO handle errors. + Type rendererClass = GenTypes.GetTypeInAnyAssembly(CustomRendererClass); + var instance = Activator.CreateInstance(rendererClass) as PartRenderer; + instance.TweakData = this; + ov.CustomRenderer = instance; + } + + return ov; + } + + public void SaveTo(string filePath) + { + var settings = new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + Error = (e, t) => + { + Core.Error($"Json saving error: {t.ErrorContext.Error.Message}"); + }, + Converters = new List() + { + new ColorConverter() + } + }; + string json = JsonConvert.SerializeObject(this, Formatting.Indented, settings); + File.WriteAllText(filePath, json); + } + } +} diff --git a/Source/AnimationMod/Tweaks/MeleeWeaponType.cs b/Source/1.4/ThingGenerator/Tweaks/MeleeWeaponType.cs similarity index 100% rename from Source/AnimationMod/Tweaks/MeleeWeaponType.cs rename to Source/1.4/ThingGenerator/Tweaks/MeleeWeaponType.cs diff --git a/Source/1.4/ThingGenerator/Tweaks/TweakDataManager.cs b/Source/1.4/ThingGenerator/Tweaks/TweakDataManager.cs new file mode 100644 index 00000000..c4cbe9d6 --- /dev/null +++ b/Source/1.4/ThingGenerator/Tweaks/TweakDataManager.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AM.Retexture; +using UnityEngine; +using Verse; + +namespace AM.Tweaks +{ + public static class TweakDataManager + { + public const string DATA_FOLDER_NAME = "WeaponTweakData"; + + public static int TweakDataLoadedCount => defWithModToTweak.Count; + + private static readonly Dictionary defToTweak = new Dictionary(); + private static readonly Dictionary<(ThingDef def, ModContentPack mod), ItemTweakData> defWithModToTweak = new Dictionary<(ThingDef def, ModContentPack mod), ItemTweakData>(); + private static Dictionary overrideTweakDataFile; + + /// + /// Gets the where the weapon tweak data is expected to exist + /// + public static FileInfo GetFileForTweak(ThingDef weaponDef, ModContentPack textureMod = null, FileInfo setOverride = null) + { + if (textureMod == null) + { + var report = RetextureUtility.GetTextureReport(weaponDef); + textureMod = report.ActiveRetextureMod; + } + + string fileName = ItemTweakData.MakeFileName(weaponDef, textureMod); + + // Allow other mods to provide tweak files. + overrideTweakDataFile ??= MakeOverrideTweakDataFileMap(); + if (setOverride != null) + overrideTweakDataFile[fileName] = setOverride; + + if (overrideTweakDataFile.TryGetValue(fileName, out var found)) + return found; + + string root = Core.ModContent.RootDir; + string fp = Path.Combine(root, DATA_FOLDER_NAME, fileName); + return new FileInfo(fp); + } + + private static Dictionary MakeOverrideTweakDataFileMap() + { + var ov = new Dictionary(); + var mods = LoadedModManager.RunningModsListForReading; + + foreach (var mod in mods) + { + // Main mod does not provide overrides - every other mod takes priority over the main one. + if (mod == Core.ModContent) + continue; + + var folder = Path.Combine(mod.RootDir, DATA_FOLDER_NAME); + if (!Directory.Exists(folder)) + continue; + + foreach (var file in Directory.EnumerateFiles(folder, "*.json", SearchOption.TopDirectoryOnly)) + { + var fi = new FileInfo(file); + ov[fi.Name] = fi; + } + } + + return ov; + } + + public static int GetTweakDataFileCount(string modID) + { + int c = 0; + string root = Core.ModContent.RootDir; + modID += ".json"; + foreach (var fn in Directory.EnumerateFiles(Path.Combine(root, DATA_FOLDER_NAME), "*.json")) + { + if (fn.EndsWith(modID)) + c++; + } + return c; + } + + public static ItemTweakData TryGetTweak(ThingDef def) => TryGetTweak(def, null); + + public static ItemTweakData TryGetTweak(ThingDef def, ModContentPack textureSupplier) + { + if (def == null) + return null; + + if (textureSupplier == null) + return defToTweak.TryGetValue(def, out var found) ? found : null; + + return defWithModToTweak.TryGetValue((def, textureSupplier), out var found2) ? found2 : null; + } + + public static ItemTweakData CreateNew(ThingDef def, ModContentPack textureSupplier) + { + var tweak = new ItemTweakData(def, textureSupplier); + return Register(tweak) ? tweak : null; + } + + public static ItemTweakData TryLoad(ThingDef weapon, ModContentPack textureMod) + { + var file = GetFileForTweak(weapon, textureMod); + if (!file.Exists) + { + return null; + } + + ItemTweakData loaded; + try + { + loaded = ItemTweakData.LoadFrom(file.FullName); + } + catch (Exception e) + { + Core.Error($"Exception loading tweak data json '{file.Name}':", e); + return null; + } + + var existing = TryGetTweak(weapon, textureMod); + if (existing != null) + { + defWithModToTweak.Remove((weapon, textureMod)); + } + + if (Register(loaded)) + return loaded; + + Core.Error("Unexpected failure to register."); + return null; + } + + private static bool Register(ItemTweakData td) + { + var def = td.GetDef(); + if (def == null) + { + Core.Error($"Failed to find item def for '{td.ItemDefName}' of type '{td.ItemTypeNamespace}.{td.ItemType}'. Tweak will not be registered."); + return false; + } + + var report = RetextureUtility.GetTextureReport(def); + if (report.HasError) + { + Core.Error($"Failed to generate retexture report for {def}. Tweak will not be registered. Reason: {report.ErrorMessage}"); + return false; + } + + var mod = report.AllRetextures.FirstOrDefault(p => ItemTweakData.MakeModID(p.mod) == td.TextureModID).mod; + if (mod == null) + { + Core.Error($"Failed to find mod '{td.TextureModID}' among active retextures of {td.ItemDefName}."); + return false; + } + + if (defWithModToTweak.ContainsKey((def, mod))) + { + Core.Error($"There is already a tweak registered for def '{def}' with texture mod '{mod.Name}'"); + return false; + } + + defToTweak[def] = td; + defWithModToTweak.Add((def, mod), td); + return true; + } + + [DebugOutput("Melee Animation")] + private static void LogAllRetextureCompletion() + { + var all = from td in DefDatabase.AllDefsListForReading + where td.IsMeleeWeapon() + let report = RetextureUtility.GetTextureReport(td) + where !report.HasError + from pair in report.AllRetextures + select ( pair.mod, report ); + + static TableDataGetter<(ModContentPack mod, ActiveTextureReport rep)> Row(string name, Func<(ModContentPack mod, ActiveTextureReport rep), string> toString) + => new TableDataGetter<(ModContentPack mod, ActiveTextureReport rep)>(name, toString); + + static TableDataGetter<(ModContentPack mod, ActiveTextureReport rep)> RowObj(string name, Func<(ModContentPack mod, ActiveTextureReport rep), object> toObj) + => new TableDataGetter<(ModContentPack mod, ActiveTextureReport rep)>(name, toObj); + + var table = new TableDataGetter<(ModContentPack mod, ActiveTextureReport rep)>[6]; + int i = 0; + table[i++] = Row("Def Name", p => p.rep.Weapon.defName); + table[i++] = Row("Label", p => p.rep.Weapon.LabelCap); + table[i++] = Row("Source", p => p.rep.SourceMod?.Name ?? "?"); + table[i++] = Row("Texture Provider", p => $"{ItemTweakData.MakeModID(p.mod)} '{p.mod.Name}'"); + table[i++] = Row("Texture Path", p => p.rep.TexturePath); + table[i++] = RowObj("Is Saved", p => GetFileForTweak(p.rep.Weapon, p.mod).Exists.ToStringCheckBlank()); + + DebugTables.MakeTablesDialog(all, table); + } + + public static IEnumerable<(string modPackageID, string modName, ThingDef weapon)> LoadAllForActiveMods(bool includeRedundant) + { + var data = from weapon in DefDatabase.AllDefsListForReading + where weapon.IsMeleeWeapon() + let report = RetextureUtility.GetTextureReport(weapon) + select (weapon, report); + + foreach (var pair in data) + { + if (pair.report.HasError) + { + Core.Error($"Failed to get texture report for {pair.weapon}: {pair.report.ErrorMessage}"); + continue; + } + + // Attempt to load the tweak for the actual active retexture. + var tweak = TryLoad(pair.weapon, pair.report.ActiveRetextureMod); + if (tweak == null) + yield return (ItemTweakData.MakeModID(pair.report.ActiveRetextureMod), pair.report.ActiveRetextureMod.Name, pair.weapon); + + // Fallback to other tweak data from non-active retextures. + if (includeRedundant || tweak == null) + { + foreach (var retex in pair.report.AllRetextures) + { + // Skip the active retexture, it has already been tried. + if (retex.mod == pair.report.ActiveRetextureMod) + continue; + + // Try to load a tweak for this retexture... + tweak = TryLoad(pair.weapon, retex.mod); + if (tweak == null) + yield return (ItemTweakData.MakeModID(retex.mod), retex.mod.Name, pair.weapon); + else if (!includeRedundant) + break; + } + } + } + } + + public static IEnumerable GetTweaksReportForActiveMods() + { + var reportsByMod = from mod in LoadedModManager.RunningModsListForReading + orderby mod.Name + let reports = RetextureUtility.GetModWeaponReports(mod) + where reports.Any() + select new { mod, reports }; + + foreach (var pair in reportsByMod) + { + yield return new ModTweakContainer + { + Mod = pair.mod, + Reports = pair.reports + }; + } + } + + public static Texture2D ToTexture2D(this RenderTexture rTex) + { + Texture2D tex = new(rTex.width, rTex.height, TextureFormat.RGBA32, false); + var oldRt = RenderTexture.active; + RenderTexture.active = rTex; + + tex.ReadPixels(new Rect(0, 0, rTex.width, rTex.height), 0, 0); + tex.Apply(); + + RenderTexture.active = oldRt; + return tex; + } + } + + public struct ModTweakContainer + { + public ModContentPack Mod; + public IEnumerable Reports; + } +} diff --git a/Source/1.4/ThingGenerator/UI/BGRenderer.cs b/Source/1.4/ThingGenerator/UI/BGRenderer.cs new file mode 100644 index 00000000..f48451ac --- /dev/null +++ b/Source/1.4/ThingGenerator/UI/BGRenderer.cs @@ -0,0 +1,152 @@ +using System; +using UnityEngine; +using Verse; + +namespace AM.UI; + +public static class BGRenderer +{ + private static readonly int errorCode = Guid.NewGuid().GetHashCode(); + + [TweakValue("Melee Animation", 0f, 12f)] + private static float BGFocusPoint = 4.5f; + [TweakValue("Melee Animation", 1, 2)] + private static float BGZoomLevel = 1.075f; + private static Texture2D[] layers; + + public static void DrawMainMenuBackground() + { + try + { + layers ??= InitLayers(); + var screenArea = new Rect(0, 0, Verse.UI.screenWidth, Verse.UI.screenHeight); + var mouseOffset = Verse.UI.MousePositionOnUIInverted - screenArea.center; + var mouseOffsetNormalized = mouseOffset / (screenArea.size * 0.5f); + + + var maxOffset = screenArea.size * (1f - BGZoomLevel) * 0.5f; + + for (int i = layers.Length - 1; i >= 0; i--) + { + var layer = layers[i]; + + float diff = (i - BGFocusPoint) / + Mathf.Abs(Mathf.Max(BGFocusPoint, (layers.Length - 1) - BGFocusPoint)); + var pos = layer.FitRect(screenArea, ScaleMode.Cover, BGZoomLevel); + pos.position += diff * mouseOffsetNormalized * maxOffset; + + GUI.color = Color.white; + Widgets.DrawTexturePart(pos, new Rect(0, 0, 1, 1), layer); + } + } + catch (Exception e) + { + Log.ErrorOnce($"Exception rendering melee animation background:\n{e}", errorCode); + } + } + + private static Texture2D[] InitLayers() + { + const int LAYER_COUNT = 13; + + var loaded = new Texture2D[LAYER_COUNT]; + for (int i = 0; i < LAYER_COUNT; i++) + loaded[i] = ContentFinder.Get($"AM/UI/BG/{i + 1}"); + + return loaded; + } + + public static Rect FitRect(this Texture tex, Rect area, ScaleMode mode, float scale = 1f) + { + var size = new Vector2(tex.width, tex.height); + return FitRect(size, area, mode, scale); + } + + public static Rect FitRect(this Vector2 texSize, Rect area, ScaleMode mode, float scale = 1f) + { + float w = texSize.x; + float h = texSize.y; + + void Shrink() + { + if (w > area.width) + { + float inc = area.width / w; + w = area.width; + h *= inc; + } + if (h > area.height) + { + float inc = area.height / h; + h = area.height; + w *= inc; + } + } + + void Expand() + { + if (w < area.width) + { + float inc = area.width / w; + w = area.width; + h *= inc; + } + if (h < area.height) + { + float inc = area.height / h; + h = area.height; + w *= inc; + } + } + + switch (mode) + { + case ScaleMode.Expand: + Expand(); + break; + + case ScaleMode.Shrink: + Shrink(); + break; + case ScaleMode.Cover: + Shrink(); + Expand(); + break; + case ScaleMode.Fit: + Expand(); + Shrink(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode), mode, null); + } + + return new Rect(0, 0, w, h).CenteredOnXIn(area).CenteredOnYIn(area).ScaledBy(scale); + } +} + +public enum ScaleMode +{ + /// + /// The texture expands until it covers the entire rect. + /// If it is already larger than the rect nothing happens. + /// + Expand, + + /// + /// The texture is shrunk until it fits entirely within the rect. + /// If it is already smaller than the rect nothing happens. + /// + Shrink, + + /// + /// The same as but the texture is scaled such that it is as small as possible while still covering the entire rect. + /// The texture may still expand beyond the rect. + /// + Cover, + + /// + /// The texture is scaled such that it expands as large as possible within the rect + /// without spilling outside the rect. + /// + Fit +} diff --git a/Source/1.4/ThingGenerator/UI/Dialog_AnimationDebugger.cs b/Source/1.4/ThingGenerator/UI/Dialog_AnimationDebugger.cs new file mode 100644 index 00000000..615e9fb5 --- /dev/null +++ b/Source/1.4/ThingGenerator/UI/Dialog_AnimationDebugger.cs @@ -0,0 +1,840 @@ +using AM.Idle; +using EpicUtils; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Jobs.LowLevel.Unsafe; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace AM.UI +{ + public class Dialog_AnimationDebugger : Window + { + public static bool IsInRehearsalMode => startRehearsal && IsStarterOpen; + + private static bool IsStarterOpen => Mathf.Abs(lastOpenStarterTime - Time.realtimeSinceStartup) < 0.25f; + private static MaterialPropertyBlock mpb; + private static Material mat; + private static AnimRenderer selectedRenderer; + private static AnimPartData selectedPart; + private static Pawn[] startPawns = new Pawn[8]; + private static AnimDef startDef; + private static bool startMX, startMY; + private static bool startRehearsal = true; + private static LocalTargetInfo startTarget = LocalTargetInfo.Invalid; + private static float lastOpenStarterTime; + private static ExecutionOutcome executionOutcome = ExecutionOutcome.Kill; + + [DebugAction("Melee Animation", "Open Debugger", actionType = DebugActionType.Action)] + private static void OpenInt() + { + Open(); + } + + public static Dialog_AnimationDebugger Open() + { + var instance = new Dialog_AnimationDebugger(); + Find.WindowStack?.Add(instance); + return instance; + } + + public readonly List<(string name, Action tab)> Tabs = new List<(string name, Action tab)>(); + public int SelectedIndex; + + private const bool AUTO_SELECT_RENDERER = true; + private readonly Vector2[] scrolls = new Vector2[32]; + private readonly Queue toDraw = new Queue(); + private readonly List allManagers = new List(); + private readonly List tempCells = new List(128); + + private int scrollIndex; + private AnimDef spaceCheckDef; + private bool spaceCheckMX; + + public Dialog_AnimationDebugger() + { + closeOnClickedOutside = false; + doCloseX = true; + doCloseButton = false; + preventCameraMotion = false; + resizeable = true; + draggable = true; + closeOnCancel = false; + closeOnAccept = false; + onlyOneOfTypeAllowed = false; + drawInScreenshotMode = false; + + Tabs.Add(("Active Animation Inspector", DrawAllAnimators)); + Tabs.Add(("Animation Starter", DrawAnimationStarter)); + Tabs.Add(("Hierarchy", DrawHierarchy)); + Tabs.Add(("Inspector", DrawInspector)); + Tabs.Add(("Performance Analyzer", DrawPerformanceAnalyzer)); + Tabs.Add(("Animation Space Checker", DrawSpaceChecker)); + Tabs.Add(("List All Active", DrawAllLists)); + + if (mpb == null) + { + mpb = new MaterialPropertyBlock(); + mat ??= MaterialPool.MatFrom(GenDraw.LineTexPath, ShaderDatabase.Transparent, Color.white); + mpb.SetTexture("_MainTex", mat.mainTexture); + } + } + + private ref Vector2 GetScroll() + { + return ref scrolls[scrollIndex++]; + } + + public override void DoWindowContents(Rect inRect) + { + if (selectedRenderer != null && selectedRenderer.IsDestroyed) + selectedRenderer = null; + if (AUTO_SELECT_RENDERER && selectedRenderer == null) + selectedRenderer = AnimRenderer.ActiveRenderers.FirstOrDefault(r => !r.IsDestroyed && r.Map == Find.CurrentMap); + + scrollIndex = 0; + + var ui = new Listing_Standard(); + ui.Begin(inRect); + + var rect = ui.GetRect(32); + if (Widgets.ButtonText(rect.LeftHalf(), $"View: {Tabs[SelectedIndex].name}")) + { + FloatMenuUtility.MakeMenu(Tabs, p => p.name, p => () => + { + SelectedIndex = Tabs.IndexOf(p); + }); + } + if (Widgets.ButtonText(rect.RightHalf(), "Open new debugger")) + Open(); + + ui.GapLine(); + + try + { + Tabs[SelectedIndex].tab(ui); + } + catch (Exception e) + { + ui.Label($"EXCEPTION DRAWING WINDOW:\n{e}"); + } + + ui.End(); + } + + public override void WindowUpdate() + { + base.WindowUpdate(); + + while (toDraw.TryDequeue(out var action)) + action(); + } + + private void DrawAllAnimators(Listing_Standard ui) + { + var area = ui.GetRect(300); + var oldUI = ui; + ui = new Listing_Standard(); + int toDraw = AnimRenderer.ActiveRenderers.Count; + int toDraw2 = AnimRenderer.TotalCapturedPawnCount; + Widgets.BeginScrollView(area, ref GetScroll(), new Rect(0, 0, area.width - 20, 110 * toDraw + 40 * toDraw2)); + ui.Begin(new Rect(0, 0, area.width - 20, area.height)); + + AnimRenderer toDestroy = null; + foreach(var renderer in AnimRenderer.ActiveRenderers) + { + var rect = ui.GetRect(130); + + if (renderer == selectedRenderer) + { + var c = Color.cyan; + c.a = 0.3f; + Widgets.DrawBoxSolid(rect, c); + } + else if (Widgets.ButtonInvisible(rect)) + { + selectedRenderer = renderer; + } + + Widgets.DrawBox(rect); + + string title = $"[{renderer.CurrentTime:F2} / {renderer.Data.Duration:F2}] {renderer.Data.Name}"; + if (renderer.IsDestroyed) + title += " [DESTROYED]"; + Widgets.Label(rect.ExpandedBy(-4, -4), title); + var bar = rect.ExpandedBy(-4); + bar.yMin += 22; + bar.height = 14; + float lerp = Mathf.Clamp01(renderer.CurrentTime / renderer.Data.Duration); + var fillBar = bar; + fillBar.width = bar.width * lerp; + Widgets.DrawBoxSolid(bar, Color.grey); + Widgets.DrawBoxSolid(fillBar, Color.green); + foreach (var e in renderer.GetEventsInPeriod(new Vector2(0, renderer.Duration + 1f))) + { + float p = e.Time / renderer.Duration; + Widgets.DrawBoxSolid(new Rect(bar.x + p * bar.width, bar.y, 3, bar.height), Color.red); + } + + bar.y += 18; + Widgets.DrawBoxSolid(bar, Color.white * 0.45f); + float newLerp = Widgets.HorizontalSlider_NewTemp(bar.ExpandedBy(0, -2), lerp, 0, 1); + if (Math.Abs(newLerp - lerp) > 0.005f) + renderer.Seek(newLerp * renderer.Data.Duration, 0, null); + + rect.y += 20; + + int i = 0; + foreach(var pawn in renderer.Pawns) + { + if (pawn == null) + continue; + + bool hasJob = pawn?.CurJobDef == AM_DefOf.AM_InAnimation; + + string label = pawn.LabelShort; + Color col = Color.white; + if (pawn.Dead) + { + label += " [DEAD]"; + col = Color.red; + } + else if (pawn.Destroyed) + { + label += " [DESTROYED]"; + col = Color.red; + } + else if (!pawn.Spawned) + { + label += " [NOT_SPAWNED]"; + col = Color.red; + } + else if (!hasJob) + { + label += $"[BADJOB:{pawn?.CurJobDef.defName ?? "null"}]"; + col = Color.red; + } + + Rect b = rect.ExpandedBy(-4, -4); + b.yMin += 40; + b.width = 100; + b.height = 28; + b.x += i * 110; + + Widgets.DrawBox(b); + GUI.color = col; + Widgets.LabelFit(b.ExpandedBy(-4, -2), label); + GUI.color = Color.white; + + i++; + } + + Rect stop = rect.ExpandedBy(-4); + stop.y += 72; + stop.size = new Vector2(100, 28); + if(Widgets.ButtonText(stop, "Stop")) + { + toDestroy = renderer; + } + + ui.GapLine(10); + } + foreach(var pawn in AnimRenderer.CapturedPawns) + { + Widgets.Label(ui.GetRect(30).ExpandedBy(-4, -4), pawn.NameFullColored); + } + + if (toDestroy != null) + toDestroy.Destroy(); + + ui.End(); + Widgets.EndScrollView(); + ui = oldUI; + ui.GapLine(); + } + + private void DrawSpaceChecker(Listing_Standard ui) + { + if (Event.current.type == EventType.Repaint && spaceCheckDef != null) + { + toDraw.Enqueue(() => + { + IntVec3 mp = Verse.UI.MouseCell(); + + ulong occupiedMask = SpaceChecker.MakeOccupiedMask(Find.CurrentMap, mp, out uint smallMask); + ulong clearMask = !spaceCheckMX ? spaceCheckDef.ClearMask : spaceCheckDef.FlipClearMask; + bool isClear = (occupiedMask & clearMask) == 0; + + tempCells.Clear(); + for (int x = mp.x - 3; x <= mp.x + 3; x++) + { + for (int z = mp.z - 3; z <= mp.z + 3; z++) + { + int offX = x - mp.x; + int offZ = z - mp.z; + bool isOccupied = occupiedMask.GetBit(offX, offZ); + + if (!isOccupied) + tempCells.Add(new IntVec3(x, 0, z)); + + } + } + + GenDraw.DrawFieldEdges(tempCells, isClear ? Color.green : Color.red); + + foreach (var cell in spaceCheckDef.GetMustBeClearCells(spaceCheckMX, false, mp)) + { + GenDraw.DrawTargetHighlight(new LocalTargetInfo(cell)); + } + }); + } + + if (ui.ButtonText(spaceCheckDef?.LabelCap.ToString() ?? "